diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 17b498e1878..98789bdc86c 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -1990,6 +1990,11 @@ android:label="Terms of Service" android:theme="@android:style/Theme.DeviceDefault.Light.Dialog" /> + + + diff --git a/res/layout/blob_list_item_view.xml b/res/layout/blob_list_item_view.xml new file mode 100644 index 00000000000..897d19c2694 --- /dev/null +++ b/res/layout/blob_list_item_view.xml @@ -0,0 +1,50 @@ + + + + + + + + + + diff --git a/res/layout/lease_list_item_view.xml b/res/layout/lease_list_item_view.xml new file mode 100644 index 00000000000..5edd9e54034 --- /dev/null +++ b/res/layout/lease_list_item_view.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + diff --git a/res/layout/shared_data_empty_list_view.xml b/res/layout/shared_data_empty_list_view.xml new file mode 100644 index 00000000000..1bb338bdaf8 --- /dev/null +++ b/res/layout/shared_data_empty_list_view.xml @@ -0,0 +1,31 @@ + + + + + + diff --git a/res/values/dimens.xml b/res/values/dimens.xml index 6984ca1132b..aadd62d5696 100755 --- a/res/values/dimens.xml +++ b/res/values/dimens.xml @@ -418,4 +418,7 @@ 8dp 48dp 16dp + + + 16dp diff --git a/res/xml/development_settings.xml b/res/xml/development_settings.xml index 48a0850c7ba..c134763d82a 100644 --- a/res/xml/development_settings.xml +++ b/res/xml/development_settings.xml @@ -652,4 +652,19 @@ android:title="@string/autofill_reset_developer_options" /> + + + + + + + diff --git a/res/xml/zen_mode_people_settings.xml b/res/xml/zen_mode_people_settings.xml index 140c2414aa4..1db438871be 100644 --- a/res/xml/zen_mode_people_settings.xml +++ b/res/xml/zen_mode_people_settings.xml @@ -25,6 +25,12 @@ android:key="zen_mode_settings_category_conversations" android:title="@string/zen_mode_conversations_section_title"> + + + { + try { + mBlobStoreManager.deleteBlob(blob); + } catch (IOException e) { + Log.e(TAG, "Unable to delete blob: " + e.getMessage()); + Toast.makeText(this, R.string.shared_data_delete_failure_text, Toast.LENGTH_LONG) + .show(); + } + queryBlobsAndUpdateList(); + }; + } + + private void queryBlobsAndUpdateList() { + try { + mAdapter.updateList(mBlobStoreManager.queryBlobsForUser(UserHandle.CURRENT)); + } catch (IOException e) { + Log.e(TAG, "Unable to fetch blobs for current user: " + e.getMessage()); + Toast.makeText(this, R.string.shared_data_query_failure_text, Toast.LENGTH_LONG).show(); + finish(); + } + } + + private class BlobListAdapter extends ArrayAdapter { + BlobListAdapter(Context context) { + super(context, 0); + } + + void updateList(List blobs) { + clear(); + if (blobs.isEmpty()) { + getListView().setEmptyView(getEmptyView()); + } else { + addAll(blobs); + } + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + final BlobInfoViewHolder holder = BlobInfoViewHolder.createOrRecycle( + mInflater, convertView); + convertView = holder.rootView; + + final BlobInfo blob = getItem(position); + holder.blobLabel.setText(blob.getLabel()); + holder.blobId.setText(getString(R.string.blob_id_text, blob.getId())); + holder.blobExpiry.setText(getString(R.string.blob_expires_text, + SharedDataUtils.formatTime(blob.getExpiryTimeMs()))); + return convertView; + } + } +} diff --git a/src/com/android/settings/development/storage/BlobInfoViewHolder.java b/src/com/android/settings/development/storage/BlobInfoViewHolder.java new file mode 100644 index 00000000000..de8c9a95a4b --- /dev/null +++ b/src/com/android/settings/development/storage/BlobInfoViewHolder.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.development.storage; + +import android.view.LayoutInflater; +import android.view.View; +import android.widget.TextView; + +import com.android.settings.R; + +/** + * View holder for {@link BlobInfoListView}. + */ +class BlobInfoViewHolder { + View rootView; + TextView blobLabel; + TextView blobId; + TextView blobExpiry; + + static BlobInfoViewHolder createOrRecycle(LayoutInflater inflater, View convertView) { + if (convertView != null) { + return (BlobInfoViewHolder) convertView.getTag(); + } + convertView = inflater.inflate(R.layout.blob_list_item_view, null); + + final BlobInfoViewHolder holder = new BlobInfoViewHolder(); + holder.rootView = convertView; + holder.blobLabel = convertView.findViewById(R.id.blob_label); + holder.blobId = convertView.findViewById(R.id.blob_id); + holder.blobExpiry = convertView.findViewById(R.id.blob_expiry); + convertView.setTag(holder); + return holder; + } +} diff --git a/src/com/android/settings/development/storage/LeaseInfoListView.java b/src/com/android/settings/development/storage/LeaseInfoListView.java new file mode 100644 index 00000000000..b9a30427397 --- /dev/null +++ b/src/com/android/settings/development/storage/LeaseInfoListView.java @@ -0,0 +1,167 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.development.storage; + +import android.app.ListActivity; +import android.app.blob.BlobInfo; +import android.app.blob.BlobStoreManager; +import android.app.blob.LeaseInfo; +import android.content.Context; +import android.content.DialogInterface; +import android.content.res.Resources; +import android.graphics.Typeface; +import android.os.Bundle; +import android.text.TextUtils; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewGroup.LayoutParams; +import android.widget.ArrayAdapter; +import android.widget.Button; +import android.widget.LinearLayout; +import android.widget.TextView; + +import androidx.appcompat.app.AlertDialog; + +import com.android.internal.util.CollectionUtils; +import com.android.settings.R; + +import java.io.IOException; +import java.util.List; + +// TODO: have this class extend DashboardFragment for consistency +public class LeaseInfoListView extends ListActivity { + private static final String TAG = "LeaseInfoListView"; + + private Context mContext; + private BlobStoreManager mBlobStoreManager; + private BlobInfo mBlobInfo; + private LeaseListAdapter mAdapter; + private LayoutInflater mInflater; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mContext = this; + mBlobStoreManager = (BlobStoreManager) getSystemService(BlobStoreManager.class); + mInflater = (LayoutInflater) getSystemService(LayoutInflater.class); + + mBlobInfo = getIntent().getParcelableExtra(SharedDataUtils.BLOB_KEY); + + mAdapter = new LeaseListAdapter(this); + if (mAdapter.isEmpty()) { + // this should never happen since we're checking the size in BlobInfoListView + Log.e(TAG, "Error fetching leases for shared data: " + mBlobInfo.toString()); + finish(); + } + + setListAdapter(mAdapter); + getListView().addHeaderView(getHeaderView()); + getListView().addFooterView(getFooterView()); + getListView().setClickable(false); + } + + private LinearLayout getHeaderView() { + final LinearLayout headerView = (LinearLayout) mInflater.inflate( + R.layout.blob_list_item_view , null); + final TextView blobLabel = headerView.findViewById(R.id.blob_label); + final TextView blobId = headerView.findViewById(R.id.blob_id); + final TextView blobExpiry = headerView.findViewById(R.id.blob_expiry); + + blobLabel.setText(mBlobInfo.getLabel()); + blobLabel.setTypeface(Typeface.DEFAULT_BOLD); + blobId.setText(getString(R.string.blob_id_text, mBlobInfo.getId())); + blobExpiry.setVisibility(View.GONE); + return headerView; + } + + private Button getFooterView() { + final Button deleteButton = new Button(this); + deleteButton.setLayoutParams( + new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); + deleteButton.setText(R.string.delete_blob_text); + deleteButton.setOnClickListener(getButtonOnClickListener()); + return deleteButton; + } + + private View.OnClickListener getButtonOnClickListener() { + return v -> { + final AlertDialog dialog = new AlertDialog.Builder(mContext) + .setMessage(R.string.delete_blob_confirmation_text) + .setPositiveButton(android.R.string.ok, getDialogOnClickListener()) + .setNegativeButton(android.R.string.cancel, null) + .create(); + dialog.show(); + }; + } + + private DialogInterface.OnClickListener getDialogOnClickListener() { + return (dialog, which) -> { + try { + mBlobStoreManager.deleteBlob(mBlobInfo); + setResult(SharedDataUtils.LEASE_VIEW_RESULT_CODE_SUCCESS); + } catch (IOException e) { + Log.e(TAG, "Unable to delete blob: " + e.getMessage()); + setResult(SharedDataUtils.LEASE_VIEW_RESULT_CODE_FAILURE); + } + finish(); + }; + } + + private class LeaseListAdapter extends ArrayAdapter { + LeaseListAdapter(Context context) { + super(context, 0); + + final List leases = mBlobInfo.getLeases(); + if (CollectionUtils.isEmpty(leases)) { + return; + } + addAll(leases); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + final LeaseInfoViewHolder holder = LeaseInfoViewHolder.createOrRecycle( + mInflater, convertView); + convertView = holder.rootView; + + final LeaseInfo lease = getItem(position); + holder.leasePackageName.setText(lease.getPackageName()); + holder.leaseDescription.setText(getDescriptionString(lease)); + holder.leaseExpiry.setText(getString(R.string.accessor_expires_text, + SharedDataUtils.formatTime(lease.getExpiryTimeMillis()))); + return convertView; + } + + private String getDescriptionString(LeaseInfo lease) { + String description = null; + try { + description = getString(lease.getDescriptionResId()); + } catch (Resources.NotFoundException ignored) { + if (lease.getDescription() != null) { + description = lease.getDescription().toString(); + } + } finally { + if (TextUtils.isEmpty(description)) { + description = getString(R.string.accessor_no_description_text); + } + } + return description; + } + } +} diff --git a/src/com/android/settings/development/storage/LeaseInfoViewHolder.java b/src/com/android/settings/development/storage/LeaseInfoViewHolder.java new file mode 100644 index 00000000000..d74c92994d7 --- /dev/null +++ b/src/com/android/settings/development/storage/LeaseInfoViewHolder.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.development.storage; + +import android.view.LayoutInflater; +import android.view.View; +import android.widget.TextView; + +import com.android.settings.R; + +/** + * View holder for {@link LeaseInfoListView}. + */ +class LeaseInfoViewHolder { + View rootView; + TextView leasePackageName; + TextView leaseDescription; + TextView leaseExpiry; + + static LeaseInfoViewHolder createOrRecycle(LayoutInflater inflater, View convertView) { + if (convertView != null) { + return (LeaseInfoViewHolder) convertView.getTag(); + } + convertView = inflater.inflate(R.layout.lease_list_item_view, null); + + final LeaseInfoViewHolder holder = new LeaseInfoViewHolder(); + holder.rootView = convertView; + holder.leasePackageName = convertView.findViewById(R.id.lease_package); + holder.leaseDescription = convertView.findViewById(R.id.lease_desc); + holder.leaseExpiry = convertView.findViewById(R.id.lease_expiry); + convertView.setTag(holder); + return holder; + } +} diff --git a/src/com/android/settings/development/storage/SharedDataPreferenceController.java b/src/com/android/settings/development/storage/SharedDataPreferenceController.java new file mode 100644 index 00000000000..1d5c3e40168 --- /dev/null +++ b/src/com/android/settings/development/storage/SharedDataPreferenceController.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.development.storage; + +import android.app.blob.BlobStoreManager; +import android.content.Context; + +import androidx.preference.Preference; + +import com.android.settingslib.development.DeveloperOptionsPreferenceController; + +public class SharedDataPreferenceController extends DeveloperOptionsPreferenceController { + + private static final String SHARED_DATA = "shared_data"; + + private BlobStoreManager mBlobStoreManager; + + public SharedDataPreferenceController(Context context) { + super(context); + mBlobStoreManager = (BlobStoreManager) context.getSystemService(BlobStoreManager.class); + } + + @Override + public String getPreferenceKey() { + return SHARED_DATA; + } + + @Override + public boolean isAvailable() { + return mBlobStoreManager != null; + } + + @Override + public void updateState(Preference preference) { + preference.setEnabled(mBlobStoreManager != null); + // TODO: update summary to indicate why this preference isn't available + } +} diff --git a/src/com/android/settings/development/storage/SharedDataUtils.java b/src/com/android/settings/development/storage/SharedDataUtils.java new file mode 100644 index 00000000000..2f48f6d41fa --- /dev/null +++ b/src/com/android/settings/development/storage/SharedDataUtils.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.development.storage; + +import android.icu.text.SimpleDateFormat; +import android.icu.util.Calendar; +import android.icu.util.TimeZone; + +import java.util.Locale; + +class SharedDataUtils { + static final String BLOB_KEY = "BLOB_KEY"; + + static final int LEASE_VIEW_REQUEST_CODE = 8108; + static final int LEASE_VIEW_RESULT_CODE_SUCCESS = 1; + static final int LEASE_VIEW_RESULT_CODE_FAILURE = -1; + + private static final String BLOB_EXPIRY_PATTERN = "MMM dd, yyyy HH:mm:ss z"; + + private static final SimpleDateFormat FORMATTER = new SimpleDateFormat(BLOB_EXPIRY_PATTERN); + private static final Calendar CALENDAR = Calendar.getInstance( + TimeZone.getDefault(), Locale.getDefault()); + + static String formatTime(long millis) { + CALENDAR.setTimeInMillis(millis); + return FORMATTER.format(CALENDAR.getTime()); + } +} diff --git a/src/com/android/settings/notification/zen/ZenModePeopleSettings.java b/src/com/android/settings/notification/zen/ZenModePeopleSettings.java index 4971e544b26..962badbd05e 100644 --- a/src/com/android/settings/notification/zen/ZenModePeopleSettings.java +++ b/src/com/android/settings/notification/zen/ZenModePeopleSettings.java @@ -25,6 +25,7 @@ import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import com.android.settings.R; +import com.android.settings.notification.NotificationBackend; import com.android.settings.search.BaseSearchIndexProvider; import com.android.settingslib.core.AbstractPreferenceController; import com.android.settingslib.core.lifecycle.Lifecycle; @@ -47,12 +48,16 @@ public class ZenModePeopleSettings extends ZenModeSettingsBase implements Indexa app = null; } return buildPreferenceControllers( - context, getSettingsLifecycle(), app, this, getFragmentManager()); + context, getSettingsLifecycle(), app, this, getFragmentManager(), + new NotificationBackend()); } private static List buildPreferenceControllers(Context context, - Lifecycle lifecycle, Application app, Fragment host, FragmentManager fragmentManager) { + Lifecycle lifecycle, Application app, Fragment host, FragmentManager fragmentManager, + NotificationBackend notificationBackend) { List controllers = new ArrayList<>(); + controllers.add(new ZenModeConversationsImagePreferenceController(context, + "zen_mode_conversations_image", lifecycle, notificationBackend)); controllers.add(new ZenModeConversationsPreferenceController(context, "zen_mode_conversations", lifecycle)); controllers.add(new ZenModeCallsPreferenceController(context, lifecycle, @@ -83,7 +88,8 @@ public class ZenModePeopleSettings extends ZenModeSettingsBase implements Indexa @Override public List createPreferenceControllers( Context context) { - return buildPreferenceControllers(context, null, null, null, null); + return buildPreferenceControllers(context, null, null, null, + null, null); } }; } diff --git a/src/com/android/settings/wifi/WifiSettings2.java b/src/com/android/settings/wifi/WifiSettings2.java index f73891dfb53..1098c013cdc 100644 --- a/src/com/android/settings/wifi/WifiSettings2.java +++ b/src/com/android/settings/wifi/WifiSettings2.java @@ -538,7 +538,7 @@ public class WifiSettings2 extends RestrictedSettingsFragment final WifiEntry selectedEntry = ((LongPressWifiEntryPreference) preference).getWifiEntry(); - if (selectedEntry.isSaved()) { + if (selectedEntry.getWifiConfiguration() != null) { if (!selectedEntry.getWifiConfiguration().getNetworkSelectionStatus() .hasEverConnected()) { launchConfigNewNetworkFragment(selectedEntry); diff --git a/tests/robotests/src/com/android/settings/development/storage/SharedDataPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/development/storage/SharedDataPreferenceControllerTest.java new file mode 100644 index 00000000000..86bb4a70883 --- /dev/null +++ b/tests/robotests/src/com/android/settings/development/storage/SharedDataPreferenceControllerTest.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.development.storage; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.blob.BlobStoreManager; +import android.content.Context; + +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; + +import com.android.settings.R; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.util.ReflectionHelpers; + +// TODO: add more detailed tests for the shared data screens +@RunWith(RobolectricTestRunner.class) +public class SharedDataPreferenceControllerTest { + + @Mock + private Context mContext; + @Mock + private BlobStoreManager mBlobStoreManager; + @Mock + private Preference mPreference; + @Mock + private PreferenceScreen mScreen; + + private SharedDataPreferenceController mController; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mController = spy(new SharedDataPreferenceController(mContext)); + + when(mScreen.findPreference(mController.getPreferenceKey())).thenReturn(mPreference); + mController.displayPreference(mScreen); + } + + @Test + public void updateState_BlobManagerIsNotNull_preferenceIsEnabled() { + ReflectionHelpers.setField(mController, "mBlobStoreManager", mBlobStoreManager); + mController.updateState(mPreference); + + verify(mPreference).setEnabled(true); + assertThat(mPreference.getSummary()) + .isEqualTo(mContext.getString(R.string.shared_data_summary)); + } + + @Test + public void updateState_BlobManagerIsNull_preferenceIsDisabled() { + ReflectionHelpers.setField(mController, "mBlobStoreManager", null); + mController.updateState(mPreference); + + verify(mPreference).setEnabled(false); + } +} diff --git a/tests/robotests/src/com/android/settings/support/SupportDashboardActivityTest.java b/tests/robotests/src/com/android/settings/support/SupportDashboardActivityTest.java index 8a7d1b510ad..0124551740e 100644 --- a/tests/robotests/src/com/android/settings/support/SupportDashboardActivityTest.java +++ b/tests/robotests/src/com/android/settings/support/SupportDashboardActivityTest.java @@ -55,7 +55,8 @@ public class SupportDashboardActivityTest { final SearchIndexableRaw value = indexables.get(0); assertThat(value.title).isEqualTo(mContext.getString(R.string.page_tab_title_support)); - assertThat(value.screenTitle).isEqualTo(mContext.getString(R.string.settings_label)); + assertThat(value.screenTitle).isEqualTo( + mContext.getString(R.string.page_tab_title_support)); assertThat(value.intentTargetPackage).isEqualTo(mContext.getPackageName()); assertThat(value.intentTargetClass).isEqualTo(SupportDashboardActivity.class.getName()); assertThat(value.intentAction).isEqualTo(Intent.ACTION_MAIN); @@ -67,7 +68,7 @@ public class SupportDashboardActivityTest { // Intent action used by setup wizard to start support settings Intent intent = new Intent("com.android.settings.action.SUPPORT_SETTINGS"); ResolveInfo resolveInfo = - packageManager.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY); + packageManager.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY); assertThat(resolveInfo).isNotNull(); assertThat(resolveInfo.activityInfo.name) .isEqualTo(SupportDashboardActivity.class.getName());