Notification listeners can be enabled in Settings>Security.

Also known as android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS.

Bug: 8454150
Change-Id: I0c2433bf58ba4c78cd461326bd014535c7f67578
This commit is contained in:
Daniel Sandler
2013-04-04 14:30:04 -04:00
committed by Android (Google) Code Review
parent e44f5e2bd6
commit 79b9bfe56e
8 changed files with 502 additions and 0 deletions

View File

@@ -1572,6 +1572,21 @@
android:resource="@id/user_settings" />
</activity>
<activity android:name="Settings$NotificationAccessSettingsActivity"
android:label="@string/manage_notification_access"
android:taskAffinity=""
android:excludeFromRecents="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<action android:name="android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<meta-data android:name="com.android.settings.FRAGMENT_CLASS"
android:value="com.android.settings.NotificationAccessSettings" />
<meta-data android:name="com.android.settings.TOP_LEVEL_HEADER_ID"
android:resource="@id/security_settings" />
</activity>
<receiver android:name=".widget.SettingsAppWidgetProvider"
android:label="@string/gadget_title"
android:exported="true"

View File

@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2013 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.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="0px"
android:layout_weight="1">
<ListView android:id="@android:id/list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:drawSelectorOnTop="false"
android:fastScrollEnabled="true" />
<TextView android:id="@android:id/empty"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:text="@string/no_notification_listeners"
android:textAppearance="?android:attr/textAppearanceMedium" />
</FrameLayout>
</LinearLayout>

View File

@@ -0,0 +1,77 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
/*
** Copyright 2013, 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.
*/
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="?android:attr/listPreferredItemHeight"
android:orientation="horizontal"
android:paddingEnd="6dip"
android:gravity="center_vertical"
>
<ImageView
android:id="@+id/icon"
android:layout_width="@android:dimen/app_icon_size"
android:layout_height="@android:dimen/app_icon_size"
android:layout_gravity="center_vertical"
android:scaleType="centerInside"
android:contentDescription="@null"
/>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dip"
android:layout_marginBottom="8dip"
android:orientation="vertical"
android:gravity="left|center_vertical"
android:layout_weight="1">
<TextView android:id="@+id/name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:singleLine="true"
android:textAppearance="?android:attr/textAppearanceMedium"
android:ellipsize="marquee"
android:fadingEdge="horizontal" />
<TextView android:id="@+id/description"
android:visibility="gone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dip"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="?android:attr/textColorSecondary"
android:maxLines="4" />
</LinearLayout>
<CheckBox xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/checkbox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginEnd="16dip"
android:layout_marginStart="16dip"
android:focusable="false"
android:clickable="false" />
</LinearLayout>

View File

@@ -983,6 +983,37 @@
<!-- Summary of preference to manage device policies -->
<string name="manage_device_admin_summary">View or deactivate device administrators</string>
<!-- Notification access settings (part of Security) -->
<!-- Title of preference to manage notification listeners -->
<string name="manage_notification_access">Notification access</string>
<!-- Summary of preference to manage notification listeners, when none are enabled -->
<string name="manage_notification_access_summary_zero">Apps cannot read notifications</string>
<!-- Summary of preference to manage notification listeners, when one or more are enabled
and are therefore able to read the user's notifications -->
<plurals name="manage_notification_access_summary_nonzero">
<item quantity="one">%d app can read notifications</item>
<item quantity="other">%d apps can read notifications</item>
</plurals>
<!-- String to show in the list of notification listeners, when none is installed -->
<string name="no_notification_listeners">No notification listeners are installed.</string>
<!-- Title for a warning message about security implications of enabling a notification
listener, displayed as a dialog message. [CHAR LIMIT=NONE] -->
<string name="notification_listener_security_warning_title">Enable
<xliff:g id="service" example="NotificationReader">%1$s</xliff:g>?</string>
<!-- Summary for a warning message about security implications of enabling a notification
listener, displayed as a dialog message. [CHAR LIMIT=NONE] -->
<string name="notification_listener_security_warning_summary">
<xliff:g id="notification_listener_name">%1$s</xliff:g> 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.
</string>
<!-- Bluetooth settings -->
<!-- Bluetooth settings check box title on Main Settings screen -->
<string name="bluetooth_quick_toggle_title">Bluetooth</string>

View File

@@ -58,6 +58,13 @@
android:summaryOff="@string/verify_applications_summary"
android:summaryOn="@string/verify_applications_summary"
android:persistent="false" />
<Preference
android:key="manage_notification_access"
android:title="@string/manage_notification_access"
android:persistent="false"
android:fragment="com.android.settings.NotificationAccessSettings"/>
</PreferenceCategory>
<PreferenceCategory android:key="credentials_management"

View File

@@ -0,0 +1,312 @@
/*
* Copyright (C) 2010 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;
import android.app.ActivityManager;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.DialogFragment;
import android.content.BroadcastReceiver;
import android.content.ContentResolver;
import android.content.DialogInterface;
import android.content.IntentFilter;
import android.content.pm.PackageItemInfo;
import android.content.pm.ServiceInfo;
import android.database.ContentObserver;
import android.net.Uri;
import android.os.Handler;
import android.service.notification.NotificationListenerService;
import android.util.Slog;
import android.widget.ArrayAdapter;
import android.app.ListFragment;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.os.Bundle;
import android.provider.Settings;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckBox;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.TextView;
import java.util.HashSet;
import java.util.List;
public class NotificationAccessSettings extends ListFragment {
static final String TAG = NotificationAccessSettings.class.getSimpleName();
private static final boolean SHOW_PACKAGE_NAME = false;
private PackageManager mPM;
private ContentResolver mCR;
private final HashSet<ComponentName> mEnabledListeners = new HashSet<ComponentName>();
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<ResolveInfo> 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<ServiceInfo> {
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));
}
}
}

View File

@@ -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;

View File

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