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" /> android:resource="@id/notification_settings" />
</activity> </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:label="@string/app_notifications_title"
android:exported="true" android:exported="true"
android:taskAffinity=""> android:taskAffinity="">
<meta-data android:name="com.android.settings.FRAGMENT_CLASS" <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" <meta-data android:name="com.android.settings.TOP_LEVEL_HEADER_ID"
android:resource="@id/notification_settings" /> android:resource="@id/notification_settings" />
</activity> </activity>
<activity android:name=".notification.AppNotificationDialog" <!-- Show application-level notification settings (app passed in as extras) -->
android:theme="@style/Theme.AlertDialog" <activity android:name="Settings$AppNotificationSettingsActivity"
android:launchMode="singleTop" android:label="@string/app_notifications_title"
android:exported="true"> android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<action android:name="android.settings.APP_NOTIFICATION_SETTINGS" /> <action android:name="android.settings.APP_NOTIFICATION_SETTINGS" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
</intent-filter> </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> </activity>
<!-- Show regulatory info (from settings item or dialing "*#07#") --> <!-- 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 See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
--> -->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="?android:attr/listChoiceBackgroundIndicator"
android:paddingStart="?android:attr/listPreferredItemPaddingStart" android:paddingStart="?android:attr/listPreferredItemPaddingStart"
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"> 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 <ImageView
android:id="@android:id/button2" android:id="@android:id/icon"
android:layout_width="@dimen/notification_app_icon_size" android:layout_width="@dimen/notification_app_icon_size"
android:layout_height="@dimen/notification_app_icon_size" android:layout_height="@dimen/notification_app_icon_size"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true" android:layout_centerVertical="true"
android:background="?android:attr/listChoiceBackgroundIndicator" android:contentDescription="@null"
android:contentDescription="@string/notification_app_settings_button" android:padding="8dp" />
android:scaleType="center"
android:src="@drawable/ic_settings_generic" /> <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 <View
android:id="@+id/row_divider" 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_size">64dp</dimen>
<dimen name="notification_app_icon_badge_size">20dp</dimen> <dimen name="notification_app_icon_badge_size">20dp</dimen>
<dimen name="notification_app_icon_badge_margin">4dp</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_mode_dropdown_width">160dp</dimen>
<dimen name="zen_downtime_checkbox_padding">7dp</dimen> <dimen name="zen_downtime_checkbox_padding">7dp</dimen>
<dimen name="zen_downtime_margin">10dp</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 --> <!-- [CHAR LIMIT=NONE] Text when loading app list in notification settings -->
<string name="loading_notification_apps">Loading apps...</string> <string name="loading_notification_apps">Loading apps...</string>
<!-- [CHAR LIMIT=NONE] Notification settings: App notifications dialog show option --> <!-- [CHAR LIMIT=NONE] App notification settings: Block option title -->
<string name="app_notifications_dialog_show">Show notifications</string> <string name="app_notification_block_title">Block</string>
<!-- [CHAR LIMIT=NONE] Notification settings: App notifications dialog priority option --> <!-- [CHAR LIMIT=NONE] App notification settings: Block option description-->
<string name="app_notifications_dialog_priority">Display at the top of the list</string> <string name="app_notification_block_summary">Never show notifications from this app</string>
<!-- [CHAR LIMIT=NONE] Notification settings: App notifications dialog visibility option --> <!-- [CHAR LIMIT=NONE] App notification settings: Priority option title -->
<string name="app_notifications_dialog_visibility">Hide sensitive content when device is locked</string> <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 --> <!-- [CHAR LIMIT=20] Notification settings: App notifications row summary when banned -->
<string name="app_notification_row_banned">Blocked</string> <string name="app_notification_row_banned">Blocked</string>
<!-- [CHAR LIMIT=20] Notification settings: App notifications row summary when high priority --> <!-- [CHAR LIMIT=40] Notification settings: App notifications row summary when high priority -->
<string name="app_notification_row_priority">Top of list</string> <string name="app_notification_row_priority">Priority</string>
<!-- [CHAR LIMIT=20] Notification settings: App notifications row summary when sensitive --> <!-- [CHAR LIMIT=20] Notification settings: App notifications row summary when sensitive -->
<string name="app_notification_row_sensitive">Sensitive</string> <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 --> <!-- [CHAR LIMIT=30] Zen mode settings: Exit condition selection dialog, default option -->
<string name="zen_mode_default_option">Until you turn this off</string> <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 <PreferenceScreen
android:key="app_notifications" android:key="app_notifications"
android:title="@string/app_notifications_title" android:title="@string/app_notifications_title"
android:fragment="com.android.settings.notification.AppNotificationSettings" /> android:fragment="com.android.settings.notification.NotificationAppList" />
<!-- Notification access --> <!-- Notification access -->
<Preference <Preference

View File

@@ -92,6 +92,7 @@ public class Settings extends SettingsActivity {
public static class PrintJobSettingsActivity extends SettingsActivity { /* empty */ } public static class PrintJobSettingsActivity extends SettingsActivity { /* empty */ }
public static class ZenModeSettingsActivity extends SettingsActivity { /* empty */ } public static class ZenModeSettingsActivity extends SettingsActivity { /* empty */ }
public static class NotificationSettingsActivity 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 AppNotificationSettingsActivity extends SettingsActivity { /* empty */ }
public static class OtherSoundSettingsActivity extends SettingsActivity { /* empty */ } public static class OtherSoundSettingsActivity extends SettingsActivity { /* empty */ }
public static class QuickLaunchSettingsActivity 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.deviceinfo.UsbSettings;
import com.android.settings.fuelgauge.BatterySaverSettings; import com.android.settings.fuelgauge.BatterySaverSettings;
import com.android.settings.fuelgauge.PowerUsageSummary; 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.notification.OtherSoundSettings;
import com.android.settings.quicklaunch.QuickLaunchSettings; import com.android.settings.quicklaunch.QuickLaunchSettings;
import com.android.settings.search.DynamicIndexableContentMonitor; 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.location.LocationSettings;
import com.android.settings.nfc.AndroidBeam; import com.android.settings.nfc.AndroidBeam;
import com.android.settings.nfc.PaymentSettings; import com.android.settings.nfc.PaymentSettings;
import com.android.settings.notification.AppNotificationSettings;
import com.android.settings.notification.ConditionProviderSettings; import com.android.settings.notification.ConditionProviderSettings;
import com.android.settings.notification.NotificationAccessSettings; import com.android.settings.notification.NotificationAccessSettings;
import com.android.settings.notification.NotificationSettings; import com.android.settings.notification.NotificationSettings;
@@ -286,6 +287,7 @@ public class SettingsActivity extends Activity
ChooseLockPattern.ChooseLockPatternFragment.class.getName(), ChooseLockPattern.ChooseLockPatternFragment.class.getName(),
InstalledAppDetails.class.getName(), InstalledAppDetails.class.getName(),
BatterySaverSettings.class.getName(), BatterySaverSettings.class.getName(),
NotificationAppList.class.getName(),
AppNotificationSettings.class.getName(), AppNotificationSettings.class.getName(),
OtherSoundSettings.class.getName(), OtherSoundSettings.class.getName(),
QuickLaunchSettings.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; package com.android.settings.notification;
import android.animation.LayoutTransition;
import android.app.INotificationManager;
import android.app.Notification;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo; import android.content.pm.PackageInfo;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException; 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.Bundle;
import android.os.Handler; import android.preference.Preference;
import android.os.Parcelable; import android.preference.Preference.OnPreferenceChangeListener;
import android.os.ServiceManager; import android.preference.SwitchPreference;
import android.os.SystemClock;
import android.provider.Settings; import android.provider.Settings;
import android.os.UserHandle; import android.text.TextUtils;
import android.os.UserManager;
import android.util.ArrayMap; import android.util.ArrayMap;
import android.util.Log; import android.util.Log;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.View.OnClickListener; import android.view.View.OnClickListener;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.AdapterView.OnItemSelectedListener;
import android.widget.AdapterView;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.SectionIndexer;
import android.widget.Spinner;
import android.widget.TextView; 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.R;
import com.android.settings.UserSpinnerAdapter; import com.android.settings.SettingsPreferenceFragment;
import com.android.settings.Utils; import com.android.settings.notification.NotificationAppList.AppRow;
import com.android.settings.notification.NotificationAppList.Backend;
import java.text.Collator; /** These settings are per app, so should not be returned in global search results. */
import java.util.ArrayList; public class AppNotificationSettings extends SettingsPreferenceFragment {
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 {
private static final String TAG = "AppNotificationSettings"; private static final String TAG = "AppNotificationSettings";
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
private static final String SECTION_BEFORE_A = "*"; private static final String KEY_BLOCK = "block";
private static final String SECTION_AFTER_Z = "**"; private static final String KEY_PRIORITY = "priority";
private static final Intent APP_NOTIFICATION_PREFS_CATEGORY_INTENT private static final String KEY_SENSITIVE = "sensitive";
= new Intent(Intent.ACTION_MAIN)
.addCategory(Notification.INTENT_CATEGORY_NOTIFICATION_PREFERENCES);
private final Handler mHandler = new Handler(); static final String EXTRA_HAS_SETTINGS_INTENT = "has_settings_intent";
private final ArrayMap<String, AppRow> mRows = new ArrayMap<String, AppRow>(); static final String EXTRA_SETTINGS_INTENT = "settings_intent";
private final ArrayList<AppRow> mSortedRows = new ArrayList<AppRow>();
private final ArrayList<String> mSections = new ArrayList<String>(); private final Backend mBackend = new Backend();
private Context mContext; private Context mContext;
private LayoutInflater mInflater; private SwitchPreference mBlock;
private NotificationAppAdapter mAdapter; private SwitchPreference mPriority;
private Signature[] mSystemSignature; private SwitchPreference mSensitive;
private Parcelable mListViewState; private AppRow mAppRow;
private Backend mBackend = new Backend(); private boolean mCreated;
private UserSpinnerAdapter mProfileSpinnerAdapter;
@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 @Override
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
mContext = getActivity(); mContext = getActivity();
mInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); Intent intent = getActivity().getIntent();
mAdapter = new NotificationAppAdapter(mContext); if (DEBUG) Log.d(TAG, "onCreate getIntent()=" + intent);
getActivity().setTitle(R.string.app_notifications_title); if (intent == null) {
} Log.w(TAG, "No intent");
toastAndFinish();
@Override return;
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);
} }
@Override final int uid = intent.getIntExtra(Settings.EXTRA_APP_UID, -1);
public boolean hasStableIds() { final String pkg = intent.getStringExtra(Settings.EXTRA_APP_PACKAGE);
return true; 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 if (DEBUG) Log.d(TAG, "Load details for pkg=" + pkg + " uid=" + uid);
public long getItemId(int position) { final PackageManager pm = getPackageManager();
return position; 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 addPreferencesFromResource(R.xml.app_notification_settings);
public int getViewTypeCount() { mBlock = (SwitchPreference) findPreference(KEY_BLOCK);
return 2; mPriority = (SwitchPreference) findPreference(KEY_PRIORITY);
} mSensitive = (SwitchPreference) findPreference(KEY_SENSITIVE);
@Override mAppRow = NotificationAppList.loadAppRow(pm, info, mBackend);
public int getItemViewType(int position) { if (intent.hasExtra(EXTRA_HAS_SETTINGS_INTENT)) {
Row r = getItem(position); // use settings intent from extra
return r instanceof AppRow ? 1 : 0; if (intent.getBooleanExtra(EXTRA_HAS_SETTINGS_INTENT, false)) {
} mAppRow.settingsIntent = intent.getParcelableExtra(EXTRA_SETTINGS_INTENT);
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*/); } else {
return v; // 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) { mBlock.setChecked(mAppRow.banned);
if (!(r instanceof AppRow)) { mPriority.setChecked(mAppRow.priority);
return mInflater.inflate(R.layout.notification_app_section, parent, false); 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) { mPriority.setOnPreferenceChangeListener(new OnPreferenceChangeListener() {
if (enabled) { @Override
vg.getLayoutTransition().enableTransitionType(LayoutTransition.APPEARING); public boolean onPreferenceChange(Preference preference, Object newValue) {
vg.getLayoutTransition().enableTransitionType(LayoutTransition.DISAPPEARING); final boolean priority = (Boolean) newValue;
} else { return mBackend.setHighPriority(pkg, uid, priority);
vg.getLayoutTransition().disableTransitionType(LayoutTransition.APPEARING);
vg.getLayoutTransition().disableTransitionType(LayoutTransition.DISAPPEARING);
} }
} });
public void bindView(final View view, Row r, boolean animate) { mSensitive.setOnPreferenceChangeListener(new OnPreferenceChangeListener() {
if (!(r instanceof AppRow)) { @Override
// it's a section row public boolean onPreferenceChange(Preference preference, Object newValue) {
final TextView tv = (TextView)view.findViewById(android.R.id.title); final boolean sensitive = (Boolean) newValue;
tv.setText(r.section); return mBackend.setSensitive(pkg, uid, sensitive);
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.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 { private void toastAndFinish() {
public String section; Toast.makeText(mContext, R.string.app_not_found_dlg_text, Toast.LENGTH_SHORT).show();
getActivity().finish();
} }
public static class AppRow extends Row { private static PackageInfo findPackageInfo(PackageManager pm, String pkg, int uid) {
public String pkg; final String[] packages = pm.getPackagesForUid(uid);
public int uid; if (packages != null && pkg != null) {
public Drawable icon; final int N = packages.length;
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;
for (int i = 0; i < N; i++) { for (int i = 0; i < N; i++) {
final AppRow row = mSortedRows.get(i); final String p = packages[i];
if (!row.section.equals(section)) { if (pkg.equals(p)) {
section = row.section; try {
Row r = new Row(); return pm.getPackageInfo(pkg, 0);
r.section = section; } catch (NameNotFoundException e) {
mAdapter.add(r); Log.w(TAG, "Failed to load package " + pkg, e);
first = true; }
} }
row.first = first;
mAdapter.add(row);
first = false;
} }
} }
if (mListViewState != null) { return 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;
}
} }
} }

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