From dd9eb35e72b2d2f39e44e4f81c5007e1ea03b021 Mon Sep 17 00:00:00 2001 From: fariba khandani Date: Mon, 30 Oct 2023 15:28:48 +0100 Subject: [PATCH] saveFiles to internal storage --- .../activities/FullScreenImageActivity.kt | 66 +++++++++++++ .../com/nextcloud/talk/chat/ChatActivity.kt | 80 ++++++++++++++- .../talk/jobs/SaveFileToStorageWorker.kt | 97 +++++++++++++++++++ .../talk/ui/dialog/MessageActionsDialog.kt | 13 +++ .../res/layout/dialog_message_actions.xml | 33 +++++++ app/src/main/res/menu/menu_preview.xml | 3 + app/src/main/res/values/strings.xml | 10 ++ 7 files changed, 300 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/com/nextcloud/talk/jobs/SaveFileToStorageWorker.kt diff --git a/app/src/main/java/com/nextcloud/talk/activities/FullScreenImageActivity.kt b/app/src/main/java/com/nextcloud/talk/activities/FullScreenImageActivity.kt index 5869540a1..6c6365e63 100644 --- a/app/src/main/java/com/nextcloud/talk/activities/FullScreenImageActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/activities/FullScreenImageActivity.kt @@ -26,6 +26,8 @@ package com.nextcloud.talk.activities +import android.annotation.SuppressLint +import android.content.DialogInterface import android.content.Intent import android.os.Bundle import android.util.Log @@ -33,6 +35,7 @@ import android.view.Menu import android.view.MenuItem import android.view.View import android.view.ViewGroup.MarginLayoutParams +import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import androidx.core.content.FileProvider import androidx.core.view.ViewCompat @@ -41,20 +44,28 @@ import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsControllerCompat import androidx.core.view.updateLayoutParams import androidx.core.view.updatePadding +import androidx.work.Data +import androidx.work.OneTimeWorkRequest +import androidx.work.WorkInfo +import androidx.work.WorkManager import com.google.android.material.snackbar.Snackbar import com.nextcloud.talk.BuildConfig import com.nextcloud.talk.R import com.nextcloud.talk.databinding.ActivityFullScreenImageBinding +import com.nextcloud.talk.jobs.SaveFileToStorageWorker +import com.nextcloud.talk.ui.theme.ViewThemeUtils import com.nextcloud.talk.utils.BitmapShrinker import com.nextcloud.talk.utils.Mimetype.IMAGE_PREFIX_GENERIC import pl.droidsonroids.gif.GifDrawable import java.io.File +import java.util.concurrent.ExecutionException class FullScreenImageActivity : AppCompatActivity() { lateinit var binding: ActivityFullScreenImageBinding private lateinit var windowInsetsController: WindowInsetsControllerCompat private lateinit var path: String private var showFullscreen = false + lateinit var viewThemeUtils: ViewThemeUtils override fun onCreateOptionsMenu(menu: Menu): Boolean { menuInflater.inflate(R.menu.menu_preview, menu) @@ -67,6 +78,7 @@ class FullScreenImageActivity : AppCompatActivity() { onBackPressedDispatcher.onBackPressed() true } + R.id.share -> { val shareUri = FileProvider.getUriForFile( this, @@ -84,12 +96,34 @@ class FullScreenImageActivity : AppCompatActivity() { true } + + R.id.save -> { + showWarningDialog() + true + } + else -> { super.onOptionsItemSelected(item) } } } + private fun showWarningDialog() { + val builder = AlertDialog.Builder(this) + builder.setTitle(R.string.nc_dialog_save_to_storage_title) + builder.setMessage(R.string.nc_dialog_save_to_storage_content) + builder.setPositiveButton(R.string.nc_dialog_save_to_storage_yes) { dialog: DialogInterface, which: Int -> + val fileName = intent.getStringExtra("FILE_NAME").toString() + saveImageToStorage(fileName) + dialog.dismiss() + } + builder.setNegativeButton(R.string.nc_dialog_save_to_storage_no) { dialog: DialogInterface, which: Int -> + dialog.dismiss() + } + val dialog = builder.create() + dialog.show() + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -188,6 +222,38 @@ class FullScreenImageActivity : AppCompatActivity() { } } + @SuppressLint("LongLogTag") + private fun saveImageToStorage( + fileName: String + ) { + val sourceFilePath = applicationContext.cacheDir.path + + val workers = WorkManager.getInstance(this).getWorkInfosByTag(fileName) + try { + for (workInfo in workers.get()) { + if (workInfo.state == WorkInfo.State.RUNNING || workInfo.state == WorkInfo.State.ENQUEUED) { + return + } + } + } catch (e: ExecutionException) { + Log.e(TAG, "Error when checking if worker already exists", e) + } catch (e: InterruptedException) { + Log.e(TAG, "Error when checking if worker already exists", e) + } + + val data: Data = Data.Builder() + .putString(SaveFileToStorageWorker.KEY_FILE_NAME, fileName) + .putString(SaveFileToStorageWorker.KEY_SOURCE_FILE_PATH, "$sourceFilePath/$fileName") + .build() + + val saveWorker: OneTimeWorkRequest = OneTimeWorkRequest.Builder(SaveFileToStorageWorker::class.java) + .setInputData(data) + .addTag(fileName) + .build() + + WorkManager.getInstance().enqueue(saveWorker) + } + companion object { private const val TAG = "FullScreenImageActivity" private const val HUNDRED_MB = 100 * 1024 * 1024 diff --git a/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt b/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt index 7518d446b..70e387038 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt @@ -34,6 +34,7 @@ import android.annotation.SuppressLint import android.content.ClipData import android.content.ClipboardManager import android.content.Context +import android.content.DialogInterface import android.content.Intent import android.content.pm.PackageManager import android.content.res.AssetFileDescriptor @@ -156,6 +157,7 @@ import com.nextcloud.talk.events.UserMentionClickEvent import com.nextcloud.talk.events.WebSocketCommunicationEvent import com.nextcloud.talk.extensions.loadAvatarOrImagePreview import com.nextcloud.talk.jobs.DownloadFileToCacheWorker +import com.nextcloud.talk.jobs.SaveFileToStorageWorker import com.nextcloud.talk.jobs.ShareOperationWorker import com.nextcloud.talk.jobs.UploadAndShareFilesWorker import com.nextcloud.talk.location.LocationPickerActivity @@ -897,7 +899,7 @@ class ChatActivity : } } else { Log.d(TAG, "Downloaded to cache") - downloadFileToCache(message, true) { + downloadFileToCache(message,true ) { setUpWaveform(message) } } @@ -2018,6 +2020,44 @@ class ChatActivity : } } + @SuppressLint("LongLogTag") + private fun saveImageToStorage( + message: ChatMessage + ) { + message.openWhenDownloaded = false + adapter?.update(message) + + val fileName = message.selectedIndividualHashMap!!["name"] + val sourceFilePath = applicationContext.cacheDir.path + val fileId = message.selectedIndividualHashMap!!["id"] + + val workers = WorkManager.getInstance(context).getWorkInfosByTag(fileId!!) + try { + for (workInfo in workers.get()) { + if (workInfo.state == WorkInfo.State.RUNNING || workInfo.state == WorkInfo.State.ENQUEUED) { + Log.d(TAG, "SaveFileToStorageWorker for $fileId is already running or scheduled") + return + } + } + } catch (e: ExecutionException) { + Log.e(TAG, "Error when checking if worker already exists", e) + } catch (e: InterruptedException) { + Log.e(TAG, "Error when checking if worker already exists", e) + } + + val data: Data = Data.Builder() + .putString(SaveFileToStorageWorker.KEY_FILE_NAME, fileName) + .putString(SaveFileToStorageWorker.KEY_SOURCE_FILE_PATH, "$sourceFilePath/$fileName") + .build() + + val saveWorker: OneTimeWorkRequest = OneTimeWorkRequest.Builder(SaveFileToStorageWorker::class.java) + .setInputData(data) + .addTag(fileId) + .build() + + WorkManager.getInstance().enqueue(saveWorker) + } + @SuppressLint("SimpleDateFormat") private fun setVoiceRecordFileName() { val simpleDateFormat = SimpleDateFormat(FILE_DATE_PATTERN) @@ -4101,12 +4141,48 @@ class ChatActivity : if (file.exists()) { share(message) } else { - downloadFileToCache(message, false) { + downloadFileToCache(message, false ) { share(message) } } } + private fun saveImage(message: ChatMessage){ + if (permissionUtil.isFilesPermissionGranted()) { + saveImageToStorage(message) + } else { + UploadAndShareFilesWorker.requestStoragePermission(this@ChatActivity) + } + } + + private fun showSaveToStorageWarning(message: ChatMessage) { + val builder = AlertDialog.Builder(this) + builder.setTitle(R.string.nc_dialog_save_to_storage_title) + builder.setMessage(R.string.nc_dialog_save_to_storage_content) + builder.setPositiveButton(R.string.nc_dialog_save_to_storage_yes) { dialog: DialogInterface, _: Int -> + saveImage(message) + dialog.dismiss() + } + builder.setNegativeButton(R.string.nc_dialog_save_to_storage_no) { dialog: DialogInterface, _: Int -> + dialog.dismiss() + } + val dialog = builder.create() + dialog.show() + } + + fun checkIfSaveable(message: ChatMessage) { + val filename = message.selectedIndividualHashMap!!["name"] + path = applicationContext.cacheDir.absolutePath + "/" + filename + val file = File(context.cacheDir, filename!!) + if (file.exists()) { + showSaveToStorageWarning(message) + } else { + downloadFileToCache(message ,false) { + showSaveToStorageWarning(message) + } + } + } + fun openInFilesApp(message: ChatMessage) { val keyID = message.selectedIndividualHashMap!![PreviewMessageViewHolder.KEY_ID] val link = message.selectedIndividualHashMap!!["link"] diff --git a/app/src/main/java/com/nextcloud/talk/jobs/SaveFileToStorageWorker.kt b/app/src/main/java/com/nextcloud/talk/jobs/SaveFileToStorageWorker.kt new file mode 100644 index 000000000..29f702955 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/jobs/SaveFileToStorageWorker.kt @@ -0,0 +1,97 @@ +/* + * Nextcloud Talk application + * + * @author Andy Scherzinger + * @author Marcel Hibbe + * Copyright (C) 2022 Andy Scherzinger + * Copyright (C) 2021 Marcel Hibbe + * + * 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.nextcloud.talk.jobs + +import android.content.ContentValues +import android.content.Context +import android.media.MediaScannerConnection +import android.os.Environment +import android.provider.MediaStore +import android.provider.MediaStore.Files.FileColumns +import android.util.Log +import androidx.work.Worker +import androidx.work.WorkerParameters +import autodagger.AutoInjector +import com.nextcloud.talk.application.NextcloudTalkApplication +import java.io.File +import java.io.IOException +import java.io.OutputStream +import java.net.URLConnection + +@AutoInjector(NextcloudTalkApplication::class) +class SaveFileToStorageWorker(val context: Context, workerParameters: WorkerParameters) : + Worker(context, workerParameters) { + + override fun doWork(): Result { + try { + val sourceFilePath = inputData.getString(KEY_SOURCE_FILE_PATH) + val cacheFile = File(sourceFilePath!!) + + val contentResolver = context.contentResolver + val mimeType = URLConnection.guessContentTypeFromName(cacheFile.name) + + val values = ContentValues().apply { + put(FileColumns.DISPLAY_NAME, cacheFile.name) + put(FileColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS) + if (mimeType != null) { + put(FileColumns.MIME_TYPE, URLConnection.guessContentTypeFromName(cacheFile.name)) + } + } + + val collection = MediaStore.Files.getContentUri("external") + val uri = contentResolver.insert(collection, values) + + uri?.let { fileUri -> + try { + val outputStream: OutputStream? = contentResolver.openOutputStream(fileUri) + outputStream.use { output -> + val inputStream = cacheFile.inputStream() + if (output != null) { + inputStream.copyTo(output) + } + } + } catch (e: Exception) { + Log.e(TAG, "Failed to create output stream") + return Result.failure() + } + } + + // Notify the media scanner about the new file + MediaScannerConnection.scanFile(context, arrayOf(cacheFile.absolutePath), null, null) + + return Result.success() + } catch (e: IOException) { + Log.e(TAG, "Something went wrong when trying to save file to internal storage", e) + return Result.failure() + } catch (e: NullPointerException) { + Log.e(TAG, "Something went wrong when trying to save file to internal storage", e) + return Result.failure() + } + } + + companion object { + private val TAG = SaveFileToStorageWorker::class.java.simpleName + const val KEY_FILE_NAME = "KEY_FILE_NAME" + const val KEY_SOURCE_FILE_PATH = "KEY_SOURCE_FILE_PATH" + } +} diff --git a/app/src/main/java/com/nextcloud/talk/ui/dialog/MessageActionsDialog.kt b/app/src/main/java/com/nextcloud/talk/ui/dialog/MessageActionsDialog.kt index 9fd90fbca..3258b64ee 100644 --- a/app/src/main/java/com/nextcloud/talk/ui/dialog/MessageActionsDialog.kt +++ b/app/src/main/java/com/nextcloud/talk/ui/dialog/MessageActionsDialog.kt @@ -118,6 +118,7 @@ class MessageActionsDialog( initMenuItemOpenNcApp( ChatMessage.MessageType.SINGLE_NC_ATTACHMENT_MESSAGE == message.getCalculateMessageType() ) + initMenuItemSave(message.getCalculateMessageType() == ChatMessage.MessageType.SINGLE_NC_ATTACHMENT_MESSAGE) } override fun onStart() { @@ -169,6 +170,8 @@ class MessageActionsDialog( dialogMessageActionsBinding.emojiMore.installForceSingleEmoji() } + + /* This method is a hacky workaround to avoid bug #1914 As the bug happens only for the very first time when the popup is opened, @@ -352,6 +355,16 @@ class MessageActionsDialog( dialogMessageActionsBinding.menuOpenInNcApp.visibility = getVisibility(visible) } + private fun initMenuItemSave (visible: Boolean) { + if (visible){ + dialogMessageActionsBinding.menuSaveMessage.setOnClickListener { + chatActivity.checkIfSaveable(message) + dismiss() + } + } + dialogMessageActionsBinding.menuSaveMessage.visibility = getVisibility(visible) + } + private fun getVisibility(visible: Boolean): Int { return if (visible) { View.VISIBLE diff --git a/app/src/main/res/layout/dialog_message_actions.xml b/app/src/main/res/layout/dialog_message_actions.xml index 2bb7db056..b34b75a74 100644 --- a/app/src/main/res/layout/dialog_message_actions.xml +++ b/app/src/main/res/layout/dialog_message_actions.xml @@ -452,6 +452,39 @@ + + + + + + + + diff --git a/app/src/main/res/menu/menu_preview.xml b/app/src/main/res/menu/menu_preview.xml index 5388e67c1..c03387f09 100644 --- a/app/src/main/res/menu/menu_preview.xml +++ b/app/src/main/res/menu/menu_preview.xml @@ -22,4 +22,7 @@ + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 13ebfe184..2ed5d77d4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -519,6 +519,16 @@ How to translate with transifex: Chat via %s Account not found + //save feature + Save + Save to storage? + Saving this media to storage will allow any other apps on + your device to access it.\nContinue? + Yes + No + + + Favorite Status Encrypted