Show the user's list of notifications in Settings.

Requires new APIs in change I41338230 and change Icce8d6f9
from frameworks/base.

Change-Id: I21b645bdc265b477453f9b177f6776e2c58bd323
This commit is contained in:
Daniel Sandler
2013-01-17 13:14:02 -05:00
parent 46e1578bfa
commit 328e2d2666
10 changed files with 550 additions and 0 deletions

View File

@@ -62,6 +62,7 @@
<uses-permission android:name="android.permission.READ_PROFILE" />
<uses-permission android:name="android.permission.CONFIGURE_WIFI_DISPLAY" />
<uses-permission android:name="android.permission.SET_TIME" />
<uses-permission android:name="android.permission.ACCESS_NOTIFICATIONS" />
<application android:label="@string/settings_label"
android:icon="@mipmap/ic_launcher_settings"
@@ -768,6 +769,19 @@
android:resource="@id/application_settings" />
</activity>
<activity android:name="Settings$NotificationStationActivity"
android:label="@string/sound_category_notification_title"
android:taskAffinity=""
android:excludeFromRecents="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="com.android.settings.SHORTCUT" />
</intent-filter>
<meta-data android:name="com.android.settings.FRAGMENT_CLASS"
android:value="com.android.settings.NotificationStation" />
</activity>
<activity android:name="Settings$AppOpsSummaryActivity"
android:label="@string/app_ops_settings"
android:taskAffinity=""

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 967 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

@@ -0,0 +1,115 @@
<!--
Copyright (C) 2013 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content" >
<!-- Dream selectable row (icon, caption, radio button) -->
<RelativeLayout
android:id="@android:id/widget_frame"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_toStartOf="@+id/divider"
android:background="?android:attr/selectableItemBackground" >
<!-- Dream icon -->
<ImageView
android:id="@+id/pkgicon"
android:layout_width="@*android:dimen/status_bar_icon_size"
android:layout_height="@*android:dimen/status_bar_icon_size"
android:layout_centerVertical="true"
android:layout_marginBottom="6dp"
android:layout_marginStart="0dp"
android:layout_marginEnd="6dp"
android:layout_marginTop="6dp"
android:contentDescription="@null"
android:maxHeight="@*android:dimen/status_bar_icon_size"
android:maxWidth="@*android:dimen/status_bar_icon_size"
android:scaleType="fitCenter" />
<ImageView
android:id="@android:id/icon"
android:layout_width="@*android:dimen/status_bar_icon_size"
android:layout_height="@*android:dimen/status_bar_icon_size"
android:layout_centerVertical="true"
android:layout_toEndOf="@id/pkgicon"
android:layout_marginBottom="6dp"
android:layout_marginStart="0dp"
android:layout_marginEnd="8dp"
android:layout_marginTop="6dp"
android:contentDescription="@null"
android:maxHeight="@*android:dimen/status_bar_icon_size"
android:maxWidth="@*android:dimen/status_bar_icon_size"
android:scaleType="fitCenter" />
<!-- Dream caption -->
<TextView
android:id="@android:id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_toStartOf="@android:id/button1"
android:layout_toEndOf="@android:id/icon"
android:ellipsize="end"
android:singleLine="true"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textAlignment="viewStart"
android:labelFor="@android:id/button2" />
<!-- Dream radio button -->
<!--<RadioButton
android:id="@android:id/button1"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:duplicateParentState="true"
android:clickable="false"
android:focusable="false" />-->
</RelativeLayout>
<!-- Divider -->
<ImageView
android:id="@id/divider"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_centerVertical="true"
android:layout_toStartOf="@android:id/button2"
android:contentDescription="@null"
android:src="@drawable/nav_divider" />
<!-- Settings icon -->
<ImageView
android:id="@android:id/button2"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_alignBottom="@android:id/widget_frame"
android:layout_alignParentEnd="true"
android:layout_alignTop="@android:id/widget_frame"
android:layout_centerVertical="true"
android:layout_margin="0dip"
android:background="?android:attr/selectableItemBackground"
android:contentDescription="@string/screensaver_settings_button"
android:padding="8dip"
android:src="@drawable/ic_bt_config" />
</RelativeLayout>

View File

@@ -0,0 +1,96 @@
<!--
Copyright (C) 2013 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@android:id/widget_frame"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_toStartOf="@+id/divider"
android:background="?android:attr/selectableItemBackground" >
<!-- Dream icon -->
<ImageView
android:id="@+id/pkgicon"
android:layout_width="@*android:dimen/status_bar_icon_size"
android:layout_height="@*android:dimen/status_bar_icon_size"
android:layout_centerVertical="true"
android:layout_marginBottom="6dp"
android:layout_marginStart="0dp"
android:layout_marginEnd="6dp"
android:layout_marginTop="6dp"
android:contentDescription="@null"
android:adjustViewBounds="true"
android:maxHeight="@*android:dimen/status_bar_icon_size"
android:maxWidth="@*android:dimen/status_bar_icon_size"
android:scaleType="fitCenter" />
<ImageView
android:id="@android:id/icon"
android:layout_width="@*android:dimen/status_bar_icon_size"
android:layout_height="@*android:dimen/status_bar_icon_size"
android:layout_centerVertical="true"
android:layout_toEndOf="@id/pkgicon"
android:layout_marginBottom="6dp"
android:layout_marginStart="0dp"
android:layout_marginEnd="8dp"
android:layout_marginTop="6dp"
android:contentDescription="@null"
android:adjustViewBounds="true"
android:maxHeight="@*android:dimen/status_bar_icon_size"
android:maxWidth="@*android:dimen/status_bar_icon_size"
android:scaleType="fitCenter" />
<!-- Dream caption -->
<TextView
android:id="@android:id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_toStartOf="@+id/timestamp"
android:layout_toEndOf="@android:id/icon"
android:ellipsize="end"
android:singleLine="true"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textAlignment="viewStart"
android:labelFor="@android:id/button2" />
<!-- Dream radio button -->
<!--<RadioButton
android:id="@android:id/button1"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:duplicateParentState="true"
android:clickable="false"
android:focusable="false" />-->
<DateTimeView
android:id="@+id/timestamp"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_alignBottom="@android:id/widget_frame"
android:layout_alignParentEnd="true"
android:layout_alignTop="@android:id/widget_frame"
android:layout_centerVertical="true"
android:ellipsize="end"
android:singleLine="true"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textAlignment="viewEnd"
/>
</RelativeLayout>

View File

@@ -588,6 +588,7 @@
<item>write ICC SMS</item>
<item>modify settings</item>
<item>draw on top</item>
<item>access notifications</item>
</string-array>
<!-- User display names for app ops codes -->
@@ -617,6 +618,7 @@
<item>Send SMS/MMS</item>
<item>Modify settings</item>
<item>Draw on top</item>
<item>Access notifications</item>
</string-array>
<!-- Titles for the list of long press timeout options. -->

View File

@@ -0,0 +1,321 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings;
import android.app.Activity;
import android.app.ActivityManager;
import android.app.INotificationManager;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.TaskStackBuilder;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.UserHandle;
import android.provider.*;
import android.util.Log;
import android.util.Slog;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnTouchListener;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.DateTimeView;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.RadioButton;
import android.widget.TextView;
import com.android.internal.statusbar.StatusBarNotification;
import com.android.settings.DreamBackend.DreamInfo;
import java.util.ArrayList;
import java.util.List;
public class NotificationStation extends SettingsPreferenceFragment {
private static final String TAG = NotificationStation.class.getSimpleName();
static final boolean DEBUG = true;
private static final String PACKAGE_SCHEME = "package";
private final PackageReceiver mPackageReceiver = new PackageReceiver();
private INotificationManager mNoMan;
private NotificationHistoryAdapter mAdapter;
private Context mContext;
@Override
public void onAttach(Activity activity) {
logd("onAttach(%s)", activity.getClass().getSimpleName());
super.onAttach(activity);
mContext = activity;
mNoMan = INotificationManager.Stub.asInterface(
ServiceManager.getService(Context.NOTIFICATION_SERVICE));
}
@Override
public void onCreate(Bundle icicle) {
logd("onCreate(%s)", icicle);
super.onCreate(icicle);
Activity activity = getActivity();
}
@Override
public void onDestroyView() {
super.onDestroyView();
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
logd("onActivityCreated(%s)", savedInstanceState);
super.onActivityCreated(savedInstanceState);
ListView listView = getListView();
// TextView emptyView = (TextView) getView().findViewById(android.R.id.empty);
// emptyView.setText(R.string.screensaver_settings_disabled_prompt);
// listView.setEmptyView(emptyView);
mAdapter = new NotificationHistoryAdapter(mContext);
listView.setAdapter(mAdapter);
}
@Override
public void onPause() {
logd("onPause()");
super.onPause();
mContext.unregisterReceiver(mPackageReceiver);
}
@Override
public void onResume() {
logd("onResume()");
super.onResume();
refreshFromBackend();
// listen for package changes
IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_PACKAGE_ADDED);
filter.addAction(Intent.ACTION_PACKAGE_CHANGED);
filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
filter.addAction(Intent.ACTION_PACKAGE_REPLACED);
filter.addDataScheme(PACKAGE_SCHEME);
mContext.registerReceiver(mPackageReceiver , filter);
}
private void refreshFromBackend() {
List<HistoricalNotificationInfo> infos = loadNotifications();
if (infos != null) {
logd("adding %d infos", infos.size());
mAdapter.clear();
mAdapter.addAll(infos);
}
}
private static void logd(String msg, Object... args) {
if (DEBUG)
Log.d(TAG, args == null || args.length == 0 ? msg : String.format(msg, args));
}
private static class HistoricalNotificationInfo {
public String pkg;
public Drawable pkgicon;
public Drawable icon;
public CharSequence title;
public int priority;
public int user;
public long timestamp;
}
private List<HistoricalNotificationInfo> loadNotifications() {
final int currentUserId = ActivityManager.getCurrentUser();
try {
StatusBarNotification[] nions = mNoMan.getHistoricalNotifications(
mContext.getPackageName(), 100);
List<HistoricalNotificationInfo> list
= new ArrayList<HistoricalNotificationInfo>(nions.length);
for (StatusBarNotification sbn : nions) {
final HistoricalNotificationInfo info = new HistoricalNotificationInfo();
info.pkg = sbn.pkg;
info.user = sbn.getUserId();
info.icon = loadIconDrawable(info.pkg, info.user, sbn.notification.icon);
info.pkgicon = loadPackageIconDrawable(info.pkg, info.user);
if (sbn.notification.extras != null) {
info.title = sbn.notification.extras.getString(Notification.EXTRA_TITLE);
}
info.timestamp = sbn.postTime;
info.priority = sbn.notification.priority;
logd(" [%d] %s: %s", info.timestamp, info.pkg, info.title);
if (info.user == UserHandle.USER_ALL
|| info.user == currentUserId) {
list.add(info);
}
}
return list;
} catch (RemoteException e) {
e.printStackTrace(); //To change body of catch statement use File | Settings | File Templates.
}
return null;
}
private Resources getResourcesForUserPackage(String pkg, int userId) {
Resources r = null;
if (pkg != null) {
try {
if (userId == UserHandle.USER_ALL) {
userId = UserHandle.USER_OWNER;
}
r = mContext.getPackageManager()
.getResourcesForApplicationAsUser(pkg, userId);
} catch (PackageManager.NameNotFoundException ex) {
Log.e(TAG, "Icon package not found: " + pkg);
return null;
}
} else {
r = mContext.getResources();
}
return r;
}
private Drawable loadPackageIconDrawable(String pkg, int userId) {
Drawable icon = null;
try {
icon = mContext.getPackageManager().getApplicationIcon(pkg);
} catch (PackageManager.NameNotFoundException e) {
}
return icon;
}
private Drawable loadIconDrawable(String pkg, int userId, int resId) {
Resources r = getResourcesForUserPackage(pkg, userId);
if (resId == 0) {
return null;
}
try {
return r.getDrawable(resId);
} catch (RuntimeException e) {
Log.w(TAG, "Icon not found in "
+ (pkg != null ? resId : "<system>")
+ ": " + Integer.toHexString(resId));
}
return null;
}
private class NotificationHistoryAdapter extends ArrayAdapter<HistoricalNotificationInfo> {
private final LayoutInflater mInflater;
public NotificationHistoryAdapter(Context context) {
super(context, 0);
mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
HistoricalNotificationInfo info = getItem(position);
logd("getView(%s/%s)", info.pkg, info.title);
final View row = convertView != null ? convertView : createRow(parent, info.pkg);
row.setTag(info);
// bind icon
if (info.icon != null) {
((ImageView) row.findViewById(android.R.id.icon)).setImageDrawable(info.icon);
}
if (info.pkgicon != null) {
((ImageView) row.findViewById(R.id.pkgicon)).setImageDrawable(info.pkgicon);
}
((DateTimeView) row.findViewById(R.id.timestamp)).setTime(info.timestamp);
// bind caption
((TextView) row.findViewById(android.R.id.title)).setText(info.title);
// // bind radio button
// RadioButton radioButton = (RadioButton) row.findViewById(android.R.id.button1);
// radioButton.setChecked(dreamInfo.isActive);
// radioButton.setOnTouchListener(new OnTouchListener() {
// @Override
// public boolean onTouch(View v, MotionEvent event) {
// row.onTouchEvent(event);
// return false;
// }});
// bind settings button + divider
// boolean showSettings = info.
// settingsComponentName != null;
// View settingsDivider = row.findViewById(R.id.divider);
// settingsDivider.setVisibility(false ? View.VISIBLE : View.INVISIBLE);
//
// ImageView settingsButton = (ImageView) row.findViewById(android.R.id.button2);
// settingsButton.setVisibility(false ? View.VISIBLE : View.INVISIBLE);
// settingsButton.setAlpha(info.isActive ? 1f : Utils.DISABLED_ALPHA);
// settingsButton.setEnabled(info.isActive);
// settingsButton.setOnClickListener(new OnClickListener(){
// @Override
// public void onClick(View v) {
// mBackend.launchSettings((DreamInfo) row.getTag());
// }});
return row;
}
private View createRow(ViewGroup parent, final String pkg) {
final View row = mInflater.inflate(R.layout.notification_log_row, parent, false);
row.setOnClickListener(new OnClickListener(){
@Override
public void onClick(View v) {
v.setPressed(true);
startApplicationDetailsActivity(pkg);
}});
return row;
}
}
private void startApplicationDetailsActivity(String packageName) {
Intent intent = new Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
Uri.fromParts("package", packageName, null));
intent.setComponent(intent.resolveActivity(mContext.getPackageManager()));
startActivity(intent);
}
private class PackageReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
logd("PackageReceiver.onReceive");
//refreshFromBackend();
}
}
}

View File

@@ -825,4 +825,5 @@ public class Settings extends PreferenceActivity
public static class AndroidBeamSettingsActivity extends Settings { /* empty */ }
public static class WifiDisplaySettingsActivity extends Settings { /* empty */ }
public static class DreamSettingsActivity extends Settings { /* empty */ }
public static class NotificationStationActivity extends Settings { /* empty */ }
}

View File

@@ -148,6 +148,7 @@ public class AppOpsState {
public static final OpsTemplate DEVICE_TEMPLATE = new OpsTemplate(
new int[] { AppOpsManager.OP_VIBRATE,
AppOpsManager.OP_POST_NOTIFICATION,
AppOpsManager.OP_ACCESS_NOTIFICATIONS,
AppOpsManager.OP_CALL_PHONE,
AppOpsManager.OP_WRITE_SETTINGS,
AppOpsManager.OP_SYSTEM_ALERT_WINDOW },