saveFiles to internal storage

This commit is contained in:
fariba khandani 2023-10-30 15:28:48 +01:00 committed by Marcel Hibbe
parent eb9d4f8167
commit dd9eb35e72
7 changed files with 300 additions and 2 deletions

View File

@ -26,6 +26,8 @@
package com.nextcloud.talk.activities package com.nextcloud.talk.activities
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
@ -33,6 +35,7 @@ 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
@ -41,20 +44,28 @@ 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.work.OneTimeWorkRequest
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.databinding.ActivityFullScreenImageBinding 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.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
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)
@ -67,6 +78,7 @@ class FullScreenImageActivity : AppCompatActivity() {
onBackPressedDispatcher.onBackPressed() onBackPressedDispatcher.onBackPressed()
true true
} }
R.id.share -> { R.id.share -> {
val shareUri = FileProvider.getUriForFile( val shareUri = FileProvider.getUriForFile(
this, this,
@ -84,12 +96,34 @@ class FullScreenImageActivity : AppCompatActivity() {
true true
} }
R.id.save -> {
showWarningDialog()
true
}
else -> { else -> {
super.onOptionsItemSelected(item) 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?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) 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 { 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

@ -34,6 +34,7 @@ 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
@ -156,6 +157,7 @@ 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
@ -897,7 +899,7 @@ class ChatActivity :
} }
} else { } else {
Log.d(TAG, "Downloaded to cache") Log.d(TAG, "Downloaded to cache")
downloadFileToCache(message, true) { downloadFileToCache(message,true ) {
setUpWaveform(message) 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") @SuppressLint("SimpleDateFormat")
private fun setVoiceRecordFileName() { private fun setVoiceRecordFileName() {
val simpleDateFormat = SimpleDateFormat(FILE_DATE_PATTERN) val simpleDateFormat = SimpleDateFormat(FILE_DATE_PATTERN)
@ -4101,12 +4141,48 @@ class ChatActivity :
if (file.exists()) { if (file.exists()) {
share(message) share(message)
} else { } else {
downloadFileToCache(message, false) { downloadFileToCache(message, false ) {
share(message) 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) { fun openInFilesApp(message: ChatMessage) {
val keyID = message.selectedIndividualHashMap!![PreviewMessageViewHolder.KEY_ID] val keyID = message.selectedIndividualHashMap!![PreviewMessageViewHolder.KEY_ID]
val link = message.selectedIndividualHashMap!!["link"] val link = message.selectedIndividualHashMap!!["link"]

View File

@ -0,0 +1,97 @@
/*
* Nextcloud Talk application
*
* @author Andy Scherzinger
* @author Marcel Hibbe
* Copyright (C) 2022 Andy Scherzinger <info@andy-scherzinger.de>
* Copyright (C) 2021 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
* 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.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"
}
}

View File

@ -118,6 +118,7 @@ class MessageActionsDialog(
initMenuItemOpenNcApp( initMenuItemOpenNcApp(
ChatMessage.MessageType.SINGLE_NC_ATTACHMENT_MESSAGE == message.getCalculateMessageType() ChatMessage.MessageType.SINGLE_NC_ATTACHMENT_MESSAGE == message.getCalculateMessageType()
) )
initMenuItemSave(message.getCalculateMessageType() == ChatMessage.MessageType.SINGLE_NC_ATTACHMENT_MESSAGE)
} }
override fun onStart() { override fun onStart() {
@ -169,6 +170,8 @@ class MessageActionsDialog(
dialogMessageActionsBinding.emojiMore.installForceSingleEmoji() dialogMessageActionsBinding.emojiMore.installForceSingleEmoji()
} }
/* /*
This method is a hacky workaround to avoid bug #1914 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, 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) 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 { private fun getVisibility(visible: Boolean): Int {
return if (visible) { return if (visible) {
View.VISIBLE View.VISIBLE

View File

@ -452,6 +452,39 @@
</LinearLayout> </LinearLayout>
<LinearLayout
android:id="@+id/menu_save_message"
android:layout_width="match_parent"
android:layout_height="@dimen/bottom_sheet_item_height"
android:background="?android:attr/selectableItemBackground"
android:gravity="center_vertical"
android:orientation="horizontal"
tools:ignore="UseCompoundDrawables">
<ImageView
android:id="@+id/menu_icon_save_message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@null"
android:paddingStart="@dimen/standard_padding"
android:paddingEnd="@dimen/zero"
android:src="@drawable/ic_baseline_arrow_downward_24px"
app:tint="@color/high_emphasis_menu_icon" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/menu_text_save_message"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="start|center_vertical"
android:paddingStart="@dimen/standard_double_padding"
android:paddingEnd="@dimen/standard_padding"
android:text="@string/nc_save_message"
android:textAlignment="viewStart"
android:textColor="@color/high_emphasis_text"
android:textSize="@dimen/bottom_sheet_text_size" />
</LinearLayout>
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>

View File

@ -22,4 +22,7 @@
<item <item
android:id="@+id/share" android:id="@+id/share"
android:title="@string/share" /> android:title="@string/share" />
<item
android:id="@+id/save"
android:title="@string/nc_save_message" />
</menu> </menu>

View File

@ -519,6 +519,16 @@ 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
<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_yes">Yes</string>
<string name="nc_dialog_save_to_storage_no">No</string>
<string name="starred">Favorite</string> <string name="starred">Favorite</string>
<string name="user_status">Status</string> <string name="user_status">Status</string>
<string name="encrypted">Encrypted</string> <string name="encrypted">Encrypted</string>