Skip to content

Commit

Permalink
SS-72 Update save attachment models + add one-time warning that other…
Browse files Browse the repository at this point in the history
… apps can access saved attachments
  • Loading branch information
alansley committed Aug 14, 2024
1 parent 0c83606 commit 621094e
Show file tree
Hide file tree
Showing 5 changed files with 117 additions and 36 deletions.
2 changes: 1 addition & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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"/>
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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"

Expand Down Expand Up @@ -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

Expand All @@ -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()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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()
Expand Down

0 comments on commit 621094e

Please sign in to comment.