Browse Source

followup changes to save file feature

- extract dialog to SaveToStorageDialogFragment
- add ability to save files of other mimetypes than images
- use MaterialAlertDialogBuilder
- save files to matching folders depending on mimeType
- show toast
- change download icon

Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
Marcel Hibbe 1 year ago
parent
commit
640007b421

+ 3 - 3
app/src/main/AndroidManifest.xml

@@ -152,17 +152,17 @@
             android:theme="@style/AppTheme.CallLauncher" />
 
         <activity
-            android:name=".activities.FullScreenImageActivity"
+            android:name=".fullscreenfile.FullScreenImageActivity"
             android:configChanges="orientation|keyboardHidden|screenSize"
             android:theme="@style/FullScreenImageTheme"/>
 
         <activity
-            android:name=".activities.FullScreenMediaActivity"
+            android:name=".fullscreenfile.FullScreenMediaActivity"
             android:configChanges="orientation|keyboardHidden|screenSize"
             android:theme="@style/FullScreenMediaTheme"/>
 
         <activity
-            android:name=".activities.FullScreenTextViewerActivity"
+            android:name=".fullscreenfile.FullScreenTextViewerActivity"
             android:configChanges="orientation|keyboardHidden|screenSize"
             android:theme="@style/FullScreenTextTheme"/>
 

+ 9 - 60
app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt

@@ -34,7 +34,6 @@ 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
@@ -157,7 +156,6 @@ 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
@@ -191,6 +189,7 @@ import com.nextcloud.talk.ui.bottom.sheet.ProfileBottomSheet
 import com.nextcloud.talk.ui.dialog.AttachmentDialog
 import com.nextcloud.talk.ui.dialog.DateTimePickerFragment
 import com.nextcloud.talk.ui.dialog.MessageActionsDialog
+import com.nextcloud.talk.ui.dialog.SaveToStorageDialogFragment
 import com.nextcloud.talk.ui.dialog.ShowReactionsDialog
 import com.nextcloud.talk.ui.recyclerview.MessageSwipeActions
 import com.nextcloud.talk.ui.recyclerview.MessageSwipeCallback
@@ -2020,44 +2019,6 @@ 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)
@@ -4147,27 +4108,14 @@ class ChatActivity :
         }
     }
 
-    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()
+        val saveFragment: DialogFragment = SaveToStorageDialogFragment.newInstance(
+            message.selectedIndividualHashMap!!["name"]!!
+        )
+        saveFragment.show(
+            supportFragmentManager,
+            SaveToStorageDialogFragment.TAG
+        )
     }
 
     fun checkIfSaveable(message: ChatMessage) {
@@ -4608,5 +4556,6 @@ class ChatActivity :
         private const val TYPING_STOPPED_SIGNALING_MESSAGE_TYPE = "stoppedTyping"
         private const val CALL_STARTED_ID = -2
         private const val MILISEC_15: Long = 15
+        private const val LINEBREAK = "\n"
     }
 }

+ 14 - 61
app/src/main/java/com/nextcloud/talk/activities/FullScreenImageActivity.kt → app/src/main/java/com/nextcloud/talk/fullscreenfile/FullScreenImageActivity.kt

@@ -24,10 +24,8 @@
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-package com.nextcloud.talk.activities
+package com.nextcloud.talk.fullscreenfile
 
-import android.annotation.SuppressLint
-import android.content.DialogInterface
 import android.content.Intent
 import android.os.Bundle
 import android.util.Log
@@ -35,7 +33,6 @@ 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
@@ -44,28 +41,25 @@ 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 androidx.fragment.app.DialogFragment
+import autodagger.AutoInjector
 import com.google.android.material.snackbar.Snackbar
 import com.nextcloud.talk.BuildConfig
 import com.nextcloud.talk.R
+import com.nextcloud.talk.application.NextcloudTalkApplication
 import com.nextcloud.talk.databinding.ActivityFullScreenImageBinding
-import com.nextcloud.talk.jobs.SaveFileToStorageWorker
-import com.nextcloud.talk.ui.theme.ViewThemeUtils
+import com.nextcloud.talk.ui.dialog.SaveToStorageDialogFragment
 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
 
+@AutoInjector(NextcloudTalkApplication::class)
 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)
@@ -98,7 +92,13 @@ class FullScreenImageActivity : AppCompatActivity() {
             }
 
             R.id.save -> {
-                showWarningDialog()
+                val saveFragment: DialogFragment = SaveToStorageDialogFragment.newInstance(
+                    intent.getStringExtra("FILE_NAME").toString()
+                )
+                saveFragment.show(
+                    supportFragmentManager,
+                    SaveToStorageDialogFragment.TAG
+                )
                 true
             }
 
@@ -108,24 +108,9 @@ class FullScreenImageActivity : AppCompatActivity() {
         }
     }
 
-    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)
+        NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
 
         binding = ActivityFullScreenImageBinding.inflate(layoutInflater)
         setContentView(binding.root)
@@ -222,38 +207,6 @@ 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

+ 16 - 1
app/src/main/java/com/nextcloud/talk/activities/FullScreenMediaActivity.kt → app/src/main/java/com/nextcloud/talk/fullscreenfile/FullScreenMediaActivity.kt

@@ -24,7 +24,7 @@
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-package com.nextcloud.talk.activities
+package com.nextcloud.talk.fullscreenfile
 
 import android.content.Intent
 import android.os.Bundle
@@ -43,6 +43,7 @@ import androidx.core.view.WindowInsetsControllerCompat
 import androidx.core.view.marginBottom
 import androidx.core.view.updateLayoutParams
 import androidx.core.view.updatePadding
+import androidx.fragment.app.DialogFragment
 import androidx.media3.common.MediaItem
 import androidx.media3.common.util.UnstableApi
 import androidx.media3.exoplayer.ExoPlayer
@@ -53,6 +54,7 @@ import com.nextcloud.talk.BuildConfig
 import com.nextcloud.talk.R
 import com.nextcloud.talk.application.NextcloudTalkApplication
 import com.nextcloud.talk.databinding.ActivityFullScreenMediaBinding
+import com.nextcloud.talk.ui.dialog.SaveToStorageDialogFragment
 import com.nextcloud.talk.utils.Mimetype.VIDEO_PREFIX_GENERIC
 import java.io.File
 
@@ -78,6 +80,7 @@ class FullScreenMediaActivity : AppCompatActivity() {
                 onBackPressedDispatcher.onBackPressed()
                 true
             }
+
             R.id.share -> {
                 val shareUri = FileProvider.getUriForFile(
                     this,
@@ -95,6 +98,18 @@ class FullScreenMediaActivity : AppCompatActivity() {
 
                 true
             }
+
+            R.id.save -> {
+                val saveFragment: DialogFragment = SaveToStorageDialogFragment.newInstance(
+                    intent.getStringExtra("FILE_NAME").toString()
+                )
+                saveFragment.show(
+                    supportFragmentManager,
+                    SaveToStorageDialogFragment.TAG
+                )
+                true
+            }
+
             else -> {
                 super.onOptionsItemSelected(item)
             }

+ 38 - 19
app/src/main/java/com/nextcloud/talk/activities/FullScreenTextViewerActivity.kt → app/src/main/java/com/nextcloud/talk/fullscreenfile/FullScreenTextViewerActivity.kt

@@ -22,7 +22,7 @@
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-package com.nextcloud.talk.activities
+package com.nextcloud.talk.fullscreenfile
 
 import android.content.Intent
 import android.os.Bundle
@@ -31,11 +31,13 @@ import android.view.MenuItem
 import androidx.appcompat.app.AppCompatActivity
 import androidx.core.content.FileProvider
 import androidx.core.content.res.ResourcesCompat
+import androidx.fragment.app.DialogFragment
 import autodagger.AutoInjector
 import com.nextcloud.talk.BuildConfig
 import com.nextcloud.talk.R
 import com.nextcloud.talk.application.NextcloudTalkApplication
 import com.nextcloud.talk.databinding.ActivityFullScreenTextBinding
+import com.nextcloud.talk.ui.dialog.SaveToStorageDialogFragment
 import com.nextcloud.talk.ui.theme.ViewThemeUtils
 import com.nextcloud.talk.utils.DisplayUtils
 import com.nextcloud.talk.utils.Mimetype.TEXT_PREFIX_GENERIC
@@ -58,27 +60,44 @@ class FullScreenTextViewerActivity : AppCompatActivity() {
     }
 
     override fun onOptionsItemSelected(item: MenuItem): Boolean {
-        return if (item.itemId == android.R.id.home) {
-            onBackPressedDispatcher.onBackPressed()
-            true
-        } else if (item.itemId == R.id.share) {
-            val shareUri = FileProvider.getUriForFile(
-                this,
-                BuildConfig.APPLICATION_ID,
-                File(path)
-            )
+        return when (item.itemId) {
+            android.R.id.home -> {
+                onBackPressedDispatcher.onBackPressed()
+                true
+            }
 
-            val shareIntent: Intent = Intent().apply {
-                action = Intent.ACTION_SEND
-                putExtra(Intent.EXTRA_STREAM, shareUri)
-                type = TEXT_PREFIX_GENERIC
-                addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
+            R.id.share -> {
+                val shareUri = FileProvider.getUriForFile(
+                    this,
+                    BuildConfig.APPLICATION_ID,
+                    File(path)
+                )
+
+                val shareIntent: Intent = Intent().apply {
+                    action = Intent.ACTION_SEND
+                    putExtra(Intent.EXTRA_STREAM, shareUri)
+                    type = TEXT_PREFIX_GENERIC
+                    addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
+                }
+                startActivity(Intent.createChooser(shareIntent, resources.getText(R.string.send_to)))
+
+                true
             }
-            startActivity(Intent.createChooser(shareIntent, resources.getText(R.string.send_to)))
 
-            true
-        } else {
-            super.onOptionsItemSelected(item)
+            R.id.save -> {
+                val saveFragment: DialogFragment = SaveToStorageDialogFragment.newInstance(
+                    intent.getStringExtra("FILE_NAME").toString()
+                )
+                saveFragment.show(
+                    supportFragmentManager,
+                    SaveToStorageDialogFragment.TAG
+                )
+                true
+            }
+
+            else -> {
+                super.onOptionsItemSelected(item)
+            }
         }
     }
 

+ 59 - 7
app/src/main/java/com/nextcloud/talk/jobs/SaveFileToStorageWorker.kt

@@ -1,10 +1,10 @@
 /*
  * Nextcloud Talk application
  *
- * @author Andy Scherzinger
+ * @author Fariba Khandani
  * @author Marcel Hibbe
- * Copyright (C) 2022 Andy Scherzinger <info@andy-scherzinger.de>
- * Copyright (C) 2021 Marcel Hibbe <dev@mhibbe.de>
+ * Copyright (C) 2023 Fariba Khandani <khandani@winworker.de>
+ * Copyright (C) 2023 Marcel Hibbe <dev@mhibbe.de>
  *
  * 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
@@ -25,14 +25,23 @@ package com.nextcloud.talk.jobs
 import android.content.ContentValues
 import android.content.Context
 import android.media.MediaScannerConnection
+import android.net.Uri
+import android.os.Build
 import android.os.Environment
+import android.os.Handler
+import android.os.Looper
 import android.provider.MediaStore
 import android.provider.MediaStore.Files.FileColumns
 import android.util.Log
+import android.widget.Toast
 import androidx.work.Worker
 import androidx.work.WorkerParameters
 import autodagger.AutoInjector
+import com.nextcloud.talk.R
 import com.nextcloud.talk.application.NextcloudTalkApplication
+import com.nextcloud.talk.utils.Mimetype.AUDIO_PREFIX
+import com.nextcloud.talk.utils.Mimetype.IMAGE_PREFIX
+import com.nextcloud.talk.utils.Mimetype.VIDEO_PREFIX
 import java.io.File
 import java.io.IOException
 import java.io.OutputStream
@@ -50,16 +59,22 @@ class SaveFileToStorageWorker(val context: Context, workerParameters: WorkerPara
             val contentResolver = context.contentResolver
             val mimeType = URLConnection.guessContentTypeFromName(cacheFile.name)
 
+            val appName = applicationContext.resources!!.getString(R.string.nc_app_product_name)
+
             val values = ContentValues().apply {
+                if (mimeType.startsWith(IMAGE_PREFIX) || mimeType.startsWith(VIDEO_PREFIX)) {
+                    put(FileColumns.RELATIVE_PATH, Environment.DIRECTORY_DCIM + "/" + appName)
+                }
                 put(FileColumns.DISPLAY_NAME, cacheFile.name)
-                put(FileColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS)
+
                 if (mimeType != null) {
-                    put(FileColumns.MIME_TYPE, URLConnection.guessContentTypeFromName(cacheFile.name))
+                    put(FileColumns.MIME_TYPE, mimeType)
                 }
             }
 
-            val collection = MediaStore.Files.getContentUri("external")
-            val uri = contentResolver.insert(collection, values)
+            val collectionUri = getUriByType(mimeType)
+
+            val uri = contentResolver.insert(collectionUri, values)
 
             uri?.let { fileUri ->
                 try {
@@ -79,16 +94,53 @@ class SaveFileToStorageWorker(val context: Context, workerParameters: WorkerPara
             // Notify the media scanner about the new file
             MediaScannerConnection.scanFile(context, arrayOf(cacheFile.absolutePath), null, null)
 
+            Handler(Looper.getMainLooper()).post {
+                Toast.makeText(
+                    context,
+                    context.resources.getString(R.string.nc_save_success),
+                    Toast.LENGTH_SHORT
+                ).show()
+            }
+
             return Result.success()
         } catch (e: IOException) {
             Log.e(TAG, "Something went wrong when trying to save file to internal storage", e)
+            Handler(Looper.getMainLooper()).post {
+                Toast.makeText(
+                    context,
+                    context.resources.getString(R.string.nc_common_error_sorry),
+                    Toast.LENGTH_SHORT
+                ).show()
+            }
+
             return Result.failure()
         } catch (e: NullPointerException) {
             Log.e(TAG, "Something went wrong when trying to save file to internal storage", e)
+            Handler(Looper.getMainLooper()).post {
+                Toast.makeText(
+                    context,
+                    context.resources.getString(R.string.nc_common_error_sorry),
+                    Toast.LENGTH_SHORT
+                ).show()
+            }
+
             return Result.failure()
         }
     }
 
+    private fun getUriByType(mimeType: String): Uri {
+        return when {
+            mimeType.startsWith(VIDEO_PREFIX) -> MediaStore.Video.Media.EXTERNAL_CONTENT_URI
+            mimeType.startsWith(AUDIO_PREFIX) -> MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
+            mimeType.startsWith(IMAGE_PREFIX) -> MediaStore.Images.Media.EXTERNAL_CONTENT_URI
+            else -> if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
+                Uri.fromFile(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS))
+            } else {
+                MediaStore.Downloads.EXTERNAL_CONTENT_URI
+            }
+        }
+    }
+
     companion object {
         private val TAG = SaveFileToStorageWorker::class.java.simpleName
         const val KEY_FILE_NAME = "KEY_FILE_NAME"

+ 155 - 0
app/src/main/java/com/nextcloud/talk/ui/dialog/SaveToStorageDialogFragment.kt

@@ -0,0 +1,155 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Marcel Hibbe
+ * @author Fariba Khandani
+ * Copyright (C) 2023 Marcel Hibbe (dev@mhibbe.de)
+ * Copyright (C) 2023 Fariba Khandani <khandani@winworker.de>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+package com.nextcloud.talk.ui.dialog
+
+import android.annotation.SuppressLint
+import android.app.Dialog
+import android.content.DialogInterface
+import android.os.Bundle
+import android.util.Log
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.appcompat.app.AlertDialog
+import androidx.fragment.app.DialogFragment
+import androidx.work.Data
+import androidx.work.OneTimeWorkRequest
+import androidx.work.WorkInfo
+import androidx.work.WorkManager
+import autodagger.AutoInjector
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import com.nextcloud.talk.R
+import com.nextcloud.talk.application.NextcloudTalkApplication
+import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
+import com.nextcloud.talk.databinding.DialogChooseAccountShareToBinding
+import com.nextcloud.talk.jobs.SaveFileToStorageWorker
+import com.nextcloud.talk.ui.theme.ViewThemeUtils
+import java.util.concurrent.ExecutionException
+import javax.inject.Inject
+
+@AutoInjector(NextcloudTalkApplication::class)
+class SaveToStorageDialogFragment : DialogFragment() {
+
+    @Inject
+    lateinit var viewThemeUtils: ViewThemeUtils
+    private var binding: DialogChooseAccountShareToBinding? = null
+    private var dialogView: View? = null
+    lateinit var fileName: String
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        sharedApplication!!.componentApplication.inject(this)
+        fileName = arguments?.getString(KEY_FILE_NAME)!!
+    }
+    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+        val dialogText = StringBuilder()
+        dialogText.append(resources.getString(R.string.nc_dialog_save_to_storage_content))
+        dialogText.append("\n")
+        dialogText.append("\n")
+        dialogText.append(resources.getString(R.string.nc_dialog_save_to_storage_continue))
+
+        val dialogBuilder = MaterialAlertDialogBuilder(requireContext())
+            .setTitle(R.string.nc_dialog_save_to_storage_title)
+            .setMessage(dialogText)
+            .setPositiveButton(R.string.nc_dialog_save_to_storage_yes) { _: DialogInterface?, _: Int ->
+                saveImageToStorage(fileName)
+            }
+            .setNegativeButton(R.string.nc_dialog_save_to_storage_no) { _: DialogInterface?, _: Int ->
+            }
+        viewThemeUtils.dialog.colorMaterialAlertDialogBackground(
+            requireContext(),
+            dialogBuilder
+        )
+        val dialog = dialogBuilder.show()
+        viewThemeUtils.platform.colorTextButtons(
+            dialog.getButton(AlertDialog.BUTTON_POSITIVE),
+            dialog.getButton(AlertDialog.BUTTON_NEGATIVE)
+        )
+
+        return dialog
+    }
+
+    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+        super.onViewCreated(view, savedInstanceState)
+        themeViews()
+    }
+
+    private fun themeViews() {
+        viewThemeUtils.platform.themeDialog(binding!!.root)
+    }
+
+    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
+        return dialogView
+    }
+
+    override fun onDestroyView() {
+        super.onDestroyView()
+        binding = null
+    }
+
+    @SuppressLint("LongLogTag")
+    private fun saveImageToStorage(
+        fileName: String
+    ) {
+        val sourceFilePath = requireContext().cacheDir.path
+        val workerTag = SAVE_TO_STORAGE_WORKER_PREFIX + fileName
+
+        val workers = WorkManager.getInstance(requireContext()).getWorkInfosByTag(workerTag)
+        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(workerTag)
+            .build()
+
+        WorkManager.getInstance().enqueue(saveWorker)
+    }
+
+    companion object {
+        val TAG = SaveToStorageDialogFragment::class.java.simpleName
+        private const val KEY_FILE_NAME = "keyFileName"
+        private const val SAVE_TO_STORAGE_WORKER_PREFIX = "saveToStorage_"
+
+        fun newInstance(fileName: String): SaveToStorageDialogFragment {
+            val args = Bundle()
+            args.putString(KEY_FILE_NAME, fileName)
+            val fragment = SaveToStorageDialogFragment()
+            fragment.arguments = args
+            return fragment
+        }
+    }
+}

+ 3 - 3
app/src/main/java/com/nextcloud/talk/utils/FileViewerUtils.kt

@@ -37,9 +37,9 @@ import androidx.work.WorkInfo
 import androidx.work.WorkManager
 import com.google.android.material.snackbar.Snackbar
 import com.nextcloud.talk.R
-import com.nextcloud.talk.activities.FullScreenImageActivity
-import com.nextcloud.talk.activities.FullScreenMediaActivity
-import com.nextcloud.talk.activities.FullScreenTextViewerActivity
+import com.nextcloud.talk.fullscreenfile.FullScreenImageActivity
+import com.nextcloud.talk.fullscreenfile.FullScreenMediaActivity
+import com.nextcloud.talk.fullscreenfile.FullScreenTextViewerActivity
 import com.nextcloud.talk.adapters.messages.PreviewMessageViewHolder
 import com.nextcloud.talk.data.user.model.User
 import com.nextcloud.talk.jobs.DownloadFileToCacheWorker

+ 22 - 0
app/src/main/res/drawable/baseline_download_24.xml

@@ -0,0 +1,22 @@
+<!--
+    @author Google LLC
+    Copyright (C) 2023 Google LLC
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+-->
+
+<vector android:height="24dp" android:tint="#000000"
+    android:viewportHeight="24" android:viewportWidth="24"
+    android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
+    <path android:fillColor="@android:color/white" android:pathData="M5,20h14v-2H5V20zM19,9h-4V3H9v6H5l7,7L19,9z"/>
+</vector>

+ 1 - 1
app/src/main/res/layout/activity_full_screen_image.xml

@@ -28,7 +28,7 @@
     android:id="@+id/image_wrapper_view"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    tools:context=".activities.FullScreenImageActivity">
+    tools:context=".fullscreenfile.FullScreenImageActivity">
 
     <com.google.android.material.appbar.MaterialToolbar
         android:id="@+id/imageview_toolbar"

+ 1 - 1
app/src/main/res/layout/activity_full_screen_media.xml

@@ -25,7 +25,7 @@
     xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    tools:context=".activities.FullScreenMediaActivity">
+    tools:context=".fullscreenfile.FullScreenMediaActivity">
 
     <androidx.appcompat.widget.Toolbar
         android:id="@+id/mediaview_toolbar"

+ 1 - 1
app/src/main/res/layout/activity_full_screen_text.xml

@@ -27,7 +27,7 @@
     android:layout_height="match_parent"
     android:background="@color/bg_default"
     android:orientation="vertical"
-    tools:context=".activities.FullScreenTextViewerActivity">
+    tools:context=".fullscreenfile.FullScreenTextViewerActivity">
 
     <com.google.android.material.appbar.AppBarLayout
         android:layout_width="match_parent"

+ 1 - 1
app/src/main/res/layout/dialog_message_actions.xml

@@ -468,7 +468,7 @@
                 android:contentDescription="@null"
                 android:paddingStart="@dimen/standard_padding"
                 android:paddingEnd="@dimen/zero"
-                android:src="@drawable/ic_baseline_arrow_downward_24px"
+                android:src="@drawable/baseline_download_24"
                 app:tint="@color/high_emphasis_menu_icon" />
 
             <androidx.appcompat.widget.AppCompatTextView

+ 3 - 1
app/src/main/res/values-v27/styles.xml

@@ -8,6 +8,7 @@
         <item name="android:windowNoTitle">true</item>
         <item name="android:windowActionBar">true</item>
         <item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
+        <item name="colorSurface">@color/bg_default</item>
     </style>
 
     <style name="FullScreenMediaTheme" parent="Theme.AppCompat.Light.NoActionBar">
@@ -17,5 +18,6 @@
         <item name="android:windowNoTitle">true</item>
         <item name="android:windowActionBar">true</item>
         <item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
+        <item name="colorSurface">@color/bg_default</item>
     </style>
-</resources>
+</resources>

+ 4 - 5
app/src/main/res/values/strings.xml

@@ -51,7 +51,6 @@ How to translate with transifex:
     <string name="nc_common_error_sorry">Sorry, something went wrong!</string>
     <string name="nc_common_create">Create</string>
 
-
     <!-- Bottom Navigation -->
     <string name="nc_settings">Settings</string>
 
@@ -519,14 +518,14 @@ How to translate with transifex:
     <string name="nc_phone_book_integration_chat_via">Chat via %s</string>
     <string name="nc_phone_book_integration_account_not_found">Account not found</string>
 
-    //save feature
+    <!--  save feature -->
     <string name="nc_save_message">Save</string>
     <string name="nc_dialog_save_to_storage_title">Save to storage?</string>
-    <string name="nc_dialog_save_to_storage_content">Saving this media to storage will allow any other apps on your device to access it.\nContinue?</string>
+    <string name="nc_dialog_save_to_storage_content">Saving this media to storage will allow any other apps on your device to access it.</string>
+    <string name="nc_dialog_save_to_storage_continue">Continue?</string>
     <string name="nc_dialog_save_to_storage_yes">Yes</string>
     <string name="nc_dialog_save_to_storage_no">No</string>
-
-
+    <string name="nc_save_success">Saved successfully</string>
 
     <string name="starred">Favorite</string>
     <string name="user_status">Status</string>

+ 2 - 0
app/src/main/res/values/styles.xml

@@ -221,6 +221,7 @@
         <item name="android:statusBarColor">@color/transparent</item>
         <item name="android:windowNoTitle">true</item>
         <item name="android:windowActionBar">true</item>
+        <item name="colorSurface">@color/bg_default</item>
     </style>
 
     <style name="FullScreenMediaTheme" parent="Theme.AppCompat.Light.NoActionBar">
@@ -229,6 +230,7 @@
         <item name="android:statusBarColor">@color/transparent</item>
         <item name="android:windowNoTitle">true</item>
         <item name="android:windowActionBar">true</item>
+        <item name="colorSurface">@color/bg_default</item>
     </style>
 
     <style name="TextInputLayoutTheme" parent="Theme.AppCompat">