mirror of
https://github.com/oxen-io/session-android.git
synced 2024-11-21 15:05:19 +00:00
SS-72 Update save attachment models + add one-time warning that other apps can access saved attachments
This commit is contained in:
parent
0c83606539
commit
621094ebe4
@ -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"/>
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
|
Loading…
Reference in New Issue
Block a user