mirror of
https://github.com/nextcloud/talk-android
synced 2025-03-12 18:40:52 +00:00
Merge pull request #3416 from FaribaKhandani/feature/savefiles
feature/2331/saveFiles
This commit is contained in:
commit
946eb84835
@ -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
|
||||
|
@ -34,6 +34,7 @@ import android.annotation.SuppressLint
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.res.AssetFileDescriptor
|
||||
@ -156,6 +157,7 @@ import com.nextcloud.talk.events.UserMentionClickEvent
|
||||
import com.nextcloud.talk.events.WebSocketCommunicationEvent
|
||||
import com.nextcloud.talk.extensions.loadAvatarOrImagePreview
|
||||
import com.nextcloud.talk.jobs.DownloadFileToCacheWorker
|
||||
import com.nextcloud.talk.jobs.SaveFileToStorageWorker
|
||||
import com.nextcloud.talk.jobs.ShareOperationWorker
|
||||
import com.nextcloud.talk.jobs.UploadAndShareFilesWorker
|
||||
import com.nextcloud.talk.location.LocationPickerActivity
|
||||
@ -897,7 +899,7 @@ class ChatActivity :
|
||||
}
|
||||
} else {
|
||||
Log.d(TAG, "Downloaded to cache")
|
||||
downloadFileToCache(message, true) {
|
||||
downloadFileToCache(message,true ) {
|
||||
setUpWaveform(message)
|
||||
}
|
||||
}
|
||||
@ -2018,6 +2020,44 @@ class ChatActivity :
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("LongLogTag")
|
||||
private fun saveImageToStorage(
|
||||
message: ChatMessage
|
||||
) {
|
||||
message.openWhenDownloaded = false
|
||||
adapter?.update(message)
|
||||
|
||||
val fileName = message.selectedIndividualHashMap!!["name"]
|
||||
val sourceFilePath = applicationContext.cacheDir.path
|
||||
val fileId = message.selectedIndividualHashMap!!["id"]
|
||||
|
||||
val workers = WorkManager.getInstance(context).getWorkInfosByTag(fileId!!)
|
||||
try {
|
||||
for (workInfo in workers.get()) {
|
||||
if (workInfo.state == WorkInfo.State.RUNNING || workInfo.state == WorkInfo.State.ENQUEUED) {
|
||||
Log.d(TAG, "SaveFileToStorageWorker for $fileId is already running or scheduled")
|
||||
return
|
||||
}
|
||||
}
|
||||
} catch (e: ExecutionException) {
|
||||
Log.e(TAG, "Error when checking if worker already exists", e)
|
||||
} catch (e: InterruptedException) {
|
||||
Log.e(TAG, "Error when checking if worker already exists", e)
|
||||
}
|
||||
|
||||
val data: Data = Data.Builder()
|
||||
.putString(SaveFileToStorageWorker.KEY_FILE_NAME, fileName)
|
||||
.putString(SaveFileToStorageWorker.KEY_SOURCE_FILE_PATH, "$sourceFilePath/$fileName")
|
||||
.build()
|
||||
|
||||
val saveWorker: OneTimeWorkRequest = OneTimeWorkRequest.Builder(SaveFileToStorageWorker::class.java)
|
||||
.setInputData(data)
|
||||
.addTag(fileId)
|
||||
.build()
|
||||
|
||||
WorkManager.getInstance().enqueue(saveWorker)
|
||||
}
|
||||
|
||||
@SuppressLint("SimpleDateFormat")
|
||||
private fun setVoiceRecordFileName() {
|
||||
val simpleDateFormat = SimpleDateFormat(FILE_DATE_PATTERN)
|
||||
@ -4101,12 +4141,48 @@ class ChatActivity :
|
||||
if (file.exists()) {
|
||||
share(message)
|
||||
} else {
|
||||
downloadFileToCache(message, false) {
|
||||
downloadFileToCache(message, false ) {
|
||||
share(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveImage(message: ChatMessage){
|
||||
if (permissionUtil.isFilesPermissionGranted()) {
|
||||
saveImageToStorage(message)
|
||||
} else {
|
||||
UploadAndShareFilesWorker.requestStoragePermission(this@ChatActivity)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showSaveToStorageWarning(message: ChatMessage) {
|
||||
val builder = AlertDialog.Builder(this)
|
||||
builder.setTitle(R.string.nc_dialog_save_to_storage_title)
|
||||
builder.setMessage(R.string.nc_dialog_save_to_storage_content)
|
||||
builder.setPositiveButton(R.string.nc_dialog_save_to_storage_yes) { dialog: DialogInterface, _: Int ->
|
||||
saveImage(message)
|
||||
dialog.dismiss()
|
||||
}
|
||||
builder.setNegativeButton(R.string.nc_dialog_save_to_storage_no) { dialog: DialogInterface, _: Int ->
|
||||
dialog.dismiss()
|
||||
}
|
||||
val dialog = builder.create()
|
||||
dialog.show()
|
||||
}
|
||||
|
||||
fun checkIfSaveable(message: ChatMessage) {
|
||||
val filename = message.selectedIndividualHashMap!!["name"]
|
||||
path = applicationContext.cacheDir.absolutePath + "/" + filename
|
||||
val file = File(context.cacheDir, filename!!)
|
||||
if (file.exists()) {
|
||||
showSaveToStorageWarning(message)
|
||||
} else {
|
||||
downloadFileToCache(message ,false) {
|
||||
showSaveToStorageWarning(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun openInFilesApp(message: ChatMessage) {
|
||||
val keyID = message.selectedIndividualHashMap!![PreviewMessageViewHolder.KEY_ID]
|
||||
val link = message.selectedIndividualHashMap!!["link"]
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
Loading…
Reference in New Issue
Block a user