NotificationJob.java 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366
  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.ui.activity.NotificationsActivity;
  58. import com.owncloud.android.ui.notifications.NotificationUtils;
  59. import com.owncloud.android.utils.PushUtils;
  60. import com.owncloud.android.utils.ThemeUtils;
  61. import org.apache.commons.httpclient.HttpMethod;
  62. import org.apache.commons.httpclient.HttpStatus;
  63. import org.apache.commons.httpclient.methods.DeleteMethod;
  64. import org.apache.commons.httpclient.methods.GetMethod;
  65. import org.apache.commons.httpclient.methods.PostMethod;
  66. import org.apache.commons.httpclient.methods.PutMethod;
  67. import java.io.IOException;
  68. import java.security.InvalidKeyException;
  69. import java.security.NoSuchAlgorithmException;
  70. import java.security.PrivateKey;
  71. import java.security.SecureRandom;
  72. import javax.crypto.Cipher;
  73. import javax.crypto.NoSuchPaddingException;
  74. import androidx.annotation.NonNull;
  75. import androidx.core.app.NotificationCompat;
  76. import androidx.core.app.NotificationManagerCompat;
  77. import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
  78. public class NotificationJob extends Job {
  79. public static final String TAG = "NotificationJob";
  80. public static final String KEY_NOTIFICATION_ACCOUNT = "KEY_NOTIFICATION_ACCOUNT";
  81. public static final String KEY_NOTIFICATION_SUBJECT = "subject";
  82. public static final String KEY_NOTIFICATION_SIGNATURE = "signature";
  83. private static final String KEY_NOTIFICATION_ACTION_LINK = "KEY_NOTIFICATION_ACTION_LINK";
  84. private static final String KEY_NOTIFICATION_ACTION_TYPE = "KEY_NOTIFICATION_ACTION_TYPE";
  85. private static final String PUSH_NOTIFICATION_ID = "PUSH_NOTIFICATION_ID";
  86. private static final String NUMERIC_NOTIFICATION_ID = "NUMERIC_NOTIFICATION_ID";
  87. private static final String APP_SPREED = "spreed";
  88. private SecureRandom randomId = new SecureRandom();
  89. private Context context;
  90. private UserAccountManager accountManager;
  91. NotificationJob(final Context context, final UserAccountManager accountManager) {
  92. this.context = context;
  93. this.accountManager = accountManager;
  94. }
  95. @NonNull
  96. @Override
  97. protected Result onRunJob(@NonNull Params params) {
  98. context = getContext();
  99. PersistableBundleCompat persistableBundleCompat = getParams().getExtras();
  100. String subject = persistableBundleCompat.getString(KEY_NOTIFICATION_SUBJECT, "");
  101. String signature = persistableBundleCompat.getString(KEY_NOTIFICATION_SIGNATURE, "");
  102. if (!TextUtils.isEmpty(subject) && !TextUtils.isEmpty(signature)) {
  103. try {
  104. byte[] base64DecodedSubject = Base64.decode(subject, Base64.DEFAULT);
  105. byte[] base64DecodedSignature = Base64.decode(signature, Base64.DEFAULT);
  106. PrivateKey privateKey = (PrivateKey) PushUtils.readKeyFromFile(false);
  107. try {
  108. SignatureVerification signatureVerification = PushUtils.verifySignature(context,
  109. accountManager,
  110. base64DecodedSignature,
  111. base64DecodedSubject);
  112. if (signatureVerification != null && signatureVerification.isSignatureValid()) {
  113. Cipher cipher = Cipher.getInstance("RSA/None/PKCS1Padding");
  114. cipher.init(Cipher.DECRYPT_MODE, privateKey);
  115. byte[] decryptedSubject = cipher.doFinal(base64DecodedSubject);
  116. Gson gson = new Gson();
  117. DecryptedPushMessage decryptedPushMessage = gson.fromJson(new String(decryptedSubject),
  118. DecryptedPushMessage.class);
  119. // We ignore Spreed messages for now
  120. if (!APP_SPREED.equals(decryptedPushMessage.getApp())) {
  121. fetchCompleteNotification(signatureVerification.getAccount(), decryptedPushMessage);
  122. }
  123. }
  124. } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException e1) {
  125. Log.d(TAG, "Error decrypting message " + e1.getClass().getName()
  126. + " " + e1.getLocalizedMessage());
  127. }
  128. } catch (Exception exception) {
  129. Log.d(TAG, "Something went very wrong" + exception.getLocalizedMessage());
  130. }
  131. }
  132. return Result.SUCCESS;
  133. }
  134. private void sendNotification(Notification notification, Account account) {
  135. Intent intent = new Intent(context, NotificationsActivity.class);
  136. intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
  137. intent.putExtra(KEY_NOTIFICATION_ACCOUNT, account.name);
  138. PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_ONE_SHOT);
  139. int pushNotificationId = randomId.nextInt();
  140. NotificationCompat.Builder notificationBuilder =
  141. new NotificationCompat.Builder(context, NotificationUtils.NOTIFICATION_CHANNEL_PUSH)
  142. .setSmallIcon(R.drawable.notification_icon)
  143. .setLargeIcon(BitmapFactory.decodeResource(context.getResources(), R.drawable.notification_icon))
  144. .setColor(ThemeUtils.primaryColor(account, false, context))
  145. .setShowWhen(true)
  146. .setSubText(account.name)
  147. .setContentTitle(notification.getSubject())
  148. .setContentText(notification.getMessage())
  149. .setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION))
  150. .setAutoCancel(true)
  151. .setVisibility(NotificationCompat.VISIBILITY_PRIVATE)
  152. .setContentIntent(pendingIntent);
  153. // Remove
  154. if (notification.getActions().isEmpty()) {
  155. Intent disableDetection = new Intent(context, NotificationJob.NotificationReceiver.class);
  156. disableDetection.putExtra(NUMERIC_NOTIFICATION_ID, notification.getNotificationId());
  157. disableDetection.putExtra(PUSH_NOTIFICATION_ID, pushNotificationId);
  158. disableDetection.putExtra(KEY_NOTIFICATION_ACCOUNT, account.name);
  159. PendingIntent disableIntent = PendingIntent.getBroadcast(context, pushNotificationId, disableDetection,
  160. PendingIntent.FLAG_CANCEL_CURRENT);
  161. notificationBuilder.addAction(new NotificationCompat.Action(R.drawable.ic_close,
  162. context.getString(R.string.remove_push_notification), disableIntent));
  163. } else {
  164. // Actions
  165. for (Action action : notification.getActions()) {
  166. Intent actionIntent = new Intent(context, NotificationJob.NotificationReceiver.class);
  167. actionIntent.putExtra(NUMERIC_NOTIFICATION_ID, notification.getNotificationId());
  168. actionIntent.putExtra(PUSH_NOTIFICATION_ID, pushNotificationId);
  169. actionIntent.putExtra(KEY_NOTIFICATION_ACCOUNT, account.name);
  170. actionIntent.putExtra(KEY_NOTIFICATION_ACTION_LINK, action.link);
  171. actionIntent.putExtra(KEY_NOTIFICATION_ACTION_TYPE, action.type);
  172. PendingIntent actionPendingIntent = PendingIntent.getBroadcast(context, randomId.nextInt(),
  173. actionIntent,
  174. PendingIntent.FLAG_CANCEL_CURRENT);
  175. int icon;
  176. if (action.primary) {
  177. icon = R.drawable.ic_check_circle;
  178. } else {
  179. icon = R.drawable.ic_check_circle_outline;
  180. }
  181. notificationBuilder.addAction(new NotificationCompat.Action(icon, action.label, actionPendingIntent));
  182. }
  183. }
  184. notificationBuilder.setPublicVersion(
  185. new NotificationCompat.Builder(context, NotificationUtils.NOTIFICATION_CHANNEL_PUSH)
  186. .setSmallIcon(R.drawable.notification_icon)
  187. .setLargeIcon(BitmapFactory.decodeResource(context.getResources(), R.drawable.notification_icon))
  188. .setColor(ThemeUtils.primaryColor(account, false, context))
  189. .setShowWhen(true)
  190. .setSubText(account.name)
  191. .setContentTitle(context.getString(R.string.new_notification))
  192. .setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION))
  193. .setAutoCancel(true)
  194. .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
  195. .setContentIntent(pendingIntent).build());
  196. NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
  197. notificationManager.notify(pushNotificationId, notificationBuilder.build());
  198. }
  199. private void fetchCompleteNotification(Account account, DecryptedPushMessage decryptedPushMessage) {
  200. Account currentAccount = AccountUtils.getOwnCloudAccountByName(context, account.name);
  201. if (currentAccount == null) {
  202. Log_OC.e(this, "Account may not be null");
  203. return;
  204. }
  205. try {
  206. OwnCloudAccount ocAccount = new OwnCloudAccount(currentAccount, context);
  207. OwnCloudClient client = OwnCloudClientManagerFactory.getDefaultSingleton()
  208. .getClientFor(ocAccount, context);
  209. client.setOwnCloudVersion(AccountUtils.getServerVersion(currentAccount));
  210. RemoteOperationResult result = new GetNotificationRemoteOperation(decryptedPushMessage.nid)
  211. .execute(client);
  212. if (result.isSuccess()) {
  213. Notification notification = result.getNotificationData().get(0);
  214. sendNotification(notification, account);
  215. }
  216. } catch (Exception e) {
  217. Log_OC.e(this, "Error creating account", e);
  218. }
  219. }
  220. public static class NotificationReceiver extends BroadcastReceiver {
  221. @Override
  222. public void onReceive(Context context, Intent intent) {
  223. int numericNotificationId = intent.getIntExtra(NUMERIC_NOTIFICATION_ID, 0);
  224. int pushNotificationId = intent.getIntExtra(PUSH_NOTIFICATION_ID, 0);
  225. String accountName = intent.getStringExtra(NotificationJob.KEY_NOTIFICATION_ACCOUNT);
  226. if (numericNotificationId != 0) {
  227. new Thread(() -> {
  228. NotificationManager notificationManager = (NotificationManager) context.getSystemService(
  229. Activity.NOTIFICATION_SERVICE);
  230. android.app.Notification oldNotification = null;
  231. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && notificationManager != null) {
  232. for (StatusBarNotification statusBarNotification : notificationManager.getActiveNotifications()) {
  233. if (pushNotificationId == statusBarNotification.getId()) {
  234. oldNotification = statusBarNotification.getNotification();
  235. break;
  236. }
  237. }
  238. cancel(context, pushNotificationId);
  239. }
  240. try {
  241. Account currentAccount = AccountUtils.getOwnCloudAccountByName(context, accountName);
  242. if (currentAccount == null) {
  243. Log_OC.e(this, "Account may not be null");
  244. return;
  245. }
  246. OwnCloudAccount ocAccount = new OwnCloudAccount(currentAccount, context);
  247. OwnCloudClient client = OwnCloudClientManagerFactory.getDefaultSingleton()
  248. .getClientFor(ocAccount, context);
  249. client.setOwnCloudVersion(AccountUtils.getServerVersion(currentAccount));
  250. String actionType = intent.getStringExtra(KEY_NOTIFICATION_ACTION_TYPE);
  251. String actionLink = intent.getStringExtra(KEY_NOTIFICATION_ACTION_LINK);
  252. boolean success;
  253. if (!TextUtils.isEmpty(actionType) && !TextUtils.isEmpty(actionLink)) {
  254. int resultCode = executeAction(actionType, actionLink, client);
  255. success = resultCode == HttpStatus.SC_OK || resultCode == HttpStatus.SC_ACCEPTED;
  256. } else {
  257. success = new DeleteNotificationRemoteOperation(numericNotificationId)
  258. .execute(client).isSuccess();
  259. }
  260. if (success) {
  261. if (oldNotification == null) {
  262. cancel(context, pushNotificationId);
  263. }
  264. } else if (notificationManager != null) {
  265. notificationManager.notify(pushNotificationId, oldNotification);
  266. }
  267. } catch (com.owncloud.android.lib.common.accounts.AccountUtils.AccountNotFoundException |
  268. IOException | OperationCanceledException | AuthenticatorException e) {
  269. Log_OC.e(TAG, "Error initializing client", e);
  270. }
  271. }).start();
  272. }
  273. }
  274. @SuppressFBWarnings(value = "HTTP_PARAMETER_POLLUTION",
  275. justification = "link and type are from server and expected to be safe")
  276. private int executeAction(String actionType, String actionLink, OwnCloudClient client) {
  277. HttpMethod method;
  278. switch (actionType) {
  279. case "GET":
  280. method = new GetMethod(actionLink);
  281. break;
  282. case "POST":
  283. method = new PostMethod(actionLink);
  284. break;
  285. case "DELETE":
  286. method = new DeleteMethod(actionLink);
  287. break;
  288. case "PUT":
  289. method = new PutMethod(actionLink);
  290. break;
  291. default:
  292. // do nothing
  293. return 0;
  294. }
  295. method.setRequestHeader(RemoteOperation.OCS_API_HEADER, RemoteOperation.OCS_API_HEADER_VALUE);
  296. try {
  297. return client.executeMethod(method);
  298. } catch (IOException e) {
  299. Log_OC.e(TAG, "Execution of notification action failed: " + e);
  300. }
  301. return 0;
  302. }
  303. private void cancel(Context context, int notificationId) {
  304. NotificationManager notificationManager = (NotificationManager) context.getSystemService(
  305. Activity.NOTIFICATION_SERVICE);
  306. if (notificationManager != null) {
  307. notificationManager.cancel(notificationId);
  308. }
  309. }
  310. }
  311. }