/* * Nextcloud application * * @author Mario Danic * @author Chris Narkiewicz * Copyright (C) 2017-2018 Mario Danic * Copyright (C) 2019 Chris Narkiewicz * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.owncloud.android.jobs; import android.accounts.Account; import android.accounts.AuthenticatorException; import android.accounts.OperationCanceledException; import android.app.Activity; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.graphics.BitmapFactory; import android.media.RingtoneManager; import android.os.Build; import android.service.notification.StatusBarNotification; import android.text.TextUtils; import android.util.Base64; import android.util.Log; import com.evernote.android.job.Job; import com.evernote.android.job.util.support.PersistableBundleCompat; import com.google.gson.Gson; import com.nextcloud.client.account.UserAccountManager; import com.owncloud.android.R; import com.owncloud.android.authentication.AccountUtils; import com.owncloud.android.datamodel.DecryptedPushMessage; import com.owncloud.android.datamodel.SignatureVerification; import com.owncloud.android.lib.common.OwnCloudAccount; import com.owncloud.android.lib.common.OwnCloudClient; import com.owncloud.android.lib.common.OwnCloudClientManagerFactory; import com.owncloud.android.lib.common.operations.RemoteOperation; import com.owncloud.android.lib.common.operations.RemoteOperationResult; import com.owncloud.android.lib.common.utils.Log_OC; import com.owncloud.android.lib.resources.notifications.DeleteNotificationRemoteOperation; import com.owncloud.android.lib.resources.notifications.GetNotificationRemoteOperation; import com.owncloud.android.lib.resources.notifications.models.Action; import com.owncloud.android.lib.resources.notifications.models.Notification; import com.owncloud.android.lib.resources.notifications.models.RichObject; import com.owncloud.android.ui.activity.FileDisplayActivity; import com.owncloud.android.ui.activity.NotificationsActivity; import com.owncloud.android.ui.notifications.NotificationUtils; import com.owncloud.android.utils.PushUtils; import com.owncloud.android.utils.ThemeUtils; import org.apache.commons.httpclient.HttpMethod; import org.apache.commons.httpclient.HttpStatus; import org.apache.commons.httpclient.methods.DeleteMethod; import org.apache.commons.httpclient.methods.GetMethod; import org.apache.commons.httpclient.methods.PostMethod; import org.apache.commons.httpclient.methods.PutMethod; import java.io.IOException; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; import java.security.SecureRandom; import javax.crypto.Cipher; import javax.crypto.NoSuchPaddingException; import javax.inject.Inject; import androidx.annotation.NonNull; import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationManagerCompat; import dagger.android.AndroidInjection; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; public class NotificationJob extends Job { public static final String TAG = "NotificationJob"; public static final String KEY_NOTIFICATION_ACCOUNT = "KEY_NOTIFICATION_ACCOUNT"; public static final String KEY_NOTIFICATION_SUBJECT = "subject"; public static final String KEY_NOTIFICATION_SIGNATURE = "signature"; private static final String KEY_NOTIFICATION_ACTION_LINK = "KEY_NOTIFICATION_ACTION_LINK"; private static final String KEY_NOTIFICATION_ACTION_TYPE = "KEY_NOTIFICATION_ACTION_TYPE"; private static final String PUSH_NOTIFICATION_ID = "PUSH_NOTIFICATION_ID"; private static final String NUMERIC_NOTIFICATION_ID = "NUMERIC_NOTIFICATION_ID"; private Context context; private UserAccountManager accountManager; NotificationJob(final Context context, final UserAccountManager accountManager) { this.context = context; this.accountManager = accountManager; } @NonNull @Override protected Result onRunJob(@NonNull Params params) { context = getContext(); NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context); PersistableBundleCompat persistableBundleCompat = getParams().getExtras(); String subject = persistableBundleCompat.getString(KEY_NOTIFICATION_SUBJECT, ""); String signature = persistableBundleCompat.getString(KEY_NOTIFICATION_SIGNATURE, ""); if (!TextUtils.isEmpty(subject) && !TextUtils.isEmpty(signature)) { try { byte[] base64DecodedSubject = Base64.decode(subject, Base64.DEFAULT); byte[] base64DecodedSignature = Base64.decode(signature, Base64.DEFAULT); PrivateKey privateKey = (PrivateKey) PushUtils.readKeyFromFile(false); try { SignatureVerification signatureVerification = PushUtils.verifySignature(context, accountManager, base64DecodedSignature, base64DecodedSubject); if (signatureVerification != null && signatureVerification.isSignatureValid()) { Cipher cipher = Cipher.getInstance("RSA/None/PKCS1Padding"); cipher.init(Cipher.DECRYPT_MODE, privateKey); byte[] decryptedSubject = cipher.doFinal(base64DecodedSubject); Gson gson = new Gson(); DecryptedPushMessage decryptedPushMessage = gson.fromJson(new String(decryptedSubject), DecryptedPushMessage.class); if (decryptedPushMessage.delete) { notificationManager.cancel(decryptedPushMessage.nid); } else if (decryptedPushMessage.deleteAll) { notificationManager.cancelAll(); } else { fetchCompleteNotification(signatureVerification.getAccount(), decryptedPushMessage); } } } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException e1) { Log.d(TAG, "Error decrypting message " + e1.getClass().getName() + " " + e1.getLocalizedMessage()); } } catch (Exception exception) { Log.d(TAG, "Something went very wrong" + exception.getLocalizedMessage()); } } return Result.SUCCESS; } private void sendNotification(Notification notification, Account account) { SecureRandom randomId = new SecureRandom(); RichObject file = notification.subjectRichParameters.get("file"); Intent intent; if (file == null) { intent = new Intent(context, NotificationsActivity.class); } else { intent = new Intent(context, FileDisplayActivity.class); intent.setAction(Intent.ACTION_VIEW); intent.putExtra(FileDisplayActivity.KEY_FILE_ID, file.id); } intent.putExtra(KEY_NOTIFICATION_ACCOUNT, account.name); intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_ONE_SHOT); int pushNotificationId = randomId.nextInt(); NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(context, NotificationUtils.NOTIFICATION_CHANNEL_PUSH) .setSmallIcon(R.drawable.notification_icon) .setLargeIcon(BitmapFactory.decodeResource(context.getResources(), R.drawable.notification_icon)) .setColor(ThemeUtils.primaryColor(account, false, context)) .setShowWhen(true) .setSubText(account.name) .setContentTitle(notification.getSubject()) .setContentText(notification.getMessage()) .setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)) .setAutoCancel(true) .setVisibility(NotificationCompat.VISIBILITY_PRIVATE) .setContentIntent(pendingIntent); // Remove if (notification.getActions().isEmpty()) { Intent disableDetection = new Intent(context, NotificationJob.NotificationReceiver.class); disableDetection.putExtra(NUMERIC_NOTIFICATION_ID, notification.getNotificationId()); disableDetection.putExtra(PUSH_NOTIFICATION_ID, pushNotificationId); disableDetection.putExtra(KEY_NOTIFICATION_ACCOUNT, account.name); PendingIntent disableIntent = PendingIntent.getBroadcast(context, pushNotificationId, disableDetection, PendingIntent.FLAG_CANCEL_CURRENT); notificationBuilder.addAction(new NotificationCompat.Action(R.drawable.ic_close, context.getString(R.string.remove_push_notification), disableIntent)); } else { // Actions for (Action action : notification.getActions()) { Intent actionIntent = new Intent(context, NotificationJob.NotificationReceiver.class); actionIntent.putExtra(NUMERIC_NOTIFICATION_ID, notification.getNotificationId()); actionIntent.putExtra(PUSH_NOTIFICATION_ID, pushNotificationId); actionIntent.putExtra(KEY_NOTIFICATION_ACCOUNT, account.name); actionIntent.putExtra(KEY_NOTIFICATION_ACTION_LINK, action.link); actionIntent.putExtra(KEY_NOTIFICATION_ACTION_TYPE, action.type); PendingIntent actionPendingIntent = PendingIntent.getBroadcast(context, randomId.nextInt(), actionIntent, PendingIntent.FLAG_CANCEL_CURRENT); int icon; if (action.primary) { icon = R.drawable.ic_check_circle; } else { icon = R.drawable.ic_check_circle_outline; } notificationBuilder.addAction(new NotificationCompat.Action(icon, action.label, actionPendingIntent)); } } notificationBuilder.setPublicVersion( new NotificationCompat.Builder(context, NotificationUtils.NOTIFICATION_CHANNEL_PUSH) .setSmallIcon(R.drawable.notification_icon) .setLargeIcon(BitmapFactory.decodeResource(context.getResources(), R.drawable.notification_icon)) .setColor(ThemeUtils.primaryColor(account, false, context)) .setShowWhen(true) .setSubText(account.name) .setContentTitle(context.getString(R.string.new_notification)) .setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)) .setAutoCancel(true) .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) .setContentIntent(pendingIntent).build()); NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context); notificationManager.notify(notification.getNotificationId(), notificationBuilder.build()); } private void fetchCompleteNotification(Account account, DecryptedPushMessage decryptedPushMessage) { Account currentAccount = accountManager.getAccountByName(account.name); if (currentAccount == null) { Log_OC.e(this, "Account may not be null"); return; } try { OwnCloudAccount ocAccount = new OwnCloudAccount(currentAccount, context); OwnCloudClient client = OwnCloudClientManagerFactory.getDefaultSingleton() .getClientFor(ocAccount, context); client.setOwnCloudVersion(accountManager.getServerVersion(currentAccount)); RemoteOperationResult result = new GetNotificationRemoteOperation(decryptedPushMessage.nid) .execute(client); if (result.isSuccess()) { Notification notification = result.getNotificationData().get(0); sendNotification(notification, account); } } catch (Exception e) { Log_OC.e(this, "Error creating account", e); } } public static class NotificationReceiver extends BroadcastReceiver { @Inject UserAccountManager accountManager; @Override public void onReceive(Context context, Intent intent) { AndroidInjection.inject(this, context); int numericNotificationId = intent.getIntExtra(NUMERIC_NOTIFICATION_ID, 0); int pushNotificationId = intent.getIntExtra(PUSH_NOTIFICATION_ID, 0); String accountName = intent.getStringExtra(NotificationJob.KEY_NOTIFICATION_ACCOUNT); if (numericNotificationId != 0) { new Thread(() -> { NotificationManager notificationManager = (NotificationManager) context.getSystemService( Activity.NOTIFICATION_SERVICE); android.app.Notification oldNotification = null; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && notificationManager != null) { for (StatusBarNotification statusBarNotification : notificationManager.getActiveNotifications()) { if (pushNotificationId == statusBarNotification.getId()) { oldNotification = statusBarNotification.getNotification(); break; } } cancel(context, pushNotificationId); } try { Account currentAccount = accountManager.getAccountByName(accountName); if (currentAccount == null) { Log_OC.e(this, "Account may not be null"); return; } OwnCloudAccount ocAccount = new OwnCloudAccount(currentAccount, context); OwnCloudClient client = OwnCloudClientManagerFactory.getDefaultSingleton() .getClientFor(ocAccount, context); client.setOwnCloudVersion(accountManager.getServerVersion(currentAccount)); String actionType = intent.getStringExtra(KEY_NOTIFICATION_ACTION_TYPE); String actionLink = intent.getStringExtra(KEY_NOTIFICATION_ACTION_LINK); boolean success; if (!TextUtils.isEmpty(actionType) && !TextUtils.isEmpty(actionLink)) { int resultCode = executeAction(actionType, actionLink, client); success = resultCode == HttpStatus.SC_OK || resultCode == HttpStatus.SC_ACCEPTED; } else { success = new DeleteNotificationRemoteOperation(numericNotificationId) .execute(client).isSuccess(); } if (success) { if (oldNotification == null) { cancel(context, pushNotificationId); } } else if (notificationManager != null) { notificationManager.notify(pushNotificationId, oldNotification); } } catch (com.owncloud.android.lib.common.accounts.AccountUtils.AccountNotFoundException | IOException | OperationCanceledException | AuthenticatorException e) { Log_OC.e(TAG, "Error initializing client", e); } }).start(); } } @SuppressFBWarnings(value = "HTTP_PARAMETER_POLLUTION", justification = "link and type are from server and expected to be safe") private int executeAction(String actionType, String actionLink, OwnCloudClient client) { HttpMethod method; switch (actionType) { case "GET": method = new GetMethod(actionLink); break; case "POST": method = new PostMethod(actionLink); break; case "DELETE": method = new DeleteMethod(actionLink); break; case "PUT": method = new PutMethod(actionLink); break; default: // do nothing return 0; } method.setRequestHeader(RemoteOperation.OCS_API_HEADER, RemoteOperation.OCS_API_HEADER_VALUE); try { return client.executeMethod(method); } catch (IOException e) { Log_OC.e(TAG, "Execution of notification action failed: " + e); } return 0; } private void cancel(Context context, int notificationId) { NotificationManager notificationManager = (NotificationManager) context.getSystemService( Activity.NOTIFICATION_SERVICE); if (notificationManager != null) { notificationManager.cancel(notificationId); } } } }