From 865bdb2838910da9526e5fb065a537ccaa433ff9 Mon Sep 17 00:00:00 2001 From: Daniel Sandler Date: Fri, 22 Mar 2013 18:29:51 -0700 Subject: [PATCH 1/3] Reference new StatusBarNotification and INotificationListener packages. Requires change I1be46f82 in f/b. Bug: 8199624 Change-Id: I2ebf7fde793dd932c5fadb308f8a0b93e18d9afa --- src/com/android/settings/NotificationStation.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/com/android/settings/NotificationStation.java b/src/com/android/settings/NotificationStation.java index 1badfca334d..1a44542b285 100644 --- a/src/com/android/settings/NotificationStation.java +++ b/src/com/android/settings/NotificationStation.java @@ -18,9 +18,11 @@ package com.android.settings; import android.app.Activity; import android.app.ActivityManager; -import android.app.INotificationListener; +import android.content.ComponentName; +import android.service.notification.INotificationListener; import android.app.INotificationManager; import android.app.Notification; +import android.service.notification.StatusBarNotification; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; @@ -45,7 +47,6 @@ import android.widget.DateTimeView; import android.widget.ImageView; import android.widget.ListView; import android.widget.TextView; -import com.android.internal.statusbar.StatusBarNotification; import java.util.ArrayList; import java.util.Comparator; @@ -107,7 +108,8 @@ public class NotificationStation extends SettingsPreferenceFragment { ServiceManager.getService(Context.NOTIFICATION_SERVICE)); try { mNoMan.registerListener(mListener, - mContext.getPackageName(), + new ComponentName(mContext.getPackageName(), + this.getClass().getCanonicalName()), ActivityManager.getCurrentUser()); } catch (RemoteException e) { // well, that didn't work out From 8f511b17099517ff76f6d2938a4c5983b8df1701 Mon Sep 17 00:00:00 2001 From: Daniel Sandler Date: Thu, 4 Apr 2013 14:30:04 -0400 Subject: [PATCH 2/3] Notification listeners can be enabled in Settings>Security. Also known as android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS. Bug: 8454150 Change-Id: I0c2433bf58ba4c78cd461326bd014535c7f67578 --- AndroidManifest.xml | 15 + res/layout/notification_access_settings.xml | 37 +++ res/layout/notification_listener_item.xml | 77 +++++ res/values/strings.xml | 31 ++ res/xml/security_settings_misc.xml | 7 + .../settings/NotificationAccessSettings.java | 312 ++++++++++++++++++ .../android/settings/SecuritySettings.java | 22 ++ src/com/android/settings/Settings.java | 1 + 8 files changed, 502 insertions(+) create mode 100644 res/layout/notification_access_settings.xml create mode 100644 res/layout/notification_listener_item.xml create mode 100644 src/com/android/settings/NotificationAccessSettings.java diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 143a394a5f8..e9218e13f5e 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -1572,6 +1572,21 @@ android:resource="@id/user_settings" /> + + + + + + + + + + + + + + + + + + diff --git a/res/layout/notification_listener_item.xml b/res/layout/notification_listener_item.xml new file mode 100644 index 00000000000..67aaa61725d --- /dev/null +++ b/res/layout/notification_listener_item.xml @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + diff --git a/res/values/strings.xml b/res/values/strings.xml index 573f4a0ef61..e56c04913a5 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -983,6 +983,37 @@ View or deactivate device administrators + + + + Notification access + + + Apps cannot read notifications + + + + %d app can read notifications + %d apps can read notifications + + + + No notification listeners are installed. + + + Enable + %1$s? + + + %1$s will be able to + read all notifications posted by the system or any installed app, which may include personal + information such as contact names and the text of messages sent to you. It will also be able + to dismiss these notifications or touch action buttons within them. + + Bluetooth diff --git a/res/xml/security_settings_misc.xml b/res/xml/security_settings_misc.xml index 10fc98f2b4f..1ead7a9ff3f 100644 --- a/res/xml/security_settings_misc.xml +++ b/res/xml/security_settings_misc.xml @@ -58,6 +58,13 @@ android:summaryOff="@string/verify_applications_summary" android:summaryOn="@string/verify_applications_summary" android:persistent="false" /> + + + mEnabledListeners = new HashSet(); + private ListenerListAdapter mList; + + private final Uri ENABLED_NOTIFICATION_LISTENERS_URI + = Settings.Secure.getUriFor(Settings.Secure.ENABLED_NOTIFICATION_LISTENERS); + + private final ContentObserver mSettingsObserver = new ContentObserver(new Handler()) { + @Override + public void onChange(boolean selfChange, Uri uri) { + updateList(); + } + }; + + private final BroadcastReceiver mPackageReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + updateList(); + } + }; + + public class ListenerWarningDialogFragment extends DialogFragment { + static final String KEY_COMPONENT = "c"; + static final String KEY_LABEL = "l"; + + public ListenerWarningDialogFragment setListenerInfo(ComponentName cn, String label) { + Bundle args = new Bundle(); + args.putString(KEY_COMPONENT, cn.flattenToString()); + args.putString(KEY_LABEL, label); + setArguments(args); + + return this; + } + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + final Bundle args = getArguments(); + final String label = args.getString(KEY_LABEL); + final ComponentName cn = ComponentName.unflattenFromString(args.getString(KEY_COMPONENT)); + + final String title = getResources().getString( + R.string.notification_listener_security_warning_title, label); + final String summary = getResources().getString( + R.string.notification_listener_security_warning_summary, label); + return new AlertDialog.Builder(getActivity()) + .setMessage(summary) + .setTitle(title) + .setIconAttribute(android.R.attr.alertDialogIcon) + .setCancelable(true) + .setPositiveButton(android.R.string.ok, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + mEnabledListeners.add(cn); + saveEnabledListeners(); + } + }) + .setNegativeButton(android.R.string.cancel, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + // pass + } + }) + .create(); + } + } + + @Override + public void onCreate(Bundle icicle) { + super.onCreate(icicle); + + mPM = getActivity().getPackageManager(); + mCR = getActivity().getContentResolver(); + mList = new ListenerListAdapter(getActivity()); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + return inflater.inflate(R.layout.device_admin_settings, container, false); + } + + @Override + public void onResume() { + super.onResume(); + updateList(); + + // listen for package changes + IntentFilter filter = new IntentFilter(); + filter.addAction(Intent.ACTION_PACKAGE_ADDED); + filter.addAction(Intent.ACTION_PACKAGE_CHANGED); + filter.addAction(Intent.ACTION_PACKAGE_REMOVED); + filter.addAction(Intent.ACTION_PACKAGE_REPLACED); + filter.addDataScheme("package"); + getActivity().registerReceiver(mPackageReceiver, filter); + + mCR.registerContentObserver(ENABLED_NOTIFICATION_LISTENERS_URI, false, mSettingsObserver); + } + + @Override + public void onPause() { + super.onPause(); + + getActivity().unregisterReceiver(mPackageReceiver); + mCR.unregisterContentObserver(mSettingsObserver); + } + + void loadEnabledListeners() { + mEnabledListeners.clear(); + final String flat = Settings.Secure.getString(mCR, + Settings.Secure.ENABLED_NOTIFICATION_LISTENERS); + if (flat != null && !"".equals(flat)) { + final String[] names = flat.split(":"); + for (int i = 0; i < names.length; i++) { + final ComponentName cn = ComponentName.unflattenFromString(names[i]); + if (cn != null) { + mEnabledListeners.add(cn); + } + } + } + } + + void saveEnabledListeners() { + StringBuilder sb = null; + for (ComponentName cn : mEnabledListeners) { + if (sb == null) { + sb = new StringBuilder(); + } else { + sb.append(':'); + } + sb.append(cn.flattenToString()); + } + Settings.Secure.putString(mCR, + Settings.Secure.ENABLED_NOTIFICATION_LISTENERS, + sb != null ? sb.toString() : ""); + } + + void updateList() { + mList.clear(); + + loadEnabledListeners(); + + final int user = ActivityManager.getCurrentUser(); + + List installedServices = mPM.queryIntentServicesAsUser( + new Intent(NotificationListenerService.SERVICE_INTERFACE), + PackageManager.GET_SERVICES | PackageManager.GET_META_DATA, + user); + + for (int i = 0, count = installedServices.size(); i < count; i++) { + ResolveInfo resolveInfo = installedServices.get(i); + ServiceInfo info = resolveInfo.serviceInfo; + + if (!android.Manifest.permission.BIND_NOTIFICATION_LISTENER_SERVICE.equals( + info.permission)) { + Slog.w(TAG, "Skipping notification listener service " + + info.packageName + "/" + info.name + + ": it does not require the permission " + + android.Manifest.permission.BIND_NOTIFICATION_LISTENER_SERVICE); + continue; + } + + mList.add(info); + } + + mList.sort(new PackageItemInfo.DisplayNameComparator(mPM)); + + getListView().setAdapter(mList); + } + + boolean isListenerEnabled(ServiceInfo info) { + final ComponentName cn = new ComponentName(info.packageName, info.name); + return mEnabledListeners.contains(cn); + } + + @Override + public void onListItemClick(ListView l, View v, int position, long id) { + ServiceInfo info = mList.getItem(position); + final ComponentName cn = new ComponentName(info.packageName, info.name); + if (mEnabledListeners.contains(cn)) { + // the simple version: disabling + mEnabledListeners.remove(cn); + saveEnabledListeners(); + } else { + // show a scary dialog + new ListenerWarningDialogFragment() + .setListenerInfo(cn, info.loadLabel(mPM).toString()) + .show(getFragmentManager(), "dialog"); + } + } + + static class ViewHolder { + ImageView icon; + TextView name; + CheckBox checkbox; + TextView description; + } + + class ListenerListAdapter extends ArrayAdapter { + final LayoutInflater mInflater; + + ListenerListAdapter(Context context) { + super(context, 0, 0); + mInflater = (LayoutInflater) + getActivity().getSystemService(Context.LAYOUT_INFLATER_SERVICE); + } + + public boolean hasStableIds() { + return true; + } + + public long getItemId(int position) { + return position; + } + + public View getView(int position, View convertView, ViewGroup parent) { + View v; + if (convertView == null) { + v = newView(parent); + } else { + v = convertView; + } + bindView(v, position); + return v; + } + + public View newView(ViewGroup parent) { + View v = mInflater.inflate(R.layout.notification_listener_item, parent, false); + ViewHolder h = new ViewHolder(); + h.icon = (ImageView) v.findViewById(R.id.icon); + h.name = (TextView) v.findViewById(R.id.name); + h.checkbox = (CheckBox) v.findViewById(R.id.checkbox); + h.description = (TextView) v.findViewById(R.id.description); + v.setTag(h); + return v; + } + + public void bindView(View view, int position) { + ViewHolder vh = (ViewHolder) view.getTag(); + ServiceInfo info = getItem(position); + + vh.icon.setImageDrawable(info.loadIcon(mPM)); + vh.name.setText(info.loadLabel(mPM)); + if (SHOW_PACKAGE_NAME) { + vh.description.setText(info.packageName); + vh.description.setVisibility(View.VISIBLE); + } else { + vh.description.setVisibility(View.GONE); + } + vh.checkbox.setChecked(isListenerEnabled(info)); + } + } +} diff --git a/src/com/android/settings/SecuritySettings.java b/src/com/android/settings/SecuritySettings.java index 971a196096f..d75d6305910 100644 --- a/src/com/android/settings/SecuritySettings.java +++ b/src/com/android/settings/SecuritySettings.java @@ -79,6 +79,7 @@ public class SecuritySettings extends SettingsPreferenceFragment private static final String KEY_TOGGLE_VERIFY_APPLICATIONS = "toggle_verify_applications"; private static final String KEY_POWER_INSTANTLY_LOCKS = "power_button_instantly_locks"; private static final String KEY_CREDENTIALS_MANAGER = "credentials_management"; + private static final String KEY_NOTIFICATION_ACCESS = "manage_notification_access"; private static final String PACKAGE_MIME_TYPE = "application/vnd.android.package-archive"; DevicePolicyManager mDPM; @@ -100,6 +101,8 @@ public class SecuritySettings extends SettingsPreferenceFragment private CheckBoxPreference mToggleVerifyApps; private CheckBoxPreference mPowerButtonInstantlyLocks; + private Preference mNotificationAccess; + private boolean mIsPrimary; @Override @@ -283,9 +286,28 @@ public class SecuritySettings extends SettingsPreferenceFragment } } + final int n = getNumEnabledNotificationListeners(); + mNotificationAccess = findPreference(KEY_NOTIFICATION_ACCESS); + if (n == 0) { + mNotificationAccess.setSummary(getResources().getString( + R.string.manage_notification_access_summary_zero)); + } else { + mNotificationAccess.setSummary(String.format(getResources().getQuantityString( + R.plurals.manage_notification_access_summary_nonzero, + n, n))); + } + return root; } + private int getNumEnabledNotificationListeners() { + final String flat = Settings.Secure.getString(getContentResolver(), + Settings.Secure.ENABLED_NOTIFICATION_LISTENERS); + if (flat == null || "".equals(flat)) return 0; + final String[] components = flat.split(":"); + return components.length; + } + private boolean isNonMarketAppsAllowed() { return Settings.Global.getInt(getContentResolver(), Settings.Global.INSTALL_NON_MARKET_APPS, 0) > 0; diff --git a/src/com/android/settings/Settings.java b/src/com/android/settings/Settings.java index e29874bd869..ed3685d685e 100644 --- a/src/com/android/settings/Settings.java +++ b/src/com/android/settings/Settings.java @@ -840,4 +840,5 @@ public class Settings extends PreferenceActivity public static class DreamSettingsActivity extends Settings { /* empty */ } public static class NotificationStationActivity extends Settings { /* empty */ } public static class UserSettingsActivity extends Settings { /* empty */ } + public static class NotificationAccessSettingsActivity extends Settings { /* empty */ } } From 3727018a5af2dd5755dfc84158b20d49253fd6b0 Mon Sep 17 00:00:00 2001 From: Amith Yamasani Date: Tue, 9 Apr 2013 10:22:47 -0700 Subject: [PATCH 3/3] Potential fix for a monkey-related NPE Cache the content resolver so that async callbacks, events can continue to get the cntent resolver when detached from the activity. This should help with stability. Bug: 8005271 Change-Id: Ia566bc312ce678b438004bdf4ed7537ac1604608 --- .../android/settings/SettingsPreferenceFragment.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/com/android/settings/SettingsPreferenceFragment.java b/src/com/android/settings/SettingsPreferenceFragment.java index 42c0b404aab..60765a99da0 100644 --- a/src/com/android/settings/SettingsPreferenceFragment.java +++ b/src/com/android/settings/SettingsPreferenceFragment.java @@ -20,6 +20,7 @@ import android.app.Dialog; import android.app.DialogFragment; import android.app.Fragment; import android.content.ContentResolver; +import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.pm.PackageManager; @@ -48,6 +49,9 @@ public class SettingsPreferenceFragment extends PreferenceFragment implements Di private String mHelpUrl; + // Cache the content resolver for async callbacks + private ContentResolver mContentResolver; + @Override public void onCreate(Bundle icicle) { super.onCreate(icicle); @@ -104,7 +108,11 @@ public class SettingsPreferenceFragment extends PreferenceFragment implements Di * Returns the ContentResolver from the owning Activity. */ protected ContentResolver getContentResolver() { - return getActivity().getContentResolver(); + Context context = getActivity(); + if (context != null) { + mContentResolver = context.getContentResolver(); + } + return mContentResolver; } /**