Settings: New application-level notification settings.

- Convert the old application-level dialog to an activity.
 - Move the settings icon to the new activity (out of the list).
 - Add a custom application header, similar to the switch bar style.
 - Use the ubiquitous vector gear for the settings icon.
 - Migrate old checkboxes to switch prefs, add new summaries.
 - Remove obsolete artifacts.

Bug:16396715
Change-Id: I857e3cf448b79f44fe1c242e6020f5214434c00c
This commit is contained in:
John Spurlock
2014-07-18 11:51:13 -04:00
parent f3dfd182e1
commit 802ddf99f5
18 changed files with 890 additions and 819 deletions

View File

@@ -1839,25 +1839,30 @@
android:resource="@id/notification_settings" />
</activity>
<activity android:name="Settings$AppNotificationSettingsActivity"
<!-- Show apps for which application-level notification settings are applicable -->
<activity android:name="Settings$NotificationAppListActivity"
android:label="@string/app_notifications_title"
android:exported="true"
android:taskAffinity="">
<meta-data android:name="com.android.settings.FRAGMENT_CLASS"
android:value="com.android.settings.notification.AppNotificationSettings" />
android:value="com.android.settings.notification.NotificationAppList" />
<meta-data android:name="com.android.settings.TOP_LEVEL_HEADER_ID"
android:resource="@id/notification_settings" />
</activity>
<activity android:name=".notification.AppNotificationDialog"
android:theme="@style/Theme.AlertDialog"
android:launchMode="singleTop"
<!-- Show application-level notification settings (app passed in as extras) -->
<activity android:name="Settings$AppNotificationSettingsActivity"
android:label="@string/app_notifications_title"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<action android:name="android.settings.APP_NOTIFICATION_SETTINGS" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<meta-data android:name="com.android.settings.FRAGMENT_CLASS"
android:value="com.android.settings.notification.AppNotificationSettings" />
<meta-data android:name="com.android.settings.TOP_LEVEL_HEADER_ID"
android:resource="@id/notification_settings" />
</activity>
<!-- Show regulatory info (from settings item or dialing "*#07#") -->

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 969 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

View File

@@ -0,0 +1,23 @@
<!-- Copyright (C) 2014 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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:pathData="M19.4,13.0c0.0,-0.3 0.1,-0.6 0.1,-1.0s0.0,-0.7 -0.1,-1.0l2.1,-1.7c0.2,-0.2 0.2,-0.4 0.1,-0.6l-2.0,-3.5C19.5,5.1 19.3,5.0 19.0,5.1l-2.5,1.0c-0.5,-0.4 -1.1,-0.7 -1.7,-1.0l-0.4,-2.6C14.5,2.2 14.2,2.0 14.0,2.0l-4.0,0.0C9.8,2.0 9.5,2.2 9.5,2.4L9.1,5.1C8.5,5.3 8.0,5.7 7.4,6.1L5.0,5.1C4.7,5.0 4.5,5.1 4.3,5.3l-2.0,3.5C2.2,8.9 2.3,9.2 2.5,9.4L4.6,11.0c0.0,0.3 -0.1,0.6 -0.1,1.0s0.0,0.7 0.1,1.0l-2.1,1.7c-0.2,0.2 -0.2,0.4 -0.1,0.6l2.0,3.5C4.5,18.9 4.7,19.0 5.0,18.9l2.5,-1.0c0.5,0.4 1.1,0.7 1.7,1.0l0.4,2.6c0.0,0.2 0.2,0.4 0.5,0.4l4.0,0.0c0.2,0.0 0.5,-0.2 0.5,-0.4l0.4,-2.6c0.6,-0.3 1.2,-0.6 1.7,-1.0l2.5,1.0c0.2,0.1 0.5,0.0 0.6,-0.2l2.0,-3.5c0.1,-0.2 0.1,-0.5 -0.1,-0.6L19.4,13.0zM12.0,15.5c-1.9,0.0 -3.5,-1.6 -3.5,-3.5s1.6,-3.5 3.5,-3.5s3.5,1.6 3.5,3.5S13.9,15.5 12.0,15.5z"
android:fillColor="#ffffffff" />
</vector>

View File

@@ -0,0 +1,63 @@
<!--
Copyright (C) 2014 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.
-->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="?android:attr/actionBarSize"
android:background="@drawable/switchbar_background"
android:gravity="center_vertical"
android:theme="?attr/switchBarTheme" >
<ImageView android:id="@+id/app_icon"
android:layout_width="@dimen/switchbar_subsettings_margin_start"
android:layout_height="40dp"
android:gravity="end"
android:layout_centerVertical="true" />
<TextView
android:id="@+id/app_name"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:layout_toStartOf="@+id/app_settings"
android:layout_marginStart="@dimen/switchbar_subsettings_margin_start"
android:layout_alignWithParentIfMissing="true"
android:layout_centerVertical="true"
android:textAppearance="@style/TextAppearance.Switch"
android:textColor="?android:attr/textColorPrimary"
android:textAlignment="viewStart" />
<ImageView
android:id="@id/app_settings"
android:layout_width="56dp"
android:layout_height="56dp"
android:layout_alignParentEnd="true"
android:layout_marginEnd="@dimen/switchbar_subsettings_margin_end"
android:layout_centerVertical="true"
android:minHeight="0dp"
android:minWidth="0dp"
android:contentDescription="@string/notification_app_settings_button"
android:scaleType="center"
android:src="@drawable/ic_settings_32dp"
style="?android:attr/borderlessButtonStyle" />
<View
android:id="@+id/row_divider"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/listDivider" />
</RelativeLayout>

View File

@@ -13,70 +13,44 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/listChoiceBackgroundIndicator"
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd">
<RelativeLayout
android:id="@android:id/button1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignWithParentIfMissing="true"
android:layout_toStartOf="@+id/settings_divider"
android:background="?android:attr/listChoiceBackgroundIndicator">
<ImageView
android:id="@android:id/icon"
android:layout_width="@dimen/notification_app_icon_size"
android:layout_height="@dimen/notification_app_icon_size"
android:layout_centerVertical="true"
android:contentDescription="@null"
android:padding="8dp" />
<TextView
android:id="@android:id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_toEndOf="@android:id/icon"
android:ellipsize="end"
android:singleLine="true"
android:textAlignment="viewStart"
android:textAppearance="?android:attr/textAppearanceMedium" />
<TextView
android:id="@android:id/text1"
android:layout_width="match_parent"
android:layout_height="@dimen/notification_app_icon_size"
android:layout_toEndOf="@android:id/icon"
android:gravity="bottom"
android:ellipsize="end"
android:singleLine="true"
android:textAlignment="viewStart"
android:textColor="?android:attr/textColorSecondary"
android:textAppearance="?android:attr/textAppearanceSmall" />
</RelativeLayout>
<View
android:id="@+id/settings_divider"
android:layout_width="1dp"
android:layout_height="@dimen/notification_app_settings_divider_height"
android:layout_centerVertical="true"
android:layout_toStartOf="@android:id/button2"
android:background="?android:attr/listDivider" />
<ImageView
android:id="@android:id/button2"
android:id="@android:id/icon"
android:layout_width="@dimen/notification_app_icon_size"
android:layout_height="@dimen/notification_app_icon_size"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:background="?android:attr/listChoiceBackgroundIndicator"
android:contentDescription="@string/notification_app_settings_button"
android:scaleType="center"
android:src="@drawable/ic_settings_generic" />
android:contentDescription="@null"
android:padding="8dp" />
<TextView
android:id="@android:id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_toEndOf="@android:id/icon"
android:ellipsize="end"
android:singleLine="true"
android:textAlignment="viewStart"
android:textAppearance="?android:attr/textAppearanceMedium" />
<TextView
android:id="@android:id/text1"
android:layout_width="match_parent"
android:layout_height="@dimen/notification_app_icon_size"
android:layout_toEndOf="@android:id/icon"
android:gravity="bottom"
android:ellipsize="end"
android:singleLine="true"
android:textAlignment="viewStart"
android:textColor="?android:attr/textColorSecondary"
android:textAppearance="?android:attr/textAppearanceSmall" />
<View
android:id="@+id/row_divider"

View File

@@ -1,65 +0,0 @@
<!--
Copyright (C) 2014 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.
-->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content" >
<ImageView
android:id="@android:id/icon"
android:padding="8dp"
android:layout_width="@dimen/notification_app_icon_size"
android:layout_height="@dimen/notification_app_icon_size"
android:contentDescription="@null" />
<TextView
android:id="@android:id/title"
android:layout_width="match_parent"
android:layout_height="@dimen/notification_app_icon_size"
android:layout_toEndOf="@android:id/icon"
android:ellipsize="end"
android:gravity="center_vertical"
android:singleLine="true"
android:textAlignment="viewStart"
android:textAppearance="?android:attr/textAppearanceLarge" />
<CheckBox
android:id="@android:id/button1"
android:layout_width="match_parent"
android:layout_height="@dimen/notification_app_icon_size"
android:layout_below="@android:id/icon"
android:layout_marginStart="@dimen/content_margin_left"
android:text="@string/app_notifications_dialog_show"
android:textAppearance="?android:attr/textAppearanceListItem" />
<CheckBox
android:id="@android:id/button2"
android:layout_width="match_parent"
android:layout_height="@dimen/notification_app_icon_size"
android:layout_below="@android:id/button1"
android:layout_marginStart="@dimen/content_margin_left"
android:text="@string/app_notifications_dialog_priority"
android:textAppearance="?android:attr/textAppearanceListItem" />
<CheckBox
android:id="@android:id/button3"
android:layout_width="match_parent"
android:layout_height="@dimen/notification_app_icon_size"
android:layout_below="@android:id/button2"
android:layout_marginStart="@dimen/content_margin_left"
android:text="@string/app_notifications_dialog_visibility"
android:textAppearance="?android:attr/textAppearanceListItem" />
</RelativeLayout>

View File

@@ -87,7 +87,6 @@
<dimen name="notification_app_icon_size">64dp</dimen>
<dimen name="notification_app_icon_badge_size">20dp</dimen>
<dimen name="notification_app_icon_badge_margin">4dp</dimen>
<dimen name="notification_app_settings_divider_height">48dp</dimen>
<dimen name="zen_mode_dropdown_width">160dp</dimen>
<dimen name="zen_downtime_checkbox_padding">7dp</dimen>
<dimen name="zen_downtime_margin">10dp</dimen>

View File

@@ -5739,27 +5739,33 @@
<!-- [CHAR LIMIT=NONE] Text when loading app list in notification settings -->
<string name="loading_notification_apps">Loading apps...</string>
<!-- [CHAR LIMIT=NONE] Notification settings: App notifications dialog show option -->
<string name="app_notifications_dialog_show">Show notifications</string>
<!-- [CHAR LIMIT=NONE] App notification settings: Block option title -->
<string name="app_notification_block_title">Block</string>
<!-- [CHAR LIMIT=NONE] Notification settings: App notifications dialog priority option -->
<string name="app_notifications_dialog_priority">Display at the top of the list</string>
<!-- [CHAR LIMIT=NONE] App notification settings: Block option description-->
<string name="app_notification_block_summary">Never show notifications from this app</string>
<!-- [CHAR LIMIT=NONE] Notification settings: App notifications dialog visibility option -->
<string name="app_notifications_dialog_visibility">Hide sensitive content when device is locked</string>
<!-- [CHAR LIMIT=NONE] App notification settings: Priority option title -->
<string name="app_notification_priority_title">Priority</string>
<!-- [CHAR LIMIT=NONE] App notification settings: Priority option description-->
<string name="app_notification_priority_summary">Show notifications at the top of the list and keep them coming when the device is set to priority interruptions only</string>
<!-- [CHAR LIMIT=NONE] App notification settings: Sensitive option title -->
<string name="app_notification_sensitive_title">Sensitive</string>
<!-- [CHAR LIMIT=NONE] App notification settings: Sensitive option description-->
<string name="app_notification_sensitive_summary">When the device is locked, hide any sensitive content from this apps notifications</string>
<!-- [CHAR LIMIT=20] Notification settings: App notifications row summary when banned -->
<string name="app_notification_row_banned">Blocked</string>
<!-- [CHAR LIMIT=20] Notification settings: App notifications row summary when high priority -->
<string name="app_notification_row_priority">Top of list</string>
<!-- [CHAR LIMIT=40] Notification settings: App notifications row summary when high priority -->
<string name="app_notification_row_priority">Priority</string>
<!-- [CHAR LIMIT=20] Notification settings: App notifications row summary when sensitive -->
<string name="app_notification_row_sensitive">Sensitive</string>
<!-- [CHAR LIMIT=20] Notification settings: App notifications dialog dismiss button caption -->
<string name="app_notifications_dialog_done">Done</string>
<!-- [CHAR LIMIT=30] Zen mode settings: Exit condition selection dialog, default option -->
<string name="zen_mode_default_option">Until you turn this off</string>

View File

@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2014 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:title="@string/app_notifications_title"
android:key="app_notification_settings">
<!-- Block -->
<SwitchPreference
android:key="block"
android:title="@string/app_notification_block_title"
android:summary="@string/app_notification_block_summary"
android:disableDependentsState="true"
android:persistent="false" />
<!-- Priority -->
<SwitchPreference
android:key="priority"
android:title="@string/app_notification_priority_title"
android:summary="@string/app_notification_priority_summary"
android:dependency="block"
android:persistent="false" />
<!-- Sensitive -->
<SwitchPreference
android:key="sensitive"
android:title="@string/app_notification_sensitive_title"
android:summary="@string/app_notification_sensitive_summary"
android:enabled="false"
android:persistent="false" />
</PreferenceScreen>

View File

@@ -108,7 +108,7 @@
<PreferenceScreen
android:key="app_notifications"
android:title="@string/app_notifications_title"
android:fragment="com.android.settings.notification.AppNotificationSettings" />
android:fragment="com.android.settings.notification.NotificationAppList" />
<!-- Notification access -->
<Preference

View File

@@ -92,6 +92,7 @@ public class Settings extends SettingsActivity {
public static class PrintJobSettingsActivity extends SettingsActivity { /* empty */ }
public static class ZenModeSettingsActivity extends SettingsActivity { /* empty */ }
public static class NotificationSettingsActivity extends SettingsActivity { /* empty */ }
public static class NotificationAppListActivity extends SettingsActivity { /* empty */ }
public static class AppNotificationSettingsActivity extends SettingsActivity { /* empty */ }
public static class OtherSoundSettingsActivity extends SettingsActivity { /* empty */ }
public static class QuickLaunchSettingsActivity extends SettingsActivity { /* empty */ }

View File

@@ -82,7 +82,7 @@ import com.android.settings.deviceinfo.Memory;
import com.android.settings.deviceinfo.UsbSettings;
import com.android.settings.fuelgauge.BatterySaverSettings;
import com.android.settings.fuelgauge.PowerUsageSummary;
import com.android.settings.notification.AppNotificationSettings;
import com.android.settings.notification.NotificationAppList;
import com.android.settings.notification.OtherSoundSettings;
import com.android.settings.quicklaunch.QuickLaunchSettings;
import com.android.settings.search.DynamicIndexableContentMonitor;
@@ -94,6 +94,7 @@ import com.android.settings.inputmethod.UserDictionaryList;
import com.android.settings.location.LocationSettings;
import com.android.settings.nfc.AndroidBeam;
import com.android.settings.nfc.PaymentSettings;
import com.android.settings.notification.AppNotificationSettings;
import com.android.settings.notification.ConditionProviderSettings;
import com.android.settings.notification.NotificationAccessSettings;
import com.android.settings.notification.NotificationSettings;
@@ -286,6 +287,7 @@ public class SettingsActivity extends Activity
ChooseLockPattern.ChooseLockPatternFragment.class.getName(),
InstalledAppDetails.class.getName(),
BatterySaverSettings.class.getName(),
NotificationAppList.class.getName(),
AppNotificationSettings.class.getName(),
OtherSoundSettings.class.getName(),
QuickLaunchSettings.class.getName()

View File

@@ -1,180 +0,0 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.notification;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.os.Bundle;
import android.provider.Settings;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import android.widget.CompoundButton.OnCheckedChangeListener;
import com.android.internal.app.AlertActivity;
import com.android.internal.app.AlertController;
import com.android.settings.R;
import com.android.settings.notification.AppNotificationSettings.Backend;
import com.android.settings.notification.AppNotificationSettings.AppRow;
public class AppNotificationDialog extends AlertActivity {
private static final String TAG = "AppNotificationDialog";
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
/**
* Show a checkbox in the per-app notification control dialog to allow the user to
* selectively redact this app's notifications on the lockscreen.
*/
private static final boolean ENABLE_APP_NOTIFICATION_PRIVACY_OPTION = false;
private final Context mContext = this;
private final Backend mBackend = new Backend();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (DEBUG) Log.d(TAG, "onCreate getIntent()=" + getIntent());
if (!buildDialog()) {
Toast.makeText(mContext, R.string.app_not_found_dlg_text, Toast.LENGTH_SHORT).show();
finish();
}
}
private boolean buildDialog() {
final Intent intent = getIntent();
if (intent != null) {
final int uid = intent.getIntExtra(Settings.EXTRA_APP_UID, -1);
final String pkg = intent.getStringExtra(Settings.EXTRA_APP_PACKAGE);
if (uid != -1 && !TextUtils.isEmpty(pkg)) {
if (DEBUG) Log.d(TAG, "Load details for pkg=" + pkg + " uid=" + uid);
final PackageManager pm = getPackageManager();
final PackageInfo info = findPackageInfo(pm, pkg, uid);
if (info != null) {
final AppRow row = AppNotificationSettings.loadAppRow(pm, info, mBackend);
final AlertController.AlertParams p = mAlertParams;
p.mView = getLayoutInflater().inflate(R.layout.notification_app_dialog,
null, false);
p.mPositiveButtonText = getString(R.string.app_notifications_dialog_done);
bindDialog(p.mView, row);
setupAlert();
return true;
} else {
Log.w(TAG, "Failed to find package info");
}
} else {
Log.w(TAG, "Missing extras: " + Settings.EXTRA_APP_PACKAGE + " was " + pkg + ", "
+ Settings.EXTRA_APP_UID + " was " + uid);
}
} else {
Log.w(TAG, "No intent");
}
return false;
}
private static PackageInfo findPackageInfo(PackageManager pm, String pkg, int uid) {
final String[] packages = pm.getPackagesForUid(uid);
if (packages != null && pkg != null) {
final int N = packages.length;
for (int i = 0; i < N; i++) {
final String p = packages[i];
if (pkg.equals(p)) {
try {
return pm.getPackageInfo(pkg, 0);
} catch (NameNotFoundException e) {
Log.w(TAG, "Failed to load package " + pkg, e);
}
}
}
}
return null;
}
private void bindDialog(final View v, final AppRow row) {
final ImageView icon = (ImageView) v.findViewById(android.R.id.icon);
icon.setImageDrawable(row.icon);
final TextView title = (TextView) v.findViewById(android.R.id.title);
title.setText(row.label);
final CheckBox showNotifications = (CheckBox) v.findViewById(android.R.id.button1);
final CheckBox highPriority = (CheckBox) v.findViewById(android.R.id.button2);
final CheckBox sensitive = (CheckBox) v.findViewById(android.R.id.button3);
if (!ENABLE_APP_NOTIFICATION_PRIVACY_OPTION) {
sensitive.setVisibility(View.GONE);
}
showNotifications.setChecked(!row.banned);
final OnCheckedChangeListener showListener = new OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
boolean success = mBackend.setNotificationsBanned(row.pkg, row.uid, !isChecked);
if (success) {
row.banned = !isChecked;
highPriority.setEnabled(!row.banned);
sensitive.setEnabled(!row.banned);
} else {
showNotifications.setOnCheckedChangeListener(null);
showNotifications.setChecked(!isChecked);
showNotifications.setOnCheckedChangeListener(this);
}
}
};
showNotifications.setOnCheckedChangeListener(showListener);
highPriority.setChecked(row.priority);
final OnCheckedChangeListener priListener = new OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
boolean success = mBackend.setHighPriority(row.pkg, row.uid, isChecked);
if (success) {
row.priority = isChecked;
} else {
highPriority.setOnCheckedChangeListener(null);
highPriority.setChecked(!isChecked);
highPriority.setOnCheckedChangeListener(this);
}
}
};
highPriority.setOnCheckedChangeListener(priListener);
sensitive.setChecked(row.sensitive);
final OnCheckedChangeListener senListener = new OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
boolean success = mBackend.setSensitive(row.pkg, row.uid, isChecked);
if (success) {
row.sensitive = isChecked;
} else {
sensitive.setOnCheckedChangeListener(null);
sensitive.setChecked(!isChecked);
sensitive.setOnCheckedChangeListener(this);
}
}
};
sensitive.setOnCheckedChangeListener(senListener);
highPriority.setEnabled(!row.banned);
sensitive.setEnabled(!row.banned);
}
}

View File

@@ -16,557 +16,186 @@
package com.android.settings.notification;
import android.animation.LayoutTransition;
import android.app.INotificationManager;
import android.app.Notification;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.ResolveInfo;
import android.content.pm.Signature;
import android.graphics.drawable.Drawable;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Handler;
import android.os.Parcelable;
import android.os.ServiceManager;
import android.os.SystemClock;
import android.preference.Preference;
import android.preference.Preference.OnPreferenceChangeListener;
import android.preference.SwitchPreference;
import android.provider.Settings;
import android.os.UserHandle;
import android.os.UserManager;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.Log;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.AdapterView.OnItemSelectedListener;
import android.widget.AdapterView;
import android.widget.ImageView;
import android.widget.SectionIndexer;
import android.widget.Spinner;
import android.widget.TextView;
import android.widget.Toast;
import com.android.settings.Settings.AppNotificationSettingsActivity;
import com.android.settings.PinnedHeaderListFragment;
import com.android.settings.R;
import com.android.settings.UserSpinnerAdapter;
import com.android.settings.Utils;
import com.android.settings.SettingsPreferenceFragment;
import com.android.settings.notification.NotificationAppList.AppRow;
import com.android.settings.notification.NotificationAppList.Backend;
import java.text.Collator;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
/** Just a sectioned list of installed applications, nothing else to index **/
public class AppNotificationSettings extends PinnedHeaderListFragment
implements OnItemSelectedListener {
/** These settings are per app, so should not be returned in global search results. */
public class AppNotificationSettings extends SettingsPreferenceFragment {
private static final String TAG = "AppNotificationSettings";
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
private static final String SECTION_BEFORE_A = "*";
private static final String SECTION_AFTER_Z = "**";
private static final Intent APP_NOTIFICATION_PREFS_CATEGORY_INTENT
= new Intent(Intent.ACTION_MAIN)
.addCategory(Notification.INTENT_CATEGORY_NOTIFICATION_PREFERENCES);
private static final String KEY_BLOCK = "block";
private static final String KEY_PRIORITY = "priority";
private static final String KEY_SENSITIVE = "sensitive";
private final Handler mHandler = new Handler();
private final ArrayMap<String, AppRow> mRows = new ArrayMap<String, AppRow>();
private final ArrayList<AppRow> mSortedRows = new ArrayList<AppRow>();
private final ArrayList<String> mSections = new ArrayList<String>();
static final String EXTRA_HAS_SETTINGS_INTENT = "has_settings_intent";
static final String EXTRA_SETTINGS_INTENT = "settings_intent";
private final Backend mBackend = new Backend();
private Context mContext;
private LayoutInflater mInflater;
private NotificationAppAdapter mAdapter;
private Signature[] mSystemSignature;
private Parcelable mListViewState;
private Backend mBackend = new Backend();
private UserSpinnerAdapter mProfileSpinnerAdapter;
private SwitchPreference mBlock;
private SwitchPreference mPriority;
private SwitchPreference mSensitive;
private AppRow mAppRow;
private boolean mCreated;
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
if (DEBUG) Log.d(TAG, "onActivityCreated mCreated=" + mCreated);
if (mCreated) {
Log.w(TAG, "onActivityCreated: ignoring duplicate call");
return;
}
mCreated = true;
if (mAppRow == null) return;
final View content = getActivity().findViewById(R.id.main_content);
final ViewGroup contentParent = (ViewGroup) content.getParent();
final View bar = getActivity().getLayoutInflater().inflate(R.layout.app_notification_header,
contentParent, false);
final ImageView appIcon = (ImageView) bar.findViewById(R.id.app_icon);
appIcon.setImageDrawable(mAppRow.icon);
final TextView appName = (TextView) bar.findViewById(R.id.app_name);
appName.setText(mAppRow.label);
final View appSettings = bar.findViewById(R.id.app_settings);
if (mAppRow.settingsIntent == null) {
appSettings.setVisibility(View.GONE);
} else {
appSettings.setClickable(true);
appSettings.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
mContext.startActivity(mAppRow.settingsIntent);
}
});
}
contentParent.addView(bar, 0);
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mContext = getActivity();
mInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
mAdapter = new NotificationAppAdapter(mContext);
getActivity().setTitle(R.string.app_notifications_title);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
return inflater.inflate(R.layout.notification_app_list, container, false);
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
final UserManager um = (UserManager) getActivity().getSystemService(Context.USER_SERVICE);
mProfileSpinnerAdapter = Utils.createUserSpinnerAdapter(um, mContext);
if (mProfileSpinnerAdapter != null) {
Spinner spinner = (Spinner) getActivity().getLayoutInflater().inflate(
R.layout.spinner_view, null);
spinner.setAdapter(mProfileSpinnerAdapter);
spinner.setOnItemSelectedListener(this);
setPinnedHeaderView(spinner);
}
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
repositionScrollbar();
getListView().setAdapter(mAdapter);
}
@Override
public void onPause() {
super.onPause();
if (DEBUG) Log.d(TAG, "Saving listView state");
mListViewState = getListView().onSaveInstanceState();
}
@Override
public void onDestroyView() {
super.onDestroyView();
mListViewState = null; // you're dead to me
}
@Override
public void onResume() {
super.onResume();
loadAppsList();
}
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
UserHandle selectedUser = mProfileSpinnerAdapter.getUserHandle(position);
if (selectedUser.getIdentifier() != UserHandle.myUserId()) {
Intent intent = new Intent(getActivity(), AppNotificationSettingsActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
mContext.startActivityAsUser(intent, selectedUser);
getActivity().finish();
}
}
@Override
public void onNothingSelected(AdapterView<?> parent) {
}
public void setBackend(Backend backend) {
mBackend = backend;
}
private void loadAppsList() {
AsyncTask.execute(mCollectAppsRunnable);
}
private String getSection(CharSequence label) {
if (label == null || label.length() == 0) return SECTION_BEFORE_A;
final char c = Character.toUpperCase(label.charAt(0));
if (c < 'A') return SECTION_BEFORE_A;
if (c > 'Z') return SECTION_AFTER_Z;
return Character.toString(c);
}
private void repositionScrollbar() {
final int sbWidthPx = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
getListView().getScrollBarSize(),
getResources().getDisplayMetrics());
final View parent = (View)getView().getParent();
final int eat = Math.min(sbWidthPx, parent.getPaddingEnd());
if (eat <= 0) return;
if (DEBUG) Log.d(TAG, String.format("Eating %dpx into %dpx padding for %dpx scroll, ld=%d",
eat, parent.getPaddingEnd(), sbWidthPx, getListView().getLayoutDirection()));
parent.setPaddingRelative(parent.getPaddingStart(), parent.getPaddingTop(),
parent.getPaddingEnd() - eat, parent.getPaddingBottom());
}
private boolean isSystemApp(PackageInfo pkg) {
if (mSystemSignature == null) {
mSystemSignature = new Signature[]{ getSystemSignature() };
}
return mSystemSignature[0] != null && mSystemSignature[0].equals(getFirstSignature(pkg));
}
private static Signature getFirstSignature(PackageInfo pkg) {
if (pkg != null && pkg.signatures != null && pkg.signatures.length > 0) {
return pkg.signatures[0];
}
return null;
}
private Signature getSystemSignature() {
final PackageManager pm = mContext.getPackageManager();
try {
final PackageInfo sys = pm.getPackageInfo("android", PackageManager.GET_SIGNATURES);
return getFirstSignature(sys);
} catch (NameNotFoundException e) {
}
return null;
}
private static class ViewHolder {
ViewGroup row;
ViewGroup appButton;
ImageView icon;
TextView title;
TextView subtitle;
View settingsDivider;
ImageView settingsButton;
View rowDivider;
}
private class NotificationAppAdapter extends ArrayAdapter<Row> implements SectionIndexer {
public NotificationAppAdapter(Context context) {
super(context, 0, 0);
Intent intent = getActivity().getIntent();
if (DEBUG) Log.d(TAG, "onCreate getIntent()=" + intent);
if (intent == null) {
Log.w(TAG, "No intent");
toastAndFinish();
return;
}
@Override
public boolean hasStableIds() {
return true;
final int uid = intent.getIntExtra(Settings.EXTRA_APP_UID, -1);
final String pkg = intent.getStringExtra(Settings.EXTRA_APP_PACKAGE);
if (uid == -1 || TextUtils.isEmpty(pkg)) {
Log.w(TAG, "Missing extras: " + Settings.EXTRA_APP_PACKAGE + " was " + pkg + ", "
+ Settings.EXTRA_APP_UID + " was " + uid);
toastAndFinish();
return;
}
@Override
public long getItemId(int position) {
return position;
if (DEBUG) Log.d(TAG, "Load details for pkg=" + pkg + " uid=" + uid);
final PackageManager pm = getPackageManager();
final PackageInfo info = findPackageInfo(pm, pkg, uid);
if (info == null) {
Log.w(TAG, "Failed to find package info: " + Settings.EXTRA_APP_PACKAGE + " was " + pkg
+ ", " + Settings.EXTRA_APP_UID + " was " + uid);
toastAndFinish();
return;
}
@Override
public int getViewTypeCount() {
return 2;
}
addPreferencesFromResource(R.xml.app_notification_settings);
mBlock = (SwitchPreference) findPreference(KEY_BLOCK);
mPriority = (SwitchPreference) findPreference(KEY_PRIORITY);
mSensitive = (SwitchPreference) findPreference(KEY_SENSITIVE);
@Override
public int getItemViewType(int position) {
Row r = getItem(position);
return r instanceof AppRow ? 1 : 0;
}
public View getView(int position, View convertView, ViewGroup parent) {
Row r = getItem(position);
View v;
if (convertView == null) {
v = newView(parent, r);
} else {
v = convertView;
mAppRow = NotificationAppList.loadAppRow(pm, info, mBackend);
if (intent.hasExtra(EXTRA_HAS_SETTINGS_INTENT)) {
// use settings intent from extra
if (intent.getBooleanExtra(EXTRA_HAS_SETTINGS_INTENT, false)) {
mAppRow.settingsIntent = intent.getParcelableExtra(EXTRA_SETTINGS_INTENT);
}
bindView(v, r, false /*animate*/);
return v;
} else {
// load settings intent
ArrayMap<String, AppRow> rows = new ArrayMap<String, AppRow>();
rows.put(mAppRow.pkg, mAppRow);
NotificationAppList.collectConfigActivities(getPackageManager(), rows);
}
public View newView(ViewGroup parent, Row r) {
if (!(r instanceof AppRow)) {
return mInflater.inflate(R.layout.notification_app_section, parent, false);
mBlock.setChecked(mAppRow.banned);
mPriority.setChecked(mAppRow.priority);
mSensitive.setChecked(mAppRow.sensitive);
mBlock.setOnPreferenceChangeListener(new OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
final boolean block = (Boolean) newValue;
return mBackend.setNotificationsBanned(pkg, uid, block);
}
final View v = mInflater.inflate(R.layout.notification_app, parent, false);
final ViewHolder vh = new ViewHolder();
vh.row = (ViewGroup) v;
vh.row.setLayoutTransition(new LayoutTransition());
vh.appButton = (ViewGroup) v.findViewById(android.R.id.button1);
vh.appButton.setLayoutTransition(new LayoutTransition());
vh.icon = (ImageView) v.findViewById(android.R.id.icon);
vh.title = (TextView) v.findViewById(android.R.id.title);
vh.subtitle = (TextView) v.findViewById(android.R.id.text1);
vh.settingsDivider = v.findViewById(R.id.settings_divider);
vh.settingsButton = (ImageView) v.findViewById(android.R.id.button2);
vh.rowDivider = v.findViewById(R.id.row_divider);
v.setTag(vh);
return v;
}
});
private void enableLayoutTransitions(ViewGroup vg, boolean enabled) {
if (enabled) {
vg.getLayoutTransition().enableTransitionType(LayoutTransition.APPEARING);
vg.getLayoutTransition().enableTransitionType(LayoutTransition.DISAPPEARING);
} else {
vg.getLayoutTransition().disableTransitionType(LayoutTransition.APPEARING);
vg.getLayoutTransition().disableTransitionType(LayoutTransition.DISAPPEARING);
mPriority.setOnPreferenceChangeListener(new OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
final boolean priority = (Boolean) newValue;
return mBackend.setHighPriority(pkg, uid, priority);
}
}
});
public void bindView(final View view, Row r, boolean animate) {
if (!(r instanceof AppRow)) {
// it's a section row
final TextView tv = (TextView)view.findViewById(android.R.id.title);
tv.setText(r.section);
return;
mSensitive.setOnPreferenceChangeListener(new OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
final boolean sensitive = (Boolean) newValue;
return mBackend.setSensitive(pkg, uid, sensitive);
}
final AppRow row = (AppRow)r;
final ViewHolder vh = (ViewHolder) view.getTag();
enableLayoutTransitions(vh.row, animate);
vh.rowDivider.setVisibility(row.first ? View.GONE : View.VISIBLE);
vh.appButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
mContext.startActivity(new Intent(mContext, AppNotificationDialog.class)
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
.putExtra(Settings.EXTRA_APP_PACKAGE, row.pkg)
.putExtra(Settings.EXTRA_APP_UID, row.uid));
}
});
enableLayoutTransitions(vh.appButton, animate);
vh.icon.setImageDrawable(row.icon);
vh.title.setText(row.label);
final String sub = getSubtitle(row);
vh.subtitle.setText(sub);
vh.subtitle.setVisibility(!sub.isEmpty() ? View.VISIBLE : View.GONE);
final boolean showSettings = !row.banned && row.settingsIntent != null;
vh.settingsDivider.setVisibility(showSettings ? View.VISIBLE : View.GONE);
vh.settingsButton.setVisibility(showSettings ? View.VISIBLE : View.GONE);
vh.settingsButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (row.settingsIntent != null) {
getContext().startActivity(row.settingsIntent);
}
}
});
}
private String getSubtitle(AppRow row) {
if (row.banned) return mContext.getString(R.string.app_notification_row_banned);
if (!row.priority && !row.sensitive) return "";
final String priString = mContext.getString(R.string.app_notification_row_priority);
final String senString = mContext.getString(R.string.app_notification_row_sensitive);
if (row.priority != row.sensitive) {
return row.priority ? priString : senString;
}
return priString + mContext.getString(R.string.summary_divider_text) + senString;
}
@Override
public Object[] getSections() {
return mSections.toArray(new Object[mSections.size()]);
}
@Override
public int getPositionForSection(int sectionIndex) {
final String section = mSections.get(sectionIndex);
final int n = getCount();
for (int i = 0; i < n; i++) {
final Row r = getItem(i);
if (r.section.equals(section)) {
return i;
}
}
return 0;
}
@Override
public int getSectionForPosition(int position) {
Row row = getItem(position);
return mSections.indexOf(row.section);
}
});
}
private static class Row {
public String section;
private void toastAndFinish() {
Toast.makeText(mContext, R.string.app_not_found_dlg_text, Toast.LENGTH_SHORT).show();
getActivity().finish();
}
public static class AppRow extends Row {
public String pkg;
public int uid;
public Drawable icon;
public CharSequence label;
public Intent settingsIntent;
public boolean banned;
public boolean priority;
public boolean sensitive;
public boolean first; // first app in section
}
private static final Comparator<AppRow> mRowComparator = new Comparator<AppRow>() {
private final Collator sCollator = Collator.getInstance();
@Override
public int compare(AppRow lhs, AppRow rhs) {
return sCollator.compare(lhs.label, rhs.label);
}
};
public static AppRow loadAppRow(PackageManager pm, PackageInfo pkg, Backend backend) {
final AppRow row = new AppRow();
row.pkg = pkg.packageName;
row.uid = pkg.applicationInfo.uid;
try {
row.label = pkg.applicationInfo.loadLabel(pm);
} catch (Throwable t) {
Log.e(TAG, "Error loading application label for " + row.pkg, t);
row.label = row.pkg;
}
row.icon = pkg.applicationInfo.loadIcon(pm);
row.banned = backend.getNotificationsBanned(row.pkg, row.uid);
row.priority = backend.getHighPriority(row.pkg, row.uid);
row.sensitive = backend.getSensitive(row.pkg, row.uid);
return row;
}
private final Runnable mCollectAppsRunnable = new Runnable() {
@Override
public void run() {
synchronized (mRows) {
final long start = SystemClock.uptimeMillis();
if (DEBUG) Log.d(TAG, "Collecting apps...");
mRows.clear();
mSortedRows.clear();
// collect all non-system apps
final PackageManager pm = mContext.getPackageManager();
for (PackageInfo pkg : pm.getInstalledPackages(PackageManager.GET_SIGNATURES)) {
if (pkg.applicationInfo == null || isSystemApp(pkg)) {
if (DEBUG) Log.d(TAG, "Skipping " + pkg.packageName);
continue;
}
final AppRow row = loadAppRow(pm, pkg, mBackend);
mRows.put(row.pkg, row);
}
// collect config activities
if (DEBUG) Log.d(TAG, "APP_NOTIFICATION_PREFS_CATEGORY_INTENT is "
+ APP_NOTIFICATION_PREFS_CATEGORY_INTENT);
final List<ResolveInfo> resolveInfos = pm.queryIntentActivities(
APP_NOTIFICATION_PREFS_CATEGORY_INTENT,
PackageManager.MATCH_DEFAULT_ONLY);
if (DEBUG) Log.d(TAG, "Found " + resolveInfos.size() + " preference activities");
for (ResolveInfo ri : resolveInfos) {
final ActivityInfo activityInfo = ri.activityInfo;
final ApplicationInfo appInfo = activityInfo.applicationInfo;
final AppRow row = mRows.get(appInfo.packageName);
if (row == null) {
Log.v(TAG, "Ignoring notification preference activity ("
+ activityInfo.name + ") for unknown package "
+ activityInfo.packageName);
continue;
}
if (row.settingsIntent != null) {
Log.v(TAG, "Ignoring duplicate notification preference activity ("
+ activityInfo.name + ") for package "
+ activityInfo.packageName);
continue;
}
row.settingsIntent = new Intent(Intent.ACTION_MAIN)
.setClassName(activityInfo.packageName, activityInfo.name);
}
// sort rows
mSortedRows.addAll(mRows.values());
Collections.sort(mSortedRows, mRowComparator);
// compute sections
mSections.clear();
String section = null;
for (AppRow r : mSortedRows) {
r.section = getSection(r.label);
if (!r.section.equals(section)) {
section = r.section;
mSections.add(section);
}
}
mHandler.post(mRefreshAppsListRunnable);
final long elapsed = SystemClock.uptimeMillis() - start;
if (DEBUG) Log.d(TAG, "Collected " + mRows.size() + " apps in " + elapsed + "ms");
}
}
};
private void refreshDisplayedItems() {
if (DEBUG) Log.d(TAG, "Refreshing apps...");
mAdapter.clear();
synchronized (mSortedRows) {
String section = null;
final int N = mSortedRows.size();
boolean first = true;
private static PackageInfo findPackageInfo(PackageManager pm, String pkg, int uid) {
final String[] packages = pm.getPackagesForUid(uid);
if (packages != null && pkg != null) {
final int N = packages.length;
for (int i = 0; i < N; i++) {
final AppRow row = mSortedRows.get(i);
if (!row.section.equals(section)) {
section = row.section;
Row r = new Row();
r.section = section;
mAdapter.add(r);
first = true;
final String p = packages[i];
if (pkg.equals(p)) {
try {
return pm.getPackageInfo(pkg, 0);
} catch (NameNotFoundException e) {
Log.w(TAG, "Failed to load package " + pkg, e);
}
}
row.first = first;
mAdapter.add(row);
first = false;
}
}
if (mListViewState != null) {
if (DEBUG) Log.d(TAG, "Restoring listView state");
getListView().onRestoreInstanceState(mListViewState);
mListViewState = null;
}
if (DEBUG) Log.d(TAG, "Refreshed " + mSortedRows.size() + " displayed items");
}
private final Runnable mRefreshAppsListRunnable = new Runnable() {
@Override
public void run() {
refreshDisplayedItems();
}
};
public static class Backend {
public boolean setNotificationsBanned(String pkg, int uid, boolean banned) {
INotificationManager nm = INotificationManager.Stub.asInterface(
ServiceManager.getService(Context.NOTIFICATION_SERVICE));
try {
nm.setNotificationsEnabledForPackage(pkg, uid, !banned);
return true;
} catch (Exception e) {
Log.w(TAG, "Error calling NoMan", e);
return false;
}
}
public boolean getNotificationsBanned(String pkg, int uid) {
INotificationManager nm = INotificationManager.Stub.asInterface(
ServiceManager.getService(Context.NOTIFICATION_SERVICE));
try {
final boolean enabled = nm.areNotificationsEnabledForPackage(pkg, uid);
return !enabled;
} catch (Exception e) {
Log.w(TAG, "Error calling NoMan", e);
return false;
}
}
public boolean getHighPriority(String pkg, int uid) {
INotificationManager nm = INotificationManager.Stub.asInterface(
ServiceManager.getService(Context.NOTIFICATION_SERVICE));
try {
return nm.getPackagePriority(pkg, uid) == Notification.PRIORITY_MAX;
} catch (Exception e) {
Log.w(TAG, "Error calling NoMan", e);
return false;
}
}
public boolean setHighPriority(String pkg, int uid, boolean highPriority) {
INotificationManager nm = INotificationManager.Stub.asInterface(
ServiceManager.getService(Context.NOTIFICATION_SERVICE));
try {
nm.setPackagePriority(pkg, uid,
highPriority ? Notification.PRIORITY_MAX : Notification.PRIORITY_DEFAULT);
return true;
} catch (Exception e) {
Log.w(TAG, "Error calling NoMan", e);
return false;
}
}
public boolean getSensitive(String pkg, int uid) {
// TODO get visibility state from NoMan
return false;
}
public boolean setSensitive(String pkg, int uid, boolean sensitive) {
// TODO save visibility state to NoMan
return true;
}
return null;
}
}

View File

@@ -0,0 +1,569 @@
/*
* Copyright (C) 2014 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 com.android.settings.notification.AppNotificationSettings.EXTRA_HAS_SETTINGS_INTENT;
import static com.android.settings.notification.AppNotificationSettings.EXTRA_SETTINGS_INTENT;
import android.animation.LayoutTransition;
import android.app.INotificationManager;
import android.app.Notification;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.ResolveInfo;
import android.content.pm.Signature;
import android.graphics.drawable.Drawable;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Handler;
import android.os.Parcelable;
import android.os.ServiceManager;
import android.os.SystemClock;
import android.os.UserHandle;
import android.os.UserManager;
import android.provider.Settings;
import android.util.ArrayMap;
import android.util.Log;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemSelectedListener;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.SectionIndexer;
import android.widget.Spinner;
import android.widget.TextView;
import com.android.settings.PinnedHeaderListFragment;
import com.android.settings.R;
import com.android.settings.Settings.NotificationAppListActivity;
import com.android.settings.UserSpinnerAdapter;
import com.android.settings.Utils;
import java.text.Collator;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
/** Just a sectioned list of installed applications, nothing else to index **/
public class NotificationAppList extends PinnedHeaderListFragment
implements OnItemSelectedListener {
private static final String TAG = "NotificationAppList";
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
private static final String EMPTY_SUBTITLE = "";
private static final String SECTION_BEFORE_A = "*";
private static final String SECTION_AFTER_Z = "**";
private static final Intent APP_NOTIFICATION_PREFS_CATEGORY_INTENT
= new Intent(Intent.ACTION_MAIN)
.addCategory(Notification.INTENT_CATEGORY_NOTIFICATION_PREFERENCES);
private final Handler mHandler = new Handler();
private final ArrayMap<String, AppRow> mRows = new ArrayMap<String, AppRow>();
private final ArrayList<AppRow> mSortedRows = new ArrayList<AppRow>();
private final ArrayList<String> mSections = new ArrayList<String>();
private Context mContext;
private LayoutInflater mInflater;
private NotificationAppAdapter mAdapter;
private Signature[] mSystemSignature;
private Parcelable mListViewState;
private Backend mBackend = new Backend();
private UserSpinnerAdapter mProfileSpinnerAdapter;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mContext = getActivity();
mInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
mAdapter = new NotificationAppAdapter(mContext);
getActivity().setTitle(R.string.app_notifications_title);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
return inflater.inflate(R.layout.notification_app_list, container, false);
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
final UserManager um = (UserManager) getActivity().getSystemService(Context.USER_SERVICE);
mProfileSpinnerAdapter = Utils.createUserSpinnerAdapter(um, mContext);
if (mProfileSpinnerAdapter != null) {
Spinner spinner = (Spinner) getActivity().getLayoutInflater().inflate(
R.layout.spinner_view, null);
spinner.setAdapter(mProfileSpinnerAdapter);
spinner.setOnItemSelectedListener(this);
setPinnedHeaderView(spinner);
}
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
repositionScrollbar();
getListView().setAdapter(mAdapter);
}
@Override
public void onPause() {
super.onPause();
if (DEBUG) Log.d(TAG, "Saving listView state");
mListViewState = getListView().onSaveInstanceState();
}
@Override
public void onDestroyView() {
super.onDestroyView();
mListViewState = null; // you're dead to me
}
@Override
public void onResume() {
super.onResume();
loadAppsList();
}
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
UserHandle selectedUser = mProfileSpinnerAdapter.getUserHandle(position);
if (selectedUser.getIdentifier() != UserHandle.myUserId()) {
Intent intent = new Intent(getActivity(), NotificationAppListActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
mContext.startActivityAsUser(intent, selectedUser);
getActivity().finish();
}
}
@Override
public void onNothingSelected(AdapterView<?> parent) {
}
public void setBackend(Backend backend) {
mBackend = backend;
}
private void loadAppsList() {
AsyncTask.execute(mCollectAppsRunnable);
}
private String getSection(CharSequence label) {
if (label == null || label.length() == 0) return SECTION_BEFORE_A;
final char c = Character.toUpperCase(label.charAt(0));
if (c < 'A') return SECTION_BEFORE_A;
if (c > 'Z') return SECTION_AFTER_Z;
return Character.toString(c);
}
private void repositionScrollbar() {
final int sbWidthPx = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
getListView().getScrollBarSize(),
getResources().getDisplayMetrics());
final View parent = (View)getView().getParent();
final int eat = Math.min(sbWidthPx, parent.getPaddingEnd());
if (eat <= 0) return;
if (DEBUG) Log.d(TAG, String.format("Eating %dpx into %dpx padding for %dpx scroll, ld=%d",
eat, parent.getPaddingEnd(), sbWidthPx, getListView().getLayoutDirection()));
parent.setPaddingRelative(parent.getPaddingStart(), parent.getPaddingTop(),
parent.getPaddingEnd() - eat, parent.getPaddingBottom());
}
private boolean isSystemApp(PackageInfo pkg) {
if (mSystemSignature == null) {
mSystemSignature = new Signature[]{ getSystemSignature() };
}
return mSystemSignature[0] != null && mSystemSignature[0].equals(getFirstSignature(pkg));
}
private static Signature getFirstSignature(PackageInfo pkg) {
if (pkg != null && pkg.signatures != null && pkg.signatures.length > 0) {
return pkg.signatures[0];
}
return null;
}
private Signature getSystemSignature() {
final PackageManager pm = mContext.getPackageManager();
try {
final PackageInfo sys = pm.getPackageInfo("android", PackageManager.GET_SIGNATURES);
return getFirstSignature(sys);
} catch (NameNotFoundException e) {
}
return null;
}
private static class ViewHolder {
ViewGroup row;
ImageView icon;
TextView title;
TextView subtitle;
View rowDivider;
}
private class NotificationAppAdapter extends ArrayAdapter<Row> implements SectionIndexer {
public NotificationAppAdapter(Context context) {
super(context, 0, 0);
}
@Override
public boolean hasStableIds() {
return true;
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public int getViewTypeCount() {
return 2;
}
@Override
public int getItemViewType(int position) {
Row r = getItem(position);
return r instanceof AppRow ? 1 : 0;
}
public View getView(int position, View convertView, ViewGroup parent) {
Row r = getItem(position);
View v;
if (convertView == null) {
v = newView(parent, r);
} else {
v = convertView;
}
bindView(v, r, false /*animate*/);
return v;
}
public View newView(ViewGroup parent, Row r) {
if (!(r instanceof AppRow)) {
return mInflater.inflate(R.layout.notification_app_section, parent, false);
}
final View v = mInflater.inflate(R.layout.notification_app, parent, false);
final ViewHolder vh = new ViewHolder();
vh.row = (ViewGroup) v;
vh.row.setLayoutTransition(new LayoutTransition());
vh.row.setLayoutTransition(new LayoutTransition());
vh.icon = (ImageView) v.findViewById(android.R.id.icon);
vh.title = (TextView) v.findViewById(android.R.id.title);
vh.subtitle = (TextView) v.findViewById(android.R.id.text1);
vh.rowDivider = v.findViewById(R.id.row_divider);
v.setTag(vh);
return v;
}
private void enableLayoutTransitions(ViewGroup vg, boolean enabled) {
if (enabled) {
vg.getLayoutTransition().enableTransitionType(LayoutTransition.APPEARING);
vg.getLayoutTransition().enableTransitionType(LayoutTransition.DISAPPEARING);
} else {
vg.getLayoutTransition().disableTransitionType(LayoutTransition.APPEARING);
vg.getLayoutTransition().disableTransitionType(LayoutTransition.DISAPPEARING);
}
}
public void bindView(final View view, Row r, boolean animate) {
if (!(r instanceof AppRow)) {
// it's a section row
final TextView tv = (TextView)view.findViewById(android.R.id.title);
tv.setText(r.section);
return;
}
final AppRow row = (AppRow)r;
final ViewHolder vh = (ViewHolder) view.getTag();
enableLayoutTransitions(vh.row, animate);
vh.rowDivider.setVisibility(row.first ? View.GONE : View.VISIBLE);
vh.row.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
mContext.startActivity(new Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS)
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
.putExtra(Settings.EXTRA_APP_PACKAGE, row.pkg)
.putExtra(Settings.EXTRA_APP_UID, row.uid)
.putExtra(EXTRA_HAS_SETTINGS_INTENT, row.settingsIntent != null)
.putExtra(EXTRA_SETTINGS_INTENT, row.settingsIntent));
}
});
enableLayoutTransitions(vh.row, animate);
vh.icon.setImageDrawable(row.icon);
vh.title.setText(row.label);
final String sub = getSubtitle(row);
vh.subtitle.setText(sub);
vh.subtitle.setVisibility(!sub.isEmpty() ? View.VISIBLE : View.GONE);
}
private String getSubtitle(AppRow row) {
if (row.banned) {
return mContext.getString(R.string.app_notification_row_banned);
}
if (!row.priority && !row.sensitive) {
return EMPTY_SUBTITLE;
}
final String priString = mContext.getString(R.string.app_notification_row_priority);
final String senString = mContext.getString(R.string.app_notification_row_sensitive);
if (row.priority != row.sensitive) {
return row.priority ? priString : senString;
}
return priString + mContext.getString(R.string.summary_divider_text) + senString;
}
@Override
public Object[] getSections() {
return mSections.toArray(new Object[mSections.size()]);
}
@Override
public int getPositionForSection(int sectionIndex) {
final String section = mSections.get(sectionIndex);
final int n = getCount();
for (int i = 0; i < n; i++) {
final Row r = getItem(i);
if (r.section.equals(section)) {
return i;
}
}
return 0;
}
@Override
public int getSectionForPosition(int position) {
Row row = getItem(position);
return mSections.indexOf(row.section);
}
}
private static class Row {
public String section;
}
public static class AppRow extends Row {
public String pkg;
public int uid;
public Drawable icon;
public CharSequence label;
public Intent settingsIntent;
public boolean banned;
public boolean priority;
public boolean sensitive;
public boolean first; // first app in section
}
private static final Comparator<AppRow> mRowComparator = new Comparator<AppRow>() {
private final Collator sCollator = Collator.getInstance();
@Override
public int compare(AppRow lhs, AppRow rhs) {
return sCollator.compare(lhs.label, rhs.label);
}
};
public static AppRow loadAppRow(PackageManager pm, PackageInfo pkg, Backend backend) {
final AppRow row = new AppRow();
row.pkg = pkg.packageName;
row.uid = pkg.applicationInfo.uid;
try {
row.label = pkg.applicationInfo.loadLabel(pm);
} catch (Throwable t) {
Log.e(TAG, "Error loading application label for " + row.pkg, t);
row.label = row.pkg;
}
row.icon = pkg.applicationInfo.loadIcon(pm);
row.banned = backend.getNotificationsBanned(row.pkg, row.uid);
row.priority = backend.getHighPriority(row.pkg, row.uid);
row.sensitive = backend.getSensitive(row.pkg, row.uid);
return row;
}
public static void collectConfigActivities(PackageManager pm, ArrayMap<String, AppRow> rows) {
if (DEBUG) Log.d(TAG, "APP_NOTIFICATION_PREFS_CATEGORY_INTENT is "
+ APP_NOTIFICATION_PREFS_CATEGORY_INTENT);
final List<ResolveInfo> resolveInfos = pm.queryIntentActivities(
APP_NOTIFICATION_PREFS_CATEGORY_INTENT,
PackageManager.MATCH_DEFAULT_ONLY);
if (DEBUG) Log.d(TAG, "Found " + resolveInfos.size() + " preference activities");
for (ResolveInfo ri : resolveInfos) {
final ActivityInfo activityInfo = ri.activityInfo;
final ApplicationInfo appInfo = activityInfo.applicationInfo;
final AppRow row = rows.get(appInfo.packageName);
if (row == null) {
Log.v(TAG, "Ignoring notification preference activity ("
+ activityInfo.name + ") for unknown package "
+ activityInfo.packageName);
continue;
}
if (row.settingsIntent != null) {
Log.v(TAG, "Ignoring duplicate notification preference activity ("
+ activityInfo.name + ") for package "
+ activityInfo.packageName);
continue;
}
row.settingsIntent = new Intent(Intent.ACTION_MAIN)
.setClassName(activityInfo.packageName, activityInfo.name);
}
}
private final Runnable mCollectAppsRunnable = new Runnable() {
@Override
public void run() {
synchronized (mRows) {
final long start = SystemClock.uptimeMillis();
if (DEBUG) Log.d(TAG, "Collecting apps...");
mRows.clear();
mSortedRows.clear();
// collect all non-system apps
final PackageManager pm = mContext.getPackageManager();
for (PackageInfo pkg : pm.getInstalledPackages(PackageManager.GET_SIGNATURES)) {
if (pkg.applicationInfo == null || isSystemApp(pkg)) {
if (DEBUG) Log.d(TAG, "Skipping " + pkg.packageName);
continue;
}
final AppRow row = loadAppRow(pm, pkg, mBackend);
mRows.put(row.pkg, row);
}
// collect config activities
collectConfigActivities(pm, mRows);
// sort rows
mSortedRows.addAll(mRows.values());
Collections.sort(mSortedRows, mRowComparator);
// compute sections
mSections.clear();
String section = null;
for (AppRow r : mSortedRows) {
r.section = getSection(r.label);
if (!r.section.equals(section)) {
section = r.section;
mSections.add(section);
}
}
mHandler.post(mRefreshAppsListRunnable);
final long elapsed = SystemClock.uptimeMillis() - start;
if (DEBUG) Log.d(TAG, "Collected " + mRows.size() + " apps in " + elapsed + "ms");
}
}
};
private void refreshDisplayedItems() {
if (DEBUG) Log.d(TAG, "Refreshing apps...");
mAdapter.clear();
synchronized (mSortedRows) {
String section = null;
final int N = mSortedRows.size();
boolean first = true;
for (int i = 0; i < N; i++) {
final AppRow row = mSortedRows.get(i);
if (!row.section.equals(section)) {
section = row.section;
Row r = new Row();
r.section = section;
mAdapter.add(r);
first = true;
}
row.first = first;
mAdapter.add(row);
first = false;
}
}
if (mListViewState != null) {
if (DEBUG) Log.d(TAG, "Restoring listView state");
getListView().onRestoreInstanceState(mListViewState);
mListViewState = null;
}
if (DEBUG) Log.d(TAG, "Refreshed " + mSortedRows.size() + " displayed items");
}
private final Runnable mRefreshAppsListRunnable = new Runnable() {
@Override
public void run() {
refreshDisplayedItems();
}
};
public static class Backend {
public boolean setNotificationsBanned(String pkg, int uid, boolean banned) {
INotificationManager nm = INotificationManager.Stub.asInterface(
ServiceManager.getService(Context.NOTIFICATION_SERVICE));
try {
nm.setNotificationsEnabledForPackage(pkg, uid, !banned);
return true;
} catch (Exception e) {
Log.w(TAG, "Error calling NoMan", e);
return false;
}
}
public boolean getNotificationsBanned(String pkg, int uid) {
INotificationManager nm = INotificationManager.Stub.asInterface(
ServiceManager.getService(Context.NOTIFICATION_SERVICE));
try {
final boolean enabled = nm.areNotificationsEnabledForPackage(pkg, uid);
return !enabled;
} catch (Exception e) {
Log.w(TAG, "Error calling NoMan", e);
return false;
}
}
public boolean getHighPriority(String pkg, int uid) {
INotificationManager nm = INotificationManager.Stub.asInterface(
ServiceManager.getService(Context.NOTIFICATION_SERVICE));
try {
return nm.getPackagePriority(pkg, uid) == Notification.PRIORITY_MAX;
} catch (Exception e) {
Log.w(TAG, "Error calling NoMan", e);
return false;
}
}
public boolean setHighPriority(String pkg, int uid, boolean highPriority) {
INotificationManager nm = INotificationManager.Stub.asInterface(
ServiceManager.getService(Context.NOTIFICATION_SERVICE));
try {
nm.setPackagePriority(pkg, uid,
highPriority ? Notification.PRIORITY_MAX : Notification.PRIORITY_DEFAULT);
return true;
} catch (Exception e) {
Log.w(TAG, "Error calling NoMan", e);
return false;
}
}
public boolean getSensitive(String pkg, int uid) {
// TODO get visibility state from NoMan
return false;
}
public boolean setSensitive(String pkg, int uid, boolean sensitive) {
// TODO save visibility state to NoMan
return true;
}
}
}