Differential D12294 Diff 53478 src/org/kde/kdeconnect/Plugins/NotificationsPlugin/NotificationsPlugin.java
Changeset View
Changeset View
Standalone View
Standalone View
src/org/kde/kdeconnect/Plugins/NotificationsPlugin/NotificationsPlugin.java
Show All 34 Lines | |||||
35 | import android.graphics.drawable.Icon; | 35 | import android.graphics.drawable.Icon; | ||
36 | import android.os.Build; | 36 | import android.os.Build; | ||
37 | import android.os.Bundle; | 37 | import android.os.Bundle; | ||
38 | import android.provider.Settings; | 38 | import android.provider.Settings; | ||
39 | import android.service.notification.StatusBarNotification; | 39 | import android.service.notification.StatusBarNotification; | ||
40 | import android.text.SpannableString; | 40 | import android.text.SpannableString; | ||
41 | import android.util.Log; | 41 | import android.util.Log; | ||
42 | 42 | | |||
43 | import org.json.JSONArray; | ||||
43 | import org.kde.kdeconnect.Helpers.AppsHelper; | 44 | import org.kde.kdeconnect.Helpers.AppsHelper; | ||
44 | import org.kde.kdeconnect.NetworkPacket; | 45 | import org.kde.kdeconnect.NetworkPacket; | ||
45 | import org.kde.kdeconnect.Plugins.Plugin; | 46 | import org.kde.kdeconnect.Plugins.Plugin; | ||
46 | import org.kde.kdeconnect.Plugins.PluginFactory; | 47 | import org.kde.kdeconnect.Plugins.PluginFactory; | ||
47 | import org.kde.kdeconnect.UserInterface.AlertDialogFragment; | 48 | import org.kde.kdeconnect.UserInterface.AlertDialogFragment; | ||
48 | import org.kde.kdeconnect.UserInterface.PluginSettingsFragment; | 49 | import org.kde.kdeconnect.UserInterface.PluginSettingsFragment; | ||
49 | import org.kde.kdeconnect.UserInterface.StartActivityAlertDialogFragment; | 50 | import org.kde.kdeconnect.UserInterface.StartActivityAlertDialogFragment; | ||
50 | import org.kde.kdeconnect_tp.R; | 51 | import org.kde.kdeconnect_tp.R; | ||
51 | 52 | | |||
52 | import java.io.ByteArrayOutputStream; | 53 | import java.io.ByteArrayOutputStream; | ||
53 | import java.security.MessageDigest; | 54 | import java.security.MessageDigest; | ||
54 | import java.security.NoSuchAlgorithmException; | 55 | import java.security.NoSuchAlgorithmException; | ||
55 | import java.util.Arrays; | 56 | import java.util.Arrays; | ||
56 | import java.util.HashMap; | 57 | import java.util.HashMap; | ||
57 | import java.util.HashSet; | 58 | import java.util.HashSet; | ||
59 | import java.util.LinkedList; | ||||
60 | import java.util.List; | ||||
58 | import java.util.Map; | 61 | import java.util.Map; | ||
59 | import java.util.Set; | 62 | import java.util.Set; | ||
60 | 63 | | |||
61 | import androidx.annotation.RequiresApi; | 64 | import androidx.annotation.RequiresApi; | ||
62 | import androidx.core.app.NotificationCompat; | 65 | import androidx.core.app.NotificationCompat; | ||
63 | 66 | | |||
64 | @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2) | 67 | @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2) | ||
65 | @PluginFactory.LoadablePlugin | 68 | @PluginFactory.LoadablePlugin | ||
66 | public class NotificationsPlugin extends Plugin implements NotificationReceiver.NotificationListener { | 69 | public class NotificationsPlugin extends Plugin implements NotificationReceiver.NotificationListener { | ||
67 | 70 | | |||
68 | private final static String PACKET_TYPE_NOTIFICATION = "kdeconnect.notification"; | 71 | private final static String PACKET_TYPE_NOTIFICATION = "kdeconnect.notification"; | ||
69 | private final static String PACKET_TYPE_NOTIFICATION_REQUEST = "kdeconnect.notification.request"; | 72 | private final static String PACKET_TYPE_NOTIFICATION_REQUEST = "kdeconnect.notification.request"; | ||
70 | private final static String PACKET_TYPE_NOTIFICATION_REPLY = "kdeconnect.notification.reply"; | 73 | private final static String PACKET_TYPE_NOTIFICATION_REPLY = "kdeconnect.notification.reply"; | ||
74 | private final static String PACKET_TYPE_NOTIFICATION_ACTION = "kdeconnect.notification.action"; | ||||
75 | | ||||
76 | private final static String TAG = "NotificationsPlugin"; | ||||
71 | 77 | | |||
72 | private AppDatabase appDatabase; | 78 | private AppDatabase appDatabase; | ||
73 | 79 | | |||
74 | private Set<String> currentNotifications; | 80 | private Set<String> currentNotifications; | ||
75 | private Map<String, RepliableNotification> pendingIntents; | 81 | private Map<String, RepliableNotification> pendingIntents; | ||
82 | private Map<String, List<Notification.Action>> actions; | ||||
mtijink: Maybe it's good to refactor this into `Map<String, RemotelyAccessedNotification>` or similar. | |||||
76 | private boolean serviceReady; | 83 | private boolean serviceReady; | ||
77 | 84 | | |||
78 | @Override | 85 | @Override | ||
79 | public String getDisplayName() { | 86 | public String getDisplayName() { | ||
80 | return context.getResources().getString(R.string.pref_plugin_notifications); | 87 | return context.getResources().getString(R.string.pref_plugin_notifications); | ||
81 | } | 88 | } | ||
82 | 89 | | |||
83 | @Override | 90 | @Override | ||
Show All 28 Lines | |||||
112 | 119 | | |||
113 | @Override | 120 | @Override | ||
114 | public boolean onCreate() { | 121 | public boolean onCreate() { | ||
115 | 122 | | |||
116 | if (!hasPermission()) return false; | 123 | if (!hasPermission()) return false; | ||
117 | 124 | | |||
118 | pendingIntents = new HashMap<>(); | 125 | pendingIntents = new HashMap<>(); | ||
119 | currentNotifications = new HashSet<>(); | 126 | currentNotifications = new HashSet<>(); | ||
127 | actions = new HashMap<>(); | ||||
120 | 128 | | |||
121 | appDatabase = new AppDatabase(context, true); | 129 | appDatabase = new AppDatabase(context, true); | ||
122 | 130 | | |||
123 | NotificationReceiver.RunCommand(context, service -> { | 131 | NotificationReceiver.RunCommand(context, service -> { | ||
124 | 132 | | |||
125 | service.addListener(NotificationsPlugin.this); | 133 | service.addListener(NotificationsPlugin.this); | ||
126 | 134 | | |||
127 | serviceReady = service.isConnected(); | 135 | serviceReady = service.isConnected(); | ||
Show All 20 Lines | |||||
148 | 156 | | |||
149 | @Override | 157 | @Override | ||
150 | public void onNotificationRemoved(StatusBarNotification statusBarNotification) { | 158 | public void onNotificationRemoved(StatusBarNotification statusBarNotification) { | ||
151 | if (statusBarNotification == null) { | 159 | if (statusBarNotification == null) { | ||
152 | Log.w("onNotificationRemoved", "notification is null"); | 160 | Log.w("onNotificationRemoved", "notification is null"); | ||
153 | return; | 161 | return; | ||
154 | } | 162 | } | ||
155 | String id = getNotificationKeyCompat(statusBarNotification); | 163 | String id = getNotificationKeyCompat(statusBarNotification); | ||
164 | | ||||
165 | actions.remove(id); | ||||
166 | | ||||
156 | NetworkPacket np = new NetworkPacket(PACKET_TYPE_NOTIFICATION); | 167 | NetworkPacket np = new NetworkPacket(PACKET_TYPE_NOTIFICATION); | ||
157 | np.set("id", id); | 168 | np.set("id", id); | ||
158 | np.set("isCancel", true); | 169 | np.set("isCancel", true); | ||
159 | device.sendPacket(np); | 170 | device.sendPacket(np); | ||
160 | currentNotifications.remove(id); | 171 | currentNotifications.remove(id); | ||
161 | } | 172 | } | ||
162 | 173 | | |||
163 | @Override | 174 | @Override | ||
▲ Show 20 Lines • Show All 83 Lines • ▼ Show 20 Line(s) | 224 | if (!isUpdate) { | |||
247 | } catch (Exception e) { | 258 | } catch (Exception e) { | ||
248 | e.printStackTrace(); | 259 | e.printStackTrace(); | ||
249 | Log.e("NotificationsPlugin", "Error retrieving icon"); | 260 | Log.e("NotificationsPlugin", "Error retrieving icon"); | ||
250 | } | 261 | } | ||
251 | } else { | 262 | } else { | ||
252 | currentNotifications.add(key); | 263 | currentNotifications.add(key); | ||
253 | } | 264 | } | ||
254 | 265 | | |||
266 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { | ||||
267 | if (notification.actions != null && notification.actions.length > 0) { | ||||
268 | actions.put(key, new LinkedList<>()); | ||||
269 | JSONArray jsonArray = new JSONArray(); | ||||
270 | for (Notification.Action action : notification.actions) { | ||||
271 | if (null == action.title) | ||||
272 | break; | ||||
273 | jsonArray.put(action.title.toString()); | ||||
274 | actions.get(key).add(action); | ||||
275 | } | ||||
276 | np.set("actions", jsonArray); | ||||
277 | } | ||||
278 | } | ||||
279 | | ||||
255 | np.set("id", key); | 280 | np.set("id", key); | ||
256 | np.set("isClearable", statusBarNotification.isClearable()); | 281 | np.set("isClearable", statusBarNotification.isClearable()); | ||
257 | np.set("appName", appName == null ? packageName : appName); | 282 | np.set("appName", appName == null ? packageName : appName); | ||
258 | np.set("time", Long.toString(statusBarNotification.getPostTime())); | 283 | np.set("time", Long.toString(statusBarNotification.getPostTime())); | ||
259 | if (!appDatabase.getPrivacy(packageName, AppDatabase.PrivacyOptions.BLOCK_CONTENTS)) { | 284 | if (!appDatabase.getPrivacy(packageName, AppDatabase.PrivacyOptions.BLOCK_CONTENTS)) { | ||
260 | RepliableNotification rn = extractRepliableNotification(statusBarNotification); | 285 | RepliableNotification rn = extractRepliableNotification(statusBarNotification); | ||
261 | if (rn.pendingIntent != null) { | 286 | if (rn.pendingIntent != null) { | ||
262 | np.set("requestReplyId", rn.id); | 287 | np.set("requestReplyId", rn.id); | ||
263 | pendingIntents.put(rn.id, rn); | 288 | pendingIntents.put(rn.id, rn); | ||
264 | } | 289 | } | ||
265 | np.set("ticker", getTickerText(notification)); | 290 | np.set("ticker", getTickerText(notification)); | ||
266 | np.set("title", getNotificationTitle(notification)); | 291 | np.set("title", getNotificationTitle(notification)); | ||
267 | np.set("text", getNotificationText(notification)); | 292 | np.set("text", getNotificationText(notification)); | ||
268 | } | 293 | } | ||
mtijink: I think renaming to `actionsJson` is clearer. | |||||
269 | 294 | | |||
270 | device.sendPacket(np); | 295 | device.sendPacket(np); | ||
271 | } | 296 | } | ||
Maybe if a single title is missing, don't send any actions? Otherwise, this can be confusing. mtijink: Maybe if a single title is missing, don't send any actions? Otherwise, this can be confusing. | |||||
272 | 297 | | |||
273 | private Bitmap drawableToBitmap(Drawable drawable) { | 298 | private Bitmap drawableToBitmap(Drawable drawable) { | ||
274 | if (drawable == null) return null; | 299 | if (drawable == null) return null; | ||
275 | 300 | | |||
276 | Bitmap res; | 301 | Bitmap res; | ||
277 | if (drawable.getIntrinsicWidth() > 128 || drawable.getIntrinsicHeight() > 128) { | 302 | if (drawable.getIntrinsicWidth() > 128 || drawable.getIntrinsicHeight() > 128) { | ||
mtijink: Double `Log` (with line 268), I'd remove at least one of them. | |||||
278 | res = Bitmap.createBitmap(96, 96, Bitmap.Config.ARGB_8888); | 303 | res = Bitmap.createBitmap(96, 96, Bitmap.Config.ARGB_8888); | ||
279 | } else if (drawable.getIntrinsicWidth() <= 64 || drawable.getIntrinsicHeight() <= 64) { | 304 | } else if (drawable.getIntrinsicWidth() <= 64 || drawable.getIntrinsicHeight() <= 64) { | ||
280 | res = Bitmap.createBitmap(96, 96, Bitmap.Config.ARGB_8888); | 305 | res = Bitmap.createBitmap(96, 96, Bitmap.Config.ARGB_8888); | ||
281 | } else { | 306 | } else { | ||
282 | res = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); | 307 | res = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); | ||
283 | } | 308 | } | ||
284 | 309 | | |||
285 | Canvas canvas = new Canvas(res); | 310 | Canvas canvas = new Canvas(res); | ||
▲ Show 20 Lines • Show All 183 Lines • ▼ Show 20 Line(s) | 492 | private void sendCurrentNotifications(NotificationReceiver service) { | |||
469 | for (StatusBarNotification notification : notifications) { | 494 | for (StatusBarNotification notification : notifications) { | ||
470 | sendNotification(notification); | 495 | sendNotification(notification); | ||
471 | } | 496 | } | ||
472 | } | 497 | } | ||
473 | 498 | | |||
474 | @Override | 499 | @Override | ||
475 | public boolean onPacketReceived(final NetworkPacket np) { | 500 | public boolean onPacketReceived(final NetworkPacket np) { | ||
476 | 501 | | |||
477 | if (np.getBoolean("request")) { | 502 | if (np.getType().equals(PACKET_TYPE_NOTIFICATION_ACTION) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { | ||
503 | | ||||
504 | String key = np.getString("key"); | ||||
505 | String title = np.getString("action"); | ||||
506 | PendingIntent intent = null; | ||||
507 | | ||||
508 | for (Notification.Action a : actions.get(key)) { | ||||
509 | if (a.title.equals(title)) { | ||||
510 | intent = a.actionIntent; | ||||
511 | } | ||||
512 | } | ||||
513 | | ||||
514 | if (intent != null) { | ||||
515 | try { | ||||
516 | intent.send(); | ||||
517 | } catch (PendingIntent.CanceledException e) { | ||||
518 | Log.e(TAG, "Triggering action failed", e); | ||||
519 | } | ||||
520 | } | ||||
521 | | ||||
Something like Log.e(TAG, "Firing action for notification failed", e) is better in general (colors the stacktrace etc., actually labels it as an error). mtijink: Something like `Log.e(TAG, "Firing action for notification failed", e)` is better in general… | |||||
522 | } else if (np.getBoolean("request")) { | ||||
478 | 523 | | |||
479 | if (serviceReady) { | 524 | if (serviceReady) { | ||
480 | NotificationReceiver.RunCommand(context, this::sendCurrentNotifications); | 525 | NotificationReceiver.RunCommand(context, this::sendCurrentNotifications); | ||
481 | } | 526 | } | ||
482 | 527 | | |||
483 | } else if (np.has("cancel")) { | 528 | } else if (np.has("cancel")) { | ||
484 | final String dismissedId = np.getString("cancel"); | 529 | final String dismissedId = np.getString("cancel"); | ||
485 | currentNotifications.remove(dismissedId); | 530 | currentNotifications.remove(dismissedId); | ||
Show All 19 Lines | 545 | return new StartActivityAlertDialogFragment.Builder() | |||
505 | .setIntentAction("android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS") | 550 | .setIntentAction("android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS") | ||
506 | .setStartForResult(true) | 551 | .setStartForResult(true) | ||
507 | .setRequestCode(requestCode) | 552 | .setRequestCode(requestCode) | ||
508 | .create(); | 553 | .create(); | ||
509 | } | 554 | } | ||
510 | 555 | | |||
511 | @Override | 556 | @Override | ||
512 | public String[] getSupportedPacketTypes() { | 557 | public String[] getSupportedPacketTypes() { | ||
513 | return new String[]{PACKET_TYPE_NOTIFICATION_REQUEST, PACKET_TYPE_NOTIFICATION_REPLY}; | 558 | return new String[]{PACKET_TYPE_NOTIFICATION_REQUEST, PACKET_TYPE_NOTIFICATION_REPLY, PACKET_TYPE_NOTIFICATION_ACTION}; | ||
514 | } | 559 | } | ||
515 | 560 | | |||
516 | @Override | 561 | @Override | ||
517 | public String[] getOutgoingPacketTypes() { | 562 | public String[] getOutgoingPacketTypes() { | ||
518 | return new String[]{PACKET_TYPE_NOTIFICATION}; | 563 | return new String[]{PACKET_TYPE_NOTIFICATION}; | ||
519 | } | 564 | } | ||
520 | 565 | | |||
521 | //For compat with API<21, because lollipop changed the way to cancel notifications | 566 | //For compat with API<21, because lollipop changed the way to cancel notifications | ||
▲ Show 20 Lines • Show All 71 Lines • Show Last 20 Lines |
Maybe it's good to refactor this into Map<String, RemotelyAccessedNotification> or similar.
RemotelyAccessedNotification can then contain the id, the available actions and reply intent.