FileViewerUtils.kt 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427
  1. /*
  2. * Nextcloud Talk application
  3. *
  4. * @author Marcel Hibbe
  5. * Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
  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.nextcloud.talk.utils
  21. import android.annotation.SuppressLint
  22. import android.content.ComponentName
  23. import android.content.Context
  24. import android.content.Intent
  25. import android.net.Uri
  26. import android.os.Build
  27. import android.util.Log
  28. import android.view.View
  29. import android.widget.ImageView
  30. import android.widget.ProgressBar
  31. import android.widget.Toast
  32. import androidx.core.content.FileProvider
  33. import androidx.emoji2.widget.EmojiTextView
  34. import androidx.work.Data
  35. import androidx.work.OneTimeWorkRequest
  36. import androidx.work.WorkInfo
  37. import androidx.work.WorkManager
  38. import com.nextcloud.talk.R
  39. import com.nextcloud.talk.activities.FullScreenImageActivity
  40. import com.nextcloud.talk.activities.FullScreenMediaActivity
  41. import com.nextcloud.talk.activities.FullScreenTextViewerActivity
  42. import com.nextcloud.talk.adapters.messages.PreviewMessageViewHolder
  43. import com.nextcloud.talk.data.user.model.User
  44. import com.nextcloud.talk.jobs.DownloadFileToCacheWorker
  45. import com.nextcloud.talk.models.json.chat.ChatMessage
  46. import com.nextcloud.talk.utils.AccountUtils.canWeOpenFilesApp
  47. import com.nextcloud.talk.utils.Mimetype.AUDIO_MPEG
  48. import com.nextcloud.talk.utils.Mimetype.AUDIO_OGG
  49. import com.nextcloud.talk.utils.Mimetype.AUDIO_WAV
  50. import com.nextcloud.talk.utils.Mimetype.IMAGE_GIF
  51. import com.nextcloud.talk.utils.Mimetype.IMAGE_JPEG
  52. import com.nextcloud.talk.utils.Mimetype.IMAGE_PNG
  53. import com.nextcloud.talk.utils.Mimetype.TEXT_MARKDOWN
  54. import com.nextcloud.talk.utils.Mimetype.TEXT_PLAIN
  55. import com.nextcloud.talk.utils.Mimetype.VIDEO_MP4
  56. import com.nextcloud.talk.utils.Mimetype.VIDEO_OGG
  57. import com.nextcloud.talk.utils.Mimetype.VIDEO_QUICKTIME
  58. import com.nextcloud.talk.utils.MimetypeUtils.isAudioOnly
  59. import com.nextcloud.talk.utils.MimetypeUtils.isGif
  60. import com.nextcloud.talk.utils.MimetypeUtils.isMarkdown
  61. import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ACCOUNT
  62. import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_FILE_ID
  63. import com.nextcloud.talk.utils.database.user.CapabilitiesUtilNew
  64. import java.io.File
  65. import java.util.concurrent.ExecutionException
  66. /*
  67. * Usage of this class forces us to do things at one location which should be separated in a activity and view model.
  68. *
  69. * Example:
  70. * - SharedItemsViewHolder
  71. */
  72. class FileViewerUtils(private val context: Context, private val user: User) {
  73. fun openFile(
  74. message: ChatMessage,
  75. progressUi: ProgressUi
  76. ) {
  77. val fileName = message.selectedIndividualHashMap!![PreviewMessageViewHolder.KEY_NAME]!!
  78. val mimetype = message.selectedIndividualHashMap!![PreviewMessageViewHolder.KEY_MIMETYPE]!!
  79. val link = message.selectedIndividualHashMap!!["link"]!!
  80. val fileId = message.selectedIndividualHashMap!![PreviewMessageViewHolder.KEY_ID]!!
  81. val path = message.selectedIndividualHashMap!![PreviewMessageViewHolder.KEY_PATH]!!
  82. var size = message.selectedIndividualHashMap!!["size"]
  83. if (size == null) {
  84. size = "-1"
  85. }
  86. val fileSize = size.toLong()
  87. openFile(
  88. FileInfo(fileId, fileName, fileSize),
  89. path,
  90. link,
  91. mimetype,
  92. progressUi
  93. )
  94. }
  95. fun openFile(
  96. fileInfo: FileInfo,
  97. path: String,
  98. link: String?,
  99. mimetype: String?,
  100. progressUi: ProgressUi
  101. ) {
  102. if (isSupportedForInternalViewer(mimetype) || canBeHandledByExternalApp(mimetype, fileInfo.fileName)) {
  103. openOrDownloadFile(
  104. fileInfo,
  105. path,
  106. mimetype,
  107. progressUi
  108. )
  109. } else if (!link.isNullOrEmpty()) {
  110. openFileInFilesApp(link, fileInfo.fileId)
  111. } else {
  112. Log.e(
  113. TAG,
  114. "File with id " + fileInfo.fileId + " can't be opened because internal viewer doesn't " +
  115. "support it, it can't be handled by an external app and there is no link " +
  116. "to open it in the nextcloud files app"
  117. )
  118. Toast.makeText(context, R.string.nc_common_error_sorry, Toast.LENGTH_LONG).show()
  119. }
  120. }
  121. private fun canBeHandledByExternalApp(mimetype: String?, fileName: String): Boolean {
  122. val path: String = context.cacheDir.absolutePath + "/" + fileName
  123. val file = File(path)
  124. val intent = Intent(Intent.ACTION_VIEW)
  125. intent.setDataAndType(Uri.fromFile(file), mimetype)
  126. return intent.resolveActivity(context.packageManager) != null
  127. }
  128. private fun openOrDownloadFile(
  129. fileInfo: FileInfo,
  130. path: String,
  131. mimetype: String?,
  132. progressUi: ProgressUi
  133. ) {
  134. val file = File(context.cacheDir, fileInfo.fileName)
  135. if (file.exists()) {
  136. openFileByMimetype(fileInfo.fileName!!, mimetype)
  137. } else {
  138. downloadFileToCache(
  139. fileInfo,
  140. path,
  141. mimetype,
  142. progressUi
  143. )
  144. }
  145. }
  146. private fun openFileByMimetype(filename: String, mimetype: String?) {
  147. if (mimetype != null) {
  148. when (mimetype) {
  149. AUDIO_MPEG,
  150. AUDIO_WAV,
  151. AUDIO_OGG,
  152. VIDEO_MP4,
  153. VIDEO_QUICKTIME,
  154. VIDEO_OGG
  155. -> openMediaView(filename, mimetype)
  156. IMAGE_PNG,
  157. IMAGE_JPEG,
  158. IMAGE_GIF
  159. -> openImageView(filename, mimetype)
  160. TEXT_MARKDOWN,
  161. TEXT_PLAIN
  162. -> openTextView(filename, mimetype)
  163. else
  164. -> openFileByExternalApp(filename, mimetype)
  165. }
  166. } else {
  167. Log.e(TAG, "can't open file with unknown mimetype")
  168. Toast.makeText(context, R.string.nc_common_error_sorry, Toast.LENGTH_LONG).show()
  169. }
  170. }
  171. @Suppress("Detekt.TooGenericExceptionCaught")
  172. private fun openFileByExternalApp(fileName: String, mimetype: String) {
  173. val path = context.cacheDir.absolutePath + "/" + fileName
  174. val file = File(path)
  175. val intent: Intent
  176. if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
  177. intent = Intent(Intent.ACTION_VIEW)
  178. intent.setDataAndType(Uri.fromFile(file), mimetype)
  179. intent.flags = Intent.FLAG_ACTIVITY_NO_HISTORY
  180. } else {
  181. intent = Intent()
  182. intent.action = Intent.ACTION_VIEW
  183. val pdfURI = FileProvider.getUriForFile(context, context.packageName, file)
  184. intent.setDataAndType(pdfURI, mimetype)
  185. intent.flags = Intent.FLAG_ACTIVITY_NO_HISTORY
  186. intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
  187. }
  188. try {
  189. if (intent.resolveActivity(context.packageManager) != null) {
  190. intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
  191. context.startActivity(intent)
  192. } else {
  193. Log.e(TAG, "No Application found to open the file. This should have been handled beforehand!")
  194. }
  195. } catch (e: Exception) {
  196. Log.e(TAG, "Error while opening file", e)
  197. }
  198. }
  199. fun openFileInFilesApp(link: String, keyID: String) {
  200. val accountString = user.username + "@" +
  201. user.baseUrl
  202. ?.replace("https://", "")
  203. ?.replace("http://", "")
  204. if (canWeOpenFilesApp(context, accountString)) {
  205. val filesAppIntent = Intent(Intent.ACTION_VIEW, null)
  206. val componentName = ComponentName(
  207. context.getString(R.string.nc_import_accounts_from),
  208. "com.owncloud.android.ui.activity.FileDisplayActivity"
  209. )
  210. filesAppIntent.component = componentName
  211. filesAppIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
  212. filesAppIntent.setPackage(context.getString(R.string.nc_import_accounts_from))
  213. filesAppIntent.putExtra(KEY_ACCOUNT, accountString)
  214. filesAppIntent.putExtra(KEY_FILE_ID, keyID)
  215. context.startActivity(filesAppIntent)
  216. } else {
  217. val browserIntent = Intent(
  218. Intent.ACTION_VIEW,
  219. Uri.parse(link)
  220. )
  221. browserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
  222. context.startActivity(browserIntent)
  223. }
  224. }
  225. private fun openImageView(filename: String, mimetype: String) {
  226. val fullScreenImageIntent = Intent(context, FullScreenImageActivity::class.java)
  227. fullScreenImageIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
  228. fullScreenImageIntent.putExtra("FILE_NAME", filename)
  229. fullScreenImageIntent.putExtra("IS_GIF", isGif(mimetype))
  230. context.startActivity(fullScreenImageIntent)
  231. }
  232. private fun openMediaView(filename: String, mimetype: String) {
  233. val fullScreenMediaIntent = Intent(context, FullScreenMediaActivity::class.java)
  234. fullScreenMediaIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
  235. fullScreenMediaIntent.putExtra("FILE_NAME", filename)
  236. fullScreenMediaIntent.putExtra("AUDIO_ONLY", isAudioOnly(mimetype))
  237. context.startActivity(fullScreenMediaIntent)
  238. }
  239. private fun openTextView(filename: String, mimetype: String) {
  240. val fullScreenTextViewerIntent = Intent(context, FullScreenTextViewerActivity::class.java)
  241. fullScreenTextViewerIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
  242. fullScreenTextViewerIntent.putExtra("FILE_NAME", filename)
  243. fullScreenTextViewerIntent.putExtra("IS_MARKDOWN", isMarkdown(mimetype))
  244. context.startActivity(fullScreenTextViewerIntent)
  245. }
  246. fun isSupportedForInternalViewer(mimetype: String?): Boolean {
  247. return when (mimetype) {
  248. IMAGE_PNG,
  249. IMAGE_JPEG,
  250. IMAGE_GIF,
  251. AUDIO_MPEG,
  252. AUDIO_WAV,
  253. AUDIO_OGG,
  254. VIDEO_MP4,
  255. VIDEO_QUICKTIME,
  256. VIDEO_OGG,
  257. TEXT_MARKDOWN,
  258. TEXT_PLAIN -> true
  259. else -> false
  260. }
  261. }
  262. @SuppressLint("LongLogTag")
  263. private fun downloadFileToCache(
  264. fileInfo: FileInfo,
  265. path: String,
  266. mimetype: String?,
  267. progressUi: ProgressUi
  268. ) {
  269. // check if download worker is already running
  270. val workers = WorkManager.getInstance(context).getWorkInfosByTag(fileInfo.fileId!!)
  271. try {
  272. for (workInfo in workers.get()) {
  273. if (workInfo.state == WorkInfo.State.RUNNING || workInfo.state == WorkInfo.State.ENQUEUED) {
  274. Log.d(TAG, "Download worker for $fileInfo.fileId is already running or scheduled")
  275. return
  276. }
  277. }
  278. } catch (e: ExecutionException) {
  279. Log.e(TAG, "Error when checking if worker already exists", e)
  280. } catch (e: InterruptedException) {
  281. Log.e(TAG, "Error when checking if worker already exists", e)
  282. }
  283. val downloadWorker: OneTimeWorkRequest
  284. val size: Long = if (fileInfo.fileSize == null) {
  285. -1
  286. } else {
  287. fileInfo.fileSize!!
  288. }
  289. val data: Data = Data.Builder()
  290. .putString(DownloadFileToCacheWorker.KEY_BASE_URL, user.baseUrl)
  291. .putString(DownloadFileToCacheWorker.KEY_USER_ID, user.userId)
  292. .putString(
  293. DownloadFileToCacheWorker.KEY_ATTACHMENT_FOLDER,
  294. CapabilitiesUtilNew.getAttachmentFolder(user)
  295. )
  296. .putString(DownloadFileToCacheWorker.KEY_FILE_NAME, fileInfo.fileName)
  297. .putString(DownloadFileToCacheWorker.KEY_FILE_PATH, path)
  298. .putLong(DownloadFileToCacheWorker.KEY_FILE_SIZE, size)
  299. .build()
  300. downloadWorker = OneTimeWorkRequest.Builder(DownloadFileToCacheWorker::class.java)
  301. .setInputData(data)
  302. .addTag(fileInfo.fileId)
  303. .build()
  304. WorkManager.getInstance().enqueue(downloadWorker)
  305. progressUi.progressBar?.visibility = View.VISIBLE
  306. WorkManager.getInstance(context).getWorkInfoByIdLiveData(downloadWorker.id)
  307. .observeForever { workInfo: WorkInfo? ->
  308. updateViewsByProgress(
  309. fileInfo.fileName,
  310. mimetype,
  311. workInfo!!,
  312. progressUi
  313. )
  314. }
  315. }
  316. private fun updateViewsByProgress(
  317. fileName: String,
  318. mimetype: String?,
  319. workInfo: WorkInfo,
  320. progressUi: ProgressUi
  321. ) {
  322. when (workInfo.state) {
  323. WorkInfo.State.RUNNING -> {
  324. val progress = workInfo.progress.getInt(DownloadFileToCacheWorker.PROGRESS, -1)
  325. if (progress > -1) {
  326. progressUi.messageText?.text = String.format(
  327. context.resources.getString(R.string.filename_progress),
  328. fileName,
  329. progress
  330. )
  331. }
  332. }
  333. WorkInfo.State.SUCCEEDED -> {
  334. if (progressUi.previewImage.isShown) {
  335. openFileByMimetype(fileName, mimetype)
  336. } else {
  337. Log.d(
  338. TAG,
  339. "file " + fileName +
  340. " was downloaded but it's not opened because view is not shown on screen"
  341. )
  342. }
  343. progressUi.messageText?.text = fileName
  344. progressUi.progressBar?.visibility = View.GONE
  345. }
  346. WorkInfo.State.FAILED -> {
  347. progressUi.messageText?.text = fileName
  348. progressUi.progressBar?.visibility = View.GONE
  349. }
  350. else -> {
  351. }
  352. }
  353. }
  354. fun resumeToUpdateViewsByProgress(
  355. fileName: String,
  356. fileId: String,
  357. mimeType: String?,
  358. progressUi: ProgressUi
  359. ) {
  360. val workers = WorkManager.getInstance(context).getWorkInfosByTag(fileId)
  361. try {
  362. for (workInfo in workers.get()) {
  363. if (workInfo.state == WorkInfo.State.RUNNING ||
  364. workInfo.state == WorkInfo.State.ENQUEUED
  365. ) {
  366. progressUi.progressBar?.visibility = View.VISIBLE
  367. WorkManager
  368. .getInstance(context)
  369. .getWorkInfoByIdLiveData(workInfo.id)
  370. .observeForever { info: WorkInfo? ->
  371. updateViewsByProgress(
  372. fileName,
  373. mimeType,
  374. info!!,
  375. progressUi
  376. )
  377. }
  378. }
  379. }
  380. } catch (e: ExecutionException) {
  381. Log.e(TAG, "Error when checking if worker already exists", e)
  382. } catch (e: InterruptedException) {
  383. Log.e(TAG, "Error when checking if worker already exists", e)
  384. }
  385. }
  386. data class ProgressUi(
  387. val progressBar: ProgressBar?,
  388. val messageText: EmojiTextView?,
  389. val previewImage: ImageView
  390. )
  391. data class FileInfo(
  392. val fileId: String,
  393. val fileName: String,
  394. var fileSize: Long?
  395. )
  396. companion object {
  397. private val TAG = FileViewerUtils::class.simpleName
  398. }
  399. }