NotificationWork.kt 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319
  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) 2020 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.nextcloud.client.jobs
  23. import android.accounts.AuthenticatorException
  24. import android.accounts.OperationCanceledException
  25. import android.app.Activity
  26. import android.app.NotificationManager
  27. import android.app.PendingIntent
  28. import android.content.BroadcastReceiver
  29. import android.content.Context
  30. import android.content.Intent
  31. import android.graphics.BitmapFactory
  32. import android.media.RingtoneManager
  33. import android.os.Build
  34. import android.text.TextUtils
  35. import android.util.Base64
  36. import android.util.Log
  37. import androidx.core.app.NotificationCompat
  38. import androidx.core.app.NotificationManagerCompat
  39. import androidx.work.Worker
  40. import androidx.work.WorkerParameters
  41. import com.google.gson.Gson
  42. import com.nextcloud.client.account.User
  43. import com.nextcloud.client.account.UserAccountManager
  44. import com.owncloud.android.R
  45. import com.owncloud.android.datamodel.DecryptedPushMessage
  46. import com.owncloud.android.lib.common.OwnCloudClient
  47. import com.owncloud.android.lib.common.OwnCloudClientManagerFactory
  48. import com.owncloud.android.lib.common.operations.RemoteOperation
  49. import com.owncloud.android.lib.common.utils.Log_OC
  50. import com.owncloud.android.lib.resources.notifications.DeleteNotificationRemoteOperation
  51. import com.owncloud.android.lib.resources.notifications.GetNotificationRemoteOperation
  52. import com.owncloud.android.lib.resources.notifications.models.Notification
  53. import com.owncloud.android.ui.activity.FileDisplayActivity
  54. import com.owncloud.android.ui.activity.NotificationsActivity
  55. import com.owncloud.android.ui.notifications.NotificationUtils
  56. import com.owncloud.android.utils.PushUtils
  57. import com.owncloud.android.utils.ThemeUtils
  58. import dagger.android.AndroidInjection
  59. import org.apache.commons.httpclient.HttpMethod
  60. import org.apache.commons.httpclient.HttpStatus
  61. import org.apache.commons.httpclient.methods.DeleteMethod
  62. import org.apache.commons.httpclient.methods.GetMethod
  63. import org.apache.commons.httpclient.methods.PutMethod
  64. import org.apache.commons.httpclient.methods.Utf8PostMethod
  65. import java.io.IOException
  66. import java.security.GeneralSecurityException
  67. import java.security.PrivateKey
  68. import java.security.SecureRandom
  69. import javax.crypto.Cipher
  70. import javax.inject.Inject
  71. class NotificationWork constructor(
  72. private val context: Context,
  73. params: WorkerParameters,
  74. private val notificationManager: NotificationManager,
  75. private val accountManager: UserAccountManager
  76. ) : Worker(context, params) {
  77. companion object {
  78. const val TAG = "NotificationJob"
  79. const val KEY_NOTIFICATION_ACCOUNT = "KEY_NOTIFICATION_ACCOUNT"
  80. const val KEY_NOTIFICATION_SUBJECT = "subject"
  81. const val KEY_NOTIFICATION_SIGNATURE = "signature"
  82. private const val KEY_NOTIFICATION_ACTION_LINK = "KEY_NOTIFICATION_ACTION_LINK"
  83. private const val KEY_NOTIFICATION_ACTION_TYPE = "KEY_NOTIFICATION_ACTION_TYPE"
  84. private const val PUSH_NOTIFICATION_ID = "PUSH_NOTIFICATION_ID"
  85. private const val NUMERIC_NOTIFICATION_ID = "NUMERIC_NOTIFICATION_ID"
  86. }
  87. @Suppress("TooGenericExceptionCaught", "NestedBlockDepth", "ComplexMethod", "LongMethod") // legacy code
  88. override fun doWork(): Result {
  89. val subject = inputData.getString(KEY_NOTIFICATION_SUBJECT) ?: ""
  90. val signature = inputData.getString(KEY_NOTIFICATION_SIGNATURE) ?: ""
  91. if (!TextUtils.isEmpty(subject) && !TextUtils.isEmpty(signature)) {
  92. try {
  93. val base64DecodedSubject = Base64.decode(subject, Base64.DEFAULT)
  94. val base64DecodedSignature = Base64.decode(signature, Base64.DEFAULT)
  95. val privateKey = PushUtils.readKeyFromFile(false) as PrivateKey
  96. try {
  97. val signatureVerification = PushUtils.verifySignature(context,
  98. accountManager,
  99. base64DecodedSignature,
  100. base64DecodedSubject)
  101. if (signatureVerification != null && signatureVerification.isSignatureValid) {
  102. val cipher = Cipher.getInstance("RSA/None/PKCS1Padding")
  103. cipher.init(Cipher.DECRYPT_MODE, privateKey)
  104. val decryptedSubject = cipher.doFinal(base64DecodedSubject)
  105. val gson = Gson()
  106. val decryptedPushMessage = gson.fromJson(String(decryptedSubject),
  107. DecryptedPushMessage::class.java)
  108. if (decryptedPushMessage.delete) {
  109. notificationManager.cancel(decryptedPushMessage.nid)
  110. } else if (decryptedPushMessage.deleteAll) {
  111. notificationManager.cancelAll()
  112. } else {
  113. val user = accountManager.getUser(signatureVerification.getAccount().name)
  114. .orElseThrow { RuntimeException() }
  115. fetchCompleteNotification(user, decryptedPushMessage)
  116. }
  117. }
  118. } catch (e1: GeneralSecurityException) {
  119. Log.d(TAG, "Error decrypting message ${e1.javaClass.name} ${e1.localizedMessage}")
  120. }
  121. } catch (exception: Exception) {
  122. Log.d(TAG, "Something went very wrong" + exception.localizedMessage)
  123. }
  124. }
  125. return Result.success()
  126. }
  127. @Suppress("LongMethod") // legacy code
  128. private fun sendNotification(notification: Notification, user: User) {
  129. val randomId = SecureRandom()
  130. val file = notification.subjectRichParameters["file"]
  131. val intent: Intent
  132. if (file == null) {
  133. intent = Intent(context, NotificationsActivity::class.java)
  134. } else {
  135. intent = Intent(context, FileDisplayActivity::class.java)
  136. intent.action = Intent.ACTION_VIEW
  137. intent.putExtra(FileDisplayActivity.KEY_FILE_ID, file.id)
  138. }
  139. intent.putExtra(KEY_NOTIFICATION_ACCOUNT, user.accountName)
  140. intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
  141. val pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_ONE_SHOT)
  142. val pushNotificationId = randomId.nextInt()
  143. val notificationBuilder = NotificationCompat.Builder(context, NotificationUtils.NOTIFICATION_CHANNEL_PUSH)
  144. .setSmallIcon(R.drawable.notification_icon)
  145. .setLargeIcon(BitmapFactory.decodeResource(context.resources, R.drawable.notification_icon))
  146. .setColor(ThemeUtils.primaryColor(user.toPlatformAccount(), false, context))
  147. .setShowWhen(true)
  148. .setSubText(user.accountName)
  149. .setContentTitle(notification.getSubject())
  150. .setContentText(notification.getMessage())
  151. .setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION))
  152. .setAutoCancel(true)
  153. .setVisibility(NotificationCompat.VISIBILITY_PRIVATE)
  154. .setContentIntent(pendingIntent)
  155. // Remove
  156. if (notification.getActions().isEmpty()) {
  157. val disableDetection = Intent(context, NotificationReceiver::class.java)
  158. disableDetection.putExtra(NUMERIC_NOTIFICATION_ID, notification.getNotificationId())
  159. disableDetection.putExtra(PUSH_NOTIFICATION_ID, pushNotificationId)
  160. disableDetection.putExtra(KEY_NOTIFICATION_ACCOUNT, user.accountName)
  161. val disableIntent = PendingIntent.getBroadcast(context, pushNotificationId, disableDetection,
  162. PendingIntent.FLAG_CANCEL_CURRENT)
  163. notificationBuilder.addAction(NotificationCompat.Action(R.drawable.ic_close,
  164. context.getString(R.string.remove_push_notification), disableIntent))
  165. } else { // Actions
  166. for (action in notification.getActions()) {
  167. val actionIntent = Intent(context, NotificationReceiver::class.java)
  168. actionIntent.putExtra(NUMERIC_NOTIFICATION_ID, notification.getNotificationId())
  169. actionIntent.putExtra(PUSH_NOTIFICATION_ID, pushNotificationId)
  170. actionIntent.putExtra(KEY_NOTIFICATION_ACCOUNT, user.accountName)
  171. actionIntent.putExtra(KEY_NOTIFICATION_ACTION_LINK, action.link)
  172. actionIntent.putExtra(KEY_NOTIFICATION_ACTION_TYPE, action.type)
  173. val actionPendingIntent = PendingIntent.getBroadcast(context, randomId.nextInt(),
  174. actionIntent,
  175. PendingIntent.FLAG_CANCEL_CURRENT)
  176. var icon: Int
  177. icon = if (action.primary) {
  178. R.drawable.ic_check_circle
  179. } else {
  180. R.drawable.ic_check_circle_outline
  181. }
  182. notificationBuilder.addAction(NotificationCompat.Action(icon, action.label, actionPendingIntent))
  183. }
  184. }
  185. notificationBuilder.setPublicVersion(
  186. NotificationCompat.Builder(context, NotificationUtils.NOTIFICATION_CHANNEL_PUSH)
  187. .setSmallIcon(R.drawable.notification_icon)
  188. .setLargeIcon(BitmapFactory.decodeResource(context.resources, R.drawable.notification_icon))
  189. .setColor(ThemeUtils.primaryColor(user.toPlatformAccount(), false, context))
  190. .setShowWhen(true)
  191. .setSubText(user.accountName)
  192. .setContentTitle(context.getString(R.string.new_notification))
  193. .setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION))
  194. .setAutoCancel(true)
  195. .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
  196. .setContentIntent(pendingIntent).build())
  197. val notificationManager = NotificationManagerCompat.from(context)
  198. notificationManager.notify(notification.getNotificationId(), notificationBuilder.build())
  199. }
  200. @Suppress("TooGenericExceptionCaught") // legacy code
  201. private fun fetchCompleteNotification(account: User, decryptedPushMessage: DecryptedPushMessage) {
  202. val optionalUser = accountManager.getUser(account.accountName)
  203. if (!optionalUser.isPresent) {
  204. Log_OC.e(this, "Account may not be null")
  205. return
  206. }
  207. val user = optionalUser.get()
  208. try {
  209. val client = OwnCloudClientManagerFactory.getDefaultSingleton()
  210. .getClientFor(user.toOwnCloudAccount(), context)
  211. val result = GetNotificationRemoteOperation(decryptedPushMessage.nid)
  212. .execute(client)
  213. if (result.isSuccess) {
  214. val notification = result.notificationData[0]
  215. sendNotification(notification, account)
  216. }
  217. } catch (e: Exception) {
  218. Log_OC.e(this, "Error creating account", e)
  219. }
  220. }
  221. class NotificationReceiver : BroadcastReceiver() {
  222. private lateinit var accountManager: UserAccountManager
  223. /**
  224. * This is a workaround for a Dagger compiler bug - it cannot inject
  225. * into a nested Kotlin class for some reason, but the helper
  226. * works.
  227. */
  228. @Inject
  229. fun inject(accountManager: UserAccountManager) {
  230. this.accountManager = accountManager
  231. }
  232. @Suppress("ComplexMethod") // legacy code
  233. override fun onReceive(context: Context, intent: Intent) {
  234. AndroidInjection.inject(this, context)
  235. val numericNotificationId = intent.getIntExtra(NUMERIC_NOTIFICATION_ID, 0)
  236. val accountName = intent.getStringExtra(KEY_NOTIFICATION_ACCOUNT)
  237. if (numericNotificationId != 0) {
  238. Thread(Runnable {
  239. val notificationManager = context.getSystemService(
  240. Activity.NOTIFICATION_SERVICE) as NotificationManager
  241. var oldNotification: android.app.Notification? = null
  242. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && notificationManager != null) {
  243. for (statusBarNotification in notificationManager.activeNotifications) {
  244. if (numericNotificationId == statusBarNotification.id) {
  245. oldNotification = statusBarNotification.notification
  246. break
  247. }
  248. }
  249. cancel(context, numericNotificationId)
  250. }
  251. try {
  252. val optionalUser = accountManager.getUser(accountName)
  253. if (optionalUser.isPresent) {
  254. val user = optionalUser.get()
  255. val client = OwnCloudClientManagerFactory.getDefaultSingleton()
  256. .getClientFor(user.toOwnCloudAccount(), context)
  257. val actionType = intent.getStringExtra(KEY_NOTIFICATION_ACTION_TYPE)
  258. val actionLink = intent.getStringExtra(KEY_NOTIFICATION_ACTION_LINK)
  259. val success: Boolean
  260. success = if (!TextUtils.isEmpty(actionType) && !TextUtils.isEmpty(actionLink)) {
  261. val resultCode = executeAction(actionType, actionLink, client)
  262. resultCode == HttpStatus.SC_OK || resultCode == HttpStatus.SC_ACCEPTED
  263. } else {
  264. DeleteNotificationRemoteOperation(numericNotificationId)
  265. .execute(client).isSuccess
  266. }
  267. if (success) {
  268. if (oldNotification == null) {
  269. cancel(context, numericNotificationId)
  270. }
  271. } else {
  272. notificationManager.notify(numericNotificationId, oldNotification)
  273. }
  274. }
  275. } catch (e: IOException) {
  276. Log_OC.e(TAG, "Error initializing client", e)
  277. } catch (e: OperationCanceledException) {
  278. Log_OC.e(TAG, "Error initializing client", e)
  279. } catch (e: AuthenticatorException) {
  280. Log_OC.e(TAG, "Error initializing client", e)
  281. }
  282. }).start()
  283. }
  284. }
  285. @Suppress("ReturnCount") // legacy code
  286. private fun executeAction(actionType: String, actionLink: String, client: OwnCloudClient): Int {
  287. val method: HttpMethod
  288. method = when (actionType) {
  289. "GET" -> GetMethod(actionLink)
  290. "POST" -> Utf8PostMethod(actionLink)
  291. "DELETE" -> DeleteMethod(actionLink)
  292. "PUT" -> PutMethod(actionLink)
  293. else -> return 0 // do nothing
  294. }
  295. method.setRequestHeader(RemoteOperation.OCS_API_HEADER, RemoteOperation.OCS_API_HEADER_VALUE)
  296. try {
  297. return client.executeMethod(method)
  298. } catch (e: IOException) {
  299. Log_OC.e(TAG, "Execution of notification action failed: $e")
  300. }
  301. return 0
  302. }
  303. private fun cancel(context: Context, notificationId: Int) {
  304. val notificationManager = context.getSystemService(Activity.NOTIFICATION_SERVICE) as NotificationManager
  305. notificationManager.cancel(notificationId)
  306. }
  307. }
  308. }