diff --git a/src/org/kde/kdeconnect/Plugins/NotificationsPlugin/NotificationReceiver.java b/src/org/kde/kdeconnect/Plugins/NotificationsPlugin/NotificationReceiver.java --- a/src/org/kde/kdeconnect/Plugins/NotificationsPlugin/NotificationReceiver.java +++ b/src/org/kde/kdeconnect/Plugins/NotificationsPlugin/NotificationReceiver.java @@ -23,13 +23,16 @@ import android.app.Service; import android.content.Context; import android.content.Intent; +import android.os.Build; import android.service.notification.NotificationListenerService; import android.service.notification.StatusBarNotification; +import android.support.annotation.RequiresApi; import java.util.ArrayList; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; +@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2) public class NotificationReceiver extends NotificationListenerService { public interface NotificationListener { diff --git a/src/org/kde/kdeconnect/Plugins/NotificationsPlugin/NotificationsPlugin.java b/src/org/kde/kdeconnect/Plugins/NotificationsPlugin/NotificationsPlugin.java --- a/src/org/kde/kdeconnect/Plugins/NotificationsPlugin/NotificationsPlugin.java +++ b/src/org/kde/kdeconnect/Plugins/NotificationsPlugin/NotificationsPlugin.java @@ -33,6 +33,7 @@ import android.os.Bundle; import android.provider.Settings; import android.service.notification.StatusBarNotification; +import android.support.annotation.RequiresApi; import android.util.Log; @@ -61,9 +62,62 @@ private boolean sendIcons = true; - + private Map pendingIntents; + //For compat with API<21, because lollipop changed the way to cancel notifications + public static void cancelNotificationCompat(NotificationReceiver service, String compatKey) { + if (Build.VERSION.SDK_INT >= 21) { + service.cancelNotification(compatKey); + } else { + int first = compatKey.indexOf(':'); + if (first == -1) { + Log.e("cancelNotificationCompa", "Not formated like a notification key: " + compatKey); + return; + } + int last = compatKey.lastIndexOf(':'); + String packageName = compatKey.substring(0, first); + String tag = compatKey.substring(first + 1, last); + if (tag.length() == 0) tag = null; + String idString = compatKey.substring(last + 1); + int id; + try { + id = Integer.parseInt(idString); + } catch (Exception e) { + id = 0; + } + service.cancelNotification(packageName, tag, id); + } + } + + public static String getNotificationKeyCompat(StatusBarNotification statusBarNotification) { + String result; + // first check if it's one of our remoteIds + String tag = statusBarNotification.getTag(); + if (tag != null && tag.startsWith("kdeconnectId:")) + result = Integer.toString(statusBarNotification.getId()); + else if (Build.VERSION.SDK_INT >= 21) { + result = statusBarNotification.getKey(); + } else { + String packageName = statusBarNotification.getPackageName(); + int id = statusBarNotification.getId(); + String safePackageName = (packageName == null) ? "" : packageName; + String safeTag = (tag == null) ? "" : tag; + result = safePackageName + ":" + safeTag + ":" + id; + } + return result; + } + + public static String bytesToHex(byte[] bytes) { + char[] hexArray = "0123456789ABCDEF".toCharArray(); + char[] hexChars = new char[bytes.length * 2]; + for (int j = 0; j < bytes.length; j++) { + int v = bytes[j] & 0xFF; + hexChars[j * 2] = hexArray[v >>> 4]; + hexChars[j * 2 + 1] = hexArray[v & 0x0F]; + } + return new String(hexChars).toLowerCase(); + } @Override public String getDisplayName() { @@ -98,44 +152,40 @@ @Override public boolean onCreate() { pendingIntents = new HashMap(); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { - if (hasPermission()) { - NotificationReceiver.RunCommand(context, new NotificationReceiver.InstanceCallback() { - @Override - public void onServiceStart(NotificationReceiver service) { - try { - service.addListener(NotificationsPlugin.this); - StatusBarNotification[] notifications = service.getActiveNotifications(); - for (StatusBarNotification notification : notifications) { - sendNotification(notification, true); - } - } catch (Exception e) { - Log.e("NotificationsPlugin", "Exception"); - e.printStackTrace(); + + if (hasPermission()) { + NotificationReceiver.RunCommand(context, new NotificationReceiver.InstanceCallback() { + @Override + public void onServiceStart(NotificationReceiver service) { + try { + service.addListener(NotificationsPlugin.this); + StatusBarNotification[] notifications = service.getActiveNotifications(); + for (StatusBarNotification notification : notifications) { + sendNotification(notification, true); } + } catch (Exception e) { + Log.e("NotificationsPlugin", "Exception"); + e.printStackTrace(); } - }); - } else { - return false; - } + } + }); + } else { + return false; } return true; } @Override public void onDestroy() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) - NotificationReceiver.RunCommand(context, new NotificationReceiver.InstanceCallback() { - @Override - public void onServiceStart(NotificationReceiver service) { - service.removeListener(NotificationsPlugin.this); - } - }); + NotificationReceiver.RunCommand(context, new NotificationReceiver.InstanceCallback() { + @Override + public void onServiceStart(NotificationReceiver service) { + service.removeListener(NotificationsPlugin.this); + } + }); } - @Override public void onNotificationRemoved(StatusBarNotification statusBarNotification) { if (statusBarNotification == null) { @@ -167,7 +217,7 @@ } appDatabase.open(); - if (!appDatabase.isEnabled(statusBarNotification.getPackageName())){ + if (!appDatabase.isEnabled(statusBarNotification.getPackageName())) { return; // we dont want notification from this app } @@ -178,7 +228,6 @@ String appName = AppsHelper.appNameLookup(context, packageName); - if ("com.facebook.orca".equals(packageName) && (statusBarNotification.getId() == 10012) && "Messenger".equals(appName) && @@ -188,16 +237,14 @@ } if ("com.android.systemui".equals(packageName) && - "low_battery".equals(statusBarNotification.getTag())) - { + "low_battery".equals(statusBarNotification.getTag())) { //HACK: Android low battery notification are posted again every few seconds. Ignore them, as we already have a battery indicator. return; } NetworkPackage np = new NetworkPackage(PACKAGE_TYPE_NOTIFICATION); - if (packageName.equals("org.kde.kdeconnect_tp")) - { + if (packageName.equals("org.kde.kdeconnect_tp")) { //Make our own notifications silent :) np.set("silent", true); np.set("requestAnswer", true); //For compatibility with old desktop versions of KDE Connect that don't support "silent" @@ -219,20 +266,20 @@ np.set("payloadHash", getChecksum(bitmapData)); } - } catch(Exception e){ + } catch (Exception e) { e.printStackTrace(); Log.e("NotificationsPlugin", "Error retrieving icon"); } } - + RepliableNotification rn = extractRepliableNotification(statusBarNotification); - if(rn.pendingIntent != null) { + if (rn.pendingIntent != null) { np.set("requestReplyId", rn.id); pendingIntents.put(rn.id, rn); } np.set("id", key); - np.set("appName", appName == null? packageName : appName); + np.set("appName", appName == null ? packageName : appName); np.set("isClearable", statusBarNotification.isClearable()); np.set("ticker", getTickerText(notification)); np.set("title", getNotificationTitle(notification)); @@ -246,14 +293,15 @@ device.sendPackage(np); } - void replyToNotification(String id, String message){ - if(pendingIntents.isEmpty() || !pendingIntents.containsKey(id)){ + @RequiresApi(api = Build.VERSION_CODES.KITKAT_WATCH) + void replyToNotification(String id, String message) { + if (pendingIntents.isEmpty() || !pendingIntents.containsKey(id)) { Log.e("NotificationsPlugin", "No such notification"); return; } RepliableNotification repliableNotification = pendingIntents.get(id); - if(repliableNotification == null) { + if (repliableNotification == null) { Log.e("NotificationsPlugin", "No such notification"); return; } @@ -263,47 +311,48 @@ localIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); Bundle localBundle = new Bundle(); int i = 0; - for(RemoteInput remoteIn : repliableNotification.remoteInputs){ + for (RemoteInput remoteIn : repliableNotification.remoteInputs) { getDetailsOfNotification(remoteIn); remoteInputs[i] = remoteIn; localBundle.putCharSequence(remoteInputs[i].getResultKey(), message); i++; } RemoteInput.addResultsToIntent(remoteInputs, localIntent, localBundle); - + try { repliableNotification.pendingIntent.send(context, 0, localIntent); } catch (PendingIntent.CanceledException e) { Log.e("NotificationPlugin", "replyToNotification error: " + e.getMessage()); } pendingIntents.remove(id); } + @RequiresApi(api = Build.VERSION_CODES.KITKAT_WATCH) private void getDetailsOfNotification(RemoteInput remoteInput) { //Some more details of RemoteInput... no idea what for but maybe it will be useful at some point String resultKey = remoteInput.getResultKey(); String label = remoteInput.getLabel().toString(); Boolean canFreeForm = remoteInput.getAllowFreeFormInput(); - if(remoteInput.getChoices() != null && remoteInput.getChoices().length > 0) { + if (remoteInput.getChoices() != null && remoteInput.getChoices().length > 0) { String[] possibleChoices = new String[remoteInput.getChoices().length]; - for(int i = 0; i < remoteInput.getChoices().length; i++){ + for (int i = 0; i < remoteInput.getChoices().length; i++) { possibleChoices[i] = remoteInput.getChoices()[i].toString(); } } } - + private String getNotificationTitle(Notification notification) { final String TITLE_KEY = "android.title"; final String TEXT_KEY = "android.text"; String title = ""; - if(notification != null) { - if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + if (notification != null) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { try { Bundle extras = notification.extras; title = extras.getCharSequence(TITLE_KEY).toString(); - } catch(Exception e) { - Log.w("NotificationPlugin","problem parsing notification extras for " + notification.tickerText); + } catch (Exception e) { + Log.w("NotificationPlugin", "problem parsing notification extras for " + notification.tickerText); e.printStackTrace(); } } @@ -313,17 +362,17 @@ return title; } - + private RepliableNotification extractRepliableNotification(StatusBarNotification statusBarNotification) { RepliableNotification repliableNotification = new RepliableNotification(); - - if(statusBarNotification != null) { - if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + + if (statusBarNotification != null) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { try { Boolean reply = false; - + //works for WhatsApp, but not for Telegram - if(statusBarNotification.getNotification().actions!=null) { + if (statusBarNotification.getNotification().actions != null) { for (Notification.Action act : statusBarNotification.getNotification().actions) { if (act != null && act.getRemoteInputs() != null) { repliableNotification.remoteInputs.addAll(Arrays.asList(act.getRemoteInputs())); @@ -337,28 +386,28 @@ repliableNotification.tag = statusBarNotification.getTag();//TODO find how to pass Tag with sending PendingIntent, might fix Hangout problem } - } catch(Exception e) { - Log.w("NotificationPlugin","problem extracting notification wear for " + statusBarNotification.getNotification().tickerText); + } catch (Exception e) { + Log.w("NotificationPlugin", "problem extracting notification wear for " + statusBarNotification.getNotification().tickerText); e.printStackTrace(); } } } - + return repliableNotification; } private String getNotificationText(Notification notification) { final String TEXT_KEY = "android.text"; String text = ""; - if(notification != null) { - if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + if (notification != null) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { try { Bundle extras = notification.extras; Object extraTextExtra = extras.get(TEXT_KEY); if (extraTextExtra != null) text = extraTextExtra.toString(); - } catch(Exception e) { - Log.w("NotificationPlugin","problem parsing notification extras for " + notification.tickerText); + } catch (Exception e) { + Log.w("NotificationPlugin", "problem parsing notification extras for " + notification.tickerText); e.printStackTrace(); } } @@ -369,7 +418,6 @@ return text; } - /** * Returns the ticker text of the notification. * If device android version is KitKat or newer, the title and text of the notification is used @@ -380,8 +428,8 @@ final String TEXT_KEY = "android.text"; String ticker = ""; - if(notification != null) { - if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + if (notification != null) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { try { Bundle extras = notification.extras; String extraTitle = extras.getCharSequence(TITLE_KEY).toString(); @@ -396,21 +444,20 @@ } else if (extraText != null) { ticker = extraText; } - } catch(Exception e) { - Log.w("NotificationPlugin","problem parsing notification extras for " + notification.tickerText); + } catch (Exception e) { + Log.w("NotificationPlugin", "problem parsing notification extras for " + notification.tickerText); e.printStackTrace(); } } if (ticker.isEmpty()) { - ticker = (notification.tickerText != null)? notification.tickerText.toString() : ""; + ticker = (notification.tickerText != null) ? notification.tickerText.toString() : ""; } } return ticker; } - @Override public boolean onPackageReceived(final NetworkPackage np) { /* @@ -420,171 +467,104 @@ */ if (np.getBoolean("request")) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) - NotificationReceiver.RunCommand(context, new NotificationReceiver.InstanceCallback() { - private void sendCurrentNotifications(NotificationReceiver service) { - StatusBarNotification[] notifications = service.getActiveNotifications(); - for (StatusBarNotification notification : notifications) { - sendNotification(notification, true); - } + NotificationReceiver.RunCommand(context, new NotificationReceiver.InstanceCallback() { + private void sendCurrentNotifications(NotificationReceiver service) { + StatusBarNotification[] notifications = service.getActiveNotifications(); + for (StatusBarNotification notification : notifications) { + sendNotification(notification, true); } + } - @Override - public void onServiceStart(final NotificationReceiver service) { - try { - //If service just started, this call will throw an exception because the answer is not ready yet - sendCurrentNotifications(service); - } catch(Exception e) { - Log.e("onPackageReceived","Error when answering 'request': Service failed to start. Retrying in 100ms..."); - new Thread(new Runnable() { - @Override - public void run() { - try { - Thread.sleep(100); - Log.e("onPackageReceived","Error when answering 'request': Service failed to start. Retrying..."); - sendCurrentNotifications(service); - } catch (Exception e) { - Log.e("onPackageReceived","Error when answering 'request': Service failed to start twice!"); - e.printStackTrace(); - } + @Override + public void onServiceStart(final NotificationReceiver service) { + try { + //If service just started, this call will throw an exception because the answer is not ready yet + sendCurrentNotifications(service); + } catch (Exception e) { + Log.e("onPackageReceived", "Error when answering 'request': Service failed to start. Retrying in 100ms..."); + new Thread(new Runnable() { + @Override + public void run() { + try { + Thread.sleep(100); + Log.e("onPackageReceived", "Error when answering 'request': Service failed to start. Retrying..."); + sendCurrentNotifications(service); + } catch (Exception e) { + Log.e("onPackageReceived", "Error when answering 'request': Service failed to start twice!"); + e.printStackTrace(); } - }).start(); - } + } + }).start(); } - }); + } + }); } else if (np.has("cancel")) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) - NotificationReceiver.RunCommand(context, new NotificationReceiver.InstanceCallback() { - @Override - public void onServiceStart(NotificationReceiver service) { - String dismissedId = np.getString("cancel"); - cancelNotificationCompat(service, dismissedId); - } - }); + NotificationReceiver.RunCommand(context, new NotificationReceiver.InstanceCallback() { + @Override + public void onServiceStart(NotificationReceiver service) { + String dismissedId = np.getString("cancel"); + cancelNotificationCompat(service, dismissedId); + } + }); } else if (np.has("requestReplyId") && np.has("message")) { - + replyToNotification(np.getString("requestReplyId"), np.getString("message")); - + } return true; } - @Override public AlertDialog getErrorDialog(final Activity deviceActivity) { - if (Build.VERSION.SDK_INT < 18) { - return new AlertDialog.Builder(deviceActivity) - .setTitle(R.string.pref_plugin_notifications) - .setMessage(R.string.plugin_not_available) - .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialogInterface, int i) { - - } - }) - .create(); - } else { - return new AlertDialog.Builder(deviceActivity) - .setTitle(R.string.pref_plugin_notifications) - .setMessage(R.string.no_permissions) - .setPositiveButton(R.string.open_settings, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialogInterface, int i) { - Intent intent = new Intent("android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS"); - deviceActivity.startActivityForResult(intent, MaterialActivity.RESULT_NEEDS_RELOAD); - } - }) - .setNegativeButton(R.string.cancel,new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialogInterface, int i) { - //Do nothing - } - }) - .create(); - } + return new AlertDialog.Builder(deviceActivity) + .setTitle(R.string.pref_plugin_notifications) + .setMessage(R.string.no_permissions) + .setPositiveButton(R.string.open_settings, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + Intent intent = new Intent("android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS"); + deviceActivity.startActivityForResult(intent, MaterialActivity.RESULT_NEEDS_RELOAD); + } + }) + .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + //Do nothing + } + }) + .create(); } @Override public String[] getSupportedPackageTypes() { - return new String[]{PACKAGE_TYPE_NOTIFICATION_REQUEST,PACKAGE_TYPE_NOTIFICATION_REPLY}; + return new String[]{PACKAGE_TYPE_NOTIFICATION_REQUEST, PACKAGE_TYPE_NOTIFICATION_REPLY}; } @Override public String[] getOutgoingPackageTypes() { return new String[]{PACKAGE_TYPE_NOTIFICATION}; } - //For compat with API<21, because lollipop changed the way to cancel notifications - public static void cancelNotificationCompat(NotificationReceiver service, String compatKey) { - if (Build.VERSION.SDK_INT >= 21) { - service.cancelNotification(compatKey); - } else { - int first = compatKey.indexOf(':'); - if (first == -1) { - Log.e("cancelNotificationCompa","Not formated like a notification key: "+ compatKey); - return; - } - int last = compatKey.lastIndexOf(':'); - String packageName = compatKey.substring(0, first); - String tag = compatKey.substring(first + 1, last); - if (tag.length() == 0) tag = null; - String idString = compatKey.substring(last + 1); - int id; - try { - id = Integer.parseInt(idString); - } catch (Exception e) { - id = 0; - } - service.cancelNotification(packageName, tag, id); - } - } - - public static String getNotificationKeyCompat(StatusBarNotification statusBarNotification) { - String result; - // first check if it's one of our remoteIds - String tag = statusBarNotification.getTag(); - if (tag != null && tag.startsWith("kdeconnectId:")) - result = Integer.toString(statusBarNotification.getId()); - else if (Build.VERSION.SDK_INT >= 21) { - result = statusBarNotification.getKey(); - } else { - String packageName = statusBarNotification.getPackageName(); - int id = statusBarNotification.getId(); - String safePackageName = (packageName == null) ? "" : packageName; - String safeTag = (tag == null) ? "" : tag; - result = safePackageName + ":" + safeTag + ":" + id; - } - return result; - } - - public String getChecksum(byte[] data){ + public String getChecksum(byte[] data) { try { MessageDigest md = MessageDigest.getInstance("MD5"); md.update(data); return bytesToHex(md.digest()); } catch (NoSuchAlgorithmException e) { - Log.e("KDEConenct", "Error while generating checksum", e); + Log.e("KDEConenct", "Error while generating checksum", e); } return null; } - - public static String bytesToHex(byte[] bytes) { - char[] hexArray = "0123456789ABCDEF".toCharArray(); - char[] hexChars = new char[bytes.length * 2]; - for ( int j = 0; j < bytes.length; j++ ) { - int v = bytes[j] & 0xFF; - hexChars[j * 2] = hexArray[v >>> 4]; - hexChars[j * 2 + 1] = hexArray[v & 0x0F]; - } - return new String(hexChars).toLowerCase(); + @Override + public int getMinSdk() { + return Build.VERSION_CODES.JELLY_BEAN_MR2; } - }