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/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"/> diff --git a/res/values/strings.xml b/res/values/strings.xml index 197202ed75c..6241850e4ec 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -6836,11 +6836,14 @@ Notification category + + Notification category group + Importance - - Let the app decide + + Allow sound Never show notifications @@ -7003,12 +7006,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/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()); diff --git a/src/com/android/settings/Settings.java b/src/com/android/settings/Settings.java index a6961726550..1ec4284c9fc 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; /** @@ -124,10 +123,14 @@ 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 */ } 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 7cc47e0c4a7..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; @@ -115,6 +116,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"; @@ -218,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; @@ -261,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. @@ -277,6 +281,7 @@ public class ManageApplications extends InstrumentedPreferenceFragment private ResetAppsHelper mResetAppsHelper; private String mVolumeUuid; private int mStorageType; + private boolean mIsWorkOnly; @Override public void onCreate(Bundle savedInstanceState) { @@ -324,10 +329,15 @@ 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; } mFilter = getDefaultFilter(); + mIsWorkOnly = args != null ? args.getBoolean(EXTRA_WORK_ONLY) : false; if (savedInstanceState != null) { mSortOrder = savedInstanceState.getInt(EXTRA_SORT_ORDER, mSortOrder); @@ -375,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); @@ -423,6 +441,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); } @@ -444,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; @@ -473,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; @@ -495,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: @@ -598,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/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))); } 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(); } } 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/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); } } 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/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 7060779ae65..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,6 +92,7 @@ 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"; @@ -106,6 +106,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 +222,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 +230,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(); @@ -248,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); @@ -269,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; } @@ -306,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() { @@ -320,6 +342,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 +359,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 +371,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 +381,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/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; + } + } 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/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/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/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/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(); } } 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 c728a608e2d..d3b5cbbea29 100644 --- a/tests/robotests/src/com/android/settings/deviceinfo/storage/StorageItemPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/deviceinfo/storage/StorageItemPreferenceControllerTest.java @@ -16,6 +16,7 @@ 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; @@ -118,9 +119,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 @@ -168,6 +172,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); 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); } 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 + } + } +}