diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 4bb1a8c530a..5b786813069 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -2389,6 +2389,19 @@ android:value="com.android.settings.applications.specialaccess.pictureinpicture.PictureInPictureDetails" /> + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/drawable/circle_outline.xml b/res/drawable/circle_outline.xml new file mode 100644 index 00000000000..1b2631d522d --- /dev/null +++ b/res/drawable/circle_outline.xml @@ -0,0 +1,22 @@ + + + + + diff --git a/res/drawable/ic_gray_scale_24dp.xml b/res/drawable/ic_gray_scale_24dp.xml new file mode 100644 index 00000000000..3fda134ade7 --- /dev/null +++ b/res/drawable/ic_gray_scale_24dp.xml @@ -0,0 +1,25 @@ + + + + + diff --git a/res/layout/advanced_bt_entity_sub.xml b/res/layout/advanced_bt_entity_sub.xml index 07ea8146fad..0f305830bdf 100644 --- a/res/layout/advanced_bt_entity_sub.xml +++ b/res/layout/advanced_bt_entity_sub.xml @@ -26,9 +26,11 @@ android:id="@+id/header_icon" android:layout_width="72dp" android:layout_height="72dp" - android:scaleType="fitCenter" android:layout_gravity="center_horizontal" - android:antialias="true"/> + android:antialias="true" + android:background="@drawable/circle_outline" + android:padding="8dp" + android:scaleType="fitCenter"/> + + + + diff --git a/res/values/config.xml b/res/values/config.xml index e6ada66a442..805469a63e5 100755 --- a/res/values/config.xml +++ b/res/values/config.xml @@ -356,4 +356,7 @@ content://com.google.android.gms.nearby.fastpair/device_status_list_item + + + diff --git a/res/values/dimens.xml b/res/values/dimens.xml index f5b6e958789..ef6fc4f567a 100755 --- a/res/values/dimens.xml +++ b/res/values/dimens.xml @@ -392,7 +392,7 @@ 4dp 16dp 16dp - 44dp + 48dp 10dp 10dp 24dp diff --git a/res/values/strings.xml b/res/values/strings.xml index e4d0d115f0d..631d9120cad 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -4525,8 +4525,8 @@ Keyboard shortcuts helper Display available shortcuts - - Work profile input & assistance + + Work profile keyboards & tools Virtual keyboard for work @@ -7939,6 +7939,9 @@ Do Not Disturb access + + Allow Do Not Disturb + No installed apps have requested Do Not Disturb access @@ -9325,6 +9328,12 @@ Screen tinted amber + + Greyscale + + + Display only in grey color + %1$d @@ -10759,7 +10768,7 @@ - Manage %1$s Notifications + Manage %1$s notifications No suggested application @@ -10769,6 +10778,8 @@ %1$d notification channels. Tap to manage all. + + You recently installed this app. Switch output diff --git a/res/xml/gestures.xml b/res/xml/gestures.xml index f7056307826..8515bd71892 100644 --- a/res/xml/gestures.xml +++ b/res/xml/gestures.xml @@ -27,12 +27,6 @@ android:fragment="com.android.settings.gestures.AssistGestureSettings" settings:controller="com.android.settings.gestures.AssistGestureSettingsPreferenceController" /> - - + + + + + + + \ No newline at end of file diff --git a/src/com/android/settings/Settings.java b/src/com/android/settings/Settings.java index 97ff613a317..ead0d6a3e21 100644 --- a/src/com/android/settings/Settings.java +++ b/src/com/android/settings/Settings.java @@ -96,6 +96,7 @@ public class Settings extends SettingsActivity { public static class PictureInPictureSettingsActivity extends SettingsActivity { /* empty */ } public static class AppPictureInPictureSettingsActivity extends SettingsActivity { /* empty */ } public static class ZenAccessSettingsActivity extends SettingsActivity { /* empty */ } + public static class ZenAccessDetailSettingsActivity extends SettingsActivity {} public static class ConditionProviderSettingsActivity extends SettingsActivity { /* empty */ } public static class UsbSettingsActivity extends SettingsActivity { /* empty */ } public static class UsbDetailsActivity extends SettingsActivity { /* empty */ } diff --git a/src/com/android/settings/applications/specialaccess/zenaccess/FriendlyWarningDialogFragment.java b/src/com/android/settings/applications/specialaccess/zenaccess/FriendlyWarningDialogFragment.java new file mode 100644 index 00000000000..fc85f7dffd0 --- /dev/null +++ b/src/com/android/settings/applications/specialaccess/zenaccess/FriendlyWarningDialogFragment.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2019 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.specialaccess.zenaccess; + +import android.app.Dialog; +import android.app.settings.SettingsEnums; +import android.os.Bundle; +import android.text.TextUtils; + +import androidx.appcompat.app.AlertDialog; + +import com.android.settings.R; +import com.android.settings.core.instrumentation.InstrumentedDialogFragment; + +/** + * Warning dialog when revoking zen access warning that zen rule instances will be deleted. + */ +public class FriendlyWarningDialogFragment extends InstrumentedDialogFragment { + static final String KEY_PKG = "p"; + static final String KEY_LABEL = "l"; + + + @Override + public int getMetricsCategory() { + return SettingsEnums.DIALOG_ZEN_ACCESS_REVOKE; + } + + public FriendlyWarningDialogFragment setPkgInfo(String pkg, CharSequence label) { + Bundle args = new Bundle(); + args.putString(KEY_PKG, pkg); + args.putString(KEY_LABEL, TextUtils.isEmpty(label) ? pkg : label.toString()); + setArguments(args); + return this; + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + final Bundle args = getArguments(); + final String pkg = args.getString(KEY_PKG); + final String label = args.getString(KEY_LABEL); + + final String title = getResources().getString( + R.string.zen_access_revoke_warning_dialog_title, label); + final String summary = getResources() + .getString(R.string.zen_access_revoke_warning_dialog_summary); + return new AlertDialog.Builder(getContext()) + .setMessage(summary) + .setTitle(title) + .setCancelable(true) + .setPositiveButton(R.string.okay, + (dialog, id) -> { + ZenAccessController.deleteRules(getContext(), pkg); + ZenAccessController.setAccess(getContext(), pkg, false); + }) + .setNegativeButton(R.string.cancel, + (dialog, id) -> { + // pass + }) + .create(); + } +} diff --git a/src/com/android/settings/applications/specialaccess/zenaccess/ScaryWarningDialogFragment.java b/src/com/android/settings/applications/specialaccess/zenaccess/ScaryWarningDialogFragment.java new file mode 100644 index 00000000000..69318f8d6af --- /dev/null +++ b/src/com/android/settings/applications/specialaccess/zenaccess/ScaryWarningDialogFragment.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2019 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.specialaccess.zenaccess; + +import android.app.Dialog; +import android.app.settings.SettingsEnums; +import android.os.Bundle; +import android.text.TextUtils; + +import androidx.appcompat.app.AlertDialog; + +import com.android.settings.R; +import com.android.settings.core.instrumentation.InstrumentedDialogFragment; +import com.android.settings.notification.ZenAccessSettings; + +/** + * Warning dialog when allowing zen access warning about the privileges being granted. + */ +public class ScaryWarningDialogFragment extends InstrumentedDialogFragment { + static final String KEY_PKG = "p"; + static final String KEY_LABEL = "l"; + + @Override + public int getMetricsCategory() { + return SettingsEnums.DIALOG_ZEN_ACCESS_GRANT; + } + + public ScaryWarningDialogFragment setPkgInfo(String pkg, CharSequence label) { + Bundle args = new Bundle(); + args.putString(KEY_PKG, pkg); + args.putString(KEY_LABEL, TextUtils.isEmpty(label) ? pkg : label.toString()); + setArguments(args); + return this; + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + final Bundle args = getArguments(); + final String pkg = args.getString(KEY_PKG); + final String label = args.getString(KEY_LABEL); + + final String title = getResources().getString(R.string.zen_access_warning_dialog_title, + label); + final String summary = getResources() + .getString(R.string.zen_access_warning_dialog_summary); + return new AlertDialog.Builder(getContext()) + .setMessage(summary) + .setTitle(title) + .setCancelable(true) + .setPositiveButton(R.string.allow, + (dialog, id) -> ZenAccessController.setAccess(getContext(), pkg, true)) + .setNegativeButton(R.string.deny, + (dialog, id) -> { + // pass + }) + .create(); + } +} diff --git a/src/com/android/settings/applications/specialaccess/zenaccess/ZenAccessController.java b/src/com/android/settings/applications/specialaccess/zenaccess/ZenAccessController.java index 88d444d485c..946599b4aa8 100644 --- a/src/com/android/settings/applications/specialaccess/zenaccess/ZenAccessController.java +++ b/src/com/android/settings/applications/specialaccess/zenaccess/ZenAccessController.java @@ -17,12 +17,29 @@ package com.android.settings.applications.specialaccess.zenaccess; import android.app.ActivityManager; +import android.app.AppGlobals; +import android.app.NotificationManager; +import android.app.settings.SettingsEnums; import android.content.Context; +import android.content.pm.PackageInfo; +import android.content.pm.ParceledListSlice; +import android.os.AsyncTask; +import android.os.RemoteException; +import android.util.ArraySet; +import android.util.Log; + +import androidx.annotation.VisibleForTesting; import com.android.settings.core.BasePreferenceController; +import com.android.settings.overlay.FeatureFactory; + +import java.util.List; +import java.util.Set; public class ZenAccessController extends BasePreferenceController { + private static final String TAG = "ZenAccessController"; + private final ActivityManager mActivityManager; public ZenAccessController(Context context, String preferenceKey) { @@ -32,8 +49,68 @@ public class ZenAccessController extends BasePreferenceController { @Override public int getAvailabilityStatus() { - return !mActivityManager.isLowRamDevice() + return isSupported(mActivityManager) ? AVAILABLE_UNSEARCHABLE : UNSUPPORTED_ON_DEVICE; } + + public static boolean isSupported(ActivityManager activityManager) { + return !activityManager.isLowRamDevice(); + } + + public static Set getPackagesRequestingNotificationPolicyAccess() { + final ArraySet requestingPackages = new ArraySet<>(); + try { + final String[] PERM = { + android.Manifest.permission.ACCESS_NOTIFICATION_POLICY + }; + final ParceledListSlice list = AppGlobals.getPackageManager() + .getPackagesHoldingPermissions(PERM, 0 /*flags*/, + ActivityManager.getCurrentUser()); + final List pkgs = list.getList(); + if (pkgs != null) { + for (PackageInfo info : pkgs) { + requestingPackages.add(info.packageName); + } + } + } catch (RemoteException e) { + Log.e(TAG, "Cannot reach packagemanager", e); + } + return requestingPackages; + } + + public static Set getAutoApprovedPackages(Context context) { + final Set autoApproved = new ArraySet<>(); + autoApproved.addAll(context.getSystemService(NotificationManager.class) + .getEnabledNotificationListenerPackages()); + return autoApproved; + } + + public static boolean hasAccess(Context context, String pkg) { + return context.getSystemService( + NotificationManager.class).isNotificationPolicyAccessGrantedForPackage(pkg); + } + + public static void setAccess(final Context context, final String pkg, final boolean access) { + logSpecialPermissionChange(access, pkg, context); + AsyncTask.execute(() -> { + final NotificationManager mgr = context.getSystemService(NotificationManager.class); + mgr.setNotificationPolicyAccessGranted(pkg, access); + }); + } + + public static void deleteRules(final Context context, final String pkg) { + AsyncTask.execute(() -> { + final NotificationManager mgr = context.getSystemService(NotificationManager.class); + mgr.removeAutomaticZenRules(pkg); + }); + } + + @VisibleForTesting + static void logSpecialPermissionChange(boolean enable, String packageName, Context context) { + int logCategory = enable ? SettingsEnums.APP_SPECIAL_PERMISSION_DND_ALLOW + : SettingsEnums.APP_SPECIAL_PERMISSION_DND_DENY; + FeatureFactory.getFactory(context).getMetricsFeatureProvider().action(context, + logCategory, packageName); + } } diff --git a/src/com/android/settings/applications/specialaccess/zenaccess/ZenAccessDetails.java b/src/com/android/settings/applications/specialaccess/zenaccess/ZenAccessDetails.java new file mode 100644 index 00000000000..a18e7d63cad --- /dev/null +++ b/src/com/android/settings/applications/specialaccess/zenaccess/ZenAccessDetails.java @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2019 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.specialaccess.zenaccess; + +import android.app.ActivityManager; +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.os.Bundle; + +import androidx.appcompat.app.AlertDialog; +import androidx.preference.SwitchPreference; + +import com.android.settings.R; +import com.android.settings.applications.AppInfoWithHeader; + +import java.util.Set; + +public class ZenAccessDetails extends AppInfoWithHeader implements + ZenAccessSettingObserverMixin.Listener { + + private static final String SWITCH_PREF_KEY = "zen_access_switch"; + + @Override + public int getMetricsCategory() { + return SettingsEnums.ZEN_ACCESS_DETAIL; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + addPreferencesFromResource(R.xml.zen_access_permission_details); + getSettingsLifecycle().addObserver( + new ZenAccessSettingObserverMixin(getContext(), this /* listener */)); + } + + @Override + protected boolean refreshUi() { + final Context context = getContext(); + if (!ZenAccessController.isSupported(context.getSystemService(ActivityManager.class))) { + return false; + } + // If this app didn't declare this permission in their manifest, don't bother showing UI. + final Set needAccessApps = + ZenAccessController.getPackagesRequestingNotificationPolicyAccess(); + if (!needAccessApps.contains(mPackageName)) { + return false; + } + updatePreference(context, findPreference(SWITCH_PREF_KEY)); + return true; + } + + @Override + protected AlertDialog createDialog(int id, int errorCode) { + return null; + } + + public void updatePreference(Context context, SwitchPreference preference) { + final CharSequence label = mPackageInfo.applicationInfo.loadLabel(mPm); + final Set autoApproved = ZenAccessController.getAutoApprovedPackages(context); + if (autoApproved.contains(mPackageName)) { + //Auto approved, user cannot do anything. Hard code summary and disable preference. + preference.setEnabled(false); + preference.setSummary(getString(R.string.zen_access_disabled_package_warning)); + return; + } + preference.setChecked(ZenAccessController.hasAccess(context, mPackageName)); + preference.setOnPreferenceChangeListener((p, newValue) -> { + final boolean access = (Boolean) newValue; + if (access) { + new ScaryWarningDialogFragment() + .setPkgInfo(mPackageName, label) + .show(getFragmentManager(), "dialog"); + } else { + new FriendlyWarningDialogFragment() + .setPkgInfo(mPackageName, label) + .show(getFragmentManager(), "dialog"); + } + return false; + }); + } + + @Override + public void onZenAccessPolicyChanged() { + refreshUi(); + } +} diff --git a/src/com/android/settings/applications/specialaccess/zenaccess/ZenAccessSettingObserverMixin.java b/src/com/android/settings/applications/specialaccess/zenaccess/ZenAccessSettingObserverMixin.java new file mode 100644 index 00000000000..30507efffa9 --- /dev/null +++ b/src/com/android/settings/applications/specialaccess/zenaccess/ZenAccessSettingObserverMixin.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2019 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.specialaccess.zenaccess; + +import android.app.ActivityManager; +import android.content.Context; +import android.database.ContentObserver; +import android.net.Uri; +import android.os.Handler; +import android.os.Looper; +import android.provider.Settings; + +import com.android.settingslib.core.lifecycle.LifecycleObserver; +import com.android.settingslib.core.lifecycle.events.OnStart; +import com.android.settingslib.core.lifecycle.events.OnStop; + +public class ZenAccessSettingObserverMixin extends ContentObserver implements LifecycleObserver, + OnStart, OnStop { + + public interface Listener { + void onZenAccessPolicyChanged(); + } + + private final Context mContext; + private final Listener mListener; + + public ZenAccessSettingObserverMixin(Context context, Listener listener) { + super(new Handler(Looper.getMainLooper())); + mContext = context; + mListener = listener; + } + + @Override + public void onChange(boolean selfChange, Uri uri) { + if (mListener != null) { + mListener.onZenAccessPolicyChanged(); + } + } + + @Override + public void onStart() { + if (!ZenAccessController.isSupported(mContext.getSystemService(ActivityManager.class))) { + return; + } + mContext.getContentResolver().registerContentObserver( + Settings.Secure.getUriFor( + Settings.Secure.ENABLED_NOTIFICATION_POLICY_ACCESS_PACKAGES), + false /* notifyForDescendants */, + this /* observer */); + mContext.getContentResolver().registerContentObserver( + Settings.Secure.getUriFor(Settings.Secure.ENABLED_NOTIFICATION_LISTENERS), + false /* notifyForDescendants */, + this /* observer */); + } + + @Override + public void onStop() { + if (!ZenAccessController.isSupported(mContext.getSystemService(ActivityManager.class))) { + return; + } + mContext.getContentResolver().unregisterContentObserver(this /* observer */); + } +} diff --git a/src/com/android/settings/backup/OWNERS b/src/com/android/settings/backup/OWNERS index c026a350e75..a7b55fd134b 100644 --- a/src/com/android/settings/backup/OWNERS +++ b/src/com/android/settings/backup/OWNERS @@ -1,6 +1,13 @@ -# Default reviewers for this and subdirectories. +# Use this reviewer by default. +br-framework-team+reviews@google.com + +# People who can approve changes for submission. +anniemeng@google.com +nathch@google.com +rthakohov@google.com + +# Others (in case above are not available). bryanmawhinney@google.com -cprins@google.com jorlow@google.com philippov@google.com stefanot@google.com \ No newline at end of file diff --git a/src/com/android/settings/core/gateway/SettingsGateway.java b/src/com/android/settings/core/gateway/SettingsGateway.java index ba64a80f20c..d522918fa6d 100644 --- a/src/com/android/settings/core/gateway/SettingsGateway.java +++ b/src/com/android/settings/core/gateway/SettingsGateway.java @@ -50,6 +50,7 @@ import com.android.settings.applications.specialaccess.deviceadmin.DeviceAdminSe import com.android.settings.applications.specialaccess.pictureinpicture.PictureInPictureDetails; import com.android.settings.applications.specialaccess.pictureinpicture.PictureInPictureSettings; import com.android.settings.applications.specialaccess.vrlistener.VrListenerSettings; +import com.android.settings.applications.specialaccess.zenaccess.ZenAccessDetails; import com.android.settings.backup.PrivacySettings; import com.android.settings.backup.ToggleBackupSettingFragment; import com.android.settings.backup.UserBackupSettingsActivity; @@ -209,6 +210,7 @@ public class SettingsGateway { UserSettings.class.getName(), NotificationAccessSettings.class.getName(), ZenAccessSettings.class.getName(), + ZenAccessDetails.class.getName(), ZenModeAutomationSettings.class.getName(), PrintSettingsFragment.class.getName(), PrintJobSettingsFragment.class.getName(), diff --git a/src/com/android/settings/datetime/timezone/BaseTimeZoneAdapter.java b/src/com/android/settings/datetime/timezone/BaseTimeZoneAdapter.java index ff980b2ceca..66735c8a5e1 100644 --- a/src/com/android/settings/datetime/timezone/BaseTimeZoneAdapter.java +++ b/src/com/android/settings/datetime/timezone/BaseTimeZoneAdapter.java @@ -77,9 +77,10 @@ public class BaseTimeZoneAdapter @Override public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { LayoutInflater inflater = LayoutInflater.from(parent.getContext()); - switch(viewType) { + switch (viewType) { case TYPE_HEADER: { - final View view = inflater.inflate(R.layout.preference_category_material, + final View view = inflater.inflate( + R.layout.time_zone_search_header, parent, false); return new HeaderViewHolder(view); } @@ -136,7 +137,8 @@ public class BaseTimeZoneAdapter return mShowHeader && position == 0; } - public @NonNull ArrayFilter getFilter() { + @NonNull + public ArrayFilter getFilter() { if (mFilter == null) { mFilter = new ArrayFilter(); } @@ -153,14 +155,18 @@ public class BaseTimeZoneAdapter public interface AdapterItem { CharSequence getTitle(); + CharSequence getSummary(); + String getIconText(); + String getCurrentTime(); /** * @return unique non-negative number */ long getItemId(); + String[] getSearchKeys(); } diff --git a/src/com/android/settings/homepage/contextualcards/ContextualCardLoader.java b/src/com/android/settings/homepage/contextualcards/ContextualCardLoader.java index ff2ee91468e..1f5f7dd5694 100644 --- a/src/com/android/settings/homepage/contextualcards/ContextualCardLoader.java +++ b/src/com/android/settings/homepage/contextualcards/ContextualCardLoader.java @@ -187,7 +187,6 @@ public class ContextualCardLoader extends AsyncLoaderCompat // Collect future and eligible cards for (Future cardFuture : eligibleCards) { try { - //TODO(b/124492762): Log latency and timeout occurrence. final ContextualCard card = cardFuture.get(ELIGIBILITY_CHECKER_TIMEOUT_MS, TimeUnit.MILLISECONDS); if (card != null) { diff --git a/src/com/android/settings/homepage/contextualcards/ContextualCardManager.java b/src/com/android/settings/homepage/contextualcards/ContextualCardManager.java index 8f7e84acd2f..7df322d5211 100644 --- a/src/com/android/settings/homepage/contextualcards/ContextualCardManager.java +++ b/src/com/android/settings/homepage/contextualcards/ContextualCardManager.java @@ -22,6 +22,7 @@ import static com.android.settings.intelligence.ContextualCardProto.ContextualCa import static java.util.stream.Collectors.groupingBy; +import android.app.settings.SettingsEnums; import android.content.Context; import android.os.Bundle; import android.provider.Settings; @@ -38,6 +39,7 @@ import androidx.loader.content.Loader; import com.android.settings.homepage.contextualcards.slices.SliceContextualCardRenderer; import com.android.settings.overlay.FeatureFactory; +import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; import com.android.settingslib.core.lifecycle.Lifecycle; import com.android.settingslib.core.lifecycle.LifecycleObserver; import com.android.settingslib.core.lifecycle.events.OnSaveInstanceState; @@ -195,8 +197,8 @@ public class ContextualCardManager implements ContextualCardLoader.CardContentLo @Override public void onFinishCardLoading(List cards) { final long loadTime = System.currentTimeMillis() - mStartTime; - //TODO(b/123668403): remove the log here once we do the change with FutureTask Log.d(TAG, "Total loading time = " + loadTime); + final List cardsToKeep = getCardsToKeep(cards); //navigate back to the homepage, screen rotate or after card dismissal @@ -206,15 +208,25 @@ public class ContextualCardManager implements ContextualCardLoader.CardContentLo return; } - //only log homepage display upon a fresh launch + final MetricsFeatureProvider metricsFeatureProvider = + FeatureFactory.getFactory(mContext).getMetricsFeatureProvider(); final long timeoutLimit = getCardLoaderTimeout(mContext); if (loadTime <= timeoutLimit) { onContextualCardUpdated(cards.stream() .collect(groupingBy(ContextualCard::getCardType))); + } else { + // log timeout occurrence + metricsFeatureProvider.action(SettingsEnums.PAGE_UNKNOWN, + SettingsEnums.ACTION_CONTEXTUAL_CARD_LOAD_TIMEOUT, + SettingsEnums.SETTINGS_HOMEPAGE, + null /* key */, (int) loadTime /* value */); } + //only log homepage display upon a fresh launch final long totalTime = System.currentTimeMillis() - mStartTime; FeatureFactory.getFactory(mContext).getContextualCardFeatureProvider(mContext) .logHomepageDisplay(totalTime); + metricsFeatureProvider.action(mContext, + SettingsEnums.ACTION_CONTEXTUAL_HOME_SHOW, (int) totalTime); mIsFirstLaunch = false; } diff --git a/src/com/android/settings/homepage/contextualcards/EligibleCardChecker.java b/src/com/android/settings/homepage/contextualcards/EligibleCardChecker.java index fe68d028533..811aaa2fa13 100644 --- a/src/com/android/settings/homepage/contextualcards/EligibleCardChecker.java +++ b/src/com/android/settings/homepage/contextualcards/EligibleCardChecker.java @@ -18,6 +18,7 @@ package com.android.settings.homepage.contextualcards; import static android.app.slice.Slice.HINT_ERROR; +import android.app.settings.SettingsEnums; import android.content.ContentResolver; import android.content.Context; import android.net.Uri; @@ -27,6 +28,9 @@ import androidx.annotation.VisibleForTesting; import androidx.slice.Slice; import androidx.slice.SliceViewManager; +import com.android.settings.overlay.FeatureFactory; +import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; + import java.util.concurrent.Callable; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -46,7 +50,32 @@ public class EligibleCardChecker implements Callable { @Override public ContextualCard call() throws Exception { - return isCardEligibleToDisplay(mCard) ? mCard : null; + final long startTime = System.currentTimeMillis(); + final MetricsFeatureProvider metricsFeatureProvider = + FeatureFactory.getFactory(mContext).getMetricsFeatureProvider(); + ContextualCard result; + + if (isCardEligibleToDisplay(mCard)) { + metricsFeatureProvider.action(SettingsEnums.PAGE_UNKNOWN, + SettingsEnums.ACTION_CONTEXTUAL_CARD_ELIGIBILITY, + SettingsEnums.SETTINGS_HOMEPAGE, + mCard.getTextSliceUri() /* key */, 1 /* true */); + result = mCard; + } else { + metricsFeatureProvider.action(SettingsEnums.PAGE_UNKNOWN, + SettingsEnums.ACTION_CONTEXTUAL_CARD_ELIGIBILITY, + SettingsEnums.SETTINGS_HOMEPAGE, + mCard.getTextSliceUri() /* key */, 0 /* false */); + result = null; + } + // Log individual card loading time + metricsFeatureProvider.action(SettingsEnums.PAGE_UNKNOWN, + SettingsEnums.ACTION_CONTEXTUAL_CARD_LOAD, + SettingsEnums.SETTINGS_HOMEPAGE, + mCard.getTextSliceUri() /* key */, + (int) (System.currentTimeMillis() - startTime) /* value */); + + return result; } @VisibleForTesting diff --git a/src/com/android/settings/homepage/contextualcards/conditional/ConditionManager.java b/src/com/android/settings/homepage/contextualcards/conditional/ConditionManager.java index c741b98c359..66f6c81e984 100644 --- a/src/com/android/settings/homepage/contextualcards/conditional/ConditionManager.java +++ b/src/com/android/settings/homepage/contextualcards/conditional/ConditionManager.java @@ -162,6 +162,7 @@ public class ConditionManager { mCardControllers.add(new RingerVibrateConditionController(mAppContext, this /* manager */)); mCardControllers.add(new RingerMutedConditionController(mAppContext, this /* manager */)); mCardControllers.add(new WorkModeConditionController(mAppContext, this /* manager */)); + mCardControllers.add(new GrayscaleConditionController(mAppContext, this /* manager */)); } /** diff --git a/src/com/android/settings/homepage/contextualcards/conditional/GrayscaleConditionController.java b/src/com/android/settings/homepage/contextualcards/conditional/GrayscaleConditionController.java new file mode 100644 index 00000000000..664707def69 --- /dev/null +++ b/src/com/android/settings/homepage/contextualcards/conditional/GrayscaleConditionController.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2019 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.homepage.contextualcards.conditional; + +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.content.Intent; +import android.hardware.display.ColorDisplayManager; +import android.util.Log; + +import com.android.settings.R; +import com.android.settings.homepage.contextualcards.ContextualCard; + +import java.net.URISyntaxException; +import java.util.Objects; + +public class GrayscaleConditionController implements ConditionalCardController { + static final int ID = Objects.hash("GrayscaleConditionController"); + + private static final String TAG = "GrayscaleCondition"; + + private final Context mAppContext; + private final ConditionManager mConditionManager; + private final ColorDisplayManager mColorDisplayManager; + + private Intent mIntent; + + public GrayscaleConditionController(Context appContext, ConditionManager conditionManager) { + mAppContext = appContext; + mConditionManager = conditionManager; + mColorDisplayManager = mAppContext.getSystemService(ColorDisplayManager.class); + } + + @Override + public long getId() { + return ID; + } + + @Override + public boolean isDisplayable() { + try { + mIntent = Intent.parseUri( + mAppContext.getString(R.string.config_grayscale_settings_intent), + Intent.URI_INTENT_SCHEME); + } catch (URISyntaxException e) { + Log.w(TAG, "Failure parsing grayscale settings intent, skipping", e); + return false; + } + return mColorDisplayManager.isSaturationActivated(); + } + + @Override + public void onPrimaryClick(Context context) { + mAppContext.startActivity(mIntent); + } + + @Override + public void onActionClick() { + // Turn off grayscale + mColorDisplayManager.setSaturationLevel(100 /* staturationLevel */); + mConditionManager.onConditionChanged(); + } + + @Override + public ContextualCard buildContextualCard() { + return new ConditionalContextualCard.Builder() + .setConditionId(ID) + .setMetricsConstant(SettingsEnums.SETTINGS_CONDITION_GRAYSCALE_MODE) + .setActionText(mAppContext.getText(R.string.condition_turn_off)) + .setName(mAppContext.getPackageName() + "/" + mAppContext.getText( + R.string.condition_grayscale_title)) + .setTitleText(mAppContext.getText(R.string.condition_grayscale_title).toString()) + .setSummaryText( + mAppContext.getText(R.string.condition_grayscale_summary).toString()) + .setIconDrawable(mAppContext.getDrawable(R.drawable.ic_gray_scale_24dp)) + .setViewType(ConditionContextualCardRenderer.VIEW_TYPE_HALF_WIDTH) + .build(); + } + + @Override + public void startMonitoringStateChange() { + + } + + @Override + public void stopMonitoringStateChange() { + + } +} diff --git a/src/com/android/settings/media/MediaOutputIndicatorSlice.java b/src/com/android/settings/media/MediaOutputIndicatorSlice.java new file mode 100644 index 00000000000..eb0c81f1c2c --- /dev/null +++ b/src/com/android/settings/media/MediaOutputIndicatorSlice.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2019 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.media; + +import static com.android.settings.slices.CustomSliceRegistry.MEDIA_OUTPUT_INDICATOR_SLICE_URI; + +import android.annotation.ColorInt; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; + +import androidx.annotation.VisibleForTesting; +import androidx.core.graphics.drawable.IconCompat; +import androidx.slice.Slice; +import androidx.slice.builders.ListBuilder; +import androidx.slice.builders.SliceAction; + +import com.android.settings.R; +import com.android.settings.Utils; +import com.android.settings.slices.CustomSliceable; +import com.android.settings.slices.SliceBackgroundWorker; +import com.android.settingslib.media.MediaOutputSliceConstants; + +public class MediaOutputIndicatorSlice implements CustomSliceable { + + private Context mContext; + @VisibleForTesting + MediaOutputIndicatorWorker mWorker; + + public MediaOutputIndicatorSlice(Context context) { + mContext = context; + } + + @Override + public Slice getSlice() { + if (!getWorker().isVisible()) { + return null; + } + final IconCompat icon = IconCompat.createWithResource(mContext, + com.android.internal.R.drawable.ic_settings_bluetooth); + final CharSequence title = mContext.getText(R.string.media_output_title); + final PendingIntent primaryActionIntent = PendingIntent.getActivity(mContext, + 0 /* requestCode */, getMediaOutputSliceIntent(), 0 /* flags */); + final SliceAction primarySliceAction = SliceAction.createDeeplink( + primaryActionIntent, icon, ListBuilder.ICON_IMAGE, title); + @ColorInt final int color = Utils.getColorAccentDefaultColor(mContext); + + final ListBuilder listBuilder = new ListBuilder(mContext, + MEDIA_OUTPUT_INDICATOR_SLICE_URI, + ListBuilder.INFINITY) + .setAccentColor(color) + .addRow(new ListBuilder.RowBuilder() + .setTitle(title) + .setSubtitle(getWorker().findActiveDeviceName()) + .setPrimaryAction(primarySliceAction)); + return listBuilder.build(); + } + + private MediaOutputIndicatorWorker getWorker() { + if (mWorker == null) { + mWorker = (MediaOutputIndicatorWorker) SliceBackgroundWorker.getInstance(getUri()); + } + return mWorker; + } + + private Intent getMediaOutputSliceIntent() { + final Intent intent = new Intent() + .setAction(MediaOutputSliceConstants.ACTION_MEDIA_OUTPUT) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + return intent; + } + + @Override + public Uri getUri() { + return MEDIA_OUTPUT_INDICATOR_SLICE_URI; + } + + @Override + public Intent getIntent() { + // This Slice reflects active media device information and launch MediaOutputSlice. It does + // not contain its owned Slice data + return null; + } + + @Override + public Class getBackgroundWorkerClass() { + return MediaOutputIndicatorWorker.class; + } +} diff --git a/src/com/android/settings/media/MediaOutputIndicatorWorker.java b/src/com/android/settings/media/MediaOutputIndicatorWorker.java new file mode 100644 index 00000000000..adee0557be9 --- /dev/null +++ b/src/com/android/settings/media/MediaOutputIndicatorWorker.java @@ -0,0 +1,161 @@ +/* + * Copyright (C) 2019 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.media; + +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothProfile; +import android.content.Context; +import android.net.Uri; +import android.util.Log; + +import com.android.internal.util.CollectionUtils; +import com.android.settings.R; +import com.android.settings.bluetooth.Utils; +import com.android.settings.slices.SliceBackgroundWorker; +import com.android.settingslib.bluetooth.A2dpProfile; +import com.android.settingslib.bluetooth.BluetoothCallback; +import com.android.settingslib.bluetooth.CachedBluetoothDevice; +import com.android.settingslib.bluetooth.HearingAidProfile; +import com.android.settingslib.bluetooth.LocalBluetoothManager; +import com.android.settingslib.bluetooth.LocalBluetoothProfileManager; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * Listener for background change from {@code BluetoothCallback} to update media output indicator. + */ +public class MediaOutputIndicatorWorker extends SliceBackgroundWorker implements BluetoothCallback { + + private static final String TAG = "MediaOutputIndicatorWorker"; + + private LocalBluetoothManager mLocalBluetoothManager; + private LocalBluetoothProfileManager mProfileManager; + + public MediaOutputIndicatorWorker(Context context, Uri uri) { + super(context, uri); + } + + @Override + protected void onSlicePinned() { + LocalBluetoothManager mLocalBluetoothManager = Utils.getLocalBtManager(getContext()); + if (mLocalBluetoothManager == null) { + Log.e(TAG, "Bluetooth is not supported on this device"); + return; + } + mProfileManager = mLocalBluetoothManager.getProfileManager(); + mLocalBluetoothManager.getEventManager().registerCallback(this); + } + + @Override + protected void onSliceUnpinned() { + if (mLocalBluetoothManager == null) { + Log.e(TAG, "Bluetooth is not supported on this device"); + return; + } + mLocalBluetoothManager.getEventManager().unregisterCallback(this); + } + + @Override + public void close() throws IOException { + mLocalBluetoothManager = null; + mProfileManager = null; + } + + @Override + public void onBluetoothStateChanged(int bluetoothState) { + // To handle the case that Bluetooth on and no connected devices + notifySliceChange(); + } + + @Override + public void onActiveDeviceChanged(CachedBluetoothDevice activeDevice, int bluetoothProfile) { + if (bluetoothProfile == BluetoothProfile.A2DP) { + notifySliceChange(); + } + } + + /** + * To decide Slice's visibility. + * + * @return true if device is connected or previously connected, false for other cases. + */ + public boolean isVisible() { + return !CollectionUtils.isEmpty(getConnectableA2dpDevices()) + || !CollectionUtils.isEmpty(getConnectableHearingAidDevices()); + } + + private List getConnectableA2dpDevices() { + // get A2dp devices on all states + // (STATE_DISCONNECTED, STATE_CONNECTING, STATE_CONNECTED, STATE_DISCONNECTING) + final A2dpProfile a2dpProfile = mProfileManager.getA2dpProfile(); + if (a2dpProfile == null) { + return new ArrayList<>(); + } + return a2dpProfile.getConnectableDevices(); + } + + private List getConnectableHearingAidDevices() { + // get hearing aid profile devices on all states + // (STATE_DISCONNECTED, STATE_CONNECTING, STATE_CONNECTED, STATE_DISCONNECTING) + final HearingAidProfile hapProfile = mProfileManager.getHearingAidProfile(); + if (hapProfile == null) { + return new ArrayList<>(); + } + + return hapProfile.getConnectableDevices(); + } + + /** + * Get active devices name. + * + * @return active Bluetooth device alias, or default summary if no active device. + */ + public CharSequence findActiveDeviceName() { + // Return Hearing Aid device name if it is active + BluetoothDevice activeDevice = findActiveHearingAidDevice(); + if (activeDevice != null) { + return activeDevice.getAliasName(); + } + // Return A2DP device name if it is active + final A2dpProfile a2dpProfile = mProfileManager.getA2dpProfile(); + if (a2dpProfile != null) { + activeDevice = a2dpProfile.getActiveDevice(); + if (activeDevice != null) { + return activeDevice.getAliasName(); + } + } + // No active device, return default summary + return getContext().getText(R.string.media_output_default_summary); + } + + private BluetoothDevice findActiveHearingAidDevice() { + final HearingAidProfile hearingAidProfile = mProfileManager.getHearingAidProfile(); + if (hearingAidProfile == null) { + return null; + } + + final List activeDevices = hearingAidProfile.getActiveDevices(); + for (BluetoothDevice btDevice : activeDevices) { + if (btDevice != null) { + return btDevice; + } + } + return null; + } +} diff --git a/src/com/android/settings/notification/ZenAccessSettings.java b/src/com/android/settings/notification/ZenAccessSettings.java index d057c755754..fca82552244 100644 --- a/src/com/android/settings/notification/ZenAccessSettings.java +++ b/src/com/android/settings/notification/ZenAccessSettings.java @@ -18,56 +18,40 @@ package com.android.settings.notification; import android.annotation.Nullable; import android.app.ActivityManager; -import android.app.AppGlobals; -import android.app.Dialog; import android.app.NotificationManager; import android.app.settings.SettingsEnums; import android.content.Context; -import android.content.DialogInterface; import android.content.pm.ApplicationInfo; -import android.content.pm.PackageInfo; import android.content.pm.PackageItemInfo; import android.content.pm.PackageManager; -import android.content.pm.ParceledListSlice; -import android.database.ContentObserver; -import android.net.Uri; -import android.os.AsyncTask; import android.os.Bundle; -import android.os.Handler; -import android.os.Looper; -import android.os.RemoteException; import android.provider.SearchIndexableResource; -import android.provider.Settings.Secure; -import android.text.TextUtils; import android.util.ArraySet; -import android.util.Log; import android.view.View; -import androidx.annotation.VisibleForTesting; -import androidx.appcompat.app.AlertDialog; -import androidx.preference.Preference; -import androidx.preference.Preference.OnPreferenceChangeListener; import androidx.preference.PreferenceScreen; -import androidx.preference.SwitchPreference; import com.android.settings.R; -import com.android.settings.core.instrumentation.InstrumentedDialogFragment; -import com.android.settings.overlay.FeatureFactory; +import com.android.settings.applications.AppInfoBase; +import com.android.settings.applications.specialaccess.zenaccess.ZenAccessController; +import com.android.settings.applications.specialaccess.zenaccess.ZenAccessDetails; +import com.android.settings.applications.specialaccess.zenaccess.ZenAccessSettingObserverMixin; import com.android.settings.search.BaseSearchIndexProvider; import com.android.settings.search.Indexable; -import com.android.settings.widget.AppSwitchPreference; import com.android.settings.widget.EmptyTextSettings; import com.android.settingslib.search.SearchIndexable; +import com.android.settingslib.widget.apppreference.AppPreference; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Set; @SearchIndexable -public class ZenAccessSettings extends EmptyTextSettings { +public class ZenAccessSettings extends EmptyTextSettings implements + ZenAccessSettingObserverMixin.Listener { private final String TAG = "ZenAccessSettings"; - private final SettingObserver mObserver = new SettingObserver(); private Context mContext; private PackageManager mPkgMan; private NotificationManager mNoMan; @@ -84,6 +68,8 @@ public class ZenAccessSettings extends EmptyTextSettings { mContext = getActivity(); mPkgMan = mContext.getPackageManager(); mNoMan = mContext.getSystemService(NotificationManager.class); + getSettingsLifecycle().addObserver( + new ZenAccessSettingObserverMixin(getContext(), this /* listener */)); } @Override @@ -102,30 +88,22 @@ public class ZenAccessSettings extends EmptyTextSettings { super.onResume(); if (!ActivityManager.isLowRamDeviceStatic()) { reloadList(); - getContentResolver().registerContentObserver( - Secure.getUriFor(Secure.ENABLED_NOTIFICATION_POLICY_ACCESS_PACKAGES), false, - mObserver); - getContentResolver().registerContentObserver( - Secure.getUriFor(Secure.ENABLED_NOTIFICATION_LISTENERS), false, - mObserver); } else { setEmptyText(R.string.disabled_low_ram_device); } } @Override - public void onPause() { - super.onPause(); - if (!ActivityManager.isLowRamDeviceStatic()) { - getContentResolver().unregisterContentObserver(mObserver); - } + public void onZenAccessPolicyChanged() { + reloadList(); } private void reloadList() { final PreferenceScreen screen = getPreferenceScreen(); screen.removeAll(); final ArrayList apps = new ArrayList<>(); - final ArraySet requesting = getPackagesRequestingNotificationPolicyAccess(); + final Set requesting = + ZenAccessController.getPackagesRequestingNotificationPolicyAccess(); if (!requesting.isEmpty()) { final List installed = mPkgMan.getInstalledApplications(0); if (installed != null) { @@ -143,204 +121,42 @@ public class ZenAccessSettings extends EmptyTextSettings { for (ApplicationInfo app : apps) { final String pkg = app.packageName; final CharSequence label = app.loadLabel(mPkgMan); - final SwitchPreference pref = new AppSwitchPreference(getPrefContext()); + final AppPreference pref = new AppPreference(getPrefContext()); pref.setKey(pkg); - pref.setPersistent(false); pref.setIcon(app.loadIcon(mPkgMan)); pref.setTitle(label); - pref.setChecked(hasAccess(pkg)); if (autoApproved.contains(pkg)) { + //Auto approved, user cannot do anything. Hard code summary and disable preference. pref.setEnabled(false); pref.setSummary(getString(R.string.zen_access_disabled_package_warning)); + } else { + // Not auto approved, update summary according to notification backend. + pref.setSummary(getPreferenceSummary(pkg)); } - pref.setOnPreferenceChangeListener(new OnPreferenceChangeListener() { - @Override - public boolean onPreferenceChange(Preference preference, Object newValue) { - final boolean access = (Boolean) newValue; - if (access) { - new ScaryWarningDialogFragment() - .setPkgInfo(pkg, label) - .show(getFragmentManager(), "dialog"); - } else { - new FriendlyWarningDialogFragment() - .setPkgInfo(pkg, label) - .show(getFragmentManager(), "dialog"); - } - return false; - } + pref.setOnPreferenceClickListener(preference -> { + AppInfoBase.startAppInfoFragment( + ZenAccessDetails.class /* fragment */, + R.string.manage_zen_access_title /* titleRes */, + pkg, + app.uid, + this /* source */, + -1 /* requestCode */, + getMetricsCategory() /* sourceMetricsCategory */); + return true; }); + screen.addPreference(pref); } } - private ArraySet getPackagesRequestingNotificationPolicyAccess() { - ArraySet requestingPackages = new ArraySet<>(); - try { - final String[] PERM = { - android.Manifest.permission.ACCESS_NOTIFICATION_POLICY - }; - final ParceledListSlice list = AppGlobals.getPackageManager() - .getPackagesHoldingPermissions(PERM, 0 /*flags*/, - ActivityManager.getCurrentUser()); - final List pkgs = list.getList(); - if (pkgs != null) { - for (PackageInfo info : pkgs) { - requestingPackages.add(info.packageName); - } - } - } catch(RemoteException e) { - Log.e(TAG, "Cannot reach packagemanager", e); - } - return requestingPackages; - } - - private boolean hasAccess(String pkg) { - return mNoMan.isNotificationPolicyAccessGrantedForPackage(pkg); - } - - private static void setAccess(final Context context, final String pkg, final boolean access) { - logSpecialPermissionChange(access, pkg, context); - AsyncTask.execute(new Runnable() { - @Override - public void run() { - final NotificationManager mgr = context.getSystemService(NotificationManager.class); - mgr.setNotificationPolicyAccessGranted(pkg, access); - } - }); - } - - @VisibleForTesting - static void logSpecialPermissionChange(boolean enable, String packageName, Context context) { - int logCategory = enable ? SettingsEnums.APP_SPECIAL_PERMISSION_DND_ALLOW - : SettingsEnums.APP_SPECIAL_PERMISSION_DND_DENY; - FeatureFactory.getFactory(context).getMetricsFeatureProvider().action(context, - logCategory, packageName); - } - - - private static void deleteRules(final Context context, final String pkg) { - AsyncTask.execute(new Runnable() { - @Override - public void run() { - final NotificationManager mgr = context.getSystemService(NotificationManager.class); - mgr.removeAutomaticZenRules(pkg); - } - }); - } - - private final class SettingObserver extends ContentObserver { - public SettingObserver() { - super(new Handler(Looper.getMainLooper())); - } - - @Override - public void onChange(boolean selfChange, Uri uri) { - reloadList(); - } - } - /** - * Warning dialog when allowing zen access warning about the privileges being granted. + * @return the summary for the current state of whether the app associated with the given + * {@param packageName} is allowed to enter picture-in-picture. */ - public static class ScaryWarningDialogFragment extends InstrumentedDialogFragment { - static final String KEY_PKG = "p"; - static final String KEY_LABEL = "l"; - - @Override - public int getMetricsCategory() { - return SettingsEnums.DIALOG_ZEN_ACCESS_GRANT; - } - - public ScaryWarningDialogFragment setPkgInfo(String pkg, CharSequence label) { - Bundle args = new Bundle(); - args.putString(KEY_PKG, pkg); - args.putString(KEY_LABEL, TextUtils.isEmpty(label) ? pkg : label.toString()); - setArguments(args); - return this; - } - - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - final Bundle args = getArguments(); - final String pkg = args.getString(KEY_PKG); - final String label = args.getString(KEY_LABEL); - - final String title = getResources().getString(R.string.zen_access_warning_dialog_title, - label); - final String summary = getResources() - .getString(R.string.zen_access_warning_dialog_summary); - return new AlertDialog.Builder(getContext()) - .setMessage(summary) - .setTitle(title) - .setCancelable(true) - .setPositiveButton(R.string.allow, - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int id) { - setAccess(getContext(), pkg, true); - } - }) - .setNegativeButton(R.string.deny, - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int id) { - // pass - } - }) - .create(); - } - } - - /** - * Warning dialog when revoking zen access warning that zen rule instances will be deleted. - */ - public static class FriendlyWarningDialogFragment extends InstrumentedDialogFragment { - static final String KEY_PKG = "p"; - static final String KEY_LABEL = "l"; - - - @Override - public int getMetricsCategory() { - return SettingsEnums.DIALOG_ZEN_ACCESS_REVOKE; - } - - public FriendlyWarningDialogFragment setPkgInfo(String pkg, CharSequence label) { - Bundle args = new Bundle(); - args.putString(KEY_PKG, pkg); - args.putString(KEY_LABEL, TextUtils.isEmpty(label) ? pkg : label.toString()); - setArguments(args); - return this; - } - - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - final Bundle args = getArguments(); - final String pkg = args.getString(KEY_PKG); - final String label = args.getString(KEY_LABEL); - - final String title = getResources().getString( - R.string.zen_access_revoke_warning_dialog_title, label); - final String summary = getResources() - .getString(R.string.zen_access_revoke_warning_dialog_summary); - return new AlertDialog.Builder(getContext()) - .setMessage(summary) - .setTitle(title) - .setCancelable(true) - .setPositiveButton(R.string.okay, - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int id) { - deleteRules(getContext(), pkg); - setAccess(getContext(), pkg, false); - } - }) - .setNegativeButton(R.string.cancel, - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int id) { - // pass - } - }) - .create(); - } + private int getPreferenceSummary(String packageName) { + final boolean enabled = ZenAccessController.hasAccess(getContext(), packageName); + return enabled ? R.string.app_permission_summary_allowed + : R.string.app_permission_summary_not_allowed; } public static final Indexable.SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = diff --git a/src/com/android/settings/panel/VolumePanel.java b/src/com/android/settings/panel/VolumePanel.java index 62eca53898b..4ea7fe7561b 100644 --- a/src/com/android/settings/panel/VolumePanel.java +++ b/src/com/android/settings/panel/VolumePanel.java @@ -16,6 +16,7 @@ package com.android.settings.panel; +import static com.android.settings.slices.CustomSliceRegistry.MEDIA_OUTPUT_INDICATOR_SLICE_URI; import static com.android.settings.slices.CustomSliceRegistry.VOLUME_ALARM_URI; import static com.android.settings.slices.CustomSliceRegistry.VOLUME_CALL_URI; import static com.android.settings.slices.CustomSliceRegistry.VOLUME_MEDIA_URI; @@ -55,6 +56,7 @@ public class VolumePanel implements PanelContent { final List uris = new ArrayList<>(); uris.add(VOLUME_REMOTE_MEDIA_URI); uris.add(VOLUME_MEDIA_URI); + uris.add(MEDIA_OUTPUT_INDICATOR_SLICE_URI); uris.add(VOLUME_CALL_URI); uris.add(VOLUME_RINGER_URI); uris.add(VOLUME_ALARM_URI); diff --git a/src/com/android/settings/slices/CustomSliceRegistry.java b/src/com/android/settings/slices/CustomSliceRegistry.java index 9d88bc587e9..6b1ead729c3 100644 --- a/src/com/android/settings/slices/CustomSliceRegistry.java +++ b/src/com/android/settings/slices/CustomSliceRegistry.java @@ -39,6 +39,7 @@ import com.android.settings.homepage.contextualcards.slices.BluetoothDevicesSlic import com.android.settings.homepage.contextualcards.slices.LowStorageSlice; import com.android.settings.homepage.contextualcards.slices.NotificationChannelSlice; import com.android.settings.location.LocationSlice; +import com.android.settings.media.MediaOutputIndicatorSlice; import com.android.settings.media.MediaOutputSlice; import com.android.settings.network.telephony.MobileDataSlice; import com.android.settings.wifi.calling.WifiCallingSliceHelper; @@ -299,6 +300,16 @@ public class CustomSliceRegistry { .appendPath(MediaOutputSliceConstants.KEY_MEDIA_OUTPUT) .build(); + /** + * Backing Uri for the Media output indicator Slice. + */ + public static Uri MEDIA_OUTPUT_INDICATOR_SLICE_URI = new Uri.Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority(SettingsSliceProvider.SLICE_AUTHORITY) + .appendPath(SettingsSlicesContract.PATH_SETTING_INTENT) + .appendPath("media_output_indicator") + .build(); + @VisibleForTesting static final Map> sUriToSlice; @@ -319,6 +330,7 @@ public class CustomSliceRegistry { sUriToSlice.put(STORAGE_SLICE_URI, StorageSlice.class); sUriToSlice.put(WIFI_SLICE_URI, WifiSlice.class); sUriToSlice.put(MEDIA_OUTPUT_SLICE_URI, MediaOutputSlice.class); + sUriToSlice.put(MEDIA_OUTPUT_INDICATOR_SLICE_URI, MediaOutputIndicatorSlice.class); } public static Class getSliceClassByUri(Uri uri) { @@ -344,5 +356,4 @@ public class CustomSliceRegistry { public static boolean isValidAction(String action) { return isValidUri(Uri.parse(action)); } - } diff --git a/src/com/android/settings/wifi/details/WifiDetailPreferenceController.java b/src/com/android/settings/wifi/details/WifiDetailPreferenceController.java index 89565dfb203..5588977554b 100644 --- a/src/com/android/settings/wifi/details/WifiDetailPreferenceController.java +++ b/src/com/android/settings/wifi/details/WifiDetailPreferenceController.java @@ -66,6 +66,7 @@ import com.android.settings.wifi.WifiDialog; import com.android.settings.wifi.WifiDialog.WifiDialogListener; import com.android.settings.wifi.WifiUtils; import com.android.settings.wifi.dpp.WifiDppUtils; +import com.android.settings.wifi.savedaccesspoints.SavedAccessPointsWifiSettings; import com.android.settingslib.core.AbstractPreferenceController; import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; import com.android.settingslib.core.lifecycle.Lifecycle; @@ -75,6 +76,8 @@ import com.android.settingslib.core.lifecycle.events.OnResume; import com.android.settingslib.widget.ActionButtonsPreference; import com.android.settingslib.widget.LayoutPreference; import com.android.settingslib.wifi.AccessPoint; +import com.android.settingslib.wifi.WifiTracker; +import com.android.settingslib.wifi.WifiTrackerFactory; import java.net.Inet4Address; import java.net.Inet6Address; @@ -136,7 +139,9 @@ public class WifiDetailPreferenceController extends AbstractPreferenceController private WifiConfiguration mWifiConfig; private WifiInfo mWifiInfo; private final WifiManager mWifiManager; + private final WifiTracker mWifiTracker; private final MetricsFeatureProvider mMetricsFeatureProvider; + private boolean mIsOutOfRange; // UI elements - in order of appearance private ActionButtonsPreference mButtonsPref; @@ -176,7 +181,7 @@ public class WifiDetailPreferenceController extends AbstractPreferenceController // fall through case WifiManager.NETWORK_STATE_CHANGED_ACTION: case WifiManager.RSSI_CHANGED_ACTION: - updateLiveNetworkInfo(); + updateNetworkInfo(); break; } } @@ -206,14 +211,16 @@ public class WifiDetailPreferenceController extends AbstractPreferenceController @Override public void onCapabilitiesChanged(Network network, NetworkCapabilities nc) { // If the network just validated or lost Internet access, refresh network state. - // Don't do this on every NetworkCapabilities change because refreshNetworkState - // sends IPCs to the system server from the UI thread, which can cause jank. + // Don't do this on every NetworkCapabilities change because update accesspoint notify + // changed for accesspoint changed on the main thread, which can cause jank. if (network.equals(mNetwork) && !nc.equals(mNetworkCapabilities)) { if (hasCapabilityChanged(nc, NET_CAPABILITY_VALIDATED) || hasCapabilityChanged(nc, NET_CAPABILITY_CAPTIVE_PORTAL)) { - refreshNetworkState(); + mAccessPoint.update(mWifiConfig, mWifiInfo, mNetworkInfo); + refreshEntityHeader(); } mNetworkCapabilities = nc; + refreshButtons(); updateIpLayerInfo(); } } @@ -226,6 +233,29 @@ public class WifiDetailPreferenceController extends AbstractPreferenceController } }; + private final WifiTracker.WifiListener mWifiListener = new WifiTracker.WifiListener() { + /** Called when the state of Wifi has changed. */ + public void onWifiStateChanged(int state) { + Log.d(TAG, "onWifiStateChanged(" + state + ")"); + // Do nothing. + } + + /** Called when the connection state of wifi has changed. */ + public void onConnectedChanged() { + Log.d(TAG, "onConnectedChanged"); + // Do nothing. + } + + /** + * Called to indicate the list of AccessPoints has been updated and + * {@link WifiTracker#getAccessPoints()} should be called to get the updated list. + */ + public void onAccessPointsChanged() { + Log.d(TAG, "onAccessPointsChanged"); + updateNetworkInfo(); + } + }; + public static WifiDetailPreferenceController newInstance( AccessPoint accessPoint, ConnectivityManager connectivityManager, @@ -270,6 +300,17 @@ public class WifiDetailPreferenceController extends AbstractPreferenceController mLifecycle = lifecycle; lifecycle.addObserver(this); + + if (SavedAccessPointsWifiSettings.usingDetailsFragment(mContext)) { + mWifiTracker = WifiTrackerFactory.create( + mFragment.getActivity(), + mWifiListener, + mLifecycle, + true /*includeSaved*/, + true /*includeScans*/); + } else { + mWifiTracker = null; + } } @Override @@ -360,7 +401,7 @@ public class WifiDetailPreferenceController extends AbstractPreferenceController mNetwork = mWifiManager.getCurrentNetwork(); mLinkProperties = mConnectivityManager.getLinkProperties(mNetwork); mNetworkCapabilities = mConnectivityManager.getNetworkCapabilities(mNetwork); - updateLiveNetworkInfo(); + updateNetworkInfo(); mContext.registerReceiver(mReceiver, mFilter); mConnectivityManager.registerNetworkCallback(mNetworkRequest, mNetworkCallback, mHandler); @@ -377,72 +418,73 @@ public class WifiDetailPreferenceController extends AbstractPreferenceController mConnectivityManager.unregisterNetworkCallback(mNetworkCallback); } - // TODO(b/124707751): Refactoring the code later, keeping it currently for stability. - protected void updateSavedNetworkInfo() { - mSignalStrengthPref.setVisible(false); - mFrequencyPref.setVisible(false); - mTxLinkSpeedPref.setVisible(false); - mRxLinkSpeedPref.setVisible(false); - - // MAC Address Pref - mMacAddressPref.setSummary(mWifiConfig.getRandomizedMacAddress().toString()); - - refreshEntityHeader(); - - updateIpLayerInfo(); - - // Update whether the forget button should be displayed. - mButtonsPref.setButton1Visible(canForgetNetwork()); - } - - private void updateLiveNetworkInfo() { - // No need to fetch LinkProperties and NetworkCapabilities, they are updated by the - // callbacks. mNetwork doesn't change except in onResume. - mNetworkInfo = mConnectivityManager.getNetworkInfo(mNetwork); - mWifiInfo = mWifiManager.getConnectionInfo(); - if (mNetwork == null || mNetworkInfo == null || mWifiInfo == null) { - exitActivity(); + private void updateNetworkInfo() { + if(!updateAccessPoint()) { return; } - // Update whether the forget button should be displayed. - mButtonsPref.setButton1Visible(canForgetNetwork()); + // refresh header + refreshEntityHeader(); - refreshNetworkState(); + // refresh Buttons + refreshButtons(); // Update Connection Header icon and Signal Strength Preference refreshRssiViews(); - - // MAC Address Pref - mMacAddressPref.setSummary(mWifiInfo.getMacAddress()); - - // Transmit Link Speed Pref - int txLinkSpeedMbps = mWifiInfo.getTxLinkSpeedMbps(); - mTxLinkSpeedPref.setVisible(txLinkSpeedMbps >= 0); - mTxLinkSpeedPref.setSummary(mContext.getString( - R.string.tx_link_speed, mWifiInfo.getTxLinkSpeedMbps())); - - // Receive Link Speed Pref - int rxLinkSpeedMbps = mWifiInfo.getRxLinkSpeedMbps(); - mRxLinkSpeedPref.setVisible(rxLinkSpeedMbps >= 0); - mRxLinkSpeedPref.setSummary(mContext.getString( - R.string.rx_link_speed, mWifiInfo.getRxLinkSpeedMbps())); - // Frequency Pref - final int frequency = mWifiInfo.getFrequency(); - String band = null; - if (frequency >= AccessPoint.LOWER_FREQ_24GHZ - && frequency < AccessPoint.HIGHER_FREQ_24GHZ) { - band = mContext.getResources().getString(R.string.wifi_band_24ghz); - } else if (frequency >= AccessPoint.LOWER_FREQ_5GHZ - && frequency < AccessPoint.HIGHER_FREQ_5GHZ) { - band = mContext.getResources().getString(R.string.wifi_band_5ghz); - } else { - Log.e(TAG, "Unexpected frequency " + frequency); - } - mFrequencyPref.setSummary(band); - + refreshFrequency(); + // Transmit Link Speed Pref + refreshTxSpeed(); + // Receive Link Speed Pref + refreshRxSpeed(); + // IP related information updateIpLayerInfo(); + // MAC Address Pref + refreshMacAddress(); + + } + + private boolean updateAccessPoint() { + boolean changed = false; + if (mWifiTracker != null) { + updateAccessPointFromScannedList(); + // refresh UI if signal level changed for disconnect network. + changed = mRssiSignalLevel != mAccessPoint.getLevel(); + } + + if (mAccessPoint.isActive()) { + // No need to fetch LinkProperties and NetworkCapabilities, they are updated by the + // callbacks. mNetwork doesn't change except in onResume. + mNetworkInfo = mConnectivityManager.getNetworkInfo(mNetwork); + mWifiInfo = mWifiManager.getConnectionInfo(); + if (mNetwork == null || mNetworkInfo == null || mWifiInfo == null) { + exitActivity(); + return false; + } + + changed |= mAccessPoint.update(mWifiConfig, mWifiInfo, mNetworkInfo); + // If feature for saved network not enabled, always return true. + return mWifiTracker == null || changed; + } + + return changed; + } + + private void updateAccessPointFromScannedList() { + mIsOutOfRange = true; + + if (mAccessPoint.getConfig() == null) { + return; + } + + for (AccessPoint ap : mWifiTracker.getAccessPoints()) { + if (ap.getConfig() != null + && mAccessPoint.matches(ap.getConfig())) { + mAccessPoint = ap; + mIsOutOfRange = false; + return; + } + } } private void exitActivity() { @@ -452,14 +494,16 @@ public class WifiDetailPreferenceController extends AbstractPreferenceController mFragment.getActivity().finish(); } - private void refreshNetworkState() { - mAccessPoint.update(mWifiConfig, mWifiInfo, mNetworkInfo); - refreshEntityHeader(); - } - private void refreshRssiViews() { int signalLevel = mAccessPoint.getLevel(); + // Disappears signal view if not in range. e.g. for saved networks. + if (mIsOutOfRange) { + mSignalStrengthPref.setVisible(false); + mRssiSignalLevel = -1; + return; + } + if (mRssiSignalLevel == signalLevel) { return; } @@ -477,6 +521,84 @@ public class WifiDetailPreferenceController extends AbstractPreferenceController mSignalStrengthPref.setIcon(wifiIconDark); mSignalStrengthPref.setSummary(mSignalStr[mRssiSignalLevel]); + mSignalStrengthPref.setVisible(true); + } + + private void refreshFrequency() { + if (mWifiInfo == null) { + mFrequencyPref.setVisible(false); + return; + } + + final int frequency = mWifiInfo.getFrequency(); + String band = null; + if (frequency >= AccessPoint.LOWER_FREQ_24GHZ + && frequency < AccessPoint.HIGHER_FREQ_24GHZ) { + band = mContext.getResources().getString(R.string.wifi_band_24ghz); + } else if (frequency >= AccessPoint.LOWER_FREQ_5GHZ + && frequency < AccessPoint.HIGHER_FREQ_5GHZ) { + band = mContext.getResources().getString(R.string.wifi_band_5ghz); + } else { + Log.e(TAG, "Unexpected frequency " + frequency); + } + mFrequencyPref.setSummary(band); + mFrequencyPref.setVisible(true); + } + + private void refreshTxSpeed() { + if (mWifiInfo == null) { + mTxLinkSpeedPref.setVisible(false); + return; + } + + int txLinkSpeedMbps = mWifiInfo.getTxLinkSpeedMbps(); + mTxLinkSpeedPref.setVisible(txLinkSpeedMbps >= 0); + mTxLinkSpeedPref.setSummary(mContext.getString( + R.string.tx_link_speed, mWifiInfo.getTxLinkSpeedMbps())); + } + + private void refreshRxSpeed() { + if (mWifiInfo == null) { + mRxLinkSpeedPref.setVisible(false); + return; + } + + int rxLinkSpeedMbps = mWifiInfo.getRxLinkSpeedMbps(); + mRxLinkSpeedPref.setVisible(rxLinkSpeedMbps >= 0); + mRxLinkSpeedPref.setSummary(mContext.getString( + R.string.rx_link_speed, mWifiInfo.getRxLinkSpeedMbps())); + } + + private void refreshMacAddress() { + String macAddress = getMacAddress(); + if (macAddress == null) { + mMacAddressPref.setVisible(false); + return; + } + + mMacAddressPref.setVisible(true); + mMacAddressPref.setSummary(macAddress); + } + + private String getMacAddress() { + if (mWifiInfo != null) { + // get MAC address from connected network information + return mWifiInfo.getMacAddress(); + } + + // return randomized MAC address + if (mWifiConfig.macRandomizationSetting == WifiConfiguration.RANDOMIZATION_PERSISTENT) { + return mWifiConfig.getRandomizedMacAddress().toString(); + } + + // return device MAC address + final String[] macAddresses = mWifiManager.getFactoryMacAddresses(); + if (macAddresses != null && macAddresses.length > 0) { + return macAddresses[0]; + } + + Log.e(TAG, "Can't get device MAC address!"); + return null; } private void updatePreference(Preference pref, String detailText) { @@ -488,13 +610,17 @@ public class WifiDetailPreferenceController extends AbstractPreferenceController } } - private void updateIpLayerInfo() { + private void refreshButtons() { + mButtonsPref.setButton1Visible(canForgetNetwork()); mButtonsPref.setButton2Visible(canSignIntoNetwork()); mButtonsPref.setButton3Visible(canShareNetwork()); mButtonsPref.setVisible( canSignIntoNetwork() || canForgetNetwork() || canShareNetwork()); + } - if (mNetwork == null || mLinkProperties == null) { + private void updateIpLayerInfo() { + // Hide IP layer info if not a connected network. + if (!mAccessPoint.isActive() || mNetwork == null || mLinkProperties == null) { mIpAddressPref.setVisible(false); mSubnetPref.setVisible(false); mGatewayPref.setVisible(false); diff --git a/src/com/android/settings/wifi/details/WifiDetailSavedNetworkPreferenceController.java b/src/com/android/settings/wifi/details/WifiDetailSavedNetworkPreferenceController.java deleted file mode 100644 index 3407890aaf3..00000000000 --- a/src/com/android/settings/wifi/details/WifiDetailSavedNetworkPreferenceController.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright (C) 2019 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.wifi.details; - -import android.content.Context; -import android.net.ConnectivityManager; -import android.net.wifi.WifiManager; -import android.os.Handler; - -import androidx.fragment.app.Fragment; - -import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; -import com.android.settingslib.core.lifecycle.Lifecycle; -import com.android.settingslib.wifi.AccessPoint; - -public class WifiDetailSavedNetworkPreferenceController extends WifiDetailPreferenceController { - - WifiDetailSavedNetworkPreferenceController(AccessPoint accessPoint, - ConnectivityManager connectivityManager, Context context, - Fragment fragment, Handler handler, - Lifecycle lifecycle, - WifiManager wifiManager, - MetricsFeatureProvider metricsFeatureProvider, - IconInjector injector) { - super(accessPoint, connectivityManager, context, fragment, handler, lifecycle, wifiManager, - metricsFeatureProvider, injector); - } - - public static WifiDetailSavedNetworkPreferenceController newInstance( - AccessPoint accessPoint, - ConnectivityManager connectivityManager, - Context context, - Fragment fragment, - Handler handler, - Lifecycle lifecycle, - WifiManager wifiManager, - MetricsFeatureProvider metricsFeatureProvider) { - return new WifiDetailSavedNetworkPreferenceController( - accessPoint, connectivityManager, context, fragment, handler, lifecycle, - wifiManager, metricsFeatureProvider, new IconInjector(context)); - } - - @Override - public void onPause() { - // Do nothing - } - - @Override - public void onResume() { - updateSavedNetworkInfo(); - } -} diff --git a/src/com/android/settings/wifi/details/WifiNetworkDetailsFragment.java b/src/com/android/settings/wifi/details/WifiNetworkDetailsFragment.java index 7edd227a559..66587edb54d 100644 --- a/src/com/android/settings/wifi/details/WifiNetworkDetailsFragment.java +++ b/src/com/android/settings/wifi/details/WifiNetworkDetailsFragment.java @@ -51,9 +51,6 @@ public class WifiNetworkDetailsFragment extends DashboardFragment { private static final String TAG = "WifiNetworkDetailsFrg"; - // Extra for if current fragment shows saved network status or not. - public static final String EXTRA_IS_SAVED_NETWORK = "SavedNetwork"; - private AccessPoint mAccessPoint; private WifiDetailPreferenceController mWifiDetailPreferenceController; @@ -126,30 +123,15 @@ public class WifiNetworkDetailsFragment extends DashboardFragment { final List controllers = new ArrayList<>(); final ConnectivityManager cm = context.getSystemService(ConnectivityManager.class); - final boolean isDisplaySavedNetworkDetails = - getArguments().getBoolean(EXTRA_IS_SAVED_NETWORK, false /* defaultValue */); - if (isDisplaySavedNetworkDetails) { - mWifiDetailPreferenceController = - WifiDetailSavedNetworkPreferenceController.newInstance( - mAccessPoint, - cm, - context, - this, - new Handler(Looper.getMainLooper()), // UI thread. - getSettingsLifecycle(), - context.getSystemService(WifiManager.class), - mMetricsFeatureProvider); - } else { - mWifiDetailPreferenceController = WifiDetailPreferenceController.newInstance( - mAccessPoint, - cm, - context, - this, - new Handler(Looper.getMainLooper()), // UI thread. - getSettingsLifecycle(), - context.getSystemService(WifiManager.class), - mMetricsFeatureProvider); - } + mWifiDetailPreferenceController = WifiDetailPreferenceController.newInstance( + mAccessPoint, + cm, + context, + this, + new Handler(Looper.getMainLooper()), // UI thread. + getSettingsLifecycle(), + context.getSystemService(WifiManager.class), + mMetricsFeatureProvider); controllers.add(mWifiDetailPreferenceController); controllers.add(new WifiMeteredPreferenceController(context, mAccessPoint.getConfig())); diff --git a/src/com/android/settings/wifi/savedaccesspoints/SavedAccessPointsWifiSettings.java b/src/com/android/settings/wifi/savedaccesspoints/SavedAccessPointsWifiSettings.java index ea858f32361..3f600e6772d 100644 --- a/src/com/android/settings/wifi/savedaccesspoints/SavedAccessPointsWifiSettings.java +++ b/src/com/android/settings/wifi/savedaccesspoints/SavedAccessPointsWifiSettings.java @@ -108,7 +108,6 @@ public class SavedAccessPointsWifiSettings extends DashboardFragment } final Bundle savedState = new Bundle(); mSelectedAccessPoint.saveWifiState(savedState); - savedState.putBoolean(WifiNetworkDetailsFragment.EXTRA_IS_SAVED_NETWORK, true); new SubSettingLauncher(getContext()) .setTitleText(mSelectedAccessPoint.getTitle()) diff --git a/tests/robotests/res/values-mcc999/config.xml b/tests/robotests/res/values-mcc999/config.xml index 776a4d0902f..1fe4bbeecf0 100644 --- a/tests/robotests/res/values-mcc999/config.xml +++ b/tests/robotests/res/values-mcc999/config.xml @@ -91,4 +91,7 @@ test@test.test + + + intent:#Intent;action=test.test;end diff --git a/tests/robotests/src/com/android/settings/applications/specialaccess/zenaccess/ZenAccessControllerTest.java b/tests/robotests/src/com/android/settings/applications/specialaccess/zenaccess/ZenAccessControllerTest.java index bcb4bb3bf4a..6041e9dead1 100644 --- a/tests/robotests/src/com/android/settings/applications/specialaccess/zenaccess/ZenAccessControllerTest.java +++ b/tests/robotests/src/com/android/settings/applications/specialaccess/zenaccess/ZenAccessControllerTest.java @@ -18,26 +18,41 @@ package com.android.settings.applications.specialaccess.zenaccess; import static com.google.common.truth.Truth.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; + +import android.app.NotificationManager; import android.content.Context; +import com.android.internal.logging.nano.MetricsProto; +import com.android.settings.testutils.FakeFeatureFactory; +import com.android.settings.testutils.shadow.ShadowNotificationManager; + import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; import org.robolectric.shadow.api.Shadow; import org.robolectric.shadows.ShadowActivityManager; @RunWith(RobolectricTestRunner.class) public class ZenAccessControllerTest { + private static final String TEST_PKG = "com.test.package"; + + private FakeFeatureFactory mFeatureFactory; private Context mContext; private ZenAccessController mController; private ShadowActivityManager mActivityManager; + @Before public void setUp() { mContext = RuntimeEnvironment.application; + mFeatureFactory = FakeFeatureFactory.setupForTest(); mController = new ZenAccessController(mContext, "key"); mActivityManager = Shadow.extract(mContext.getSystemService(Context.ACTIVITY_SERVICE)); } @@ -52,4 +67,32 @@ public class ZenAccessControllerTest { mActivityManager.setIsLowRamDevice(true); assertThat(mController.isAvailable()).isFalse(); } + + @Test + public void logSpecialPermissionChange() { + ZenAccessController.logSpecialPermissionChange(true, "app", mContext); + verify(mFeatureFactory.metricsFeatureProvider).action(any(Context.class), + eq(MetricsProto.MetricsEvent.APP_SPECIAL_PERMISSION_DND_ALLOW), + eq("app")); + + ZenAccessController.logSpecialPermissionChange(false, "app", mContext); + verify(mFeatureFactory.metricsFeatureProvider).action(any(Context.class), + eq(MetricsProto.MetricsEvent.APP_SPECIAL_PERMISSION_DND_DENY), + eq("app")); + } + + @Test + @Config(shadows = ShadowNotificationManager.class) + public void hasAccess_granted_yes() { + final ShadowNotificationManager snm = Shadow.extract(mContext.getSystemService( + NotificationManager.class)); + snm.setNotificationPolicyAccessGrantedForPackage(TEST_PKG); + assertThat(ZenAccessController.hasAccess(mContext, TEST_PKG)).isTrue(); + } + + @Test + @Config(shadows = ShadowNotificationManager.class) + public void hasAccess_notGranted_no() { + assertThat(ZenAccessController.hasAccess(mContext, TEST_PKG)).isFalse(); + } } diff --git a/tests/robotests/src/com/android/settings/applications/specialaccess/zenaccess/ZenAccessSettingObserverMixinTest.java b/tests/robotests/src/com/android/settings/applications/specialaccess/zenaccess/ZenAccessSettingObserverMixinTest.java new file mode 100644 index 00000000000..cba1a5199ed --- /dev/null +++ b/tests/robotests/src/com/android/settings/applications/specialaccess/zenaccess/ZenAccessSettingObserverMixinTest.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2019 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.specialaccess.zenaccess; + +import static androidx.lifecycle.Lifecycle.Event.ON_START; +import static androidx.lifecycle.Lifecycle.Event.ON_STOP; + +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +import android.app.ActivityManager; +import android.content.Context; +import android.provider.Settings; + +import androidx.lifecycle.LifecycleOwner; + +import com.android.settingslib.core.lifecycle.Lifecycle; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.shadow.api.Shadow; +import org.robolectric.shadows.ShadowActivityManager; + +@RunWith(RobolectricTestRunner.class) +public class ZenAccessSettingObserverMixinTest { + + @Mock + private ZenAccessSettingObserverMixin.Listener mListener; + + private Context mContext; + private LifecycleOwner mLifecycleOwner; + private Lifecycle mLifecycle; + private ZenAccessSettingObserverMixin mMixin; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mContext = spy(RuntimeEnvironment.application); + mLifecycleOwner = () -> mLifecycle; + mLifecycle = new Lifecycle(mLifecycleOwner); + + mMixin = new ZenAccessSettingObserverMixin(mContext, mListener); + + mLifecycle.addObserver(mMixin); + } + + @Test + public void onStart_lowMemory_shouldNotRegisterListener() { + final ShadowActivityManager sam = Shadow.extract( + mContext.getSystemService(ActivityManager.class)); + sam.setIsLowRamDevice(true); + + mLifecycle.handleLifecycleEvent(ON_START); + + mContext.getContentResolver().notifyChange(Settings.Secure.getUriFor( + Settings.Secure.ENABLED_NOTIFICATION_POLICY_ACCESS_PACKAGES), null); + + verify(mListener, never()).onZenAccessPolicyChanged(); + } + + @Test + public void onStart_highMemory_shouldRegisterListener() { + final ShadowActivityManager sam = Shadow.extract( + mContext.getSystemService(ActivityManager.class)); + sam.setIsLowRamDevice(false); + + mLifecycle.handleLifecycleEvent(ON_START); + + mContext.getContentResolver().notifyChange(Settings.Secure.getUriFor( + Settings.Secure.ENABLED_NOTIFICATION_POLICY_ACCESS_PACKAGES), null); + + verify(mListener).onZenAccessPolicyChanged(); + } + + @Test + public void onStop_shouldUnregisterListener() { + final ShadowActivityManager sam = Shadow.extract( + mContext.getSystemService(ActivityManager.class)); + sam.setIsLowRamDevice(false); + + mLifecycle.handleLifecycleEvent(ON_START); + mLifecycle.handleLifecycleEvent(ON_STOP); + + mContext.getContentResolver().notifyChange(Settings.Secure.getUriFor( + Settings.Secure.ENABLED_NOTIFICATION_POLICY_ACCESS_PACKAGES), null); + + verify(mListener, never()).onZenAccessPolicyChanged(); + } +} diff --git a/tests/robotests/src/com/android/settings/homepage/contextualcards/conditional/GrayscaleConditionControllerTest.java b/tests/robotests/src/com/android/settings/homepage/contextualcards/conditional/GrayscaleConditionControllerTest.java new file mode 100644 index 00000000000..8c24735c77a --- /dev/null +++ b/tests/robotests/src/com/android/settings/homepage/contextualcards/conditional/GrayscaleConditionControllerTest.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2019 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.homepage.contextualcards.conditional; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +import android.content.Context; +import android.hardware.display.ColorDisplayManager; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; + +@RunWith(RobolectricTestRunner.class) +public class GrayscaleConditionControllerTest { + + @Mock + private ConditionManager mConditionManager; + + private ColorDisplayManager mColorDisplayManager; + private Context mContext; + private GrayscaleConditionController mController; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mContext = spy(RuntimeEnvironment.application); + mColorDisplayManager = spy(mContext.getSystemService(ColorDisplayManager.class)); + doReturn(mColorDisplayManager).when(mContext).getSystemService(ColorDisplayManager.class); + mController = new GrayscaleConditionController(mContext, mConditionManager); + } + + @Test + public void isDisplayable_noIntent_shouldReturnFalse() { + assertThat(mController.isDisplayable()).isFalse(); + } + + @Test + @Config(qualifiers = "mcc999") + public void isDisplayable_validIntentAndGrayscaleOn_shouldReturnTrue() { + doReturn(true).when(mColorDisplayManager).isSaturationActivated(); + + assertThat(mController.isDisplayable()).isTrue(); + } + + @Test + @Config(qualifiers = "mcc999") + public void isDisplayable_validIntentAndGrayscaleOff_shouldReturnFalse() { + doReturn(false).when(mColorDisplayManager).isSaturationActivated(); + + assertThat(mController.isDisplayable()).isFalse(); + } + + @Test + public void onActionClick_shouldRefreshCondition() { + mController.onActionClick(); + + verify(mConditionManager).onConditionChanged(); + } +} diff --git a/tests/robotests/src/com/android/settings/media/MediaOutputIndicatorSliceTest.java b/tests/robotests/src/com/android/settings/media/MediaOutputIndicatorSliceTest.java new file mode 100644 index 00000000000..ab3f4de26f9 --- /dev/null +++ b/tests/robotests/src/com/android/settings/media/MediaOutputIndicatorSliceTest.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2019 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.media; + +import static com.android.settings.slices.CustomSliceRegistry.MEDIA_OUTPUT_INDICATOR_SLICE_URI; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.bluetooth.BluetoothAdapter; +import android.content.Context; +import android.content.Intent; + +import androidx.slice.Slice; +import androidx.slice.SliceItem; +import androidx.slice.SliceMetadata; +import androidx.slice.SliceProvider; +import androidx.slice.core.SliceAction; +import androidx.slice.widget.SliceLiveData; + +import com.android.settings.R; +import com.android.settings.testutils.shadow.ShadowBluetoothAdapter; +import com.android.settingslib.media.LocalMediaManager; +import com.android.settingslib.media.MediaDevice; +import com.android.settingslib.media.MediaOutputSliceConstants; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; +import org.robolectric.shadow.api.Shadow; + +import java.util.ArrayList; +import java.util.List; + +@RunWith(RobolectricTestRunner.class) +@Config(shadows = {ShadowBluetoothAdapter.class}) +public class MediaOutputIndicatorSliceTest { + + private static final String TEST_DEVICE_NAME = "test_device_name"; + private static final int TEST_DEVICE_1_ICON = + com.android.internal.R.drawable.ic_bt_headphones_a2dp; + + @Mock + private LocalMediaManager mLocalMediaManager; + + private final List mDevices = new ArrayList<>(); + + private Context mContext; + private MediaOutputIndicatorSlice mMediaOutputIndicatorSlice; + private MediaOutputIndicatorWorker mMediaOutputIndicatorWorker; + private ShadowBluetoothAdapter mShadowBluetoothAdapter; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + mContext = spy(RuntimeEnvironment.application); + + // Set-up specs for SliceMetadata. + SliceProvider.setSpecs(SliceLiveData.SUPPORTED_SPECS); + + mMediaOutputIndicatorSlice = new MediaOutputIndicatorSlice(mContext); + mMediaOutputIndicatorWorker = spy(new MediaOutputIndicatorWorker( + mContext, MEDIA_OUTPUT_INDICATOR_SLICE_URI)); + mMediaOutputIndicatorSlice.mWorker = mMediaOutputIndicatorWorker; + } + + @Test + public void getSlice_invisible_returnNull() { + when(mMediaOutputIndicatorWorker.isVisible()).thenReturn(false); + + assertThat(mMediaOutputIndicatorSlice.getSlice()).isNull(); + } + + @Test + public void getSlice_withActiveDevice_checkContent() { + when(mMediaOutputIndicatorWorker.isVisible()).thenReturn(true); + when(mMediaOutputIndicatorWorker.findActiveDeviceName()).thenReturn(TEST_DEVICE_NAME); + final Slice mediaSlice = mMediaOutputIndicatorSlice.getSlice(); + final SliceMetadata metadata = SliceMetadata.from(mContext, mediaSlice); + // Verify slice title and subtitle + assertThat(metadata.getTitle()).isEqualTo(mContext.getText(R.string.media_output_title)); + assertThat(metadata.getSubtitle()).isEqualTo(TEST_DEVICE_NAME); + } +} diff --git a/tests/robotests/src/com/android/settings/media/MediaOutputIndicatorWorkerTest.java b/tests/robotests/src/com/android/settings/media/MediaOutputIndicatorWorkerTest.java new file mode 100644 index 00000000000..4a5662e53de --- /dev/null +++ b/tests/robotests/src/com/android/settings/media/MediaOutputIndicatorWorkerTest.java @@ -0,0 +1,161 @@ +/* + * Copyright (C) 2019 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.media; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothManager; +import android.content.Context; +import android.net.Uri; + +import com.android.settings.R; +import com.android.settings.bluetooth.Utils; +import com.android.settings.testutils.shadow.ShadowBluetoothUtils; +import com.android.settingslib.bluetooth.A2dpProfile; +import com.android.settingslib.bluetooth.BluetoothEventManager; +import com.android.settingslib.bluetooth.HearingAidProfile; +import com.android.settingslib.bluetooth.LocalBluetoothManager; +import com.android.settingslib.bluetooth.LocalBluetoothProfileManager; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; +import org.robolectric.shadows.ShadowBluetoothDevice; + +import java.util.ArrayList; +import java.util.List; + +@RunWith(RobolectricTestRunner.class) +@Config(shadows = {ShadowBluetoothUtils.class, + ShadowBluetoothDevice.class}) +public class MediaOutputIndicatorWorkerTest { + + private static final String TEST_A2DP_DEVICE_NAME = "Test_A2DP_BT_Device_NAME"; + private static final String TEST_HAP_DEVICE_NAME = "Test_HAP_BT_Device_NAME"; + private static final String TEST_A2DP_DEVICE_ADDRESS = "00:A1:A1:A1:A1:A1"; + private static final String TEST_HAP_DEVICE_ADDRESS = "00:B2:B2:B2:B2:B2"; + private static final Uri URI = Uri.parse("content://com.android.settings.slices/test"); + + @Mock + private A2dpProfile mA2dpProfile; + @Mock + private HearingAidProfile mHearingAidProfile; + @Mock + private LocalBluetoothManager mLocalManager; + @Mock + private BluetoothEventManager mBluetoothEventManager; + @Mock + private LocalBluetoothProfileManager mLocalBluetoothProfileManager; + + private BluetoothAdapter mBluetoothAdapter; + private BluetoothDevice mA2dpDevice; + private BluetoothDevice mHapDevice; + private BluetoothManager mBluetoothManager; + private Context mContext; + private List mDevicesList; + private LocalBluetoothManager mLocalBluetoothManager; + private MediaOutputIndicatorWorker mMediaDeviceUpdateWorker; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mContext = spy(RuntimeEnvironment.application); + ShadowBluetoothUtils.sLocalBluetoothManager = mLocalManager; + mLocalBluetoothManager = Utils.getLocalBtManager(mContext); + when(mLocalBluetoothManager.getEventManager()).thenReturn(mBluetoothEventManager); + when(mLocalBluetoothManager.getProfileManager()).thenReturn(mLocalBluetoothProfileManager); + when(mLocalBluetoothProfileManager.getA2dpProfile()).thenReturn(mA2dpProfile); + when(mLocalBluetoothProfileManager.getHearingAidProfile()).thenReturn(mHearingAidProfile); + mBluetoothManager = new BluetoothManager(mContext); + mBluetoothAdapter = mBluetoothManager.getAdapter(); + + // Setup A2dp device + mA2dpDevice = spy(mBluetoothAdapter.getRemoteDevice(TEST_A2DP_DEVICE_ADDRESS)); + when(mA2dpDevice.getName()).thenReturn(TEST_A2DP_DEVICE_NAME); + when(mA2dpDevice.isConnected()).thenReturn(true); + // Setup HearingAid device + mHapDevice = spy(mBluetoothAdapter.getRemoteDevice(TEST_HAP_DEVICE_ADDRESS)); + when(mHapDevice.getName()).thenReturn(TEST_HAP_DEVICE_NAME); + when(mHapDevice.isConnected()).thenReturn(true); + + mMediaDeviceUpdateWorker = new MediaOutputIndicatorWorker(mContext, URI); + mDevicesList = new ArrayList<>(); + } + + @Test + public void isVisible_noConnectableDevice_returnFalse() { + mDevicesList.clear(); + when(mA2dpProfile.getConnectableDevices()).thenReturn(mDevicesList); + + assertThat(mMediaDeviceUpdateWorker.isVisible()).isFalse(); + } + + @Test + public void isVisible_withConnectableA2dpDevice_returnTrue() { + mDevicesList.clear(); + mDevicesList.add(mA2dpDevice); + when(mHearingAidProfile.getConnectableDevices()).thenReturn(mDevicesList); + + assertThat(mMediaDeviceUpdateWorker.isVisible()).isTrue(); + } + + @Test + public void isVisible_withConnectableHADevice_returnTrue() { + mDevicesList.clear(); + mDevicesList.add(mHapDevice); + when(mA2dpProfile.getConnectableDevices()).thenReturn(mDevicesList); + + assertThat(mMediaDeviceUpdateWorker.isVisible()).isTrue(); + } + + @Test + public void findActiveDeviceName_A2dpDeviceActive_verifyName() { + when(mA2dpProfile.getActiveDevice()).thenReturn(mA2dpDevice); + + assertThat(mMediaDeviceUpdateWorker.findActiveDeviceName()) + .isEqualTo(mA2dpDevice.getAliasName()); + } + + @Test + public void findActiveDeviceName_HADeviceActive_verifyName() { + mDevicesList.add(mHapDevice); + when(mHearingAidProfile.getActiveDevices()).thenReturn(mDevicesList); + + assertThat(mMediaDeviceUpdateWorker.findActiveDeviceName()) + .isEqualTo(mHapDevice.getAliasName()); + } + + @Test + public void findActiveDeviceName_noActiveDevice_verifyDefaultName() { + when(mA2dpProfile.getActiveDevice()).thenReturn(null); + mDevicesList.clear(); + when(mHearingAidProfile.getActiveDevices()).thenReturn(mDevicesList); + + assertThat(mMediaDeviceUpdateWorker.findActiveDeviceName()) + .isEqualTo(mContext.getText(R.string.media_output_default_summary)); + } +} diff --git a/tests/robotests/src/com/android/settings/notification/ZenAccessSettingsTest.java b/tests/robotests/src/com/android/settings/notification/ZenAccessSettingsTest.java deleted file mode 100644 index c2a6f4f892c..00000000000 --- a/tests/robotests/src/com/android/settings/notification/ZenAccessSettingsTest.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * 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 org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.verify; - -import android.content.Context; - -import com.android.internal.logging.nano.MetricsProto; -import com.android.settings.testutils.FakeFeatureFactory; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Answers; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; -import org.robolectric.RobolectricTestRunner; - -@RunWith(RobolectricTestRunner.class) -public class ZenAccessSettingsTest { - - @Mock(answer = Answers.RETURNS_DEEP_STUBS) - private Context mContext; - - private FakeFeatureFactory mFeatureFactory; - - @Before - public void setUp() { - MockitoAnnotations.initMocks(this); - - mFeatureFactory = FakeFeatureFactory.setupForTest(); - } - - @Test - public void logSpecialPermissionChange() { - ZenAccessSettings.logSpecialPermissionChange(true, "app", mContext); - verify(mFeatureFactory.metricsFeatureProvider).action(any(Context.class), - eq(MetricsProto.MetricsEvent.APP_SPECIAL_PERMISSION_DND_ALLOW), - eq("app")); - - ZenAccessSettings.logSpecialPermissionChange(false, "app", mContext); - verify(mFeatureFactory.metricsFeatureProvider).action(any(Context.class), - eq(MetricsProto.MetricsEvent.APP_SPECIAL_PERMISSION_DND_DENY), - eq("app")); - } -} diff --git a/tests/robotests/src/com/android/settings/panel/VolumePanelTest.java b/tests/robotests/src/com/android/settings/panel/VolumePanelTest.java index 4665dc9b4fd..11de7b31b60 100644 --- a/tests/robotests/src/com/android/settings/panel/VolumePanelTest.java +++ b/tests/robotests/src/com/android/settings/panel/VolumePanelTest.java @@ -48,6 +48,7 @@ public class VolumePanelTest { CustomSliceRegistry.VOLUME_REMOTE_MEDIA_URI, CustomSliceRegistry.VOLUME_CALL_URI, CustomSliceRegistry.VOLUME_MEDIA_URI, + CustomSliceRegistry.MEDIA_OUTPUT_INDICATOR_SLICE_URI, CustomSliceRegistry.VOLUME_RINGER_URI, CustomSliceRegistry.VOLUME_ALARM_URI); } diff --git a/tests/robotests/src/com/android/settings/testutils/shadow/ShadowNotificationManager.java b/tests/robotests/src/com/android/settings/testutils/shadow/ShadowNotificationManager.java index 83257776289..78fb23f3133 100644 --- a/tests/robotests/src/com/android/settings/testutils/shadow/ShadowNotificationManager.java +++ b/tests/robotests/src/com/android/settings/testutils/shadow/ShadowNotificationManager.java @@ -19,15 +19,19 @@ package com.android.settings.testutils.shadow; import android.app.NotificationManager; import android.net.Uri; import android.service.notification.ZenModeConfig; +import android.util.ArraySet; import org.robolectric.annotation.Implementation; import org.robolectric.annotation.Implements; +import java.util.Set; + @Implements(NotificationManager.class) public class ShadowNotificationManager { private int mZenMode; private ZenModeConfig mZenModeConfig; + private Set mNotificationPolicyGrantedPackages = new ArraySet<>(); @Implementation protected void setZenMode(int mode, Uri conditionId, String reason) { @@ -39,6 +43,11 @@ public class ShadowNotificationManager { return mZenMode; } + @Implementation + protected boolean isNotificationPolicyAccessGrantedForPackage(String pkg) { + return mNotificationPolicyGrantedPackages.contains(pkg); + } + @Implementation public ZenModeConfig getZenModeConfig() { return mZenModeConfig; @@ -47,4 +56,8 @@ public class ShadowNotificationManager { public void setZenModeConfig(ZenModeConfig config) { mZenModeConfig = config; } + + public void setNotificationPolicyAccessGrantedForPackage(String pkg) { + mNotificationPolicyGrantedPackages.add(pkg); + } }