NotificationJob.java 18 KB

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