aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChris Wren <cwren@android.com>2014-05-07 17:44:21 -0400
committerQiwen Zhao <zhao@google.com>2014-05-28 07:35:23 -0700
commitf715fd998b172c4e8658715a26a41c69144380be (patch)
tree03fd268c9f813c49a9aff719212c91fe1b42775b
parente008f2f02529be466b91a8456ea736a48e3f13e4 (diff)
downloadexperimental-f715fd998b172c4e8658715a26a41c69144380be.tar.gz
example notification listener
demonstrate order-sensitive features of the listeners also launch contentIntents and handle dismissal Change-Id: I9e58c72a8d570f4b9a23e4642632b19fcd682dca Depends-On: I7244c65944a9657df41fb313b3cb5a52e149709d
-rw-r--r--NotificationListenerSample/Android.mk33
-rw-r--r--NotificationListenerSample/AndroidManifest.xml37
-rw-r--r--NotificationListenerSample/res/drawable-hdpi/dialog_ic_dismiss.pngbin0 -> 347 bytes
-rw-r--r--NotificationListenerSample/res/drawable-hdpi/icon.pngbin0 -> 7828 bytes
-rw-r--r--NotificationListenerSample/res/drawable-mdpi/dialog_ic_dismiss.pngbin0 -> 313 bytes
-rw-r--r--NotificationListenerSample/res/drawable-mdpi/icon.pngbin0 -> 4287 bytes
-rw-r--r--NotificationListenerSample/res/drawable-xhdpi/dialog_ic_dismiss.pngbin0 -> 398 bytes
-rw-r--r--NotificationListenerSample/res/drawable-xhdpi/icon.pngbin0 -> 12264 bytes
-rw-r--r--NotificationListenerSample/res/drawable-xxhdpi/dialog_ic_dismiss.pngbin0 -> 321 bytes
-rw-r--r--NotificationListenerSample/res/drawable-xxhdpi/icon.pngbin0 -> 22796 bytes
-rw-r--r--NotificationListenerSample/res/layout/item.xml32
-rw-r--r--NotificationListenerSample/res/layout/main.xml50
-rw-r--r--NotificationListenerSample/res/values/strings.xml31
-rw-r--r--NotificationListenerSample/src/com/android/example/notificationlistener/Listener.java237
-rw-r--r--NotificationListenerSample/src/com/android/example/notificationlistener/NotificationListenerActivity.java260
-rw-r--r--NotificationListenerSample/src/com/android/example/notificationlistener/OrderedNotificationsHelper.java94
16 files changed, 774 insertions, 0 deletions
diff --git a/NotificationListenerSample/Android.mk b/NotificationListenerSample/Android.mk
new file mode 100644
index 0000000..5075a96
--- /dev/null
+++ b/NotificationListenerSample/Android.mk
@@ -0,0 +1,33 @@
+#
+# 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.
+#
+
+LOCAL_PATH:= $(call my-dir)
+
+include $(CLEAR_VARS)
+
+LOCAL_MODULE_TAGS := optional
+
+LOCAL_AAPT_FLAGS += -c mdpi,hdpi,xhdpi
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_STATIC_JAVA_LIBRARIES := android-support-v4
+
+LOCAL_PACKAGE_NAME := NotificationListenerSample
+LOCAL_CERTIFICATE := platform
+include $(BUILD_PACKAGE)
+
+include $(call all-makefiles-under,$(LOCAL_PATH))
diff --git a/NotificationListenerSample/AndroidManifest.xml b/NotificationListenerSample/AndroidManifest.xml
new file mode 100644
index 0000000..e36d422
--- /dev/null
+++ b/NotificationListenerSample/AndroidManifest.xml
@@ -0,0 +1,37 @@
+<?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.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.example.notificationlistener"
+ android:versionCode="1"
+ android:versionName="1.0">
+
+ <application android:icon="@drawable/icon" android:label="@string/app_name">
+ <activity android:name=".NotificationListenerActivity"
+ android:label="@string/app_name">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+ <service android:name="com.android.example.notificationlistener.Listener"
+ android:label="@string/service_name"
+ android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">
+ <intent-filter>
+ <action android:name="android.service.notification.NotificationListenerService" />
+ </intent-filter>
+ </service>
+ </application>
+</manifest>
diff --git a/NotificationListenerSample/res/drawable-hdpi/dialog_ic_dismiss.png b/NotificationListenerSample/res/drawable-hdpi/dialog_ic_dismiss.png
new file mode 100644
index 0000000..1aca4b7
--- /dev/null
+++ b/NotificationListenerSample/res/drawable-hdpi/dialog_ic_dismiss.png
Binary files differ
diff --git a/NotificationListenerSample/res/drawable-hdpi/icon.png b/NotificationListenerSample/res/drawable-hdpi/icon.png
new file mode 100644
index 0000000..bc976fa
--- /dev/null
+++ b/NotificationListenerSample/res/drawable-hdpi/icon.png
Binary files differ
diff --git a/NotificationListenerSample/res/drawable-mdpi/dialog_ic_dismiss.png b/NotificationListenerSample/res/drawable-mdpi/dialog_ic_dismiss.png
new file mode 100644
index 0000000..6a2fb5e
--- /dev/null
+++ b/NotificationListenerSample/res/drawable-mdpi/dialog_ic_dismiss.png
Binary files differ
diff --git a/NotificationListenerSample/res/drawable-mdpi/icon.png b/NotificationListenerSample/res/drawable-mdpi/icon.png
new file mode 100644
index 0000000..e8f7538
--- /dev/null
+++ b/NotificationListenerSample/res/drawable-mdpi/icon.png
Binary files differ
diff --git a/NotificationListenerSample/res/drawable-xhdpi/dialog_ic_dismiss.png b/NotificationListenerSample/res/drawable-xhdpi/dialog_ic_dismiss.png
new file mode 100644
index 0000000..e11f2e3
--- /dev/null
+++ b/NotificationListenerSample/res/drawable-xhdpi/dialog_ic_dismiss.png
Binary files differ
diff --git a/NotificationListenerSample/res/drawable-xhdpi/icon.png b/NotificationListenerSample/res/drawable-xhdpi/icon.png
new file mode 100644
index 0000000..002515c
--- /dev/null
+++ b/NotificationListenerSample/res/drawable-xhdpi/icon.png
Binary files differ
diff --git a/NotificationListenerSample/res/drawable-xxhdpi/dialog_ic_dismiss.png b/NotificationListenerSample/res/drawable-xxhdpi/dialog_ic_dismiss.png
new file mode 100644
index 0000000..8d185f4
--- /dev/null
+++ b/NotificationListenerSample/res/drawable-xxhdpi/dialog_ic_dismiss.png
Binary files differ
diff --git a/NotificationListenerSample/res/drawable-xxhdpi/icon.png b/NotificationListenerSample/res/drawable-xxhdpi/icon.png
new file mode 100644
index 0000000..e78554f
--- /dev/null
+++ b/NotificationListenerSample/res/drawable-xxhdpi/icon.png
Binary files differ
diff --git a/NotificationListenerSample/res/layout/item.xml b/NotificationListenerSample/res/layout/item.xml
new file mode 100644
index 0000000..2d9a5ed
--- /dev/null
+++ b/NotificationListenerSample/res/layout/item.xml
@@ -0,0 +1,32 @@
+<?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.
+-->
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_height="match_parent"
+ android:layout_width="wrap_content"
+ android:padding="4dp">
+ <FrameLayout android:id="@+id/remote_view"
+ android:layout_height="match_parent"
+ android:layout_width="wrap_content"
+ android:onClick="launch"
+ />
+ <ImageButton android:id="@+id/dismiss"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="top|end"
+ android:src="@drawable/dialog_ic_dismiss"
+ android:onClick="dismiss"
+ />
+</FrameLayout> \ No newline at end of file
diff --git a/NotificationListenerSample/res/layout/main.xml b/NotificationListenerSample/res/layout/main.xml
new file mode 100644
index 0000000..44c9da4
--- /dev/null
+++ b/NotificationListenerSample/res/layout/main.xml
@@ -0,0 +1,50 @@
+<?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.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ android:padding="12dp"
+ >
+ <ListView
+ android:id="@android:id/list"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:padding="4dp"
+ android:layout_marginBottom="16dp"
+ android:text="@string/working_as_intended"
+ android:layout_weight="1"
+ android:choiceMode="singleChoice"
+ />
+
+ <TextView android:id="@android:id/empty"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:padding="4dp"
+ android:layout_marginBottom="16dp"
+ android:text="@string/working_as_intended"
+ android:layout_weight="1"
+ />
+ <Button
+ android:id="@+id/launch_settings"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/launch_to_disable"
+ android:onClick="launchSettings"
+ android:layout_weight="0"
+ />
+</LinearLayout>
+
diff --git a/NotificationListenerSample/res/values/strings.xml b/NotificationListenerSample/res/values/strings.xml
new file mode 100644
index 0000000..5c74fda
--- /dev/null
+++ b/NotificationListenerSample/res/values/strings.xml
@@ -0,0 +1,31 @@
+<?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.
+-->
+<resources>
+ <string name="app_name">NotificationListener</string>
+ <string name="long_app_name">Notification Listener Sample</string>
+ <string name="service_name">Sample Notification Listener</string>
+ <string name="working_as_intended">Service is running. Disable it in Security Settings.</string>
+ <string name="waiting_for_content">Service is running. Waiting for notifications to arrive.</string>
+ <string name="nothing_to_see">Service is disabled. You must enabled it to see notifications.</string>
+ <string name="launch_to_disable">Press to disable service</string>
+ <string name="launch_to_enable">Press to enable service</string>
+ <string name="explanation">This app demonstrates use of the Notification Listener Service API.
+ You must enable the listener service in Security Settings to see it work.
+ </string>
+ <string name="disabled">Service is not running.</string>
+ <string name="enable_it">Enable</string>
+ <string name="cancel">Cancel</string>
+</resources>
diff --git a/NotificationListenerSample/src/com/android/example/notificationlistener/Listener.java b/NotificationListenerSample/src/com/android/example/notificationlistener/Listener.java
new file mode 100644
index 0000000..1ddb0e7
--- /dev/null
+++ b/NotificationListenerSample/src/com/android/example/notificationlistener/Listener.java
@@ -0,0 +1,237 @@
+/*
+ * 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.example.notificationlistener;
+
+
+import android.app.Notification;
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Handler;
+import android.os.Message;
+import android.service.notification.NotificationListenerService;
+import android.service.notification.StatusBarNotification;
+import android.support.v4.content.LocalBroadcastManager;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+public class Listener extends NotificationListenerService {
+ private static final String TAG = "SampleListener";
+
+ // Message tags
+ private static final int MSG_NOTIFY = 1;
+ private static final int MSG_CANCEL = 2;
+ private static final int MSG_STARTUP = 3;
+ private static final int MSG_ORDER = 4;
+ private static final int MSG_DISMISS = 5;
+ private static final int MSG_LAUNCH = 6;
+ private static final int PAGE = 10;
+
+ static final String ACTION_DISMISS = "com.android.example.notificationlistener.DISMISS";
+ static final String ACTION_LAUNCH = "com.android.example.notificationlistener.LAUNCH";
+ static final String ACTION_REFRESH = "com.android.example.notificationlistener.REFRESH";
+ static final String EXTRA_KEY = "key";
+
+ private static ArrayList<StatusBarNotification> sNotifications;
+
+ public static List<StatusBarNotification> getNotifications() {
+ return sNotifications;
+ }
+
+ private OrderedNotificationsHelper mOrderHelper;
+
+ private class Delta {
+ final StatusBarNotification mSbn;
+ final String[] mKeys;
+
+ public Delta(StatusBarNotification sbn, String[] update) {
+ mSbn = sbn;
+ mKeys = update;
+ }
+ }
+
+ private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String key = intent.getStringExtra(EXTRA_KEY);
+ int what = MSG_DISMISS;
+ if (ACTION_LAUNCH.equals(intent.getAction())) {
+ what = MSG_LAUNCH;
+ }
+ Log.d(TAG, "received an action broadcast " + intent.getAction());
+ if (!TextUtils.isEmpty(key)) {
+ Log.d(TAG, " on " + key);
+ Message.obtain(mHandler, what, key).sendToTarget();
+ }
+ }
+ };
+
+ Handler mHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ Delta delta = null;
+ if (msg.obj instanceof Delta) {
+ delta = (Delta) msg.obj;
+ }
+ switch (msg.what) {
+ case MSG_NOTIFY:
+ Log.i(TAG, "notify: " + delta.mSbn.getKey());
+ synchronized (sNotifications) {
+ int position = mOrderHelper.getIndex(delta.mSbn.getKey());
+ if (position == -1) {
+ sNotifications.add(delta.mSbn);
+ } else {
+ sNotifications.set(position, delta.mSbn);
+ }
+ mOrderHelper.updateKeyOrder(delta.mKeys);
+ Collections.sort(sNotifications, mOrderHelper);
+ Log.i(TAG, "finish with: " + sNotifications.size());
+ }
+ LocalBroadcastManager.getInstance(Listener.this)
+ .sendBroadcast(new Intent(ACTION_REFRESH)
+ .putExtra(EXTRA_KEY, delta.mSbn.getKey()));
+ break;
+
+ case MSG_CANCEL:
+ Log.i(TAG, "remove: " + delta.mSbn.getKey());
+ synchronized (sNotifications) {
+ int position = mOrderHelper.getIndex(delta.mSbn.getKey());
+ if (position != -1) {
+ sNotifications.remove(position);
+ }
+ mOrderHelper.updateKeyOrder(delta.mKeys);
+ Collections.sort(sNotifications, mOrderHelper);
+ }
+ LocalBroadcastManager.getInstance(Listener.this)
+ .sendBroadcast(new Intent(ACTION_REFRESH));
+ break;
+
+ case MSG_ORDER:
+ Log.i(TAG, "reorder");
+ synchronized (sNotifications) {
+ mOrderHelper.updateKeyOrder(delta.mKeys);
+ Collections.sort(sNotifications, mOrderHelper);
+ }
+ LocalBroadcastManager.getInstance(Listener.this)
+ .sendBroadcast(new Intent(ACTION_REFRESH));
+ break;
+
+ case MSG_STARTUP:
+ fetchActive();
+ Log.i(TAG, "start with: " + sNotifications.size() + " notifications.");
+ LocalBroadcastManager.getInstance(Listener.this)
+ .sendBroadcast(new Intent(ACTION_REFRESH));
+ break;
+
+ case MSG_DISMISS:
+ if (msg.obj instanceof String) {
+ final String key = (String) msg.obj;
+ StatusBarNotification sbn = sNotifications.get(mOrderHelper.getIndex(key));
+ if ((sbn.getNotification().flags & Notification.FLAG_AUTO_CANCEL) != 0 &&
+ sbn.getNotification().contentIntent != null) {
+ try {
+ sbn.getNotification().contentIntent.send();
+ } catch (PendingIntent.CanceledException e) {
+ Log.d(TAG, "failed to send intent for " + sbn.getKey(), e);
+ }
+ }
+ cancelNotification(key);
+ }
+ break;
+
+ case MSG_LAUNCH:
+ if (msg.obj instanceof String) {
+ final String key = (String) msg.obj;
+ StatusBarNotification sbn = sNotifications.get(mOrderHelper.getIndex(key));
+ if (sbn.getNotification().contentIntent != null) {
+ try {
+ sbn.getNotification().contentIntent.send();
+ } catch (PendingIntent.CanceledException e) {
+ Log.d(TAG, "failed to send intent for " + sbn.getKey(), e);
+ }
+ }
+ if ((sbn.getNotification().flags & Notification.FLAG_AUTO_CANCEL) != 0) {
+ cancelNotification(key);
+ }
+ }
+ break;
+ }
+ }
+ };
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ Log.d(TAG, "registering broadcast listener");
+ final IntentFilter intentFilter = new IntentFilter();
+ intentFilter.addAction(ACTION_DISMISS);
+ intentFilter.addAction(ACTION_LAUNCH);
+ LocalBroadcastManager.getInstance(this).registerReceiver(mBroadcastReceiver, intentFilter);
+ }
+
+ @Override
+ public void onDestroy() {
+ LocalBroadcastManager.getInstance(this).unregisterReceiver(mBroadcastReceiver);
+ super.onDestroy();
+ }
+
+ @Override
+ public void onListenerConnected(String[] notificationKeys) {
+ Message.obtain(mHandler, MSG_STARTUP).sendToTarget();
+ }
+
+ @Override
+ public void onNotificationOrderUpdate() {
+ Message.obtain(mHandler, MSG_ORDER,
+ new Delta(null, getOrderedNotificationKeys())).sendToTarget();
+ }
+
+ @Override
+ public void onNotificationPosted(StatusBarNotification sbn) {
+ Message.obtain(mHandler, MSG_NOTIFY,
+ new Delta(sbn, getOrderedNotificationKeys())).sendToTarget();
+ }
+
+ @Override
+ public void onNotificationRemoved(StatusBarNotification sbn) {
+ Message.obtain(mHandler, MSG_CANCEL,
+ new Delta(sbn, getOrderedNotificationKeys())).sendToTarget();
+ }
+
+ private void fetchActive() {
+ String[] keys = getOrderedNotificationKeys();
+ sNotifications = new ArrayList<StatusBarNotification>();
+ sNotifications.clear();
+ for (int i = 0; i < keys.length; i += PAGE) {
+ final int j = (i + PAGE < keys.length ? i + PAGE : keys.length);
+ String[] fetchKeys = Arrays.copyOfRange(keys, i, j);
+ StatusBarNotification[] sbns = getActiveNotifications(fetchKeys);
+ for(int s = 0; s < sbns.length; s++) {
+ // unfortunately cloneLight() is hidden
+ sNotifications.add(sbns[s]);
+ }
+ }
+ mOrderHelper = new OrderedNotificationsHelper(keys);
+ Collections.sort(sNotifications, mOrderHelper);
+ }
+}
diff --git a/NotificationListenerSample/src/com/android/example/notificationlistener/NotificationListenerActivity.java b/NotificationListenerSample/src/com/android/example/notificationlistener/NotificationListenerActivity.java
new file mode 100644
index 0000000..7699d33
--- /dev/null
+++ b/NotificationListenerSample/src/com/android/example/notificationlistener/NotificationListenerActivity.java
@@ -0,0 +1,260 @@
+/*
+ * 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.example.notificationlistener;
+
+import android.app.AlertDialog;
+import android.app.ListActivity;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Bundle;
+import android.provider.Settings.Secure;
+import android.service.notification.StatusBarNotification;
+import android.support.v4.content.LocalBroadcastManager;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.Button;
+import android.widget.FrameLayout;
+import android.widget.TextView;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+
+public class NotificationListenerActivity extends ListActivity {
+ private static final String LISTENER_PATH = "com.android.example.notificationlistener/" +
+ "com.android.example.notificationlistener.Listener";
+ private static final String TAG = "NotificationListenerActivity";
+
+ private Button mLaunchButton;
+ private TextView mEmptyText;
+ private StatusAdaptor mStatusAdaptor;
+ private final BroadcastReceiver mRefreshListener = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ Log.i(TAG, "update tickle");
+ updateList(intent.getStringExtra(Listener.EXTRA_KEY));
+ }
+ };
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setTitle(R.string.long_app_name);
+ setContentView(R.layout.main);
+ mLaunchButton = (Button) findViewById(R.id.launch_settings);
+ mEmptyText = (TextView) findViewById(android.R.id.empty);
+ mStatusAdaptor = new StatusAdaptor(this);
+ setListAdapter(mStatusAdaptor);
+ }
+
+ @Override
+ protected void onStop() {
+ LocalBroadcastManager.getInstance(this).unregisterReceiver(mRefreshListener);
+ super.onStop();
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ final IntentFilter intentFilter = new IntentFilter(Listener.ACTION_REFRESH);
+ LocalBroadcastManager.getInstance(this).registerReceiver(mRefreshListener, intentFilter);
+ updateList(null);
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ checkEnabled();
+ }
+
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ checkEnabled();
+ }
+
+ private void checkEnabled() {
+ String listeners = Secure.getString(getContentResolver(),
+ "enabled_notification_listeners");
+ if (listeners != null && listeners.contains(LISTENER_PATH)) {
+ mLaunchButton.setText(R.string.launch_to_disable);
+ mEmptyText.setText(R.string.waiting_for_content);
+ } else {
+ mLaunchButton.setText(R.string.launch_to_enable);
+ mEmptyText.setText(R.string.nothing_to_see);
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setMessage(R.string.explanation)
+ .setTitle(R.string.disabled);
+ builder.setPositiveButton(R.string.enable_it, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ launchSettings(null);
+ }
+ });
+ builder.setNegativeButton(R.string.cancel, null);
+ builder.create().show();
+ }
+ }
+
+ public void launchSettings(View v) {
+ startActivityForResult(
+ new Intent("android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS"), 0);
+ }
+
+ public void dismiss(View v) {
+ Log.d(TAG, "clicked dismiss ");
+ Object tag = v.getTag();
+ if (tag instanceof StatusBarNotification) {
+ StatusBarNotification sbn = (StatusBarNotification) tag;
+ Log.d(TAG, " on " + sbn.getKey());
+ LocalBroadcastManager.getInstance(this).
+ sendBroadcast(new Intent(Listener.ACTION_DISMISS)
+ .putExtra(Listener.EXTRA_KEY, sbn.getKey()));
+ }
+ }
+
+ public void launch(View v) {
+ Log.d(TAG, "clicked launch");
+ Object tag = v.getTag();
+ if (tag instanceof StatusBarNotification) {
+ StatusBarNotification sbn = (StatusBarNotification) tag;
+ Log.d(TAG, " on " + sbn.getKey());
+ LocalBroadcastManager.getInstance(this).
+ sendBroadcast(new Intent(Listener.ACTION_LAUNCH)
+ .putExtra(Listener.EXTRA_KEY, sbn.getKey()));
+ }
+ }
+
+ private void updateList(String key) {
+ if (mStatusAdaptor.requiresInitialization()) {
+ final List<StatusBarNotification> notifications = Listener.getNotifications();
+ if (notifications != null) {
+ mStatusAdaptor.init(notifications);
+ }
+ }
+ mStatusAdaptor.update(key);
+ }
+
+ private class StatusAdaptor extends BaseAdapter {
+ private final Context mContext;
+ private List<StatusBarNotification> mNotifications;
+ private HashMap<String, Long> mKeyToId;
+ private HashSet<String> mKeys;
+ private long mNextId;
+ private HashMap<String, View> mRecycledViews;
+ private String mUpdateKey;
+
+ public StatusAdaptor(Context context) {
+ mContext = context;
+ mKeyToId = new HashMap<String, Long>();
+ mKeys = new HashSet<String>();
+ mNextId = 0;
+ mRecycledViews = new HashMap<String, View>();
+ }
+
+ @Override
+ public int getCount() {
+ return mNotifications == null ? 0 : mNotifications.size();
+ }
+
+ @Override
+ public Object getItem(int position) {
+ return mNotifications.get(position);
+ }
+
+ @Override
+ public boolean hasStableIds() {
+ return true;
+ }
+
+ @Override
+ public long getItemId(int position) {
+ final StatusBarNotification sbn = mNotifications.get(position);
+ final String key = sbn.getKey();
+ if (!mKeyToId.containsKey(key)) {
+ mKeyToId.put(key, mNextId);
+ mNextId ++;
+ }
+ return mKeyToId.get(key);
+ }
+
+ @Override
+ public View getView(int position, View view, ViewGroup list) {
+ if (view == null) {
+ view = View.inflate(mContext, R.layout.item, null);
+ }
+ FrameLayout container = (FrameLayout) view.findViewById(R.id.remote_view);
+ View dismiss = view.findViewById(R.id.dismiss);
+ StatusBarNotification sbn = mNotifications.get(position);
+ View child;
+ if (container.getTag() instanceof StatusBarNotification &&
+ container.getChildCount() > 0) {
+ // recycle the view
+ StatusBarNotification old = (StatusBarNotification) container.getTag();
+ if (sbn.getKey().equals(mUpdateKey)) {
+ //this view is out of date, discard it
+ mUpdateKey = null;
+ } else {
+ View content = container.getChildAt(0);
+ container.removeView(content);
+ mRecycledViews.put(old.getKey(), content);
+ }
+ }
+ child = mRecycledViews.get(sbn.getKey());
+ if (child == null) {
+ child = sbn.getNotification().contentView.apply(mContext, null);
+ }
+ container.setTag(sbn);
+ container.removeAllViews();
+ container.addView(child);
+ dismiss.setVisibility(sbn.isClearable() ? View.VISIBLE : View.GONE);
+ dismiss.setTag(sbn);
+ return view;
+ }
+
+ public void update(String key) {
+ if (mNotifications != null) {
+ synchronized (mNotifications) {
+ mKeys.clear();
+ for (int i = 0; i < mNotifications.size(); i++) {
+ mKeys.add(mNotifications.get(i).getKey());
+ }
+ mKeyToId.keySet().retainAll(mKeys);
+ }
+ if (key == null) {
+ mRecycledViews.clear();
+ } else {
+ mUpdateKey = key;
+ mRecycledViews.remove(key);
+ }
+ Log.d(TAG, "notifyDataSetChanged");
+ notifyDataSetChanged();
+ } else {
+ Log.d(TAG, "missed and update");
+ }
+ }
+
+ public boolean requiresInitialization() {
+ return mNotifications == null;
+ }
+
+ public void init(List<StatusBarNotification> notifications) {
+ mNotifications = notifications;
+ }
+ }
+}
diff --git a/NotificationListenerSample/src/com/android/example/notificationlistener/OrderedNotificationsHelper.java b/NotificationListenerSample/src/com/android/example/notificationlistener/OrderedNotificationsHelper.java
new file mode 100644
index 0000000..789f052
--- /dev/null
+++ b/NotificationListenerSample/src/com/android/example/notificationlistener/OrderedNotificationsHelper.java
@@ -0,0 +1,94 @@
+/*
+ * 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.example.notificationlistener;
+
+import android.service.notification.StatusBarNotification;
+
+import java.util.Comparator;
+import java.util.HashMap;
+
+/**
+ * A {@link java.util.Comparator} that will sort notifications into the proper display order.
+ */
+public class OrderedNotificationsHelper implements Comparator<StatusBarNotification> {
+ private HashMap<String, Integer> rankCache;
+ private String[] mOrderedKeys;
+
+ public OrderedNotificationsHelper(String[] notificationOrder) {
+ this.rankCache = new HashMap<String, Integer>();
+ mOrderedKeys = notificationOrder;
+ }
+
+ public String[] getOrderedKeys() {
+ return mOrderedKeys;
+ }
+
+ public int getIndex(String key) {
+ if (rankCache.isEmpty()) {
+ fillCache();
+ }
+ Integer rank = rankCache.get(key);
+ return rank == null ? -1 : rank;
+ }
+
+ /**
+ * Update the order impleneted by {@link #compare(Object, Object)}.
+ * @param orderedKeys notification keys in order from
+ * {@link android.service.notification.NotificationListenerService#getOrderedNotificationKeys()}
+ */
+ public void updateKeyOrder(String[] orderedKeys) {
+ // TODO implement incremental updates
+ // Defer as much work as possible to fillCache()
+ mOrderedKeys = orderedKeys;
+ rankCache.clear();
+ }
+
+ private void fillCache() {
+ final int N = mOrderedKeys.length;
+ for (int i = 0; i < N; i++) {
+ rankCache.put(mOrderedKeys[i], i);
+ }
+ }
+
+ /**
+ * More important notifications will sort to the front
+ *
+ * @param lhs an {@code Object}.
+ * @param rhs a second {@code Object} to compare with {@code lhs}.
+ * @return 0 if lhs and rhs are of equal importance, less than 0 if lhs more
+ * important that rhs, and greater than 0 if lhs less important than rhs.
+ */
+ public int compare(StatusBarNotification lhs, StatusBarNotification rhs) {
+ if (rankCache.isEmpty()) {
+ fillCache();
+ }
+ if (lhs == null || rhs == null) {
+ throw new IllegalArgumentException("passed null to the Comparator");
+ }
+ final Integer lIndex = rankCache.get(lhs.getKey());
+ final Integer rIndex = rankCache.get(rhs.getKey());
+ if (rIndex == null && rIndex == null) {
+ return 0;
+ }
+ if (lIndex == null) {
+ return 1; // down sort unknown notification
+ }
+ if (rIndex == -1) {
+ return -1; // down sort unknown notification
+ }
+ return Integer.compare(lIndex, rIndex);
+ }
+} \ No newline at end of file