Merge pull request #3416 from FaribaKhandani/feature/savefiles

feature/2331/saveFiles
This commit is contained in:
Marcel Hibbe 2023-11-07 13:34:13 +01:00 committed by GitHub
commit 946eb84835
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 300 additions and 2 deletions

View File

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

View File

@ -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
@ -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)
@ -4107,6 +4147,42 @@ 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()
}
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"]

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

View File

@ -452,6 +452,39 @@
</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>

View File

@ -22,4 +22,7 @@
<item
android:id="@+id/share"
android:title="@string/share" />
<item
android:id="@+id/save"
android:title="@string/nc_save_message" />
</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_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="user_status">Status</string>
<string name="encrypted">Encrypted</string>