NotificationJob.java 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319
  1. /*
  2. * Nextcloud application
  3. *
  4. * @author Mario Danic
  5. * Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
  6. *
  7. * This program is free software: you can redistribute it and/or modify
  8. * it under the terms of the GNU General Public License as published by
  9. * the Free Software Foundation, either version 3 of the License, or
  10. * at your option) any later version.
  11. *
  12. * This program is distributed in the hope that it will be useful,
  13. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. * GNU General Public License for more details.
  16. *
  17. * You should have received a copy of the GNU General Public License
  18. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  19. */
  20. package com.owncloud.android.jobs;
  21. import android.accounts.Account;
  22. import android.accounts.AuthenticatorException;
  23. import android.accounts.OperationCanceledException;
  24. import android.app.Activity;
  25. import android.app.NotificationManager;
  26. import android.app.PendingIntent;
  27. import android.content.BroadcastReceiver;
  28. import android.content.Context;
  29. import android.content.Intent;
  30. import android.graphics.BitmapFactory;
  31. import android.media.RingtoneManager;
  32. import android.text.TextUtils;
  33. import android.util.Base64;
  34. import android.util.Log;
  35. import com.evernote.android.job.Job;
  36. import com.evernote.android.job.util.support.PersistableBundleCompat;
  37. import com.google.gson.Gson;
  38. import com.owncloud.android.R;
  39. import com.owncloud.android.authentication.AccountUtils;
  40. import com.owncloud.android.datamodel.DecryptedPushMessage;
  41. import com.owncloud.android.datamodel.SignatureVerification;
  42. import com.owncloud.android.lib.common.OwnCloudAccount;
  43. import com.owncloud.android.lib.common.OwnCloudClient;
  44. import com.owncloud.android.lib.common.OwnCloudClientManagerFactory;
  45. import com.owncloud.android.lib.common.operations.RemoteOperation;
  46. import com.owncloud.android.lib.common.operations.RemoteOperationResult;
  47. import com.owncloud.android.lib.common.utils.Log_OC;
  48. import com.owncloud.android.lib.resources.notifications.DeleteNotificationRemoteOperation;
  49. import com.owncloud.android.lib.resources.notifications.GetNotificationRemoteOperation;
  50. import com.owncloud.android.lib.resources.notifications.models.Action;
  51. import com.owncloud.android.lib.resources.notifications.models.Notification;
  52. import com.owncloud.android.ui.activity.NotificationsActivity;
  53. import com.owncloud.android.ui.notifications.NotificationUtils;
  54. import com.owncloud.android.utils.PushUtils;
  55. import com.owncloud.android.utils.ThemeUtils;
  56. import org.apache.commons.httpclient.HttpMethod;
  57. import org.apache.commons.httpclient.HttpStatus;
  58. import org.apache.commons.httpclient.methods.DeleteMethod;
  59. import org.apache.commons.httpclient.methods.GetMethod;
  60. import org.apache.commons.httpclient.methods.PostMethod;
  61. import java.io.IOException;
  62. import java.security.InvalidKeyException;
  63. import java.security.NoSuchAlgorithmException;
  64. import java.security.PrivateKey;
  65. import java.security.SecureRandom;
  66. import javax.crypto.Cipher;
  67. import javax.crypto.NoSuchPaddingException;
  68. import androidx.annotation.NonNull;
  69. import androidx.core.app.NotificationCompat;
  70. import androidx.core.app.NotificationManagerCompat;
  71. public class NotificationJob extends Job {
  72. public static final String TAG = "NotificationJob";
  73. public static final String KEY_NOTIFICATION_ACCOUNT = "KEY_NOTIFICATION_ACCOUNT";
  74. public static final String KEY_NOTIFICATION_SUBJECT = "subject";
  75. public static final String KEY_NOTIFICATION_SIGNATURE = "signature";
  76. private static final String KEY_NOTIFICATION_ACTION_LINK = "KEY_NOTIFICATION_ACTION_LINK";
  77. private static final String KEY_NOTIFICATION_ACTION_TYPE = "KEY_NOTIFICATION_ACTION_TYPE";
  78. private static final String PUSH_NOTIFICATION_ID = "PUSH_NOTIFICATION_ID";
  79. private static final String NUMERIC_NOTIFICATION_ID = "NUMERIC_NOTIFICATION_ID";
  80. private SecureRandom randomId = new SecureRandom();
  81. private Context context;
  82. @NonNull
  83. @Override
  84. protected Result onRunJob(@NonNull Params params) {
  85. context = getContext();
  86. PersistableBundleCompat persistableBundleCompat = getParams().getExtras();
  87. String subject = persistableBundleCompat.getString(KEY_NOTIFICATION_SUBJECT, "");
  88. String signature = persistableBundleCompat.getString(KEY_NOTIFICATION_SIGNATURE, "");
  89. if (!TextUtils.isEmpty(subject) && !TextUtils.isEmpty(signature)) {
  90. try {
  91. byte[] base64DecodedSubject = Base64.decode(subject, Base64.DEFAULT);
  92. byte[] base64DecodedSignature = Base64.decode(signature, Base64.DEFAULT);
  93. PrivateKey privateKey = (PrivateKey) PushUtils.readKeyFromFile(false);
  94. try {
  95. SignatureVerification signatureVerification = PushUtils.verifySignature(context,
  96. base64DecodedSignature,
  97. base64DecodedSubject);
  98. if (signatureVerification != null && signatureVerification.isSignatureValid()) {
  99. Cipher cipher = Cipher.getInstance("RSA/None/PKCS1Padding");
  100. cipher.init(Cipher.DECRYPT_MODE, privateKey);
  101. byte[] decryptedSubject = cipher.doFinal(base64DecodedSubject);
  102. Gson gson = new Gson();
  103. DecryptedPushMessage decryptedPushMessage = gson.fromJson(new String(decryptedSubject),
  104. DecryptedPushMessage.class);
  105. // We ignore Spreed messages for now
  106. if (!"spreed".equals(decryptedPushMessage.getApp())) {
  107. fetchCompleteNotification(signatureVerification.getAccount(), decryptedPushMessage);
  108. }
  109. }
  110. } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException e1) {
  111. Log.d(TAG, "Error decrypting message " + e1.getClass().getName()
  112. + " " + e1.getLocalizedMessage());
  113. }
  114. } catch (Exception exception) {
  115. Log.d(TAG, "Something went very wrong" + exception.getLocalizedMessage());
  116. }
  117. }
  118. return Result.SUCCESS;
  119. }
  120. private void sendNotification(Notification notification, Account account) {
  121. Intent intent = new Intent(context, NotificationsActivity.class);
  122. intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
  123. intent.putExtra(KEY_NOTIFICATION_ACCOUNT, account.name);
  124. PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_ONE_SHOT);
  125. int pushNotificationId = randomId.nextInt();
  126. NotificationCompat.Builder notificationBuilder =
  127. new NotificationCompat.Builder(context, NotificationUtils.NOTIFICATION_CHANNEL_PUSH)
  128. .setSmallIcon(R.drawable.notification_icon)
  129. .setLargeIcon(BitmapFactory.decodeResource(context.getResources(), R.drawable.notification_icon))
  130. .setColor(ThemeUtils.primaryColor(account, false, context))
  131. .setShowWhen(true)
  132. .setSubText(account.name)
  133. .setContentTitle(notification.getSubject())
  134. .setContentText(notification.getMessage())
  135. .setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION))
  136. .setAutoCancel(true)
  137. .setVisibility(NotificationCompat.VISIBILITY_PRIVATE)
  138. .setContentIntent(pendingIntent);
  139. // Remove
  140. if (notification.getActions().isEmpty()) {
  141. Intent disableDetection = new Intent(context, NotificationJob.NotificationReceiver.class);
  142. disableDetection.putExtra(NUMERIC_NOTIFICATION_ID, notification.getNotificationId());
  143. disableDetection.putExtra(PUSH_NOTIFICATION_ID, pushNotificationId);
  144. disableDetection.putExtra(KEY_NOTIFICATION_ACCOUNT, account.name);
  145. PendingIntent disableIntent = PendingIntent.getBroadcast(context, pushNotificationId, disableDetection,
  146. PendingIntent.FLAG_CANCEL_CURRENT);
  147. notificationBuilder.addAction(new NotificationCompat.Action(R.drawable.ic_close,
  148. context.getString(R.string.remove_push_notification), disableIntent));
  149. } else {
  150. // Actions
  151. for (Action action : notification.getActions()) {
  152. Intent actionIntent = new Intent(context, NotificationJob.NotificationReceiver.class);
  153. actionIntent.putExtra(NUMERIC_NOTIFICATION_ID, notification.getNotificationId());
  154. actionIntent.putExtra(PUSH_NOTIFICATION_ID, pushNotificationId);
  155. actionIntent.putExtra(KEY_NOTIFICATION_ACCOUNT, account.name);
  156. actionIntent.putExtra(KEY_NOTIFICATION_ACTION_LINK, action.link);
  157. actionIntent.putExtra(KEY_NOTIFICATION_ACTION_TYPE, action.type);
  158. PendingIntent actionPendingIntent = PendingIntent.getBroadcast(context, randomId.nextInt(),
  159. actionIntent,
  160. PendingIntent.FLAG_CANCEL_CURRENT);
  161. notificationBuilder.addAction(new NotificationCompat.Action(R.drawable.ic_notification, action.label,
  162. actionPendingIntent));
  163. }
  164. }
  165. notificationBuilder.setPublicVersion(
  166. new NotificationCompat.Builder(context, NotificationUtils.NOTIFICATION_CHANNEL_PUSH)
  167. .setSmallIcon(R.drawable.notification_icon)
  168. .setLargeIcon(BitmapFactory.decodeResource(context.getResources(), R.drawable.notification_icon))
  169. .setColor(ThemeUtils.primaryColor(account, false, context))
  170. .setShowWhen(true)
  171. .setSubText(account.name)
  172. .setContentTitle("New Notification")
  173. .setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION))
  174. .setAutoCancel(true)
  175. .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
  176. .setContentIntent(pendingIntent).build());
  177. NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
  178. notificationManager.notify(pushNotificationId, notificationBuilder.build());
  179. }
  180. private void fetchCompleteNotification(Account account, DecryptedPushMessage decryptedPushMessage) {
  181. Account currentAccount = AccountUtils.getOwnCloudAccountByName(context, account.name);
  182. if (currentAccount == null) {
  183. Log_OC.e(this, "Account may not be null");
  184. return;
  185. }
  186. try {
  187. OwnCloudAccount ocAccount = new OwnCloudAccount(currentAccount, context);
  188. OwnCloudClient client = OwnCloudClientManagerFactory.getDefaultSingleton()
  189. .getClientFor(ocAccount, context);
  190. client.setOwnCloudVersion(AccountUtils.getServerVersion(currentAccount));
  191. RemoteOperationResult result = new GetNotificationRemoteOperation(decryptedPushMessage.nid)
  192. .execute(client);
  193. if (result.isSuccess()) {
  194. Notification notification = result.getNotificationData().get(0);
  195. sendNotification(notification, account);
  196. }
  197. } catch (Exception e) {
  198. Log_OC.e(this, "Error creating account", e);
  199. }
  200. }
  201. public static class NotificationReceiver extends BroadcastReceiver {
  202. @Override
  203. public void onReceive(Context context, Intent intent) {
  204. int numericNotificationId = intent.getIntExtra(NUMERIC_NOTIFICATION_ID, 0);
  205. int pushNotificationId = intent.getIntExtra(PUSH_NOTIFICATION_ID, 0);
  206. String accountName = intent.getStringExtra(NotificationJob.KEY_NOTIFICATION_ACCOUNT);
  207. if (numericNotificationId != 0) {
  208. new Thread(() -> {
  209. try {
  210. Account currentAccount = AccountUtils.getOwnCloudAccountByName(context, accountName);
  211. if (currentAccount == null) {
  212. Log_OC.e(this, "Account may not be null");
  213. return;
  214. }
  215. OwnCloudAccount ocAccount = new OwnCloudAccount(currentAccount, context);
  216. OwnCloudClient client = OwnCloudClientManagerFactory.getDefaultSingleton()
  217. .getClientFor(ocAccount, context);
  218. client.setOwnCloudVersion(AccountUtils.getServerVersion(currentAccount));
  219. String actionType = intent.getStringExtra(KEY_NOTIFICATION_ACTION_TYPE);
  220. String actionLink = intent.getStringExtra(KEY_NOTIFICATION_ACTION_LINK);
  221. boolean success;
  222. if (!TextUtils.isEmpty(actionType) && !TextUtils.isEmpty(actionLink)) {
  223. success = executeAction(actionType, actionLink, client) == HttpStatus.SC_OK;
  224. } else {
  225. success = new DeleteNotificationRemoteOperation(numericNotificationId)
  226. .execute(client).isSuccess();
  227. }
  228. if (success) {
  229. cancel(context, pushNotificationId);
  230. }
  231. } catch (com.owncloud.android.lib.common.accounts.AccountUtils.AccountNotFoundException |
  232. IOException | OperationCanceledException | AuthenticatorException e) {
  233. Log_OC.e(TAG, "Error initializing client", e);
  234. }
  235. }).start();
  236. }
  237. }
  238. private int executeAction(String actionType, String actionLink, OwnCloudClient client) {
  239. HttpMethod method;
  240. switch (actionType) {
  241. case "GET":
  242. method = new GetMethod(actionLink);
  243. break;
  244. case "POST":
  245. method = new PostMethod(actionLink);
  246. break;
  247. case "DELETE":
  248. method = new DeleteMethod(actionLink);
  249. break;
  250. default:
  251. // do nothing
  252. return 0;
  253. }
  254. method.setRequestHeader(RemoteOperation.OCS_API_HEADER, RemoteOperation.OCS_API_HEADER_VALUE);
  255. try {
  256. return client.executeMethod(method);
  257. } catch (IOException e) {
  258. Log_OC.e(TAG, "Execution of notification action failed: " + e);
  259. }
  260. return 0;
  261. }
  262. private void cancel(Context context, int notificationId) {
  263. NotificationManager notificationManager = (NotificationManager) context.getSystemService(
  264. Activity.NOTIFICATION_SERVICE);
  265. if (notificationManager != null) {
  266. notificationManager.cancel(notificationId);
  267. }
  268. }
  269. }
  270. }