SS-72 Update save attachment models + add one-time warning that other apps can access saved attachments

This commit is contained in:
alansley 2024-08-14 13:40:01 +10:00
parent 0c83606539
commit 621094ebe4
5 changed files with 117 additions and 36 deletions

View File

@ -38,7 +38,7 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE"/>
<uses-permission android:name="network.loki.messenger.ACCESS_SESSION_SECRETS" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <!-- Only used on Android API 29 and lower -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO"/>

View File

@ -1,12 +1,14 @@
package org.thoughtcrime.securesms.conversation.v2
import android.Manifest
import android.Manifest.permission.ACCESS_FINE_LOCATION
import android.animation.FloatEvaluator
import android.animation.ValueAnimator
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.content.res.Resources
import android.database.Cursor
import android.graphics.Rect
@ -35,6 +37,7 @@ import android.widget.Toast
import androidx.activity.result.ActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.core.content.ContextCompat
import androidx.core.view.drawToBitmap
import androidx.core.view.isGone
import androidx.core.view.isVisible
@ -50,19 +53,9 @@ import androidx.loader.content.Loader
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.annimon.stream.Stream
import com.bumptech.glide.Glide
import com.squareup.phrase.Phrase
import dagger.hilt.android.AndroidEntryPoint
import java.lang.ref.WeakReference
import java.util.Locale
import java.util.concurrent.ExecutionException
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicLong
import java.util.concurrent.atomic.AtomicReference
import javax.inject.Inject
import kotlin.math.abs
import kotlin.math.min
import kotlin.math.roundToInt
import kotlin.math.sqrt
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.channels.Channel
@ -169,7 +162,6 @@ import org.thoughtcrime.securesms.mediasend.Media
import org.thoughtcrime.securesms.mediasend.MediaSendActivity
import org.thoughtcrime.securesms.mms.AudioSlide
import org.thoughtcrime.securesms.mms.GifSlide
import com.bumptech.glide.Glide
import org.thoughtcrime.securesms.mms.ImageSlide
import org.thoughtcrime.securesms.mms.MediaConstraints
import org.thoughtcrime.securesms.mms.Slide
@ -190,6 +182,18 @@ import org.thoughtcrime.securesms.util.isScrolledToWithin30dpOfBottom
import org.thoughtcrime.securesms.util.push
import org.thoughtcrime.securesms.util.show
import org.thoughtcrime.securesms.util.toPx
import java.lang.ref.WeakReference
import java.util.Locale
import java.util.concurrent.ExecutionException
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicLong
import java.util.concurrent.atomic.AtomicReference
import javax.inject.Inject
import kotlin.math.abs
import kotlin.math.min
import kotlin.math.roundToInt
import kotlin.math.sqrt
private const val TAG = "ConversationActivityV2"
@ -2148,6 +2152,26 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
endActionMode()
}
private fun saveAttachments(message: MmsMessageRecord) {
val attachments: List<SaveAttachmentTask.Attachment?> = Stream.of(message.slideDeck.slides)
.filter { s: Slide -> s.uri != null && (s.hasImage() || s.hasVideo() || s.hasAudio() || s.hasDocument()) }
.map { s: Slide -> SaveAttachmentTask.Attachment(s.uri!!, s.contentType, message.dateReceived, s.fileName.orNull()) }
.toList()
if (attachments.isNotEmpty()) {
val saveTask = SaveAttachmentTask(this)
saveTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, *attachments.toTypedArray())
if (!message.isOutgoing) { sendMediaSavedNotification() }
return
}
// Implied else that there were no attachment(s)
Toast.makeText(this, resources.getString(R.string.attachmentsSaveError), Toast.LENGTH_LONG).show()
}
private fun hasPermission(permission: String): Boolean {
val result = ContextCompat.checkSelfPermission(this, permission)
return result == PackageManager.PERMISSION_GRANTED
}
override fun saveAttachment(messages: Set<MessageRecord>) {
val message = messages.first() as MmsMessageRecord
@ -2159,37 +2183,64 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
return
}
// On Android versions below 30 we require the WRITE_EXTERNAL_STORAGE permission to save attachments.
// However, we would like to on more recent Android API versions there is scoped storage
// If we already have permission to write to external storage then just get on with it & return..
//
// Android versions will j
if (Build.VERSION.SDK_INT < 30) {
// Save the attachment(s) then bail if we already have permission to do so
if (hasPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
saveAttachments(message)
return
}
} else {
// On more modern versions of Android on API 30+ WRITE_EXTERNAL_STORAGE is no longer used and we can just
// save files to the public directories like "Downloads", "Pictures" etc. - but... we would still like to
// inform the user just _once_ that saving attachments means that other apps can access them - so we'll
val haveWarned = TextSecurePreferences.getHaveWarnedUserAboutSavingAttachments(this)
if (haveWarned) {
saveAttachments(message)
return
}
}
// ..otherwise we must ask for it first.
SaveAttachmentTask.showWarningDialog(this) {
Permissions.with(this)
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE)
.maxSdkVersion(Build.VERSION_CODES.P)
.maxSdkVersion(Build.VERSION_CODES.P) // P is 28
.withPermanentDenialDialog(Phrase.from(applicationContext, R.string.permissionsStorageSaveDenied)
.put(APP_NAME_KEY, getString(R.string.app_name))
.format().toString())
.onAnyDenied {
endActionMode()
val txt = Phrase.from(applicationContext, R.string.permissionsStorageSaveDenied)
.put(APP_NAME_KEY, getString(R.string.app_name))
.format().toString()
Toast.makeText(this@ConversationActivityV2, txt, Toast.LENGTH_LONG).show()
showSessionDialog {
title(R.string.permissionsRequired)
val txt = Phrase.from(applicationContext, R.string.permissionsStorageSaveDenied)
.put(APP_NAME_KEY, getString(R.string.app_name))
.format().toString()
text(txt)
// Take the user directly to the settings app for Session to grant the permission if they
// initially denied it but then have a change of heart when they realise they can't
// proceed without it.
dangerButton(R.string.theContinue) {
val intent = Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
val uri = Uri.fromParts("package", packageName, null)
intent.setData(uri)
startActivity(intent)
}
button(R.string.cancel)
}
}
.onAllGranted {
endActionMode()
val attachments: List<SaveAttachmentTask.Attachment?> = Stream.of(message.slideDeck.slides)
.filter { s: Slide -> s.uri != null && (s.hasImage() || s.hasVideo() || s.hasAudio() || s.hasDocument()) }
.map { s: Slide -> SaveAttachmentTask.Attachment(s.uri!!, s.contentType, message.dateReceived, s.fileName.orNull()) }
.toList()
if (attachments.isNotEmpty()) {
val saveTask = SaveAttachmentTask(this)
saveTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, *attachments.toTypedArray())
if (!message.isOutgoing) {
sendMediaSavedNotification()
}
return@onAllGranted
}
Toast.makeText(this,
resources.getString(R.string.attachmentsSaveError),
Toast.LENGTH_LONG).show()
saveAttachments(message)
}
.execute()
}

View File

@ -39,7 +39,7 @@ class UntrustedAttachmentView: LinearLayout {
iconDrawable.mutate().setTint(textColor)
val text = Phrase.from(context, R.string.attachmentsTapToDownload)
.put(FILE_TYPE_KEY, stringRes)
.put(FILE_TYPE_KEY, context.getString(stringRes))
.format()
binding.untrustedAttachmentTitle.text = text

View File

@ -12,6 +12,7 @@ import android.text.TextUtils
import android.webkit.MimeTypeMap
import android.widget.Toast
import network.loki.messenger.R
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.task.ProgressDialogAsyncTask
import org.session.libsignal.utilities.ExternalStorageUtil
import org.session.libsignal.utilities.Log
@ -47,10 +48,20 @@ class SaveAttachmentTask @JvmOverloads constructor(context: Context, count: Int
@JvmOverloads
fun showWarningDialog(context: Context, count: Int = 1, onAcceptListener: () -> Unit = {}) {
context.showSessionDialog {
title(R.string.permissionsRequired)
title(R.string.warning)
iconAttribute(R.attr.dialog_alert_icon)
text(context.getString(R.string.attachmentsWarning))
button(R.string.accept) { onAcceptListener() }
dangerButton(R.string.save) {
// On Android API 30+ there is no WRITE_EXTERNAL_STORAGE permission to save files so we can't
// check against that to show a one-time warning that saved attachments can be accessed by other
// apps - so on such devices we'll use a saved boolean preference.
val haveWarned = TextSecurePreferences.getHaveWarnedUserAboutSavingAttachments(context)
if (!haveWarned && Build.VERSION.SDK_INT >= 30) {
TextSecurePreferences.setHaveWarnedUserAboutSavingAttachments(context)
}
onAcceptListener()
}
button(R.string.cancel)
}
}

View File

@ -301,6 +301,12 @@ interface TextSecurePreferences {
const val ALLOW_MESSAGE_REQUESTS = "libsession.ALLOW_MESSAGE_REQUESTS"
// Key name for if we've warned the user that saving attachments will allow other apps to access them.
// Note: This is only a concern on Android API 30+ which does not have the WRITE_EXTERNAL_STORAGE permission
// for us to check against - and we only display this once, or until the user consents to this and continues
// to save the attachment(s).
const val HAVE_WARNED_USER_ABOUT_SAVING_ATTACHMENTS = "libsession.HAVE_WARNED_USER_ABOUT_SAVING_ATTACHMENTS"
@JvmStatic
fun getLastConfigurationSyncTime(context: Context): Long {
return getLongPreference(context, LAST_CONFIGURATION_SYNC_TIME, 0)
@ -981,6 +987,19 @@ interface TextSecurePreferences {
setBooleanPreference(context, FINGERPRINT_KEY_GENERATED, true)
}
// ----- Get / set methods for if we have already warned the user that saving attachments will allow other apps to access them -----
@JvmStatic
fun getHaveWarnedUserAboutSavingAttachments(context: Context): Boolean {
return getBooleanPreference(context, HAVE_WARNED_USER_ABOUT_SAVING_ATTACHMENTS, false)
}
@JvmStatic
fun setHaveWarnedUserAboutSavingAttachments(context: Context) {
setBooleanPreference(context, HAVE_WARNED_USER_ABOUT_SAVING_ATTACHMENTS, true)
}
// ---------------------------------------------------------------------------------------------------------------------------------
@JvmStatic
fun clearAll(context: Context) {
getDefaultSharedPreferences(context).edit().clear().commit()