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>
This commit is contained in:
Marcel Hibbe 2023-11-07 19:52:16 +01:00
parent 2f24c130ca
commit 640007b421
No known key found for this signature in database
GPG Key ID: C793F8B59F43CE7B
16 changed files with 333 additions and 165 deletions

View File

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

View File

@ -34,7 +34,6 @@ import android.annotation.SuppressLint
import android.content.ClipData import android.content.ClipData
import android.content.ClipboardManager import android.content.ClipboardManager
import android.content.Context import android.content.Context
import android.content.DialogInterface
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.content.res.AssetFileDescriptor 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.events.WebSocketCommunicationEvent
import com.nextcloud.talk.extensions.loadAvatarOrImagePreview import com.nextcloud.talk.extensions.loadAvatarOrImagePreview
import com.nextcloud.talk.jobs.DownloadFileToCacheWorker import com.nextcloud.talk.jobs.DownloadFileToCacheWorker
import com.nextcloud.talk.jobs.SaveFileToStorageWorker
import com.nextcloud.talk.jobs.ShareOperationWorker import com.nextcloud.talk.jobs.ShareOperationWorker
import com.nextcloud.talk.jobs.UploadAndShareFilesWorker import com.nextcloud.talk.jobs.UploadAndShareFilesWorker
import com.nextcloud.talk.location.LocationPickerActivity 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.AttachmentDialog
import com.nextcloud.talk.ui.dialog.DateTimePickerFragment import com.nextcloud.talk.ui.dialog.DateTimePickerFragment
import com.nextcloud.talk.ui.dialog.MessageActionsDialog 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.dialog.ShowReactionsDialog
import com.nextcloud.talk.ui.recyclerview.MessageSwipeActions import com.nextcloud.talk.ui.recyclerview.MessageSwipeActions
import com.nextcloud.talk.ui.recyclerview.MessageSwipeCallback 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") @SuppressLint("SimpleDateFormat")
private fun setVoiceRecordFileName() { private fun setVoiceRecordFileName() {
val simpleDateFormat = SimpleDateFormat(FILE_DATE_PATTERN) 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) { private fun showSaveToStorageWarning(message: ChatMessage) {
val builder = AlertDialog.Builder(this) val saveFragment: DialogFragment = SaveToStorageDialogFragment.newInstance(
builder.setTitle(R.string.nc_dialog_save_to_storage_title) message.selectedIndividualHashMap!!["name"]!!
builder.setMessage(R.string.nc_dialog_save_to_storage_content) )
builder.setPositiveButton(R.string.nc_dialog_save_to_storage_yes) { dialog: DialogInterface, _: Int -> saveFragment.show(
saveImage(message) supportFragmentManager,
dialog.dismiss() SaveToStorageDialogFragment.TAG
} )
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) { fun checkIfSaveable(message: ChatMessage) {
@ -4608,5 +4556,6 @@ class ChatActivity :
private const val TYPING_STOPPED_SIGNALING_MESSAGE_TYPE = "stoppedTyping" private const val TYPING_STOPPED_SIGNALING_MESSAGE_TYPE = "stoppedTyping"
private const val CALL_STARTED_ID = -2 private const val CALL_STARTED_ID = -2
private const val MILISEC_15: Long = 15 private const val MILISEC_15: Long = 15
private const val LINEBREAK = "\n"
} }
} }

View File

@ -24,10 +24,8 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * 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.content.Intent
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
@ -35,7 +33,6 @@ import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup.MarginLayoutParams import android.view.ViewGroup.MarginLayoutParams
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
@ -44,28 +41,25 @@ import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat import androidx.core.view.WindowInsetsControllerCompat
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import androidx.work.Data import androidx.fragment.app.DialogFragment
import androidx.work.OneTimeWorkRequest import autodagger.AutoInjector
import androidx.work.WorkInfo
import androidx.work.WorkManager
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.nextcloud.talk.BuildConfig import com.nextcloud.talk.BuildConfig
import com.nextcloud.talk.R import com.nextcloud.talk.R
import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.databinding.ActivityFullScreenImageBinding import com.nextcloud.talk.databinding.ActivityFullScreenImageBinding
import com.nextcloud.talk.jobs.SaveFileToStorageWorker import com.nextcloud.talk.ui.dialog.SaveToStorageDialogFragment
import com.nextcloud.talk.ui.theme.ViewThemeUtils
import com.nextcloud.talk.utils.BitmapShrinker import com.nextcloud.talk.utils.BitmapShrinker
import com.nextcloud.talk.utils.Mimetype.IMAGE_PREFIX_GENERIC import com.nextcloud.talk.utils.Mimetype.IMAGE_PREFIX_GENERIC
import pl.droidsonroids.gif.GifDrawable import pl.droidsonroids.gif.GifDrawable
import java.io.File import java.io.File
import java.util.concurrent.ExecutionException
@AutoInjector(NextcloudTalkApplication::class)
class FullScreenImageActivity : AppCompatActivity() { class FullScreenImageActivity : AppCompatActivity() {
lateinit var binding: ActivityFullScreenImageBinding lateinit var binding: ActivityFullScreenImageBinding
private lateinit var windowInsetsController: WindowInsetsControllerCompat private lateinit var windowInsetsController: WindowInsetsControllerCompat
private lateinit var path: String private lateinit var path: String
private var showFullscreen = false private var showFullscreen = false
lateinit var viewThemeUtils: ViewThemeUtils
override fun onCreateOptionsMenu(menu: Menu): Boolean { override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.menu_preview, menu) menuInflater.inflate(R.menu.menu_preview, menu)
@ -98,7 +92,13 @@ class FullScreenImageActivity : AppCompatActivity() {
} }
R.id.save -> { R.id.save -> {
showWarningDialog() val saveFragment: DialogFragment = SaveToStorageDialogFragment.newInstance(
intent.getStringExtra("FILE_NAME").toString()
)
saveFragment.show(
supportFragmentManager,
SaveToStorageDialogFragment.TAG
)
true 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?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
binding = ActivityFullScreenImageBinding.inflate(layoutInflater) binding = ActivityFullScreenImageBinding.inflate(layoutInflater)
setContentView(binding.root) 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 { companion object {
private const val TAG = "FullScreenImageActivity" private const val TAG = "FullScreenImageActivity"
private const val HUNDRED_MB = 100 * 1024 * 1024 private const val HUNDRED_MB = 100 * 1024 * 1024

View File

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

View File

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

View File

@ -1,10 +1,10 @@
/* /*
* Nextcloud Talk application * Nextcloud Talk application
* *
* @author Andy Scherzinger * @author Fariba Khandani
* @author Marcel Hibbe * @author Marcel Hibbe
* Copyright (C) 2022 Andy Scherzinger <info@andy-scherzinger.de> * Copyright (C) 2023 Fariba Khandani <khandani@winworker.de>
* Copyright (C) 2021 Marcel Hibbe <dev@mhibbe.de> * Copyright (C) 2023 Marcel Hibbe <dev@mhibbe.de>
* *
* This program is free software: you can redistribute it and/or modify * 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 * 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.ContentValues
import android.content.Context import android.content.Context
import android.media.MediaScannerConnection import android.media.MediaScannerConnection
import android.net.Uri
import android.os.Build
import android.os.Environment import android.os.Environment
import android.os.Handler
import android.os.Looper
import android.provider.MediaStore import android.provider.MediaStore
import android.provider.MediaStore.Files.FileColumns import android.provider.MediaStore.Files.FileColumns
import android.util.Log import android.util.Log
import android.widget.Toast
import androidx.work.Worker import androidx.work.Worker
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import autodagger.AutoInjector import autodagger.AutoInjector
import com.nextcloud.talk.R
import com.nextcloud.talk.application.NextcloudTalkApplication 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.File
import java.io.IOException import java.io.IOException
import java.io.OutputStream import java.io.OutputStream
@ -50,16 +59,22 @@ class SaveFileToStorageWorker(val context: Context, workerParameters: WorkerPara
val contentResolver = context.contentResolver val contentResolver = context.contentResolver
val mimeType = URLConnection.guessContentTypeFromName(cacheFile.name) val mimeType = URLConnection.guessContentTypeFromName(cacheFile.name)
val appName = applicationContext.resources!!.getString(R.string.nc_app_product_name)
val values = ContentValues().apply { 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.DISPLAY_NAME, cacheFile.name)
put(FileColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS)
if (mimeType != null) { if (mimeType != null) {
put(FileColumns.MIME_TYPE, URLConnection.guessContentTypeFromName(cacheFile.name)) put(FileColumns.MIME_TYPE, mimeType)
} }
} }
val collection = MediaStore.Files.getContentUri("external") val collectionUri = getUriByType(mimeType)
val uri = contentResolver.insert(collection, values)
val uri = contentResolver.insert(collectionUri, values)
uri?.let { fileUri -> uri?.let { fileUri ->
try { try {
@ -79,16 +94,53 @@ class SaveFileToStorageWorker(val context: Context, workerParameters: WorkerPara
// Notify the media scanner about the new file // Notify the media scanner about the new file
MediaScannerConnection.scanFile(context, arrayOf(cacheFile.absolutePath), null, null) 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() return Result.success()
} catch (e: IOException) { } catch (e: IOException) {
Log.e(TAG, "Something went wrong when trying to save file to internal storage", e) 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() return Result.failure()
} catch (e: NullPointerException) { } catch (e: NullPointerException) {
Log.e(TAG, "Something went wrong when trying to save file to internal storage", e) 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() 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 { companion object {
private val TAG = SaveFileToStorageWorker::class.java.simpleName private val TAG = SaveFileToStorageWorker::class.java.simpleName
const val KEY_FILE_NAME = "KEY_FILE_NAME" const val KEY_FILE_NAME = "KEY_FILE_NAME"

View File

@ -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
}
}
}

View File

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

View File

@ -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>

View File

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

View File

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

View File

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

View File

@ -468,7 +468,7 @@
android:contentDescription="@null" android:contentDescription="@null"
android:paddingStart="@dimen/standard_padding" android:paddingStart="@dimen/standard_padding"
android:paddingEnd="@dimen/zero" 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" /> app:tint="@color/high_emphasis_menu_icon" />
<androidx.appcompat.widget.AppCompatTextView <androidx.appcompat.widget.AppCompatTextView

View File

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

View File

@ -51,7 +51,6 @@ How to translate with transifex:
<string name="nc_common_error_sorry">Sorry, something went wrong!</string> <string name="nc_common_error_sorry">Sorry, something went wrong!</string>
<string name="nc_common_create">Create</string> <string name="nc_common_create">Create</string>
<!-- Bottom Navigation --> <!-- Bottom Navigation -->
<string name="nc_settings">Settings</string> <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_chat_via">Chat via %s</string>
<string name="nc_phone_book_integration_account_not_found">Account not found</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_save_message">Save</string>
<string name="nc_dialog_save_to_storage_title">Save to storage?</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_yes">Yes</string>
<string name="nc_dialog_save_to_storage_no">No</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="starred">Favorite</string>
<string name="user_status">Status</string> <string name="user_status">Status</string>

View File

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