From 14c4b41f789541628070f3a2d23207239d4f7ae8 Mon Sep 17 00:00:00 2001 From: Chien-Yu Chen Date: Wed, 26 Jul 2017 13:39:34 -0700 Subject: [PATCH 01/14] Settings: Enable HAL HDR+ by default Test: Settings app Bug: 63045786 Change-Id: I39ed40a3a74bac13a40c556b9cb60b495955a065 --- .../development/CameraHalHdrplusPreferenceController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/com/android/settings/development/CameraHalHdrplusPreferenceController.java b/src/com/android/settings/development/CameraHalHdrplusPreferenceController.java index e8e2c2d8da1..a4f087a97f2 100644 --- a/src/com/android/settings/development/CameraHalHdrplusPreferenceController.java +++ b/src/com/android/settings/development/CameraHalHdrplusPreferenceController.java @@ -102,6 +102,6 @@ public class CameraHalHdrplusPreferenceController extends AbstractPreferenceCont } private boolean isHalHdrplusEnabled() { - return SystemProperties.getBoolean(PROPERTY_CAMERA_HAL_HDRPLUS, false); + return SystemProperties.getBoolean(PROPERTY_CAMERA_HAL_HDRPLUS, true); } } From c3570914f6e5c6ae61eda876c4e6cdefd9618ca6 Mon Sep 17 00:00:00 2001 From: Alison Cichowlas Date: Thu, 24 Aug 2017 14:57:28 -0400 Subject: [PATCH 02/14] Legacy channels say "allow sound" instead of "let the app decide". We had this string already translated from use in previous ux revs. Change-Id: I91a9ce001a0d875c0ae12643e6b386328e9cd8de Fixes: 62873496 Test: String change, manually verified --- res/values/strings.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/res/values/strings.xml b/res/values/strings.xml index a154135d7fa..b53dbde40c6 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -6833,8 +6833,8 @@ Importance - - Let the app decide + + Allow sound Never show notifications From 94c52029653426846c50c639e7f6b5404cedd472 Mon Sep 17 00:00:00 2001 From: Phil Weaver Date: Fri, 18 Aug 2017 18:08:12 -0700 Subject: [PATCH 03/14] Backport overlay security fix Replacing app opps fix with the flag that is used elsewhere. Bug: 37442941 Test: Verified that toast and system overlay still goes away on permission and a11y service capability screens. Merged-In: I7c8d8b4143a5dd1cb684c31c4503608c8d5be0e3 Change-Id: I858f3585b2e7d334cddcf38bd0ac6481e778b6b6 --- AndroidManifest.xml | 1 + src/com/android/settings/Utils.java | 16 ---------------- .../AccessibilityServiceWarning.java | 8 ++++++++ .../ShortcutServicePickerFragment.java | 18 ------------------ ...AccessibilityServicePreferenceFragment.java | 10 ---------- 5 files changed, 9 insertions(+), 44 deletions(-) diff --git a/AndroidManifest.xml b/AndroidManifest.xml index c5961524520..25216db214f 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -82,6 +82,7 @@ + Date: Tue, 29 Aug 2017 12:14:54 -0700 Subject: [PATCH 04/14] Fix errorprone build Fixes: packages/apps/Settings/tests/robotests/src/com/android/settings/language/LanguageAndInputSettingsTest.java:95: error: [MockitoCast] A bug in Mockito will cause this test to fail at runtime with a ClassCastException when(mContext.getSystemService(AutofillManager.class)).thenReturn(mAutofillManager); ^ (see http://errorprone.info/bugpattern/MockitoCast) Did you mean 'when((Object) mContext.getSystemService(AutofillManager.class)).thenReturn(mAutofillManager);'? packages/apps/Settings/tests/robotests/src/com/android/settings/search/IntentSearchViewHolderTest.java:195: error: [JUnit4TestNotRun] Test method will not be run; please add @Test annotation public void testBindViewElements_appSearchResult() { ^ (see http://errorprone.info/bugpattern/JUnit4TestNotRun) Did you mean '@Test'? Bug: 64489631 Test: m -j RUN_ERROR_PRONE=true javac-check Merged-In: I79477f331ae447d2505a1519da09886bf07ba1a2 Merged-in: I333372699b263d02cc4083289dc746c7aacd414d Change-Id: I8fd30fc741927de3f6527aca6d98d8851ef23794 --- .../settings/language/LanguageAndInputSettingsTest.java | 3 ++- .../android/settings/search/IntentSearchViewHolderTest.java | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/robotests/src/com/android/settings/language/LanguageAndInputSettingsTest.java b/tests/robotests/src/com/android/settings/language/LanguageAndInputSettingsTest.java index 527b1cdd668..6f5b6b0daa5 100644 --- a/tests/robotests/src/com/android/settings/language/LanguageAndInputSettingsTest.java +++ b/tests/robotests/src/com/android/settings/language/LanguageAndInputSettingsTest.java @@ -92,7 +92,8 @@ public class LanguageAndInputSettingsTest { .thenReturn(mock(TextServicesManager.class)); when(mContext.getSystemService(Context.DEVICE_POLICY_SERVICE)).thenReturn(mDpm); when(mContext.getSystemService(Context.INPUT_METHOD_SERVICE)).thenReturn(mImm); - when(mContext.getSystemService(AutofillManager.class)).thenReturn(mAutofillManager); + when((Object) mContext.getSystemService(AutofillManager.class)) + .thenReturn(mAutofillManager); mFragment = new TestFragment(mContext); } diff --git a/tests/robotests/src/com/android/settings/search/IntentSearchViewHolderTest.java b/tests/robotests/src/com/android/settings/search/IntentSearchViewHolderTest.java index 39da7180a56..63eec1028ee 100644 --- a/tests/robotests/src/com/android/settings/search/IntentSearchViewHolderTest.java +++ b/tests/robotests/src/com/android/settings/search/IntentSearchViewHolderTest.java @@ -50,6 +50,7 @@ import com.android.settings.search2.SearchResult.Builder; import com.android.settings.testutils.FakeFeatureFactory; import org.junit.Before; +import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Answers; @@ -192,6 +193,8 @@ public class IntentSearchViewHolderTest { assertThat(mHolder.summaryView.getVisibility()).isEqualTo(View.GONE); } + @Test + @Ignore public void testBindViewElements_appSearchResult() { when(mPackageManager.getUserBadgedLabel(any(CharSequence.class), eq(new UserHandle(USER_ID)))).thenReturn(BADGED_LABEL); From 1568c56de9f1bc0623454b82c464096ee7706e86 Mon Sep 17 00:00:00 2001 From: Daniel Nishi Date: Mon, 21 Aug 2017 15:39:54 -0700 Subject: [PATCH 05/14] Tint the work icon. Bug: 64475406 Test: Manual Change-Id: Ibe817a9c099439db91aca1a858e5cf5562fa6a4a --- .../deviceinfo/storage/UserProfileController.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/com/android/settings/deviceinfo/storage/UserProfileController.java b/src/com/android/settings/deviceinfo/storage/UserProfileController.java index 684ac52352b..cf1e3603bbe 100644 --- a/src/com/android/settings/deviceinfo/storage/UserProfileController.java +++ b/src/com/android/settings/deviceinfo/storage/UserProfileController.java @@ -19,6 +19,7 @@ package com.android.settings.deviceinfo.storage; import android.content.Context; import android.content.Intent; import android.content.pm.UserInfo; +import android.content.res.TypedArray; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.os.storage.VolumeInfo; @@ -126,7 +127,14 @@ public class UserProfileController extends AbstractPreferenceController implemen public void handleUserIcons(SparseArray fetchedIcons) { Drawable userIcon = fetchedIcons.get(mUser.id); if (userIcon != null) { - mStoragePreference.setIcon(userIcon); + mStoragePreference.setIcon(applyTint(mContext, userIcon)); } } + + private static Drawable applyTint(Context context, Drawable icon) { + icon = icon.mutate(); + icon.setTint(Utils.getColorAttr(context, android.R.attr.colorControlNormal)); + return icon; + } + } From 6108d6c22e1997b2c5295e5b7ec7228976b21229 Mon Sep 17 00:00:00 2001 From: Jack He Date: Wed, 30 Aug 2017 16:12:45 -0700 Subject: [PATCH 06/14] Bluetooth: do not dimiss fragment when Activity.finish() is called * When Activity.finish() is called, it's associtated fragments are all dismissed automatically * Cached used fragments are dimissed in onCreate() before new ones are created Bug: 62230203 Test: Pair with Bluetooth device, Settings unit tests Change-Id: Ieca88ba0660c5407f0d88d572d06a722c642ac39 --- .../android/settings/bluetooth/BluetoothPairingDialog.java | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/com/android/settings/bluetooth/BluetoothPairingDialog.java b/src/com/android/settings/bluetooth/BluetoothPairingDialog.java index 97382c3c480..22cb3a683ec 100644 --- a/src/com/android/settings/bluetooth/BluetoothPairingDialog.java +++ b/src/com/android/settings/bluetooth/BluetoothPairingDialog.java @@ -108,12 +108,6 @@ public class BluetoothPairingDialog extends Activity { @VisibleForTesting void dismiss() { if (!isFinishing()) { - BluetoothPairingDialogFragment bluetoothFragment = - (BluetoothPairingDialogFragment) getFragmentManager() - .findFragmentByTag(FRAGMENT_TAG); - if (bluetoothFragment != null) { - bluetoothFragment.dismiss(); - } finish(); } } From 0c3f4bce57ea8d846aa6a3e6d2769c221492d15f Mon Sep 17 00:00:00 2001 From: Julia Reynolds Date: Fri, 25 Aug 2017 09:40:24 -0400 Subject: [PATCH 07/14] Add settings page for notification channel groups Bug: 63927402 Test: tests/unit/src/com/android/settings/notification/.* Change-Id: Iebf7d8ba54f0cf5801a42f3161354d3cc5e5c848 --- AndroidManifest.xml | 16 ++ res/values/strings.xml | 12 ++ ...upgraded_channel_notification_settings.xml | 1 + src/com/android/settings/Settings.java | 1 + .../core/gateway/SettingsGateway.java | 2 + .../notification/AppNotificationSettings.java | 148 +++++--------- .../ChannelGroupNotificationSettings.java | 188 ++++++++++++++++++ .../ChannelNotificationSettings.java | 29 +-- .../notification/NotificationBackend.java | 26 ++- .../NotificationSettingsBase.java | 100 +++++++++- .../grandfather_not_implementing_indexable | 1 + .../AppNotificationSettingsTest.java | 115 ++++++++++- .../ChannelGroupNotificationSettingsTest.java | 133 +++++++++++++ .../ChannelNotificationSettingsTest.java | 106 ++++++++++ 14 files changed, 751 insertions(+), 127 deletions(-) create mode 100644 src/com/android/settings/notification/ChannelGroupNotificationSettings.java create mode 100644 tests/unit/src/com/android/settings/notification/ChannelGroupNotificationSettingsTest.java create mode 100644 tests/unit/src/com/android/settings/notification/ChannelNotificationSettingsTest.java diff --git a/AndroidManifest.xml b/AndroidManifest.xml index e3decb0f1e3..7702257c060 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -2700,6 +2700,22 @@ android:value="com.android.settings.notification.AppNotificationSettings" /> + + + + + + + + + + + + + + diff --git a/res/values/strings.xml b/res/values/strings.xml index ec830536a63..0edb017fd1c 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -6835,6 +6835,9 @@ Notification category + + Notification category group + Importance @@ -7002,12 +7005,21 @@ Android is blocking this category of notifications from appearing on this device + + Android is blocking this group of notifications from appearing on this device + Categories Other + + + %d category + %d categories + + This app has not posted any notifications diff --git a/res/xml/upgraded_channel_notification_settings.xml b/res/xml/upgraded_channel_notification_settings.xml index 2cece9e62ff..ee23435576b 100644 --- a/res/xml/upgraded_channel_notification_settings.xml +++ b/res/xml/upgraded_channel_notification_settings.xml @@ -38,6 +38,7 @@ settings:useAdditionalSummary="true" /> diff --git a/src/com/android/settings/Settings.java b/src/com/android/settings/Settings.java index a6961726550..ecdf4ba2287 100644 --- a/src/com/android/settings/Settings.java +++ b/src/com/android/settings/Settings.java @@ -124,6 +124,7 @@ public class Settings extends SettingsActivity { public static class NotificationAppListActivity extends SettingsActivity { /* empty */ } public static class AppNotificationSettingsActivity extends SettingsActivity { /* empty */ } public static class ChannelNotificationSettingsActivity extends SettingsActivity { /* empty */ } + public static class ChannelGroupNotificationSettingsActivity extends SettingsActivity { /* empty */ } public static class ManageDomainUrlsActivity extends SettingsActivity { /* empty */ } public static class AutomaticStorageManagerSettingsActivity extends SettingsActivity { /* empty */ } public static class GamesStorageActivity extends SettingsActivity { /* empty */ } diff --git a/src/com/android/settings/core/gateway/SettingsGateway.java b/src/com/android/settings/core/gateway/SettingsGateway.java index 380c0704a7a..a03314ca2e2 100644 --- a/src/com/android/settings/core/gateway/SettingsGateway.java +++ b/src/com/android/settings/core/gateway/SettingsGateway.java @@ -100,6 +100,7 @@ import com.android.settings.nfc.AndroidBeam; import com.android.settings.nfc.PaymentSettings; import com.android.settings.notification.AppNotificationSettings; import com.android.settings.notification.ChannelNotificationSettings; +import com.android.settings.notification.ChannelGroupNotificationSettings; import com.android.settings.notification.ConfigureNotificationSettings; import com.android.settings.notification.NotificationAccessSettings; import com.android.settings.notification.NotificationStation; @@ -209,6 +210,7 @@ public class SettingsGateway { BatterySaverSettings.class.getName(), AppNotificationSettings.class.getName(), ChannelNotificationSettings.class.getName(), + ChannelGroupNotificationSettings.class.getName(), ApnSettings.class.getName(), ApnEditor.class.getName(), WifiCallingSettings.class.getName(), diff --git a/src/com/android/settings/notification/AppNotificationSettings.java b/src/com/android/settings/notification/AppNotificationSettings.java index 78a0a74999f..29eb4a3098e 100644 --- a/src/com/android/settings/notification/AppNotificationSettings.java +++ b/src/com/android/settings/notification/AppNotificationSettings.java @@ -26,6 +26,7 @@ import android.os.Bundle; import android.provider.Settings; import android.support.v7.preference.Preference; import android.support.v7.preference.PreferenceCategory; +import android.support.v7.preference.PreferenceGroup; import android.text.TextUtils; import android.util.ArrayMap; import android.util.Log; @@ -50,7 +51,6 @@ import java.util.Collections; import java.util.Comparator; import java.util.List; -import static android.app.NotificationManager.IMPORTANCE_LOW; import static android.app.NotificationManager.IMPORTANCE_NONE; import static android.app.NotificationManager.IMPORTANCE_UNSPECIFIED; @@ -105,7 +105,7 @@ public class AppNotificationSettings extends NotificationSettingsBase { new AsyncTask() { @Override protected Void doInBackground(Void... unused) { - mChannelGroupList = mBackend.getChannelGroups(mPkg, mUid).getList(); + mChannelGroupList = mBackend.getGroups(mPkg, mUid).getList(); Collections.sort(mChannelGroupList, mChannelGroupComparator); return null; } @@ -115,7 +115,7 @@ public class AppNotificationSettings extends NotificationSettingsBase { if (getHost() == null) { return; } - populateChannelList(); + populateList(); addAppLinkPref(); } }.execute(); @@ -144,7 +144,7 @@ public class AppNotificationSettings extends NotificationSettingsBase { getPreferenceScreen().addPreference(pref); } - private void populateChannelList() { + private void populateList() { if (!mChannelGroups.isEmpty()) { // If there's anything in mChannelGroups, we've called populateChannelList twice. // Clear out existing channels and log. @@ -166,30 +166,7 @@ public class AppNotificationSettings extends NotificationSettingsBase { empty.setEnabled(false); groupCategory.addPreference(empty); } else { - for (NotificationChannelGroup group : mChannelGroupList) { - PreferenceCategory groupCategory = new PreferenceCategory(getPrefContext()); - if (group.getId() == null) { - groupCategory.setTitle(mChannelGroupList.size() > 1 - ? R.string.notification_channels_other - : R.string.notification_channels); - groupCategory.setKey(KEY_GENERAL_CATEGORY); - } else { - groupCategory.setTitle(group.getName()); - groupCategory.setKey(group.getId()); - } - groupCategory.setOrderingAsAdded(true); - getPreferenceScreen().addPreference(groupCategory); - mChannelGroups.add(groupCategory); - - final List channels = group.getChannels(); - Collections.sort(channels, mChannelComparator); - int N = channels.size(); - for (int i = 0; i < N; i++) { - final NotificationChannel channel = channels.get(i); - populateSingleChannelPrefs(groupCategory, channel); - } - } - + populateGroupList(); int deletedChannelCount = mBackend.getDeletedChannelCount(mAppRow.pkg, mAppRow.uid); if (deletedChannelCount > 0) { mDeletedChannels = new FooterPreference(getPrefContext()); @@ -202,48 +179,63 @@ public class AppNotificationSettings extends NotificationSettingsBase { getPreferenceScreen().addPreference(mDeletedChannels); } } - updateDependents(mAppRow.banned); } - private void populateSingleChannelPrefs(PreferenceCategory groupCategory, - final NotificationChannel channel) { - MasterSwitchPreference channelPref = new MasterSwitchPreference( + private void populateGroupList() { + PreferenceCategory groupCategory = new PreferenceCategory(getPrefContext()); + groupCategory.setTitle(R.string.notification_channels); + groupCategory.setKey(KEY_GENERAL_CATEGORY); + groupCategory.setOrderingAsAdded(true); + getPreferenceScreen().addPreference(groupCategory); + mChannelGroups.add(groupCategory); + for (NotificationChannelGroup group : mChannelGroupList) { + final List channels = group.getChannels(); + int N = channels.size(); + // app defined groups with one channel and channels with no group display the channel + // name and no summary and link directly to the channel page unless the group is blocked + if ((group.getId() == null || N < 2) && !group.isBlocked()) { + Collections.sort(channels, mChannelComparator); + for (int i = 0; i < N; i++) { + final NotificationChannel channel = channels.get(i); + populateSingleChannelPrefs(groupCategory, channel, ""); + } + } else { + populateGroupPreference(groupCategory, group, N); + } + } + } + + void populateGroupPreference(PreferenceGroup parent, + final NotificationChannelGroup group, int channelCount) { + MasterSwitchPreference groupPref = new MasterSwitchPreference( getPrefContext()); - channelPref.setSwitchEnabled(mSuspendedAppsAdmin == null - && isChannelBlockable(mAppRow.systemApp, channel) - && isChannelConfigurable(channel)); - channelPref.setKey(channel.getId()); - channelPref.setTitle(channel.getName()); - channelPref.setChecked(channel.getImportance() != IMPORTANCE_NONE); - channelPref.setSummary(getImportanceSummary(channel)); - Bundle channelArgs = new Bundle(); - channelArgs.putInt(AppInfoBase.ARG_PACKAGE_UID, mUid); - channelArgs.putString(AppInfoBase.ARG_PACKAGE_NAME, mPkg); - channelArgs.putString(Settings.EXTRA_CHANNEL_ID, channel.getId()); - Intent channelIntent = Utils.onBuildStartFragmentIntent(getActivity(), - ChannelNotificationSettings.class.getName(), - channelArgs, null, R.string.notification_channel_title, null, false, + groupPref.setSwitchEnabled(mSuspendedAppsAdmin == null + && isChannelGroupBlockable(group)); + groupPref.setKey(group.getId()); + groupPref.setTitle(group.getName()); + groupPref.setChecked(!group.isBlocked()); + groupPref.setSummary(getResources().getQuantityString( + R.plurals.notification_group_summary, channelCount, channelCount)); + Bundle groupArgs = new Bundle(); + groupArgs.putInt(AppInfoBase.ARG_PACKAGE_UID, mUid); + groupArgs.putString(AppInfoBase.ARG_PACKAGE_NAME, mPkg); + groupArgs.putString(Settings.EXTRA_CHANNEL_GROUP_ID, group.getId()); + Intent groupIntent = Utils.onBuildStartFragmentIntent(getActivity(), + ChannelGroupNotificationSettings.class.getName(), + groupArgs, null, R.string.notification_group_title, null, false, getMetricsCategory()); - channelPref.setIntent(channelIntent); + groupPref.setIntent(groupIntent); - channelPref.setOnPreferenceChangeListener( - new Preference.OnPreferenceChangeListener() { - @Override - public boolean onPreferenceChange(Preference preference, - Object o) { - boolean value = (Boolean) o; - int importance = value ? IMPORTANCE_LOW : IMPORTANCE_NONE; - channel.setImportance(importance); - channel.lockFields( - NotificationChannel.USER_LOCKED_IMPORTANCE); - channelPref.setSummary(getImportanceSummary(channel)); - mBackend.updateChannel(mPkg, mUid, channel); + groupPref.setOnPreferenceChangeListener( + (preference, o) -> { + boolean value = (Boolean) o; + group.setBlocked(!value); + mBackend.updateChannelGroup(mPkg, mUid, group); - return true; - } + return true; }); - groupCategory.addPreference(channelPref); + parent.addPreference(groupPref); } void setupBadge() { @@ -330,38 +322,6 @@ public class AppNotificationSettings extends NotificationSettingsBase { } } - private String getImportanceSummary(NotificationChannel channel) { - switch (channel.getImportance()) { - case NotificationManager.IMPORTANCE_UNSPECIFIED: - return getContext().getString(R.string.notification_importance_unspecified); - case NotificationManager.IMPORTANCE_NONE: - return getContext().getString(R.string.notification_toggle_off); - case NotificationManager.IMPORTANCE_MIN: - return getContext().getString(R.string.notification_importance_min); - case NotificationManager.IMPORTANCE_LOW: - return getContext().getString(R.string.notification_importance_low); - case NotificationManager.IMPORTANCE_DEFAULT: - return getContext().getString(R.string.notification_importance_default); - case NotificationManager.IMPORTANCE_HIGH: - case NotificationManager.IMPORTANCE_MAX: - default: - return getContext().getString(R.string.notification_importance_high); - } - - } - - private Comparator mChannelComparator = - new Comparator() { - - @Override - public int compare(NotificationChannel left, NotificationChannel right) { - if (left.isDeleted() != right.isDeleted()) { - return Boolean.compare(left.isDeleted(), right.isDeleted()); - } - return left.getId().compareTo(right.getId()); - } - }; - private Comparator mChannelGroupComparator = new Comparator() { diff --git a/src/com/android/settings/notification/ChannelGroupNotificationSettings.java b/src/com/android/settings/notification/ChannelGroupNotificationSettings.java new file mode 100644 index 00000000000..7837ec866df --- /dev/null +++ b/src/com/android/settings/notification/ChannelGroupNotificationSettings.java @@ -0,0 +1,188 @@ +/* + * Copyright (C) 2017 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.notification; + +import android.app.Activity; +import android.app.NotificationChannel; +import android.support.v7.preference.Preference; +import android.text.TextUtils; +import android.util.ArrayMap; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; + +import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import com.android.settings.R; +import com.android.settings.applications.LayoutPreference; +import com.android.settings.widget.EntityHeaderController; +import com.android.settingslib.widget.FooterPreference; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class ChannelGroupNotificationSettings extends NotificationSettingsBase { + private static final String TAG = "ChannelGroupSettings"; + + private static String KEY_DELETED = "deleted"; + + private EntityHeaderController mHeaderPref; + private List mChannels = new ArrayList(); + private FooterPreference mDeletedChannels; + + @Override + public int getMetricsCategory() { + return MetricsEvent.NOTIFICATION_CHANNEL_GROUP; + } + + @Override + public void onResume() { + super.onResume(); + if (mUid < 0 || TextUtils.isEmpty(mPkg) || mPkgInfo == null || mChannelGroup == null) { + Log.w(TAG, "Missing package or uid or packageinfo or group"); + finish(); + return; + } + + if (getPreferenceScreen() != null) { + getPreferenceScreen().removeAll(); + } + addPreferencesFromResource(R.xml.notification_settings); + setupBlock(); + addHeaderPref(); + addAppLinkPref(); + addFooterPref(); + populateChannelList(); + + updateDependents(mChannelGroup.isBlocked()); + } + + @Override + void setupBadge() { + + } + + private void populateChannelList() { + if (!mChannels.isEmpty()) { + // If there's anything in mChannels, we've called populateChannelList twice. + // Clear out existing channels and log. + Log.w(TAG, "Notification channel group posted twice to settings - old size " + + mChannels.size() + ", new size " + mChannels.size()); + for (Preference p : mChannels) { + getPreferenceScreen().removePreference(p); + } + } + if (mChannelGroup.getChannels().isEmpty()) { + Preference empty = new Preference(getPrefContext()); + empty.setTitle(R.string.no_channels); + empty.setEnabled(false); + getPreferenceScreen().addPreference(empty); + mChannels.add(empty); + + } else { + final List channels = mChannelGroup.getChannels(); + Collections.sort(channels, mChannelComparator); + for (NotificationChannel channel : channels) { + mChannels.add(populateSingleChannelPrefs( + getPreferenceScreen(), channel, getImportanceSummary(channel))); + } + + int deletedChannelCount = mBackend.getDeletedChannelCount(mAppRow.pkg, mAppRow.uid); + if (deletedChannelCount > 0) { + mDeletedChannels = new FooterPreference(getPrefContext()); + mDeletedChannels.setSelectable(false); + mDeletedChannels.setTitle(getResources().getQuantityString( + R.plurals.deleted_channels, deletedChannelCount, deletedChannelCount)); + mDeletedChannels.setEnabled(false); + mDeletedChannels.setKey(KEY_DELETED); + mDeletedChannels.setOrder(ORDER_LAST); + getPreferenceScreen().addPreference(mDeletedChannels); + mChannels.add(mDeletedChannels); + } + } + + updateDependents(mAppRow.banned); + } + + private void addHeaderPref() { + ArrayMap rows = new ArrayMap<>(); + rows.put(mAppRow.pkg, mAppRow); + collectConfigActivities(rows); + final Activity activity = getActivity(); + mHeaderPref = EntityHeaderController + .newInstance(activity, this /* fragment */, null /* header */) + .setRecyclerView(getListView(), getLifecycle()); + final Preference pref = mHeaderPref + .setIcon(mAppRow.icon) + .setLabel(mChannelGroup.getName()) + .setSummary(mAppRow.label) + .setPackageName(mAppRow.pkg) + .setUid(mAppRow.uid) + .setButtonActions(EntityHeaderController.ActionType.ACTION_NOTIF_PREFERENCE, + EntityHeaderController.ActionType.ACTION_NONE) + .setHasAppInfoLink(true) + .done(activity, getPrefContext()); + getPreferenceScreen().addPreference(pref); + } + + private void addFooterPref() { + if (!TextUtils.isEmpty(mChannelGroup.getDescription())) { + FooterPreference descPref = new FooterPreference(getPrefContext()); + descPref.setOrder(ORDER_LAST); + descPref.setSelectable(false); + descPref.setTitle(mChannelGroup.getDescription()); + getPreferenceScreen().addPreference(descPref); + mChannels.add(descPref); + } + } + + private void setupBlock() { + View switchBarContainer = LayoutInflater.from( + getPrefContext()).inflate(R.layout.styled_switch_bar, null); + mSwitchBar = switchBarContainer.findViewById(R.id.switch_bar); + mSwitchBar.show(); + mSwitchBar.setDisabledByAdmin(mSuspendedAppsAdmin); + mSwitchBar.setChecked(!mChannelGroup.isBlocked()); + mSwitchBar.addOnSwitchChangeListener((switchView, isChecked) -> { + mChannelGroup.setBlocked(!isChecked); + mBackend.updateChannelGroup(mPkg, mUid, mChannelGroup); + updateDependents(!isChecked); + }); + + mBlockBar = new LayoutPreference(getPrefContext(), switchBarContainer); + mBlockBar.setOrder(ORDER_FIRST); + mBlockBar.setKey(KEY_BLOCK); + getPreferenceScreen().addPreference(mBlockBar); + + if (!isChannelGroupBlockable(mChannelGroup)) { + setVisible(mBlockBar, false); + } + + setupBlockDesc(R.string.channel_group_notifications_off_desc); + } + + protected void updateDependents(boolean banned) { + for (Preference channel : mChannels) { + setVisible(channel, !banned); + } + if (mAppLink != null) { + setVisible(mAppLink, !banned); + } + setVisible(mBlockBar, isChannelGroupBlockable(mChannelGroup)); + setVisible(mBlockedDesc, mAppRow.banned || mChannelGroup.isBlocked()); + } +} diff --git a/src/com/android/settings/notification/ChannelNotificationSettings.java b/src/com/android/settings/notification/ChannelNotificationSettings.java index 2f95dd29f38..41f98014535 100644 --- a/src/com/android/settings/notification/ChannelNotificationSettings.java +++ b/src/com/android/settings/notification/ChannelNotificationSettings.java @@ -26,6 +26,7 @@ import android.os.Bundle; import android.os.AsyncTask; import android.provider.Settings; import android.support.v7.preference.Preference; +import android.support.v7.preference.PreferenceGroup; import android.text.TextUtils; import android.text.BidiFormatter; import android.text.SpannableStringBuilder; @@ -57,6 +58,7 @@ public class ChannelNotificationSettings extends NotificationSettingsBase { private static final String KEY_VIBRATE = "vibrate"; private static final String KEY_RINGTONE = "ringtone"; private static final String KEY_IMPORTANCE = "importance"; + private static final String KEY_ADVANCED = "advanced"; private Preference mImportance; private RestrictedSwitchPreference mLights; @@ -65,6 +67,7 @@ public class ChannelNotificationSettings extends NotificationSettingsBase { private FooterPreference mFooter; private NotificationChannelGroup mChannelGroup; private EntityHeaderController mHeaderPref; + private PreferenceGroup mAdvanced; @Override public int getMetricsCategory() { @@ -96,24 +99,10 @@ public class ChannelNotificationSettings extends NotificationSettingsBase { populateUpgradedChannelPrefs(); if (mChannel.getGroup() != null) { - // Go look up group name - new AsyncTask() { - @Override - protected Void doInBackground(Void... unused) { - if (mChannel.getGroup() != null) { - mChannelGroup = mBackend.getGroup(mChannel.getGroup(), mPkg, mUid); - } - return null; - } - - @Override - protected void onPostExecute(Void unused) { - if (getHost() == null || mChannelGroup == null) { - return; - } - setChannelGroupLabel(mChannelGroup.getName()); - } - }.execute(); + mChannelGroup = mBackend.getGroup(mPkg, mUid, mChannel.getGroup()); + if (mChannelGroup != null) { + setChannelGroupLabel(mChannelGroup.getName()); + } } } @@ -129,6 +118,7 @@ public class ChannelNotificationSettings extends NotificationSettingsBase { setupVibrate(); setupRingtone(); setupImportance(); + mAdvanced = (PreferenceGroup) findPreference(KEY_ADVANCED); } private void addHeaderPref() { @@ -272,7 +262,7 @@ public class ChannelNotificationSettings extends NotificationSettingsBase { mBlockBar.setKey(KEY_BLOCK); getPreferenceScreen().addPreference(mBlockBar); - if (!isChannelBlockable(mAppRow.systemApp, mChannel)) { + if (!isChannelBlockable(mChannel)) { setVisible(mBlockBar, false); } @@ -373,6 +363,7 @@ public class ChannelNotificationSettings extends NotificationSettingsBase { if (mShowLegacyChannelConfig) { setVisible(mImportanceToggle, checkCanBeVisible(NotificationManager.IMPORTANCE_MIN)); } else { + setVisible(mAdvanced, checkCanBeVisible(NotificationManager.IMPORTANCE_MIN)); setVisible(mImportance, checkCanBeVisible(NotificationManager.IMPORTANCE_MIN)); setVisible(mLights, checkCanBeVisible( NotificationManager.IMPORTANCE_DEFAULT) && canPulseLight()); diff --git a/src/com/android/settings/notification/NotificationBackend.java b/src/com/android/settings/notification/NotificationBackend.java index 82e3a9e13ef..4de528e0a03 100644 --- a/src/com/android/settings/notification/NotificationBackend.java +++ b/src/com/android/settings/notification/NotificationBackend.java @@ -136,8 +136,7 @@ public class NotificationBackend { } } - - public NotificationChannelGroup getGroup(String groupId, String pkg, int uid) { + public NotificationChannelGroup getGroup(String pkg, int uid, String groupId) { if (groupId == null) { return null; } @@ -149,7 +148,19 @@ public class NotificationBackend { } } - public ParceledListSlice getChannelGroups(String pkg, int uid) { + public NotificationChannelGroup getGroupWithChannels(String pkg, int uid, String groupId) { + if (groupId == null) { + return null; + } + try { + return sINM.getPopulatedNotificationChannelGroupForPackage(pkg, uid, groupId, true); + } catch (Exception e) { + Log.w(TAG, "Error calling NoMan", e); + return null; + } + } + + public ParceledListSlice getGroups(String pkg, int uid) { try { return sINM.getNotificationChannelGroupsForPackage(pkg, uid, false); } catch (Exception e) { @@ -166,6 +177,15 @@ public class NotificationBackend { } } + public void updateChannelGroup(String pkg, int uid, NotificationChannelGroup group) { + try { + sINM.updateNotificationChannelGroupForPackage(pkg, uid, group); + } catch (Exception e) { + Log.w(TAG, "Error calling NoMan", e); + } + } + + public int getDeletedChannelCount(String pkg, int uid) { try { return sINM.getDeletedChannelCount(pkg, uid); diff --git a/src/com/android/settings/notification/NotificationSettingsBase.java b/src/com/android/settings/notification/NotificationSettingsBase.java index 38498829764..8c70a20e966 100644 --- a/src/com/android/settings/notification/NotificationSettingsBase.java +++ b/src/com/android/settings/notification/NotificationSettingsBase.java @@ -24,8 +24,10 @@ import static android.app.NotificationManager.IMPORTANCE_UNSPECIFIED; import com.android.internal.widget.LockPatternUtils; import com.android.settings.R; import com.android.settings.SettingsPreferenceFragment; +import com.android.settings.Utils; import com.android.settings.applications.AppInfoBase; import com.android.settings.applications.LayoutPreference; +import com.android.settings.widget.MasterSwitchPreference; import com.android.settings.widget.SwitchBar; import com.android.settingslib.RestrictedLockUtils; import com.android.settingslib.RestrictedSwitchPreference; @@ -33,6 +35,7 @@ import com.android.settingslib.widget.FooterPreference; import android.app.Notification; import android.app.NotificationChannel; +import android.app.NotificationChannelGroup; import android.app.NotificationManager; import android.app.admin.DevicePolicyManager; import android.content.BroadcastReceiver; @@ -51,8 +54,8 @@ import android.os.UserHandle; import android.os.UserManager; import android.provider.Settings; import android.service.notification.NotificationListenerService; -import android.support.v7.preference.DropDownPreference; import android.support.v7.preference.Preference; +import android.support.v7.preference.PreferenceGroup; import android.text.TextUtils; import android.util.ArrayMap; import android.util.Log; @@ -61,6 +64,7 @@ import android.widget.Toast; import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; import java.util.ArrayList; +import java.util.Comparator; import java.util.List; abstract public class NotificationSettingsBase extends SettingsPreferenceFragment { @@ -106,6 +110,7 @@ abstract public class NotificationSettingsBase extends SettingsPreferenceFragmen protected EnforcedAdmin mSuspendedAppsAdmin; protected boolean mDndVisualEffectsSuppressed; + protected NotificationChannelGroup mChannelGroup; protected NotificationChannel mChannel; protected NotificationBackend.AppRow mAppRow; protected boolean mShowLegacyChannelConfig = false; @@ -185,6 +190,11 @@ abstract public class NotificationSettingsBase extends SettingsPreferenceFragmen mChannel = (args != null && args.containsKey(Settings.EXTRA_CHANNEL_ID)) ? mBackend.getChannel(mPkg, mUid, args.getString(Settings.EXTRA_CHANNEL_ID)) : null; + mChannelGroup = (args != null && args.containsKey(Settings.EXTRA_CHANNEL_GROUP_ID)) ? + mBackend.getGroupWithChannels(mPkg, mUid, + args.getString(Settings.EXTRA_CHANNEL_GROUP_ID)) + : null; + mSuspendedAppsAdmin = RestrictedLockUtils.checkIfApplicationIsSuspended( mContext, mPkg, mUserId); NotificationManager.Policy policy = mNm.getNotificationPolicy(); @@ -249,6 +259,10 @@ abstract public class NotificationSettingsBase extends SettingsPreferenceFragmen if (mChannel != null) { row.settingsIntent.putExtra(Notification.EXTRA_CHANNEL_ID, mChannel.getId()); } + if (mChannelGroup != null) { + row.settingsIntent.putExtra( + Notification.EXTRA_CHANNEL_GROUP_ID, mChannelGroup.getId()); + } } } @@ -276,7 +290,7 @@ abstract public class NotificationSettingsBase extends SettingsPreferenceFragmen protected void addAppLinkPref() { if (mAppRow.settingsIntent != null && mAppLink == null) { addPreferencesFromResource(R.xml.inapp_notification_settings); - mAppLink = (Preference) findPreference(KEY_APP_LINK); + mAppLink = findPreference(KEY_APP_LINK); mAppLink.setIntent(mAppRow.settingsIntent); } } @@ -392,16 +406,56 @@ abstract public class NotificationSettingsBase extends SettingsPreferenceFragmen } protected void setupBlockDesc(int summaryResId) { - mBlockedDesc = (FooterPreference) getPreferenceScreen().findPreference( - KEY_BLOCKED_DESC); mBlockedDesc = new FooterPreference(getPrefContext()); mBlockedDesc.setSelectable(false); mBlockedDesc.setTitle(summaryResId); mBlockedDesc.setEnabled(false); mBlockedDesc.setOrder(50); + mBlockedDesc.setKey(KEY_BLOCKED_DESC); getPreferenceScreen().addPreference(mBlockedDesc); } + protected Preference populateSingleChannelPrefs(PreferenceGroup parent, + final NotificationChannel channel, String summary) { + MasterSwitchPreference channelPref = new MasterSwitchPreference( + getPrefContext()); + channelPref.setSwitchEnabled(mSuspendedAppsAdmin == null + && isChannelBlockable(channel) + && isChannelConfigurable(channel)); + channelPref.setKey(channel.getId()); + channelPref.setTitle(channel.getName()); + channelPref.setChecked(channel.getImportance() != IMPORTANCE_NONE); + channelPref.setSummary(summary); + Bundle channelArgs = new Bundle(); + channelArgs.putInt(AppInfoBase.ARG_PACKAGE_UID, mUid); + channelArgs.putString(AppInfoBase.ARG_PACKAGE_NAME, mPkg); + channelArgs.putString(Settings.EXTRA_CHANNEL_ID, channel.getId()); + Intent channelIntent = Utils.onBuildStartFragmentIntent(getActivity(), + ChannelNotificationSettings.class.getName(), + channelArgs, null, R.string.notification_channel_title, null, false, + getMetricsCategory()); + channelPref.setIntent(channelIntent); + + channelPref.setOnPreferenceChangeListener( + new Preference.OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange(Preference preference, + Object o) { + boolean value = (Boolean) o; + int importance = value ? IMPORTANCE_LOW : IMPORTANCE_NONE; + channel.setImportance(importance); + channel.lockFields( + NotificationChannel.USER_LOCKED_IMPORTANCE); + channelPref.setSummary(summary); + mBackend.updateChannel(mPkg, mUid, channel); + + return true; + } + }); + parent.addPreference(channelPref); + return channelPref; + } + protected boolean checkCanBeVisible(int minImportanceVisible) { int importance = mChannel.getImportance(); if (importance == NotificationManager.IMPORTANCE_UNSPECIFIED) { @@ -410,6 +464,26 @@ abstract public class NotificationSettingsBase extends SettingsPreferenceFragmen return importance >= minImportanceVisible; } + protected String getImportanceSummary(NotificationChannel channel) { + switch (channel.getImportance()) { + case NotificationManager.IMPORTANCE_UNSPECIFIED: + return getContext().getString(R.string.notification_importance_unspecified); + case NotificationManager.IMPORTANCE_NONE: + return getContext().getString(R.string.notification_toggle_off); + case NotificationManager.IMPORTANCE_MIN: + return getContext().getString(R.string.notification_importance_min); + case NotificationManager.IMPORTANCE_LOW: + return getContext().getString(R.string.notification_importance_low); + case NotificationManager.IMPORTANCE_DEFAULT: + return getContext().getString(R.string.notification_importance_default); + case NotificationManager.IMPORTANCE_HIGH: + case NotificationManager.IMPORTANCE_MAX: + default: + return getContext().getString(R.string.notification_importance_high); + } + + } + private void setRestrictedIfNotificationFeaturesDisabled(CharSequence entry, CharSequence entryValue, int keyguardNotificationFeatures) { RestrictedLockUtils.EnforcedAdmin admin = @@ -459,7 +533,7 @@ abstract public class NotificationSettingsBase extends SettingsPreferenceFragmen return !channel.getId().equals(mAppRow.lockedChannelId); } - protected boolean isChannelBlockable(boolean systemApp, NotificationChannel channel) { + protected boolean isChannelBlockable(NotificationChannel channel) { if (!mAppRow.systemApp) { return true; } @@ -468,6 +542,14 @@ abstract public class NotificationSettingsBase extends SettingsPreferenceFragmen || channel.getImportance() == NotificationManager.IMPORTANCE_NONE; } + protected boolean isChannelGroupBlockable(NotificationChannelGroup group) { + if (!mAppRow.systemApp) { + return true; + } + + return group.isBlocked(); + } + protected void startListeningToPackageRemove() { if (mListeningToPackageRemove) { return; @@ -501,4 +583,12 @@ abstract public class NotificationSettingsBase extends SettingsPreferenceFragmen } } }; + + protected Comparator mChannelComparator = + (left, right) -> { + if (left.isDeleted() != right.isDeleted()) { + return Boolean.compare(left.isDeleted(), right.isDeleted()); + } + return left.getId().compareTo(right.getId()); + }; } diff --git a/tests/robotests/assets/grandfather_not_implementing_indexable b/tests/robotests/assets/grandfather_not_implementing_indexable index 695342e3be0..a08536a94d5 100644 --- a/tests/robotests/assets/grandfather_not_implementing_indexable +++ b/tests/robotests/assets/grandfather_not_implementing_indexable @@ -23,6 +23,7 @@ com.android.settings.inputmethod.UserDictionaryList com.android.settings.deviceinfo.Status com.android.settings.datausage.DataSaverSummary com.android.settings.notification.ChannelNotificationSettings +com.android.settings.notification.ChannelGroupNotificationSettings com.android.settings.datausage.AppDataUsage com.android.settings.accessibility.FontSizePreferenceFragmentForSetupWizard com.android.settings.applications.ManageDomainUrls diff --git a/tests/unit/src/com/android/settings/notification/AppNotificationSettingsTest.java b/tests/unit/src/com/android/settings/notification/AppNotificationSettingsTest.java index 22e98c71e5e..16a0b4324ad 100644 --- a/tests/unit/src/com/android/settings/notification/AppNotificationSettingsTest.java +++ b/tests/unit/src/com/android/settings/notification/AppNotificationSettingsTest.java @@ -16,7 +16,29 @@ package com.android.settings.notification; +import static android.app.NotificationManager.IMPORTANCE_DEFAULT; +import static android.support.test.espresso.Espresso.onView; +import static android.support.test.espresso.action.ViewActions.click; +import static android.support.test.espresso.assertion.ViewAssertions.doesNotExist; +import static android.support.test.espresso.assertion.ViewAssertions.matches; +import android.support.test.espresso.intent.Intents; + +import static android.support.test.espresso.intent.Intents.intended; +import static android.support.test.espresso.intent.matcher.IntentMatchers.hasExtra; +import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed; +import static android.support.test.espresso.matcher.ViewMatchers.withEffectiveVisibility; +import static android.support.test.espresso.matcher.ViewMatchers.withId; +import static android.support.test.espresso.matcher.ViewMatchers.withText; + +import static com.android.settings.SettingsActivity.EXTRA_SHOW_FRAGMENT; + +import static org.hamcrest.Matchers.allOf; +import static org.junit.Assert.fail; + import android.app.Instrumentation; +import android.app.NotificationChannel; +import android.app.NotificationChannelGroup; +import android.app.NotificationManager; import android.content.Context; import android.content.Intent; import android.provider.Settings; @@ -29,12 +51,6 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import static android.support.test.espresso.Espresso.onView; -import static android.support.test.espresso.assertion.ViewAssertions.doesNotExist; -import static android.support.test.espresso.matcher.ViewMatchers.withEffectiveVisibility; -import static android.support.test.espresso.matcher.ViewMatchers.withId; -import static org.hamcrest.Matchers.allOf; - @RunWith(AndroidJUnit4.class) @SmallTest public class AppNotificationSettingsTest { @@ -42,10 +58,29 @@ public class AppNotificationSettingsTest { private Context mTargetContext; private Instrumentation mInstrumentation; + NotificationManager mNm; + private NotificationChannelGroup mGroup1; + private NotificationChannel mGroup1Channel1; + private NotificationChannel mGroup1Channel2; + private NotificationChannelGroup mGroup2; + private NotificationChannel mGroup2Channel1; + private NotificationChannel mUngroupedChannel; + @Before public void setUp() { mInstrumentation = InstrumentationRegistry.getInstrumentation(); mTargetContext = mInstrumentation.getTargetContext(); + mNm = (NotificationManager) mTargetContext.getSystemService(Context.NOTIFICATION_SERVICE); + + mGroup1 = new NotificationChannelGroup(this.getClass().getName() + "1", "group1"); + mGroup2 = new NotificationChannelGroup(this.getClass().getName() + "2", "group2"); + mNm.createNotificationChannelGroup(mGroup1); + mNm.createNotificationChannelGroup(mGroup2); + + mGroup1Channel1 = createChannel(mGroup1, this.getClass().getName()+ "c1-1"); + mGroup1Channel2 = createChannel(mGroup1, this.getClass().getName()+ "c1-2"); + mGroup2Channel1 = createChannel(mGroup2, this.getClass().getName()+ "c2-1"); + mUngroupedChannel = createChannel(null, this.getClass().getName()+ "c"); } @Test @@ -60,4 +95,72 @@ public class AppNotificationSettingsTest { .check(doesNotExist()); } + @Test + public void launchNotificationSetting_showGroupsWithMultipleChannels() { + final Intent intent = new Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS) + .putExtra(Settings.EXTRA_APP_PACKAGE, mTargetContext.getPackageName()); + mInstrumentation.startActivitySync(intent); + onView(allOf(withText(mGroup1.getName().toString()))).check( + matches(isDisplayed())); + try { + onView(allOf(withText(mGroup1Channel1.getName().toString()))) + .check(matches(isDisplayed())); + fail("Channel erroneously appearing"); + } catch (Exception e) { + // expected + } + // links to group page + Intents.init(); + onView(allOf(withText(mGroup1.getName().toString()))).perform(click()); + intended(allOf(hasExtra(EXTRA_SHOW_FRAGMENT, + ChannelGroupNotificationSettings.class.getName()))); + Intents.release(); + } + + @Test + public void launchNotificationSetting_showUngroupedChannels() { + final Intent intent = new Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS) + .putExtra(Settings.EXTRA_APP_PACKAGE, mTargetContext.getPackageName()); + mInstrumentation.startActivitySync(intent); + onView(allOf(withText(mUngroupedChannel.getName().toString()))) + .check(matches(isDisplayed())); + // links directly to channel page + Intents.init(); + onView(allOf(withText(mUngroupedChannel.getName().toString()))).perform(click()); + intended(allOf(hasExtra(EXTRA_SHOW_FRAGMENT, ChannelNotificationSettings.class.getName()))); + Intents.release(); + } + + @Test + public void launchNotificationSetting_showGroupsWithOneChannel() { + final Intent intent = new Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS) + .putExtra(Settings.EXTRA_APP_PACKAGE, mTargetContext.getPackageName()); + mInstrumentation.startActivitySync(intent); + + onView(allOf(withText(mGroup2Channel1.getName().toString()))) + .check(matches(isDisplayed())); + try { + onView(allOf(withText(mGroup2.getName().toString()))).check( + matches(isDisplayed())); + fail("Group erroneously appearing"); + } catch (Exception e) { + // expected + } + + // links directly to channel page + Intents.init(); + onView(allOf(withText(mGroup2Channel1.getName().toString()))).perform(click()); + intended(allOf(hasExtra(EXTRA_SHOW_FRAGMENT, ChannelNotificationSettings.class.getName()))); + Intents.release(); + } + + private NotificationChannel createChannel(NotificationChannelGroup group, + String id) { + NotificationChannel channel = new NotificationChannel(id, id, IMPORTANCE_DEFAULT); + if (group != null) { + channel.setGroup(group.getId()); + } + mNm.createNotificationChannel(channel); + return channel; + } } diff --git a/tests/unit/src/com/android/settings/notification/ChannelGroupNotificationSettingsTest.java b/tests/unit/src/com/android/settings/notification/ChannelGroupNotificationSettingsTest.java new file mode 100644 index 00000000000..ce2c408fa50 --- /dev/null +++ b/tests/unit/src/com/android/settings/notification/ChannelGroupNotificationSettingsTest.java @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2017 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.notification; + +import static android.app.NotificationManager.IMPORTANCE_HIGH; +import static android.app.NotificationManager.IMPORTANCE_MIN; +import static android.support.test.espresso.Espresso.onView; +import static android.support.test.espresso.assertion.ViewAssertions.matches; +import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed; +import static android.support.test.espresso.matcher.ViewMatchers.withId; +import static android.support.test.espresso.matcher.ViewMatchers.withText; + +import static org.hamcrest.Matchers.allOf; +import static org.junit.Assert.fail; + +import android.app.INotificationManager; +import android.app.Instrumentation; +import android.app.NotificationChannel; +import android.app.NotificationChannelGroup; +import android.app.NotificationManager; +import android.content.Context; +import android.content.Intent; +import android.os.Process; +import android.os.ServiceManager; +import android.provider.Settings; +import android.support.test.InstrumentationRegistry; +import android.support.test.filters.SmallTest; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class ChannelGroupNotificationSettingsTest { + + private Context mTargetContext; + private Instrumentation mInstrumentation; + private NotificationManager mNm; + + @Before + public void setUp() { + mInstrumentation = InstrumentationRegistry.getInstrumentation(); + mTargetContext = mInstrumentation.getTargetContext(); + mNm = (NotificationManager) mTargetContext.getSystemService(Context.NOTIFICATION_SERVICE); + } + + @Test + public void launchNotificationSetting_displaysChannels() { + NotificationChannelGroup group = + new NotificationChannelGroup(this.getClass().getName(), this.getClass().getName()); + group.setDescription("description"); + NotificationChannel channel = new NotificationChannel(this.getClass().getName(), + "channel" + this.getClass().getName(), IMPORTANCE_MIN); + channel.setGroup(this.getClass().getName()); + NotificationChannel channel2 = new NotificationChannel("2"+this.getClass().getName(), + "2channel" + this.getClass().getName(), IMPORTANCE_MIN); + channel2.setGroup(this.getClass().getName()); + + mNm.createNotificationChannelGroup(group); + mNm.createNotificationChannel(channel); + mNm.createNotificationChannel(channel2); + + final Intent intent = new Intent(Settings.ACTION_CHANNEL_GROUP_NOTIFICATION_SETTINGS) + .putExtra(Settings.EXTRA_APP_PACKAGE, mTargetContext.getPackageName()) + .putExtra(Settings.EXTRA_CHANNEL_GROUP_ID, group.getId()); + + mInstrumentation.startActivitySync(intent); + + onView(allOf(withText(group.getName().toString()))).check(matches(isDisplayed())); + onView(allOf(withText(channel.getName().toString()))).check( + matches(isDisplayed())); + onView(allOf(withText(group.getDescription().toString()))).check( + matches(isDisplayed())); + onView(allOf(withText(channel2.getName().toString()))).check( + matches(isDisplayed())); + try { + onView(allOf(withText("Android is blocking this group of notifications from" + + " appearing on this device"))).check(matches(isDisplayed())); + fail("Blocking footer erroneously appearing"); + } catch (Exception e) { + // expected + } + } + + @Test + public void launchNotificationSettings_blockedGroup() throws Exception { + NotificationChannelGroup blocked = + new NotificationChannelGroup("blocked", "blocked"); + NotificationChannel channel = + new NotificationChannel("channel", "channel", IMPORTANCE_HIGH); + channel.setGroup(blocked.getId()); + mNm.createNotificationChannelGroup(blocked); + mNm.createNotificationChannel(channel); + + INotificationManager sINM = INotificationManager.Stub.asInterface( + ServiceManager.getService(Context.NOTIFICATION_SERVICE)); + blocked.setBlocked(true); + sINM.updateNotificationChannelGroupForPackage( + mTargetContext.getPackageName(), Process.myUid(), blocked); + + final Intent intent = new Intent(Settings.ACTION_CHANNEL_GROUP_NOTIFICATION_SETTINGS) + .putExtra(Settings.EXTRA_APP_PACKAGE, mTargetContext.getPackageName()) + .putExtra(Settings.EXTRA_CHANNEL_GROUP_ID, blocked.getId()); + mInstrumentation.startActivitySync(intent); + + onView(allOf(withText("Off"), isDisplayed())).check(matches(isDisplayed())); + onView(allOf(withText("Android is blocking this group of notifications from" + + " appearing on this device"))).check(matches(isDisplayed())); + + try { + onView(allOf(withText(channel.getName().toString()))).check(matches(isDisplayed())); + fail("settings appearing for blocked group"); + } catch (Exception e) { + // expected + } + } +} diff --git a/tests/unit/src/com/android/settings/notification/ChannelNotificationSettingsTest.java b/tests/unit/src/com/android/settings/notification/ChannelNotificationSettingsTest.java new file mode 100644 index 00000000000..1244dcda4f5 --- /dev/null +++ b/tests/unit/src/com/android/settings/notification/ChannelNotificationSettingsTest.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2017 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.notification; + +import static android.app.NotificationManager.IMPORTANCE_MIN; +import static android.app.NotificationManager.IMPORTANCE_NONE; +import static android.support.test.espresso.Espresso.onView; +import static android.support.test.espresso.assertion.ViewAssertions.matches; +import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed; +import static android.support.test.espresso.matcher.ViewMatchers.withId; +import static android.support.test.espresso.matcher.ViewMatchers.withText; + +import static org.hamcrest.Matchers.allOf; +import static org.junit.Assert.fail; + +import android.app.INotificationManager; +import android.app.Instrumentation; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.content.Context; +import android.content.Intent; +import android.os.Process; +import android.os.ServiceManager; +import android.provider.Settings; +import android.support.test.InstrumentationRegistry; +import android.support.test.filters.SmallTest; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class ChannelNotificationSettingsTest { + + private Context mTargetContext; + private Instrumentation mInstrumentation; + private NotificationChannel mNotificationChannel; + private NotificationManager mNm; + + @Before + public void setUp() { + mInstrumentation = InstrumentationRegistry.getInstrumentation(); + mTargetContext = mInstrumentation.getTargetContext(); + + mNm = (NotificationManager) mTargetContext.getSystemService(Context.NOTIFICATION_SERVICE); + mNotificationChannel = new NotificationChannel(this.getClass().getName(), + this.getClass().getName(), IMPORTANCE_MIN); + mNm.createNotificationChannel(mNotificationChannel); + } + + @Test + public void launchNotificationSetting_shouldNotCrash() { + final Intent intent = new Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS) + .putExtra(Settings.EXTRA_APP_PACKAGE, mTargetContext.getPackageName()) + .putExtra(Settings.EXTRA_CHANNEL_ID, mNotificationChannel.getId()); + mInstrumentation.startActivitySync(intent); + + onView(allOf(withText(mNotificationChannel.getName().toString()))).check( + matches(isDisplayed())); + } + + @Test + public void launchNotificationSettings_blockedChannel() throws Exception { + NotificationChannel blocked = + new NotificationChannel("blocked", "blocked", IMPORTANCE_NONE); + mNm.createNotificationChannel(blocked); + + INotificationManager sINM = INotificationManager.Stub.asInterface( + ServiceManager.getService(Context.NOTIFICATION_SERVICE)); + blocked.setImportance(IMPORTANCE_NONE); + sINM.updateNotificationChannelForPackage( + mTargetContext.getPackageName(), Process.myUid(), blocked); + + final Intent intent = new Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS) + .putExtra(Settings.EXTRA_APP_PACKAGE, mTargetContext.getPackageName()) + .putExtra(Settings.EXTRA_CHANNEL_ID, blocked.getId()); + mInstrumentation.startActivitySync(intent); + + onView(allOf(withText("Off"), isDisplayed())).check(matches(isDisplayed())); + onView(allOf(withText("Android is blocking this category of notifications from" + + " appearing on this device"))).check(matches(isDisplayed())); + + try { + onView(allOf(withText("On the lock screen"))).check(matches(isDisplayed())); + fail("settings appearing for blocked channel"); + } catch (Exception e) { + // expected + } + } +} From ec452e58a5642de2d7a41bfcece43df1fec51d16 Mon Sep 17 00:00:00 2001 From: Antony Sargent Date: Thu, 31 Aug 2017 13:34:06 -0700 Subject: [PATCH 08/14] Fix ClassScanner and re-enable CodeInspectionTest tests This fixes the code in ClassScanner for finding all classes in a given package to not depend on directory entries in the .jar files generated by the build system. This dependency caused our tests in CodeInspepectionTest.java to fail when this CL: https://android-review.googlesource.com/#/c/platform/build/+/456418/ stopped adding directory entries in the .jar files generated by the build process. Instead of depending on directories being present in the list of resources provided by the classloader, this CL switches to using Guava's ClassPath class to enumerate all loadable classes and filter them to the ones in the package of interest. Change-Id: I583919096450b61d4816256be280e2f5f1ce2316 Fixes: 64840107 Test: make RunSettingsRoboTests --- .../core/codeinspection/ClassScanner.java | 114 ++++-------------- .../codeinspection/CodeInspectionTest.java | 8 +- 2 files changed, 29 insertions(+), 93 deletions(-) diff --git a/tests/robotests/src/com/android/settings/core/codeinspection/ClassScanner.java b/tests/robotests/src/com/android/settings/core/codeinspection/ClassScanner.java index 09af870fdfd..bf57f406e3a 100644 --- a/tests/robotests/src/com/android/settings/core/codeinspection/ClassScanner.java +++ b/tests/robotests/src/com/android/settings/core/codeinspection/ClassScanner.java @@ -16,50 +16,47 @@ package com.android.settings.core.codeinspection; -import java.io.File; +import com.google.common.reflect.ClassPath; + import java.io.IOException; -import java.net.JarURLConnection; -import java.net.URL; -import java.net.URLConnection; -import java.net.URLDecoder; import java.util.ArrayList; -import java.util.Enumeration; import java.util.List; -import java.util.jar.JarEntry; -import java.util.jar.JarFile; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** * Scans and builds all classes in current classloader. */ public class ClassScanner { - private static final String CLASS_SUFFIX = ".class"; - public List> getClassesForPackage(String packageName) throws ClassNotFoundException { final List> classes = new ArrayList<>(); try { - final Enumeration resources = Thread.currentThread().getContextClassLoader() - .getResources(packageName.replace('.', '/')); - if (!resources.hasMoreElements()) { - return classes; - } - URL url = resources.nextElement(); - while (url != null) { - final URLConnection connection = url.openConnection(); + ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + ClassPath classPath = ClassPath.from(classLoader); - if (connection instanceof JarURLConnection) { - loadClassFromJar((JarURLConnection) connection, packageName, - classes); - } else { - loadClassFromDirectory(new File(URLDecoder.decode(url.getPath(), "UTF-8")), - packageName, classes); - } - if (resources.hasMoreElements()) { - url = resources.nextElement(); - } else { - break; + // Some anonymous classes don't return true when calling isAnonymousClass(), but they + // always seem to be nested anonymous classes like com.android.settings.Foo$1$2. In + // general we don't want any anonymous classes so we just filter these out by searching + // for $[0-9] in the name. + Pattern anonymousClassPattern = Pattern.compile(".*\\$\\d+.*"); + Matcher anonymousClassMatcher = anonymousClassPattern.matcher(""); + + for (ClassPath.ClassInfo info : classPath.getAllClasses()) { + if (info.getPackageName().startsWith(packageName)) { + try { + Class clazz = classLoader.loadClass(info.getName()); + if (clazz.isAnonymousClass() || anonymousClassMatcher.reset( + clazz.getName()).matches()) { + continue; + } + classes.add(clazz); + } catch (NoClassDefFoundError e) { + // do nothing. this class hasn't been found by the + // loader, and we don't care. + } } } } catch (final IOException e) { @@ -68,63 +65,4 @@ public class ClassScanner { return classes; } - private void loadClassFromDirectory(File directory, String packageName, List> classes) - throws ClassNotFoundException { - if (directory.exists() && directory.isDirectory()) { - final String[] files = directory.list(); - - for (final String file : files) { - if (file.endsWith(CLASS_SUFFIX)) { - try { - classes.add(Class.forName( - packageName + '.' + file.substring(0, file.length() - 6), - false /* init */, - Thread.currentThread().getContextClassLoader())); - } catch (NoClassDefFoundError e) { - // do nothing. this class hasn't been found by the - // loader, and we don't care. - } - } else { - final File tmpDirectory = new File(directory, file); - if (tmpDirectory.isDirectory()) { - loadClassFromDirectory(tmpDirectory, packageName + "." + file, classes); - } - } - } - } - } - - private void loadClassFromJar(JarURLConnection connection, String packageName, - List> classes) throws ClassNotFoundException, IOException { - final JarFile jarFile = connection.getJarFile(); - final Enumeration entries = jarFile.entries(); - String name; - if (!entries.hasMoreElements()) { - return; - } - JarEntry jarEntry = entries.nextElement(); - while (jarEntry != null) { - name = jarEntry.getName(); - - if (name.contains(CLASS_SUFFIX)) { - name = name.substring(0, name.length() - CLASS_SUFFIX.length()).replace('/', '.'); - - if (name.startsWith(packageName)) { - try { - classes.add(Class.forName(name, - false /* init */, - Thread.currentThread().getContextClassLoader())); - } catch (NoClassDefFoundError e) { - // do nothing. this class hasn't been found by the - // loader, and we don't care. - } - } - } - if (entries.hasMoreElements()) { - jarEntry = entries.nextElement(); - } else { - break; - } - } - } } diff --git a/tests/robotests/src/com/android/settings/core/codeinspection/CodeInspectionTest.java b/tests/robotests/src/com/android/settings/core/codeinspection/CodeInspectionTest.java index faaf338d15b..126a346da02 100644 --- a/tests/robotests/src/com/android/settings/core/codeinspection/CodeInspectionTest.java +++ b/tests/robotests/src/com/android/settings/core/codeinspection/CodeInspectionTest.java @@ -44,14 +44,12 @@ public class CodeInspectionTest { @Before public void setUp() throws Exception { mClasses = new ClassScanner().getClassesForPackage(CodeInspector.PACKAGE_NAME); - // Disabled temporarily - see b/64840107 - //assertThat(mClasses).isNotEmpty(); + assertThat(mClasses).isNotEmpty(); } @Test public void runCodeInspections() { - // Disabled temporarily - see b/64840107 - // new InstrumentableFragmentCodeInspector(mClasses).run(); - // new SearchIndexProviderCodeInspector(mClasses).run(); + new InstrumentableFragmentCodeInspector(mClasses).run(); + new SearchIndexProviderCodeInspector(mClasses).run(); } } From a00af97ab10fe3b21700175d671bc08a6e8b2f40 Mon Sep 17 00:00:00 2001 From: Amit Mahajan Date: Thu, 31 Aug 2017 13:59:20 -0700 Subject: [PATCH 09/14] Change to disable mvno data field in ApnEditor if needed. Test: manual Bug: 65243262 Change-Id: I4b3dec6d9dc7fecf0b0a8131dccc349c7daffe48 --- src/com/android/settings/ApnEditor.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/com/android/settings/ApnEditor.java b/src/com/android/settings/ApnEditor.java index ddcdd8c3a2b..e61f9594dac 100644 --- a/src/com/android/settings/ApnEditor.java +++ b/src/com/android/settings/ApnEditor.java @@ -660,7 +660,11 @@ public class ApnEditor extends SettingsPreferenceFragment return null; } else { String[] values = mRes.getStringArray(R.array.mvno_type_entries); - mMvnoMatchData.setEnabled(mvnoIndex != 0); + boolean mvnoMatchDataUneditable = + mReadOnlyApn || (mReadOnlyApnFields != null + && Arrays.asList(mReadOnlyApnFields) + .contains(Telephony.Carriers.MVNO_MATCH_DATA)); + mMvnoMatchData.setEnabled(!mvnoMatchDataUneditable && mvnoIndex != 0); if (newValue != null && newValue.equals(oldValue) == false) { if (values[mvnoIndex].equals("SPN")) { mMvnoMatchData.setText(mTelephonyManager.getSimOperatorName()); From b628d1ed53a73b00ffafaf60a1979ed978bca87b Mon Sep 17 00:00:00 2001 From: Felipe Leme Date: Thu, 31 Aug 2017 14:27:08 -0700 Subject: [PATCH 10/14] Removed deprecated BIND_AUTOFILL permission. Test: make -j100 RunSettingsRoboTests Bug: 37563972 Change-Id: I423f9c6234e8c1898ed66b22e1078c6f91c58422 --- .../applications/defaultapps/DefaultAutofillPicker.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/com/android/settings/applications/defaultapps/DefaultAutofillPicker.java b/src/com/android/settings/applications/defaultapps/DefaultAutofillPicker.java index d674522873b..34d13388ed9 100644 --- a/src/com/android/settings/applications/defaultapps/DefaultAutofillPicker.java +++ b/src/com/android/settings/applications/defaultapps/DefaultAutofillPicker.java @@ -189,9 +189,7 @@ public class DefaultAutofillPicker extends DefaultAppPickerFragment { .queryIntentServices(AUTOFILL_PROBE, PackageManager.GET_META_DATA); for (ResolveInfo info : resolveInfos) { final String permission = info.serviceInfo.permission; - // TODO(b/37563972): remove BIND_AUTOFILL once clients use BIND_AUTOFILL_SERVICE - if (Manifest.permission.BIND_AUTOFILL_SERVICE.equals(permission) - || Manifest.permission.BIND_AUTOFILL.equals(permission)) { + if (Manifest.permission.BIND_AUTOFILL_SERVICE.equals(permission)) { candidates.add(new DefaultAppInfo(mPm, mUserId, new ComponentName( info.serviceInfo.packageName, info.serviceInfo.name))); } From f664c77f317bd345c8a1e9544171a94a467e8aaa Mon Sep 17 00:00:00 2001 From: Doris Ling Date: Thu, 31 Aug 2017 14:58:15 -0700 Subject: [PATCH 11/14] Update accessibility preference icon. Change-Id: I84fb84e0a8bb422b9e453bd9b94e99bfb8a73be3 Fixes: 65256821 Test: visual --- res/drawable/ic_settings_accessibility.xml | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/res/drawable/ic_settings_accessibility.xml b/res/drawable/ic_settings_accessibility.xml index 7054d78073c..4cf518254d5 100644 --- a/res/drawable/ic_settings_accessibility.xml +++ b/res/drawable/ic_settings_accessibility.xml @@ -21,10 +21,5 @@ android:tint="?android:attr/colorControlNormal"> + android:pathData="M20.75,6.99c-0.14,-0.55 -0.69,-0.87 -1.24,-0.75C17.13,6.77 14.48,7 12,7S6.87,6.77 4.49,6.24c-0.55,-0.12 -1.1,0.2 -1.24,0.75l0,0C3.11,7.55 3.45,8.12 4,8.25C5.61,8.61 7.35,8.86 9,9v12c0,0.55 0.45,1 1,1h0c0.55,0 1,-0.45 1,-1v-5h2v5c0,0.55 0.45,1 1,1h0c0.55,0 1,-0.45 1,-1V9c1.65,-0.14 3.39,-0.39 5,-0.75C20.55,8.12 20.89,7.55 20.75,6.99L20.75,6.99zM12,6c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2s-2,0.9 -2,2S10.9,6 12,6z"/> From 83ceab82e4f6dd55e03a5832cf4b4ceca71b0722 Mon Sep 17 00:00:00 2001 From: Daniel Nishi Date: Thu, 10 Aug 2017 18:32:25 -0700 Subject: [PATCH 12/14] Show work profile apps only on app list. For the work profile drilldown, we used to show all apps when the user drilled down into the categories. This makes it so that the drill down only shows the work apps when that deep. Change-Id: I492cd3e9b9b923b87b68645a871dcfb2b91b4f95 Fixes: 62963093 Test: Settings robotest --- .../applications/ManageApplications.java | 6 +++++ .../deviceinfo/StorageProfileFragment.java | 9 +++++-- .../StorageItemPreferenceController.java | 19 ++++++++++++-- .../StorageItemPreferenceControllerTest.java | 25 ++++++++++++++++++- 4 files changed, 54 insertions(+), 5 deletions(-) diff --git a/src/com/android/settings/applications/ManageApplications.java b/src/com/android/settings/applications/ManageApplications.java index 7cc47e0c4a7..b93249664fb 100644 --- a/src/com/android/settings/applications/ManageApplications.java +++ b/src/com/android/settings/applications/ManageApplications.java @@ -115,6 +115,7 @@ public class ManageApplications extends InstrumentedPreferenceFragment public static final String EXTRA_VOLUME_UUID = "volumeUuid"; public static final String EXTRA_VOLUME_NAME = "volumeName"; public static final String EXTRA_STORAGE_TYPE = "storageType"; + public static final String EXTRA_WORK_ONLY = "workProfileOnly"; private static final String EXTRA_SORT_ORDER = "sortOrder"; private static final String EXTRA_SHOW_SYSTEM = "showSystem"; @@ -277,6 +278,7 @@ public class ManageApplications extends InstrumentedPreferenceFragment private ResetAppsHelper mResetAppsHelper; private String mVolumeUuid; private int mStorageType; + private boolean mIsWorkOnly; @Override public void onCreate(Bundle savedInstanceState) { @@ -328,6 +330,7 @@ public class ManageApplications extends InstrumentedPreferenceFragment mListType = LIST_TYPE_MAIN; } mFilter = getDefaultFilter(); + mIsWorkOnly = args != null ? args.getBoolean(EXTRA_WORK_ONLY) : false; if (savedInstanceState != null) { mSortOrder = savedInstanceState.getInt(EXTRA_SORT_ORDER, mSortOrder); @@ -423,6 +426,9 @@ public class ManageApplications extends InstrumentedPreferenceFragment } AppFilter compositeFilter = getCompositeFilter(mListType, mStorageType, mVolumeUuid); + if (mIsWorkOnly) { + compositeFilter = new CompoundFilter(compositeFilter, FILTERS[FILTER_APPS_WORK]); + } if (compositeFilter != null) { mApplications.setCompositeFilter(compositeFilter); } diff --git a/src/com/android/settings/deviceinfo/StorageProfileFragment.java b/src/com/android/settings/deviceinfo/StorageProfileFragment.java index 7a0a59e35e2..9f3ce0ca8d0 100644 --- a/src/com/android/settings/deviceinfo/StorageProfileFragment.java +++ b/src/com/android/settings/deviceinfo/StorageProfileFragment.java @@ -101,8 +101,13 @@ public class StorageProfileFragment extends DashboardFragment protected List getPreferenceControllers(Context context) { final List controllers = new ArrayList<>(); final StorageManager sm = context.getSystemService(StorageManager.class); - mPreferenceController = new StorageItemPreferenceController(context, this, - mVolume, new StorageManagerVolumeProvider(sm)); + mPreferenceController = + new StorageItemPreferenceController( + context, + this, + mVolume, + new StorageManagerVolumeProvider(sm), + /* isWorkProfile */ true); controllers.add(mPreferenceController); return controllers; } diff --git a/src/com/android/settings/deviceinfo/storage/StorageItemPreferenceController.java b/src/com/android/settings/deviceinfo/storage/StorageItemPreferenceController.java index 7060779ae65..99679d6f1a7 100644 --- a/src/com/android/settings/deviceinfo/storage/StorageItemPreferenceController.java +++ b/src/com/android/settings/deviceinfo/storage/StorageItemPreferenceController.java @@ -95,6 +95,7 @@ public class StorageItemPreferenceController extends AbstractPreferenceControlle private StorageItemPreference mSystemPreference; private static final String AUTHORITY_MEDIA = "com.android.providers.media.documents"; + private boolean mIsWorkProfile; public StorageItemPreferenceController( Context context, Fragment hostFragment, VolumeInfo volume, StorageVolumeProvider svp) { @@ -106,6 +107,16 @@ public class StorageItemPreferenceController extends AbstractPreferenceControlle mUserId = UserHandle.myUserId(); } + public StorageItemPreferenceController( + Context context, + Fragment hostFragment, + VolumeInfo volume, + StorageVolumeProvider svp, + boolean isWorkProfile) { + this(context, hostFragment, volume, svp); + mIsWorkProfile = isWorkProfile; + } + @Override public boolean isAvailable() { return true; @@ -212,7 +223,7 @@ public class StorageItemPreferenceController extends AbstractPreferenceControlle if (preference != null) { Drawable currentIcon = preference.getIcon(); // Sigh... Applying the badge to the icon clobbers the tint on the base drawable. - // For some reason, re-applying it here means the tint remains. + // For some reason, reapplying it here means the tint remains. currentIcon = applyTint(mContext, currentIcon); preference.setIcon(pm.getUserBadgedIcon(currentIcon, userHandle)); } @@ -220,7 +231,7 @@ public class StorageItemPreferenceController extends AbstractPreferenceControlle private static Drawable applyTint(Context context, Drawable icon) { TypedArray array = - context.obtainStyledAttributes(new int[]{android.R.attr.colorControlNormal}); + context.obtainStyledAttributes(new int[] {android.R.attr.colorControlNormal}); icon = icon.mutate(); icon.setTint(array.getColor(0, 0)); array.recycle(); @@ -320,6 +331,7 @@ public class StorageItemPreferenceController extends AbstractPreferenceControlle } Bundle args = new Bundle(); + args.putBoolean(ManageApplications.EXTRA_WORK_ONLY, mIsWorkProfile); args.putString(ManageApplications.EXTRA_CLASSNAME, Settings.StorageUseActivity.class.getName()); args.putString(ManageApplications.EXTRA_VOLUME_UUID, mVolume.getFsUuid()); @@ -336,6 +348,7 @@ public class StorageItemPreferenceController extends AbstractPreferenceControlle } Bundle args = new Bundle(); + args.putBoolean(ManageApplications.EXTRA_WORK_ONLY, mIsWorkProfile); args.putString(ManageApplications.EXTRA_CLASSNAME, Settings.StorageUseActivity.class.getName()); args.putString(ManageApplications.EXTRA_VOLUME_UUID, mVolume.getFsUuid()); @@ -347,6 +360,7 @@ public class StorageItemPreferenceController extends AbstractPreferenceControlle private Intent getGamesIntent() { Bundle args = new Bundle(1); + args.putBoolean(ManageApplications.EXTRA_WORK_ONLY, mIsWorkProfile); args.putString(ManageApplications.EXTRA_CLASSNAME, Settings.GamesStorageActivity.class.getName()); return Utils.onBuildStartFragmentIntent(mContext, @@ -356,6 +370,7 @@ public class StorageItemPreferenceController extends AbstractPreferenceControlle private Intent getMoviesIntent() { Bundle args = new Bundle(1); + args.putBoolean(ManageApplications.EXTRA_WORK_ONLY, mIsWorkProfile); args.putString(ManageApplications.EXTRA_CLASSNAME, Settings.MoviesStorageActivity.class.getName()); return Utils.onBuildStartFragmentIntent(mContext, diff --git a/tests/robotests/src/com/android/settings/deviceinfo/storage/StorageItemPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/deviceinfo/storage/StorageItemPreferenceControllerTest.java index 159944033dc..f3634ccf2ad 100644 --- a/tests/robotests/src/com/android/settings/deviceinfo/storage/StorageItemPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/deviceinfo/storage/StorageItemPreferenceControllerTest.java @@ -16,12 +16,12 @@ package com.android.settings.deviceinfo.storage; +import static com.android.settings.applications.ManageApplications.EXTRA_WORK_ONLY; import static com.android.settings.utils.FileSizeFormatter.MEGABYTE_IN_BYTES; import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.nullable; -import static org.mockito.Matchers.any; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; @@ -171,6 +171,29 @@ public class StorageItemPreferenceControllerTest { .isEqualTo(R.string.apps_storage); } + @Test + public void testClickAppsForWork() { + mController = new StorageItemPreferenceController(mContext, mFragment, mVolume, mSvp, true); + mPreference.setKey("pref_other_apps"); + mController.handlePreferenceTreeClick(mPreference); + + final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Intent.class); + verify(mFragment.getActivity()) + .startActivityAsUser(argumentCaptor.capture(), nullable(UserHandle.class)); + + Intent intent = argumentCaptor.getValue(); + assertThat(intent.getAction()).isEqualTo(Intent.ACTION_MAIN); + assertThat(intent.getComponent().getClassName()).isEqualTo(SubSettings.class.getName()); + assertThat(intent.getStringExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT)) + .isEqualTo(ManageApplications.class.getName()); + assertThat(intent.getIntExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT_TITLE_RESID, 0)) + .isEqualTo(R.string.apps_storage); + assertThat( + intent.getBundleExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT_ARGUMENTS) + .getBoolean(EXTRA_WORK_ONLY)) + .isTrue(); + } + @Test public void handlePreferenceTreeClick_tappingAppsWhileUninitializedDoesntCrash() { mController.setVolume(null); From a7b40995de90e41a6512ba5a8911af4e2cccdf6f Mon Sep 17 00:00:00 2001 From: Doris Ling Date: Thu, 24 Aug 2017 15:24:33 -0700 Subject: [PATCH 13/14] Set drawable for the entity header app preference button. In the entity header layout, the action buttons resource is set to null by default. When we bind the button with the app preference action, we should also set the drawable to the settings icon as well. Change-Id: Ic259b4c538f529671ca5a9c67664ef32fbbb25ae Fixes: 64826061 Test: make RunSettingsRoboTests --- .../settings/widget/EntityHeaderController.java | 2 ++ .../settings/widget/EntityHeaderControllerTest.java | 12 +++++++----- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/com/android/settings/widget/EntityHeaderController.java b/src/com/android/settings/widget/EntityHeaderController.java index 70b040de935..c38ad0297dc 100644 --- a/src/com/android/settings/widget/EntityHeaderController.java +++ b/src/com/android/settings/widget/EntityHeaderController.java @@ -336,6 +336,7 @@ public class EntityHeaderController { final Intent intent = resolveIntent( new Intent(Intent.ACTION_APPLICATION_PREFERENCES).setPackage(mPackageName)); if (intent == null) { + button.setImageDrawable(null); button.setVisibility(View.GONE); return; } @@ -348,6 +349,7 @@ public class EntityHeaderController { mFragment.startActivity(intent); } }); + button.setImageResource(R.drawable.ic_settings_24dp); button.setVisibility(View.VISIBLE); return; } diff --git a/tests/robotests/src/com/android/settings/widget/EntityHeaderControllerTest.java b/tests/robotests/src/com/android/settings/widget/EntityHeaderControllerTest.java index fc6071e552a..9c6ee457c42 100644 --- a/tests/robotests/src/com/android/settings/widget/EntityHeaderControllerTest.java +++ b/tests/robotests/src/com/android/settings/widget/EntityHeaderControllerTest.java @@ -16,7 +16,6 @@ package com.android.settings.widget; - import android.app.ActionBar; import android.app.Activity; import android.app.Fragment; @@ -30,6 +29,7 @@ import android.os.UserHandle; import android.support.v7.preference.Preference; import android.view.LayoutInflater; import android.view.View; +import android.widget.ImageButton; import android.widget.TextView; import com.android.internal.logging.nano.MetricsProto; @@ -148,8 +148,9 @@ public class EntityHeaderControllerTest { EntityHeaderController.ActionType.ACTION_NONE); mController.done(mActivity); - assertThat(appLinks.findViewById(android.R.id.button1).getVisibility()) - .isEqualTo(View.VISIBLE); + final ImageButton button1 = appLinks.findViewById(android.R.id.button1); + assertThat(button1.getVisibility()).isEqualTo(View.VISIBLE); + assertThat(button1.getDrawable()).isNotNull(); assertThat(appLinks.findViewById(android.R.id.button2).getVisibility()) .isEqualTo(View.GONE); try { @@ -176,8 +177,9 @@ public class EntityHeaderControllerTest { EntityHeaderController.ActionType.ACTION_NONE); mController.done(mActivity); - assertThat(appLinks.findViewById(android.R.id.button1).getVisibility()) - .isEqualTo(View.GONE); + final ImageButton button1 = appLinks.findViewById(android.R.id.button1); + assertThat(button1.getVisibility()).isEqualTo(View.GONE); + assertThat(button1.getDrawable()).isNull(); assertThat(appLinks.findViewById(android.R.id.button2).getVisibility()) .isEqualTo(View.GONE); } From 9be0ce09c9b3eb2e2d0bac038a3f469f087af652 Mon Sep 17 00:00:00 2001 From: Daniel Nishi Date: Wed, 23 Aug 2017 10:28:36 -0700 Subject: [PATCH 14/14] Make photos/videos storage preference normal. We had special behavior for it in the past, but this defines new behavior that is much closer to what the other storage preferences do. A photo app filter is used and a photos/video files preference exists on it which intents over to the gallery app. Fixes: 64147318 Test: Settings robotests Change-Id: I47284515fe2dfcc924ae61a44bc47051e9f5fda6 --- src/com/android/settings/Settings.java | 4 +- .../applications/ManageApplications.java | 23 +++++ .../PhotosViewHolderController.java | 90 +++++++++++++++++++ .../storage/StorageAsyncLoader.java | 5 ++ .../StorageItemPreferenceController.java | 37 +++++--- .../PhotosViewHolderControllerTest.java | 88 ++++++++++++++++++ .../StorageItemPreferenceControllerTest.java | 9 +- 7 files changed, 239 insertions(+), 17 deletions(-) create mode 100644 src/com/android/settings/applications/PhotosViewHolderController.java create mode 100644 tests/robotests/src/com/android/settings/applications/PhotosViewHolderControllerTest.java diff --git a/src/com/android/settings/Settings.java b/src/com/android/settings/Settings.java index a6961726550..ee041e823ca 100644 --- a/src/com/android/settings/Settings.java +++ b/src/com/android/settings/Settings.java @@ -21,7 +21,6 @@ import android.os.Bundle; import com.android.settings.applications.AppOpsSummary; import com.android.settings.enterprise.EnterprisePrivacySettings; import com.android.settings.fingerprint.FingerprintEnrollIntroduction; -import com.android.settings.fingerprint.FingerprintSettings; import com.android.settings.password.ChooseLockGeneric; /** @@ -128,6 +127,9 @@ public class Settings extends SettingsActivity { public static class AutomaticStorageManagerSettingsActivity extends SettingsActivity { /* empty */ } public static class GamesStorageActivity extends SettingsActivity { /* empty */ } public static class MoviesStorageActivity extends SettingsActivity { /* empty */ } + public static class PhotosStorageActivity extends SettingsActivity { + /* empty */ + } public static class TopLevelSettings extends SettingsActivity { /* empty */ } public static class ApnSettingsActivity extends SettingsActivity { /* empty */ } diff --git a/src/com/android/settings/applications/ManageApplications.java b/src/com/android/settings/applications/ManageApplications.java index b93249664fb..11eb0ccefd1 100644 --- a/src/com/android/settings/applications/ManageApplications.java +++ b/src/com/android/settings/applications/ManageApplications.java @@ -59,6 +59,7 @@ import android.widget.TextView; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settings.R; +import com.android.settings.Settings; import com.android.settings.Settings.AllApplicationsActivity; import com.android.settings.Settings.GamesStorageActivity; import com.android.settings.Settings.HighPowerApplicationsActivity; @@ -219,6 +220,7 @@ public class ManageApplications extends InstrumentedPreferenceFragment public static final int STORAGE_TYPE_DEFAULT = 0; // Show all apps that are not categorized. public static final int STORAGE_TYPE_MUSIC = 1; public static final int STORAGE_TYPE_LEGACY = 2; // Show apps even if they can be categorized. + public static final int STORAGE_TYPE_PHOTOS_VIDEOS = 3; // sort order private int mSortOrder = R.id.sort_order_alpha; @@ -262,6 +264,7 @@ public class ManageApplications extends InstrumentedPreferenceFragment public static final int LIST_TYPE_MANAGE_SOURCES = 8; public static final int LIST_TYPE_GAMES = 9; public static final int LIST_TYPE_MOVIES = 10; + public static final int LIST_TYPE_PHOTOGRAPHY = 11; // List types that should show instant apps. @@ -326,6 +329,10 @@ public class ManageApplications extends InstrumentedPreferenceFragment } else if (className.equals(MoviesStorageActivity.class.getName())) { mListType = LIST_TYPE_MOVIES; mSortOrder = R.id.sort_order_size; + } else if (className.equals(Settings.PhotosStorageActivity.class.getName())) { + mListType = LIST_TYPE_PHOTOGRAPHY; + mSortOrder = R.id.sort_order_size; + mStorageType = args.getInt(EXTRA_STORAGE_TYPE, STORAGE_TYPE_DEFAULT); } else { mListType = LIST_TYPE_MAIN; } @@ -378,6 +385,14 @@ public class ManageApplications extends InstrumentedPreferenceFragment new StorageStatsSource(context), mVolumeUuid, UserHandle.of(UserHandle.getUserId(mCurrentUid)))); + } else if (mStorageType == STORAGE_TYPE_PHOTOS_VIDEOS) { + Context context = getContext(); + mApplications.setExtraViewController( + new PhotosViewHolderController( + context, + new StorageStatsSource(context), + mVolumeUuid, + UserHandle.of(UserHandle.getUserId(mCurrentUid)))); } mListView.setAdapter(mApplications); mListView.setRecyclerListener(mApplications); @@ -450,6 +465,8 @@ public class ManageApplications extends InstrumentedPreferenceFragment return new CompoundFilter(ApplicationsState.FILTER_GAMES, filter); } else if (listType == LIST_TYPE_MOVIES) { return new CompoundFilter(ApplicationsState.FILTER_MOVIES, filter); + } else if (listType == LIST_TYPE_PHOTOGRAPHY) { + return new CompoundFilter(ApplicationsState.FILTER_PHOTOS, filter); } return null; @@ -479,6 +496,7 @@ public class ManageApplications extends InstrumentedPreferenceFragment case LIST_TYPE_STORAGE: case LIST_TYPE_GAMES: case LIST_TYPE_MOVIES: + case LIST_TYPE_PHOTOGRAPHY: return mSortOrder == R.id.sort_order_alpha; default: return false; @@ -501,6 +519,8 @@ public class ManageApplications extends InstrumentedPreferenceFragment return MetricsEvent.APPLICATIONS_STORAGE_GAMES; case LIST_TYPE_MOVIES: return MetricsEvent.APPLICATIONS_STORAGE_MOVIES; + case LIST_TYPE_PHOTOGRAPHY: + return MetricsEvent.APPLICATIONS_STORAGE_PHOTOS; case LIST_TYPE_USAGE_ACCESS: return MetricsEvent.USAGE_ACCESS; case LIST_TYPE_HIGH_POWER: @@ -604,6 +624,9 @@ public class ManageApplications extends InstrumentedPreferenceFragment case LIST_TYPE_MOVIES: startAppInfoFragment(AppStorageSettings.class, R.string.storage_movies_tv); break; + case LIST_TYPE_PHOTOGRAPHY: + startAppInfoFragment(AppStorageSettings.class, R.string.storage_photos_videos); + break; // TODO: Figure out if there is a way where we can spin up the profile's settings // process ahead of time, to avoid a long load of data when user clicks on a managed // app. Maybe when they load the list of apps that contains managed profile apps. diff --git a/src/com/android/settings/applications/PhotosViewHolderController.java b/src/com/android/settings/applications/PhotosViewHolderController.java new file mode 100644 index 00000000000..a652bb15159 --- /dev/null +++ b/src/com/android/settings/applications/PhotosViewHolderController.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2016 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.applications; + +import android.app.Fragment; +import android.content.Context; +import android.content.Intent; +import android.graphics.drawable.InsetDrawable; +import android.os.UserHandle; +import android.support.annotation.WorkerThread; +import android.text.format.Formatter; +import android.util.Log; + +import com.android.settings.R; +import com.android.settings.Utils; +import com.android.settingslib.applications.StorageStatsSource; + +import java.io.IOException; + +/** PhotosViewHolderController controls an Audio/Music file view in the ManageApplications view. */ +public class PhotosViewHolderController implements FileViewHolderController { + private static final String TAG = "PhotosViewHolderController"; + + private static final String IMAGE_MIME_TYPE = "image/*"; + private static final int INSET_SIZE = 24; // dp + + private Context mContext; + private StorageStatsSource mSource; + private String mVolumeUuid; + private long mFilesSize; + private UserHandle mUser; + + public PhotosViewHolderController( + Context context, StorageStatsSource source, String volumeUuid, UserHandle user) { + mContext = context; + mSource = source; + mVolumeUuid = volumeUuid; + mUser = user; + } + + @Override + @WorkerThread + public void queryStats() { + try { + StorageStatsSource.ExternalStorageStats stats = + mSource.getExternalStorageStats(mVolumeUuid, mUser); + mFilesSize = stats.imageBytes + stats.videoBytes; + } catch (IOException e) { + mFilesSize = 0; + Log.w(TAG, e); + } + } + + @Override + public boolean shouldShow() { + return true; + } + + @Override + public void setupView(AppViewHolder holder) { + holder.appIcon.setImageDrawable( + new InsetDrawable(mContext.getDrawable(R.drawable.ic_photo_library), INSET_SIZE)); + holder.appName.setText(mContext.getText(R.string.storage_detail_images)); + holder.summary.setText(Formatter.formatFileSize(mContext, mFilesSize)); + } + + @Override + public void onClick(Fragment fragment) { + Intent intent = new Intent(); + intent.setAction(android.content.Intent.ACTION_VIEW); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT); + intent.setType(IMAGE_MIME_TYPE); + intent.putExtra(Intent.EXTRA_FROM_STORAGE, true); + Utils.launchIntent(fragment, intent); + } +} diff --git a/src/com/android/settings/deviceinfo/storage/StorageAsyncLoader.java b/src/com/android/settings/deviceinfo/storage/StorageAsyncLoader.java index 630df8576ab..f92a24e9f9d 100644 --- a/src/com/android/settings/deviceinfo/storage/StorageAsyncLoader.java +++ b/src/com/android/settings/deviceinfo/storage/StorageAsyncLoader.java @@ -18,6 +18,7 @@ package com.android.settings.deviceinfo.storage; import static android.content.pm.ApplicationInfo.CATEGORY_AUDIO; import static android.content.pm.ApplicationInfo.CATEGORY_GAME; +import static android.content.pm.ApplicationInfo.CATEGORY_IMAGE; import static android.content.pm.ApplicationInfo.CATEGORY_VIDEO; import android.content.Context; @@ -134,6 +135,9 @@ public class StorageAsyncLoader case CATEGORY_VIDEO: result.videoAppsSize += blamedSize; break; + case CATEGORY_IMAGE: + result.photosAppsSize += blamedSize; + break; default: // The deprecated game flag does not set the category. if ((app.flags & ApplicationInfo.FLAG_IS_GAME) != 0) { @@ -163,6 +167,7 @@ public class StorageAsyncLoader public static class AppsStorageResult { public long gamesSize; public long musicAppsSize; + public long photosAppsSize; public long videoAppsSize; public long otherAppsSize; public long cacheSize; diff --git a/src/com/android/settings/deviceinfo/storage/StorageItemPreferenceController.java b/src/com/android/settings/deviceinfo/storage/StorageItemPreferenceController.java index 99679d6f1a7..ca85f69d0be 100644 --- a/src/com/android/settings/deviceinfo/storage/StorageItemPreferenceController.java +++ b/src/com/android/settings/deviceinfo/storage/StorageItemPreferenceController.java @@ -59,7 +59,6 @@ public class StorageItemPreferenceController extends AbstractPreferenceControlle PreferenceControllerMixin { private static final String TAG = "StorageItemPreference"; - private static final String IMAGE_MIME_TYPE = "image/*"; private static final String SYSTEM_FRAGMENT_TAG = "SystemInfo"; @VisibleForTesting @@ -93,9 +92,9 @@ public class StorageItemPreferenceController extends AbstractPreferenceControlle private StorageItemPreference mAppPreference; private StorageItemPreference mFilePreference; private StorageItemPreference mSystemPreference; + private boolean mIsWorkProfile; private static final String AUTHORITY_MEDIA = "com.android.providers.media.documents"; - private boolean mIsWorkProfile; public StorageItemPreferenceController( Context context, Fragment hostFragment, VolumeInfo volume, StorageVolumeProvider svp) { @@ -259,7 +258,8 @@ public class StorageItemPreferenceController extends AbstractPreferenceControlle // TODO(b/35927909): Figure out how to split out apps which are only installed for work // profiles in order to attribute those app's code bytes only to that profile. mPhotoPreference.setStorageSize( - data.externalStats.imageBytes + data.externalStats.videoBytes, mTotalSize); + data.photosAppsSize + data.externalStats.imageBytes + data.externalStats.videoBytes, + mTotalSize); mAudioPreference.setStorageSize( data.musicAppsSize + data.externalStats.audioBytes, mTotalSize); mGamePreference.setStorageSize(data.gamesSize, mTotalSize); @@ -280,10 +280,12 @@ public class StorageItemPreferenceController extends AbstractPreferenceControlle long attributedSize = 0; for (int i = 0; i < result.size(); i++) { final StorageAsyncLoader.AppsStorageResult otherData = result.valueAt(i); - attributedSize += otherData.gamesSize - + otherData.musicAppsSize - + otherData.videoAppsSize - + otherData.otherAppsSize; + attributedSize += + otherData.gamesSize + + otherData.musicAppsSize + + otherData.videoAppsSize + + otherData.photosAppsSize + + otherData.otherAppsSize; attributedSize += otherData.externalStats.totalBytes - otherData.externalStats.appBytes; } @@ -317,12 +319,21 @@ public class StorageItemPreferenceController extends AbstractPreferenceControlle } private Intent getPhotosIntent() { - Intent intent = new Intent(); - intent.setAction(android.content.Intent.ACTION_VIEW); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT); - intent.setType(IMAGE_MIME_TYPE); - intent.putExtra(Intent.EXTRA_FROM_STORAGE, true); - return intent; + Bundle args = new Bundle(2); + args.putString( + ManageApplications.EXTRA_CLASSNAME, Settings.PhotosStorageActivity.class.getName()); + args.putInt( + ManageApplications.EXTRA_STORAGE_TYPE, + ManageApplications.STORAGE_TYPE_PHOTOS_VIDEOS); + return Utils.onBuildStartFragmentIntent( + mContext, + ManageApplications.class.getName(), + args, + null, + R.string.storage_photos_videos, + null, + false, + mMetricsFeatureProvider.getMetricsCategory(mFragment)); } private Intent getAudioIntent() { diff --git a/tests/robotests/src/com/android/settings/applications/PhotosViewHolderControllerTest.java b/tests/robotests/src/com/android/settings/applications/PhotosViewHolderControllerTest.java new file mode 100644 index 00000000000..7eacba2e285 --- /dev/null +++ b/tests/robotests/src/com/android/settings/applications/PhotosViewHolderControllerTest.java @@ -0,0 +1,88 @@ +package com.android.settings.applications; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Matchers.nullable; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.Fragment; +import android.content.Context; +import android.content.Intent; +import android.os.UserHandle; +import android.os.storage.VolumeInfo; +import android.view.LayoutInflater; + +import com.android.settings.TestConfig; +import com.android.settings.testutils.SettingsRobolectricTestRunner; +import com.android.settingslib.applications.StorageStatsSource; +import com.android.settingslib.deviceinfo.StorageVolumeProvider; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Answers; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; + +@RunWith(SettingsRobolectricTestRunner.class) +@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION) +public class PhotosViewHolderControllerTest { + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + private Fragment mFragment; + + @Mock private StorageVolumeProvider mSvp; + @Mock private StorageStatsSource mSource; + + private Context mContext; + private PhotosViewHolderController mController; + private VolumeInfo mVolume; + private AppViewHolder mHolder; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + mContext = RuntimeEnvironment.application; + mVolume = new VolumeInfo("id", 0, null, "id"); + mController = + new PhotosViewHolderController( + mContext, mSource, mVolume.fsUuid, new UserHandle(0)); + + LayoutInflater inflater = LayoutInflater.from(mContext); + mHolder = AppViewHolder.createOrRecycle(inflater, null); + } + + @Test + public void storageShouldBeZeroBytesIfQueriedBeforeStorageQueryFinishes() { + mController.setupView(mHolder); + + assertThat(mHolder.summary.getText().toString()).isEqualTo("0.00B"); + } + + @Test + public void storageShouldRepresentStorageStatsQuery() throws Exception { + when(mSource.getExternalStorageStats(nullable(String.class), nullable(UserHandle.class))) + .thenReturn(new StorageStatsSource.ExternalStorageStats(1, 0, 1, 10, 0)); + + mController.queryStats(); + mController.setupView(mHolder); + + assertThat(mHolder.summary.getText().toString()).isEqualTo("11.00B"); + } + + @Test + public void clickingShouldIntentIntoFilesApp() { + mController.onClick(mFragment); + + final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Intent.class); + verify(mFragment).startActivity(argumentCaptor.capture()); + Intent intent = argumentCaptor.getValue(); + + assertThat(intent.getType()).isEqualTo("image/*"); + assertThat(intent.getAction()).isEqualTo(android.content.Intent.ACTION_VIEW); + assertThat(intent.getBooleanExtra(Intent.EXTRA_FROM_STORAGE, false)).isTrue(); + } +} diff --git a/tests/robotests/src/com/android/settings/deviceinfo/storage/StorageItemPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/deviceinfo/storage/StorageItemPreferenceControllerTest.java index f3634ccf2ad..9d69349f4e0 100644 --- a/tests/robotests/src/com/android/settings/deviceinfo/storage/StorageItemPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/deviceinfo/storage/StorageItemPreferenceControllerTest.java @@ -121,9 +121,12 @@ public class StorageItemPreferenceControllerTest { nullable(UserHandle.class)); Intent intent = argumentCaptor.getValue(); - assertThat(intent.getType()).isEqualTo("image/*"); - assertThat(intent.getAction()).isEqualTo(android.content.Intent.ACTION_VIEW); - assertThat(intent.getBooleanExtra(Intent.EXTRA_FROM_STORAGE, false)).isTrue(); + assertThat(intent.getAction()).isEqualTo(Intent.ACTION_MAIN); + assertThat(intent.getComponent().getClassName()).isEqualTo(SubSettings.class.getName()); + assertThat(intent.getStringExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT)) + .isEqualTo(ManageApplications.class.getName()); + assertThat(intent.getIntExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT_TITLE_RESID, 0)) + .isEqualTo(R.string.storage_photos_videos); } @Test