Merge "Create a page to manage dnd permission for individual app"

This commit is contained in:
Fan Zhang
2019-03-19 17:14:07 +00:00
committed by Android (Google) Code Review
12 changed files with 635 additions and 284 deletions

View File

@@ -7939,6 +7939,9 @@
<!-- Sound & notification > Advanced section: Title for managing Do Not Disturb access option. [CHAR LIMIT=40] --> <!-- Sound & notification > Advanced section: Title for managing Do Not Disturb access option. [CHAR LIMIT=40] -->
<string name="manage_zen_access_title">Do Not Disturb access</string> <string name="manage_zen_access_title">Do Not Disturb access</string>
<!-- Button title that grants 'Do Not Disturb' permission to an app [CHAR_LIMIT=60]-->
<string name="zen_access_detail_switch">Allow Do Not Disturb</string>
<!-- Sound & notification > Do Not Disturb access > Text to display when the list is empty. [CHAR LIMIT=NONE] --> <!-- Sound & notification > Do Not Disturb access > Text to display when the list is empty. [CHAR LIMIT=NONE] -->
<string name="zen_access_empty_text">No installed apps have requested Do Not Disturb access</string> <string name="zen_access_empty_text">No installed apps have requested Do Not Disturb access</string>

View File

@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android"
android:key="zen_access_permission_detail_settings"
android:title="@string/manage_zen_access_title">
<SwitchPreference
android:key="zen_access_switch"
android:title="@string/zen_access_detail_switch"/>
</PreferenceScreen>

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -17,12 +17,29 @@
package com.android.settings.applications.specialaccess.zenaccess; package com.android.settings.applications.specialaccess.zenaccess;
import android.app.ActivityManager; import android.app.ActivityManager;
import android.app.AppGlobals;
import android.app.NotificationManager;
import android.app.settings.SettingsEnums;
import android.content.Context; 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.core.BasePreferenceController;
import com.android.settings.overlay.FeatureFactory;
import java.util.List;
import java.util.Set;
public class ZenAccessController extends BasePreferenceController { public class ZenAccessController extends BasePreferenceController {
private static final String TAG = "ZenAccessController";
private final ActivityManager mActivityManager; private final ActivityManager mActivityManager;
public ZenAccessController(Context context, String preferenceKey) { public ZenAccessController(Context context, String preferenceKey) {
@@ -32,8 +49,68 @@ public class ZenAccessController extends BasePreferenceController {
@Override @Override
public int getAvailabilityStatus() { public int getAvailabilityStatus() {
return !mActivityManager.isLowRamDevice() return isSupported(mActivityManager)
? AVAILABLE_UNSEARCHABLE ? AVAILABLE_UNSEARCHABLE
: UNSUPPORTED_ON_DEVICE; : UNSUPPORTED_ON_DEVICE;
} }
public static boolean isSupported(ActivityManager activityManager) {
return !activityManager.isLowRamDevice();
}
public static Set<String> getPackagesRequestingNotificationPolicyAccess() {
final ArraySet<String> 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<PackageInfo> 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<String> getAutoApprovedPackages(Context context) {
final Set<String> 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);
}
} }

View File

@@ -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<String> 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<String> 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();
}
}

View File

@@ -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 */);
}
}

View File

@@ -18,56 +18,40 @@ package com.android.settings.notification;
import android.annotation.Nullable; import android.annotation.Nullable;
import android.app.ActivityManager; import android.app.ActivityManager;
import android.app.AppGlobals;
import android.app.Dialog;
import android.app.NotificationManager; import android.app.NotificationManager;
import android.app.settings.SettingsEnums; import android.app.settings.SettingsEnums;
import android.content.Context; import android.content.Context;
import android.content.DialogInterface;
import android.content.pm.ApplicationInfo; import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageItemInfo; import android.content.pm.PackageItemInfo;
import android.content.pm.PackageManager; 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.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.RemoteException;
import android.provider.SearchIndexableResource; import android.provider.SearchIndexableResource;
import android.provider.Settings.Secure;
import android.text.TextUtils;
import android.util.ArraySet; import android.util.ArraySet;
import android.util.Log;
import android.view.View; 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.PreferenceScreen;
import androidx.preference.SwitchPreference;
import com.android.settings.R; import com.android.settings.R;
import com.android.settings.core.instrumentation.InstrumentedDialogFragment; import com.android.settings.applications.AppInfoBase;
import com.android.settings.overlay.FeatureFactory; 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.BaseSearchIndexProvider;
import com.android.settings.search.Indexable; import com.android.settings.search.Indexable;
import com.android.settings.widget.AppSwitchPreference;
import com.android.settings.widget.EmptyTextSettings; import com.android.settings.widget.EmptyTextSettings;
import com.android.settingslib.search.SearchIndexable; import com.android.settingslib.search.SearchIndexable;
import com.android.settingslib.widget.apppreference.AppPreference;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Set;
@SearchIndexable @SearchIndexable
public class ZenAccessSettings extends EmptyTextSettings { public class ZenAccessSettings extends EmptyTextSettings implements
ZenAccessSettingObserverMixin.Listener {
private final String TAG = "ZenAccessSettings"; private final String TAG = "ZenAccessSettings";
private final SettingObserver mObserver = new SettingObserver();
private Context mContext; private Context mContext;
private PackageManager mPkgMan; private PackageManager mPkgMan;
private NotificationManager mNoMan; private NotificationManager mNoMan;
@@ -84,6 +68,8 @@ public class ZenAccessSettings extends EmptyTextSettings {
mContext = getActivity(); mContext = getActivity();
mPkgMan = mContext.getPackageManager(); mPkgMan = mContext.getPackageManager();
mNoMan = mContext.getSystemService(NotificationManager.class); mNoMan = mContext.getSystemService(NotificationManager.class);
getSettingsLifecycle().addObserver(
new ZenAccessSettingObserverMixin(getContext(), this /* listener */));
} }
@Override @Override
@@ -102,30 +88,22 @@ public class ZenAccessSettings extends EmptyTextSettings {
super.onResume(); super.onResume();
if (!ActivityManager.isLowRamDeviceStatic()) { if (!ActivityManager.isLowRamDeviceStatic()) {
reloadList(); reloadList();
getContentResolver().registerContentObserver(
Secure.getUriFor(Secure.ENABLED_NOTIFICATION_POLICY_ACCESS_PACKAGES), false,
mObserver);
getContentResolver().registerContentObserver(
Secure.getUriFor(Secure.ENABLED_NOTIFICATION_LISTENERS), false,
mObserver);
} else { } else {
setEmptyText(R.string.disabled_low_ram_device); setEmptyText(R.string.disabled_low_ram_device);
} }
} }
@Override @Override
public void onPause() { public void onZenAccessPolicyChanged() {
super.onPause(); reloadList();
if (!ActivityManager.isLowRamDeviceStatic()) {
getContentResolver().unregisterContentObserver(mObserver);
}
} }
private void reloadList() { private void reloadList() {
final PreferenceScreen screen = getPreferenceScreen(); final PreferenceScreen screen = getPreferenceScreen();
screen.removeAll(); screen.removeAll();
final ArrayList<ApplicationInfo> apps = new ArrayList<>(); final ArrayList<ApplicationInfo> apps = new ArrayList<>();
final ArraySet<String> requesting = getPackagesRequestingNotificationPolicyAccess(); final Set<String> requesting =
ZenAccessController.getPackagesRequestingNotificationPolicyAccess();
if (!requesting.isEmpty()) { if (!requesting.isEmpty()) {
final List<ApplicationInfo> installed = mPkgMan.getInstalledApplications(0); final List<ApplicationInfo> installed = mPkgMan.getInstalledApplications(0);
if (installed != null) { if (installed != null) {
@@ -143,204 +121,42 @@ public class ZenAccessSettings extends EmptyTextSettings {
for (ApplicationInfo app : apps) { for (ApplicationInfo app : apps) {
final String pkg = app.packageName; final String pkg = app.packageName;
final CharSequence label = app.loadLabel(mPkgMan); final CharSequence label = app.loadLabel(mPkgMan);
final SwitchPreference pref = new AppSwitchPreference(getPrefContext()); final AppPreference pref = new AppPreference(getPrefContext());
pref.setKey(pkg); pref.setKey(pkg);
pref.setPersistent(false);
pref.setIcon(app.loadIcon(mPkgMan)); pref.setIcon(app.loadIcon(mPkgMan));
pref.setTitle(label); pref.setTitle(label);
pref.setChecked(hasAccess(pkg));
if (autoApproved.contains(pkg)) { if (autoApproved.contains(pkg)) {
//Auto approved, user cannot do anything. Hard code summary and disable preference.
pref.setEnabled(false); pref.setEnabled(false);
pref.setSummary(getString(R.string.zen_access_disabled_package_warning)); 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() { pref.setOnPreferenceClickListener(preference -> {
@Override AppInfoBase.startAppInfoFragment(
public boolean onPreferenceChange(Preference preference, Object newValue) { ZenAccessDetails.class /* fragment */,
final boolean access = (Boolean) newValue; R.string.manage_zen_access_title /* titleRes */,
if (access) { pkg,
new ScaryWarningDialogFragment() app.uid,
.setPkgInfo(pkg, label) this /* source */,
.show(getFragmentManager(), "dialog"); -1 /* requestCode */,
} else { getMetricsCategory() /* sourceMetricsCategory */);
new FriendlyWarningDialogFragment() return true;
.setPkgInfo(pkg, label)
.show(getFragmentManager(), "dialog");
}
return false;
}
}); });
screen.addPreference(pref); screen.addPreference(pref);
} }
} }
private ArraySet<String> getPackagesRequestingNotificationPolicyAccess() {
ArraySet<String> 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<PackageInfo> 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 { private int getPreferenceSummary(String packageName) {
static final String KEY_PKG = "p"; final boolean enabled = ZenAccessController.hasAccess(getContext(), packageName);
static final String KEY_LABEL = "l"; return enabled ? R.string.app_permission_summary_allowed
: R.string.app_permission_summary_not_allowed;
@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();
}
} }
public static final Indexable.SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = public static final Indexable.SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER =

View File

@@ -18,26 +18,41 @@ package com.android.settings.applications.specialaccess.zenaccess;
import static com.google.common.truth.Truth.assertThat; 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 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.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner; import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment; import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
import org.robolectric.shadow.api.Shadow; import org.robolectric.shadow.api.Shadow;
import org.robolectric.shadows.ShadowActivityManager; import org.robolectric.shadows.ShadowActivityManager;
@RunWith(RobolectricTestRunner.class) @RunWith(RobolectricTestRunner.class)
public class ZenAccessControllerTest { public class ZenAccessControllerTest {
private static final String TEST_PKG = "com.test.package";
private FakeFeatureFactory mFeatureFactory;
private Context mContext; private Context mContext;
private ZenAccessController mController; private ZenAccessController mController;
private ShadowActivityManager mActivityManager; private ShadowActivityManager mActivityManager;
@Before @Before
public void setUp() { public void setUp() {
mContext = RuntimeEnvironment.application; mContext = RuntimeEnvironment.application;
mFeatureFactory = FakeFeatureFactory.setupForTest();
mController = new ZenAccessController(mContext, "key"); mController = new ZenAccessController(mContext, "key");
mActivityManager = Shadow.extract(mContext.getSystemService(Context.ACTIVITY_SERVICE)); mActivityManager = Shadow.extract(mContext.getSystemService(Context.ACTIVITY_SERVICE));
} }
@@ -52,4 +67,32 @@ public class ZenAccessControllerTest {
mActivityManager.setIsLowRamDevice(true); mActivityManager.setIsLowRamDevice(true);
assertThat(mController.isAvailable()).isFalse(); 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();
}
} }

View File

@@ -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();
}
}

View File

@@ -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"));
}
}

View File

@@ -19,15 +19,19 @@ package com.android.settings.testutils.shadow;
import android.app.NotificationManager; import android.app.NotificationManager;
import android.net.Uri; import android.net.Uri;
import android.service.notification.ZenModeConfig; import android.service.notification.ZenModeConfig;
import android.util.ArraySet;
import org.robolectric.annotation.Implementation; import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements; import org.robolectric.annotation.Implements;
import java.util.Set;
@Implements(NotificationManager.class) @Implements(NotificationManager.class)
public class ShadowNotificationManager { public class ShadowNotificationManager {
private int mZenMode; private int mZenMode;
private ZenModeConfig mZenModeConfig; private ZenModeConfig mZenModeConfig;
private Set<String> mNotificationPolicyGrantedPackages = new ArraySet<>();
@Implementation @Implementation
protected void setZenMode(int mode, Uri conditionId, String reason) { protected void setZenMode(int mode, Uri conditionId, String reason) {
@@ -39,6 +43,11 @@ public class ShadowNotificationManager {
return mZenMode; return mZenMode;
} }
@Implementation
protected boolean isNotificationPolicyAccessGrantedForPackage(String pkg) {
return mNotificationPolicyGrantedPackages.contains(pkg);
}
@Implementation @Implementation
public ZenModeConfig getZenModeConfig() { public ZenModeConfig getZenModeConfig() {
return mZenModeConfig; return mZenModeConfig;
@@ -47,4 +56,8 @@ public class ShadowNotificationManager {
public void setZenModeConfig(ZenModeConfig config) { public void setZenModeConfig(ZenModeConfig config) {
mZenModeConfig = config; mZenModeConfig = config;
} }
public void setNotificationPolicyAccessGrantedForPackage(String pkg) {
mNotificationPolicyGrantedPackages.add(pkg);
}
} }