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_SPECIAL_USE"/>
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE"/>
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE"/>
|
||||||
<uses-permission android:name="network.loki.messenger.ACCESS_SESSION_SECRETS" />
|
<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_EXTERNAL_STORAGE"/>
|
||||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
|
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
|
||||||
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO"/>
|
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO"/>
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
package org.thoughtcrime.securesms.conversation.v2
|
package org.thoughtcrime.securesms.conversation.v2
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
|
import android.Manifest.permission.ACCESS_FINE_LOCATION
|
||||||
import android.animation.FloatEvaluator
|
import android.animation.FloatEvaluator
|
||||||
import android.animation.ValueAnimator
|
import android.animation.ValueAnimator
|
||||||
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.Intent
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
import android.database.Cursor
|
import android.database.Cursor
|
||||||
import android.graphics.Rect
|
import android.graphics.Rect
|
||||||
@ -35,6 +37,7 @@ import android.widget.Toast
|
|||||||
import androidx.activity.result.ActivityResult
|
import androidx.activity.result.ActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.view.drawToBitmap
|
import androidx.core.view.drawToBitmap
|
||||||
import androidx.core.view.isGone
|
import androidx.core.view.isGone
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
@ -50,19 +53,9 @@ import androidx.loader.content.Loader
|
|||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.annimon.stream.Stream
|
import com.annimon.stream.Stream
|
||||||
|
import com.bumptech.glide.Glide
|
||||||
import com.squareup.phrase.Phrase
|
import com.squareup.phrase.Phrase
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
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.Dispatchers
|
||||||
import kotlinx.coroutines.channels.BufferOverflow
|
import kotlinx.coroutines.channels.BufferOverflow
|
||||||
import kotlinx.coroutines.channels.Channel
|
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.mediasend.MediaSendActivity
|
||||||
import org.thoughtcrime.securesms.mms.AudioSlide
|
import org.thoughtcrime.securesms.mms.AudioSlide
|
||||||
import org.thoughtcrime.securesms.mms.GifSlide
|
import org.thoughtcrime.securesms.mms.GifSlide
|
||||||
import com.bumptech.glide.Glide
|
|
||||||
import org.thoughtcrime.securesms.mms.ImageSlide
|
import org.thoughtcrime.securesms.mms.ImageSlide
|
||||||
import org.thoughtcrime.securesms.mms.MediaConstraints
|
import org.thoughtcrime.securesms.mms.MediaConstraints
|
||||||
import org.thoughtcrime.securesms.mms.Slide
|
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.push
|
||||||
import org.thoughtcrime.securesms.util.show
|
import org.thoughtcrime.securesms.util.show
|
||||||
import org.thoughtcrime.securesms.util.toPx
|
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"
|
private const val TAG = "ConversationActivityV2"
|
||||||
|
|
||||||
@ -2148,6 +2152,26 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
endActionMode()
|
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>) {
|
override fun saveAttachment(messages: Set<MessageRecord>) {
|
||||||
val message = messages.first() as MmsMessageRecord
|
val message = messages.first() as MmsMessageRecord
|
||||||
|
|
||||||
@ -2159,37 +2183,64 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
return
|
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) {
|
SaveAttachmentTask.showWarningDialog(this) {
|
||||||
Permissions.with(this)
|
Permissions.with(this)
|
||||||
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
.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)
|
.withPermanentDenialDialog(Phrase.from(applicationContext, R.string.permissionsStorageSaveDenied)
|
||||||
.put(APP_NAME_KEY, getString(R.string.app_name))
|
.put(APP_NAME_KEY, getString(R.string.app_name))
|
||||||
.format().toString())
|
.format().toString())
|
||||||
.onAnyDenied {
|
.onAnyDenied {
|
||||||
endActionMode()
|
endActionMode()
|
||||||
val txt = Phrase.from(applicationContext, R.string.permissionsStorageSaveDenied)
|
|
||||||
.put(APP_NAME_KEY, getString(R.string.app_name))
|
showSessionDialog {
|
||||||
.format().toString()
|
title(R.string.permissionsRequired)
|
||||||
Toast.makeText(this@ConversationActivityV2, txt, Toast.LENGTH_LONG).show()
|
|
||||||
|
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 {
|
.onAllGranted {
|
||||||
endActionMode()
|
endActionMode()
|
||||||
val attachments: List<SaveAttachmentTask.Attachment?> = Stream.of(message.slideDeck.slides)
|
saveAttachments(message)
|
||||||
.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()
|
|
||||||
}
|
}
|
||||||
.execute()
|
.execute()
|
||||||
}
|
}
|
||||||
|
@ -39,7 +39,7 @@ class UntrustedAttachmentView: LinearLayout {
|
|||||||
iconDrawable.mutate().setTint(textColor)
|
iconDrawable.mutate().setTint(textColor)
|
||||||
|
|
||||||
val text = Phrase.from(context, R.string.attachmentsTapToDownload)
|
val text = Phrase.from(context, R.string.attachmentsTapToDownload)
|
||||||
.put(FILE_TYPE_KEY, stringRes)
|
.put(FILE_TYPE_KEY, context.getString(stringRes))
|
||||||
.format()
|
.format()
|
||||||
binding.untrustedAttachmentTitle.text = text
|
binding.untrustedAttachmentTitle.text = text
|
||||||
|
|
||||||
|
@ -12,6 +12,7 @@ import android.text.TextUtils
|
|||||||
import android.webkit.MimeTypeMap
|
import android.webkit.MimeTypeMap
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
import org.session.libsession.utilities.task.ProgressDialogAsyncTask
|
import org.session.libsession.utilities.task.ProgressDialogAsyncTask
|
||||||
import org.session.libsignal.utilities.ExternalStorageUtil
|
import org.session.libsignal.utilities.ExternalStorageUtil
|
||||||
import org.session.libsignal.utilities.Log
|
import org.session.libsignal.utilities.Log
|
||||||
@ -47,10 +48,20 @@ class SaveAttachmentTask @JvmOverloads constructor(context: Context, count: Int
|
|||||||
@JvmOverloads
|
@JvmOverloads
|
||||||
fun showWarningDialog(context: Context, count: Int = 1, onAcceptListener: () -> Unit = {}) {
|
fun showWarningDialog(context: Context, count: Int = 1, onAcceptListener: () -> Unit = {}) {
|
||||||
context.showSessionDialog {
|
context.showSessionDialog {
|
||||||
title(R.string.permissionsRequired)
|
title(R.string.warning)
|
||||||
iconAttribute(R.attr.dialog_alert_icon)
|
iconAttribute(R.attr.dialog_alert_icon)
|
||||||
text(context.getString(R.string.attachmentsWarning))
|
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)
|
button(R.string.cancel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -301,6 +301,12 @@ interface TextSecurePreferences {
|
|||||||
|
|
||||||
const val ALLOW_MESSAGE_REQUESTS = "libsession.ALLOW_MESSAGE_REQUESTS"
|
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
|
@JvmStatic
|
||||||
fun getLastConfigurationSyncTime(context: Context): Long {
|
fun getLastConfigurationSyncTime(context: Context): Long {
|
||||||
return getLongPreference(context, LAST_CONFIGURATION_SYNC_TIME, 0)
|
return getLongPreference(context, LAST_CONFIGURATION_SYNC_TIME, 0)
|
||||||
@ -981,6 +987,19 @@ interface TextSecurePreferences {
|
|||||||
setBooleanPreference(context, FINGERPRINT_KEY_GENERATED, true)
|
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
|
@JvmStatic
|
||||||
fun clearAll(context: Context) {
|
fun clearAll(context: Context) {
|
||||||
getDefaultSharedPreferences(context).edit().clear().commit()
|
getDefaultSharedPreferences(context).edit().clear().commit()
|
||||||
|
Loading…
Reference in New Issue
Block a user