Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add a possibility to listen to system notifications #50

Merged
merged 1 commit into from
Dec 24, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,57 @@ adb shell ime set com.google.android.inputmethod.latin/com.android.inputmethod.l
```


## Notifications

Since version 2.16.0 Appium Settings supports retrieval of system notifications.
You need to manually switch the corresponding security switcher next to `Appium Settings`
application name in `Settings->Notification Access` (the path to this page under Settings
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍
Note for Android 9 (Android One, Nokia)
Settings > App & Notifications > special app access > Notification access

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On my Samsung it's different ;)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello! Tell me please, how can I grant permission for get status bar notifications via adb? Is it available?

may vary depending on Android version and the device model)
in order to make this feature available. The next step would be to send the following broadcast command:
```bash
$ adb shell am broadcast -a io.appium.settings.notifications
```
The notifications listener service is running in the background and collects
all the active and newly created notifications into the internal buffer with maximum
size of `100`. The collected data (e.g. the properties and texts of each notification)
is returned as JSON-formatted string. An error description string is returned instead if the
notifications list cannot be retrieved.
The example of the resulting data:
```json
{
"statusBarNotifications": [
{
"isGroup":false,
"packageName":"io.appium.settings",
"isClearable":false,
"isOngoing":true,
"id":1,
"tag":null,
"notification":{
"title":null,
"bigTitle":"Appium Settings",
"text":null,
"bigText":"Keep this service running, so Appium for Android can properly interact with several system APIs",
"tickerText":null,
"subText":null,
"infoText":null,
"template":"android.app.Notification$BigTextStyle"
},
"userHandle":0,
"groupKey":"0|io.appium.settings|1|null|10133",
"overrideGroupKey":null,
"postTime":1576853518850,
"key":"0|io.appium.settings|1|null|10133",
"isRemoved":false
}
]
}
```
See https://developer.android.com/reference/android/service/notification/StatusBarNotification
and https://developer.android.com/reference/android/app/Notification.html
for more information on available notification properties and their values.


## Notes:

* You have to specify the receiver class if the app has never been executed before:
Expand Down
2 changes: 1 addition & 1 deletion app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ android {
buildToolsVersion '28.0.3'

defaultConfig {
minSdkVersion 17
minSdkVersion 18
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah, NotificationListenerService requires this version 👀
We should bump http://appium.io/docs/en/about-appium/platform-support/ from 4.2 to 4.3 after introducing this.

It is safe to merge this after Appium 1.16.0 release and bump this major version to 3, I believe.
(Or not to bump current 1.16.0 release candidate to this merged version.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that we need to bump the major version.

@imurchie Any chance to release appium 1.16 soon?

targetSdkVersion 28
versionCode 22
versionName "2.16.1"
Expand Down
9 changes: 9 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,15 @@
android:resource="@xml/method" />
</service>

<service
android:label="@string/app_name"
android:name=".NLService"
android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">
<intent-filter>
<action android:name="android.service.notification.NotificationListenerService"/>
</intent-filter>
</service>

<service android:name=".ForegroundService"
android:enabled="true"
android:exported="true" />
Expand Down
132 changes: 132 additions & 0 deletions app/src/main/java/io/appium/settings/NLService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
/*
Copyright 2012-present Appium Committers
<p>
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
<p>
http://www.apache.org/licenses/LICENSE-2.0
<p>
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 io.appium.settings;

import android.content.Intent;
import android.os.IBinder;
import android.service.notification.NotificationListenerService;
import android.service.notification.StatusBarNotification;
import android.support.annotation.Nullable;
import android.util.Log;
import io.appium.settings.notifications.StoredNotification;
import io.appium.settings.notifications.StoredNotifications;

import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;

public class NLService extends NotificationListenerService {
private static final String TAG = NLService.class.getSimpleName();
private static final int MAX_BUFFER_SIZE = 100;

private final List<StoredNotification> notificationsBuffer = new LinkedList<>();

@Override
public void onCreate() {
super.onCreate();

synchronized (notificationsBuffer) {
StoredNotifications.getInstance().bindNotificationsBuffer(notificationsBuffer);
}
}

@Nullable
@Override
public IBinder onBind(Intent intent) {
return super.onBind(intent);
}

@Override
public void onListenerDisconnected() {
Log.i(TAG, "The notification listener has been disconnected");
synchronized (notificationsBuffer) {
notificationsBuffer.clear();
}

super.onListenerDisconnected();
}

@Override
public void onListenerConnected() {
super.onListenerConnected();
Log.i(TAG, "The notification listener is connected");

synchronized (notificationsBuffer) {
StatusBarNotification[] activeNotifications = getActiveNotifications();
StatusBarNotification[] notificationsSlice = Arrays.copyOfRange(activeNotifications,
0, Math.min(MAX_BUFFER_SIZE, activeNotifications.length));
notificationsBuffer.clear();
for (StatusBarNotification sbn : notificationsSlice) {
notificationsBuffer.add(new StoredNotification(sbn));
}
Log.d(TAG, String.format("Successfully synchronized %s active notifications", notificationsBuffer.size()));
}
}

@Override
public void onNotificationPosted(StatusBarNotification sbn) {
synchronized (notificationsBuffer) {
if (notificationsBuffer.size() >= MAX_BUFFER_SIZE) {
Log.d(TAG, String.format("The notifications buffer size has reached its maximum size of %s items. " +
"Shrinking it in order to satisfy the constraints.", notificationsBuffer.size()));
StoredNotification itemToRemove = null;
ListIterator<StoredNotification> iter = notificationsBuffer.listIterator(notificationsBuffer.size());
while (iter.hasPrevious()) {
StoredNotification currentItem = iter.previous();
if (itemToRemove == null) {
// Remove the last item in the list if nothing else matches
itemToRemove = currentItem;
}
if (currentItem.isRemoved()) {
itemToRemove = currentItem;
// Quit the loop as soon as we found the oldest item marked as removed
break;
}
}
if (itemToRemove != null) {
notificationsBuffer.remove(itemToRemove);
}
}
try {
if (notificationsBuffer.isEmpty()) {
notificationsBuffer.add(new StoredNotification((sbn)));
} else {
notificationsBuffer.add(0, new StoredNotification(sbn));
}
Log.d(TAG, String.format("Successfully stored the newly arrived notification identified by %s",
sbn.getId()));
} catch (Exception e) {
Log.e(TAG, "Cannot store the newly arrived notification", e);
}
}
}

@Override
public void onNotificationRemoved(StatusBarNotification sbn) {
synchronized (notificationsBuffer) {
for (StoredNotification storedNotification : notificationsBuffer) {
if (storedNotification.getNotification().getId() == sbn.getId()) {
storedNotification.setRemoved(true);
Log.d(TAG, String.format("Successfully marked the removed notification identified by %s",
sbn.getId()));
break;
}
}
}
}
}
2 changes: 2 additions & 0 deletions app/src/main/java/io/appium/settings/Settings.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import io.appium.settings.receivers.HasAction;
import io.appium.settings.receivers.LocaleSettingReceiver;
import io.appium.settings.receivers.LocationInfoReceiver;
import io.appium.settings.receivers.NotificationsReceiver;
import io.appium.settings.receivers.UnpairBluetoothDevicesReceiver;
import io.appium.settings.receivers.WiFiConnectionSettingReceiver;

Expand All @@ -57,6 +58,7 @@ public void onCreate(Bundle savedInstanceState) {
receiverClasses.add(ClipboardReceiver.class);
receiverClasses.add(BluetoothConnectionSettingReceiver.class);
receiverClasses.add(UnpairBluetoothDevicesReceiver.class);
receiverClasses.add(NotificationsReceiver.class);
registerSettingsReceivers(receiverClasses);

// https://developer.android.com/about/versions/oreo/background-location-limits
Expand Down
29 changes: 29 additions & 0 deletions app/src/main/java/io/appium/settings/helpers/Utils.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
Copyright 2012-present Appium Committers
<p>
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
<p>
http://www.apache.org/licenses/LICENSE-2.0
<p>
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 io.appium.settings.helpers;

import org.json.JSONObject;

public class Utils {
public static Object formatJsonNull(Object o) {
return o == null ? JSONObject.NULL : o;
}

public static String toNullableString(CharSequence cs) {
return cs == null ? null : cs.toString();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/*
Copyright 2012-present Appium Committers
<p>
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
<p>
http://www.apache.org/licenses/LICENSE-2.0
<p>
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 io.appium.settings.notifications;

import android.os.Build;
import android.os.Bundle;
import android.service.notification.StatusBarNotification;
import org.json.JSONException;
import org.json.JSONObject;

import static io.appium.settings.helpers.Utils.formatJsonNull;
import static io.appium.settings.helpers.Utils.toNullableString;

public class StoredNotification {
private final StatusBarNotification sbn;
private boolean isRemoved = false;

public StoredNotification(StatusBarNotification sbn) {
this.sbn = sbn;
}

public StatusBarNotification getNotification() {
return sbn;
}

private void storeCharSequenceProperty(JSONObject dst, String name, String propertyName,
Bundle extras) throws JSONException {
CharSequence value = extras.getCharSequence(propertyName);
dst.put(name, formatJsonNull(toNullableString(value)));
}

public JSONObject toJson() throws JSONException {
JSONObject result = new JSONObject();
result.put("packageName", formatJsonNull(sbn.getPackageName()));
result.put("isClearable", sbn.isClearable());
result.put("isOngoing", sbn.isOngoing());
result.put("id", sbn.getId());
result.put("tag", formatJsonNull(sbn.getTag()));
result.put("postTime", sbn.getPostTime());
result.put("isRemoved", isRemoved());
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
JSONObject notification = new JSONObject();
Bundle extras = sbn.getNotification().extras;
storeCharSequenceProperty(notification, "title", "android.title", extras);
storeCharSequenceProperty(notification, "bigTitle", "android.title.big", extras);
storeCharSequenceProperty(notification, "text", "android.text", extras);
storeCharSequenceProperty(notification, "bigText", "android.bigText", extras);
storeCharSequenceProperty(notification, "tickerText", "android.tickerText", extras);
storeCharSequenceProperty(notification, "subText", "android.subText", extras);
storeCharSequenceProperty(notification, "infoText", "android.infoText", extras);
storeCharSequenceProperty(notification, "template", "android.template", extras);
result.put("notification", notification);
} else {
result.put("notification", JSONObject.NULL);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
result.put("isGroup", sbn.isGroup());
} else {
result.put("isGroup", false);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
result.put("userHandle", sbn.getUser().hashCode());
result.put("groupKey", formatJsonNull(sbn.getGroupKey()));
} else {
//noinspection deprecation
result.put("userHandle", sbn.getUserId());
result.put("groupKey", JSONObject.NULL);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
result.put("overrideGroupKey", formatJsonNull(sbn.getOverrideGroupKey()));
} else {
result.put("overrideGroupKey", JSONObject.NULL);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH) {
result.put("key", formatJsonNull(sbn.getKey()));
} else {
result.put("key", JSONObject.NULL);
}
return result;
}

public boolean isRemoved() {
return isRemoved;
}

public void setRemoved(boolean removed) {
this.isRemoved = removed;
}
}
Loading