Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import com.ramcosta.composedestinations.spec.Direction
import com.wire.android.BuildConfig
import com.wire.android.R
import com.wire.android.util.EmailComposer
import com.wire.android.util.externalShareChooserIntent
import com.wire.android.util.getDeviceIdString
import com.wire.android.util.getGitBuildId
import com.wire.android.util.sha256
Expand Down Expand Up @@ -100,7 +101,7 @@ object GiveFeedbackDestination : IntentDirection {
)
)
intent.selector = Intent(Intent.ACTION_SENDTO).setData(Uri.parse("mailto:"))
return Intent.createChooser(intent, context.getString(R.string.send_feedback_choose_email))
return context.externalShareChooserIntent(intent, context.getString(R.string.send_feedback_choose_email))
}

override val route: String
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ package com.wire.android.ui.userprofile.qr
import android.content.Context
import android.content.Intent
import android.net.Uri
import com.wire.android.util.externalShareChooserIntent

fun Context.shareLinkToProfile(selfProfileUrl: String) {
val sendIntent: Intent =
Expand All @@ -31,8 +32,7 @@ fun Context.shareLinkToProfile(selfProfileUrl: String) {
type = "text/plain"
}

val shareIntent = Intent.createChooser(sendIntent, null)
startActivity(shareIntent)
startActivity(externalShareChooserIntent(sendIntent))
}

fun Context.shareQRToProfile(uri: Uri) {
Expand All @@ -41,8 +41,9 @@ fun Context.shareQRToProfile(uri: Uri) {
action = Intent.ACTION_SEND
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
putExtra(Intent.EXTRA_STREAM, uri)
type = "image/jpg"
}
startActivity(sendIntent)
startActivity(externalShareChooserIntent(sendIntent))
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ package com.wire.android.util

import android.content.Context
import android.net.Uri
import androidx.core.content.FileProvider
import androidx.core.net.toUri
import okio.Path
import dev.zacsweers.metro.Inject
Expand All @@ -33,6 +32,6 @@ class AvatarImageManager @Inject constructor(val context: Context) {
}

fun getShareableTempAvatarUri(filePath: Path): Uri {
return FileProvider.getUriForFile(context, context.getProviderAuthority(), filePath.toFile())
return context.shareableFileProviderUri(context.fileProviderSharedCacheFile(filePath.name))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you clarify this part? It looks like we're creating a new file, but I can't see where it's populated with the avatar data

}
}
123 changes: 106 additions & 17 deletions app/src/main/kotlin/com/wire/android/util/FileUtil.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,21 @@

package com.wire.android.util

import android.app.ActivityOptions
import android.app.DownloadManager
import android.content.ActivityNotFoundException
import android.content.ComponentName
import android.content.ContentResolver
import android.content.ContentValues
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.database.Cursor
import android.media.MediaMetadataRetriever
import android.media.MediaMetadataRetriever.METADATA_KEY_DURATION
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.os.Environment.DIRECTORY_DOWNLOADS
import android.os.Parcelable
Expand Down Expand Up @@ -62,6 +66,7 @@ import java.io.File
import java.io.FileNotFoundException
import java.io.IOException
import java.io.InputStream
import java.nio.file.Files
import java.util.Locale
import kotlin.time.Duration.Companion.milliseconds

Expand All @@ -73,9 +78,9 @@ suspend fun Uri.toByteArray(context: Context, dispatcher: DispatcherProvider = D
}

fun getTempWritableAttachmentUri(context: Context, attachmentPath: Path): Uri {
val file = attachmentPath.toFile()
val file = context.fileProviderSharedCacheFile(attachmentPath.name)
file.setWritable(true)
return FileProvider.getUriForFile(context, context.getProviderAuthority(), file)
return context.fileProviderUri(file)
}

suspend fun createPemFile(
Expand Down Expand Up @@ -191,7 +196,7 @@ private fun Context.saveFileDataToMediaFolder(assetName: String, downloadedDataP
fun Context.fromNioPathToContentUri(nioPath: java.nio.file.Path): Uri = this.pathToUri(nioPath.toOkioPath(), null)

fun Context.pathToUri(assetDataPath: Path, assetName: String?): Uri =
FileProvider.getUriForFile(this, getProviderAuthority(), assetDataPath.toFile(), assetName ?: assetDataPath.name)
shareableFileProviderUri(assetDataPath.toFile(), assetName ?: assetDataPath.name)

fun Uri.getMimeType(context: Context): String? {
val mimeType: String? = if (this.scheme == ContentResolver.SCHEME_CONTENT) {
Expand Down Expand Up @@ -264,13 +269,7 @@ private fun Context.getContentFileName(uri: Uri): String? = runCatching {
}.getOrNull()

fun Context.startFileShareIntent(path: Path, assetName: String?) {
val assetDisplayName = assetName ?: path.name
val fileURI = FileProvider.getUriForFile(
this,
getProviderAuthority(),
path.toFile(),
assetDisplayName
)
val fileURI = fileShareUri(path, assetName)
val shareIntent = Intent(Intent.ACTION_SEND)
shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
shareIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
Expand All @@ -280,9 +279,12 @@ fun Context.startFileShareIntent(path: Path, assetName: String?) {
shareIntent.putExtra(Intent.EXTRA_STREAM, fileURI)
assetName?.let { shareIntent.putExtra(Intent.EXTRA_SUBJECT, it) }
shareIntent.type = fileURI.getMimeType(context = this)
startActivity(shareIntent)
startShareIntentWithTrustedWireTarget(shareIntent)
}

fun Context.fileShareUri(path: Path, assetName: String?): Uri =
shareableFileProviderUri(path.toFile(), assetName ?: path.name)

fun saveFileToDownloadsFolder(assetName: String, assetDataPath: Path, assetDataSize: Long, context: Context): Uri? =
context.saveFileDataToDownloadsFolder(assetName, assetDataPath, assetDataSize)

Expand All @@ -302,11 +304,7 @@ fun Context.getUrisOfFilesInDirectory(dir: File): ArrayList<Uri> {
val files = ArrayList<Uri>()

dir.listFiles()?.map {
val uri = FileProvider.getUriForFile(
this,
getProviderAuthority(),
it
)
val uri = shareableFileProviderUri(it)
files.add(uri)
}

Expand Down Expand Up @@ -377,7 +375,7 @@ fun shareAssetFileWithExternalApp(assetDataPath: Path, context: Context, assetNa
setDataAndType(assetUri, mimeType)
putExtra(Intent.EXTRA_STREAM, assetUri)
}
context.startActivity(intent)
context.startActivity(context.externalShareChooserIntent(intent))
} catch (e: java.lang.IllegalArgumentException) {
appLogger.e("The file couldn't be found on the internal storage \n$e")
onError()
Expand All @@ -387,6 +385,94 @@ fun shareAssetFileWithExternalApp(assetDataPath: Path, context: Context, assetNa
}
}

fun Context.externalShareChooserIntent(
sendIntent: Intent,
title: CharSequence? = null,
excludeOwnComponents: Boolean = true
): Intent =
Intent.createChooser(sendIntent, title).apply {
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
if (!excludeOwnComponents) return@apply
val ownShareComponents = packageManager.queryIntentActivitiesCompat(sendIntent)
.map { ComponentName(it.activityInfo.packageName, it.activityInfo.name) }
.filter { it.packageName == packageName }
.toTypedArray()
if (ownShareComponents.isNotEmpty()) {
putExtra(Intent.EXTRA_EXCLUDE_COMPONENTS, ownShareComponents)
}
}
Comment on lines +396 to +404

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this would read a bit more naturally as:

if (excludeOwnComponents) {
    ...
}

instead of if (!excludeOwnComponents) return@apply

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is intentional to return early avoiding nested logic

if (excludeOwnComponents) {
    ...
        if (ownShareComponents.isNotEmpty()) {
            ...
        }
}


fun Context.startShareIntentWithTrustedWireTarget(sendIntent: Intent, title: CharSequence? = null) {
val chooserIntent = externalShareChooserIntent(
sendIntent = sendIntent,
title = title,
excludeOwnComponents = !supportsTrustedWireShareCaller()
)
startActivity(chooserIntent, shareIdentityOptionsBundle())
}

fun supportsTrustedWireShareCaller(): Boolean = Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM

private fun shareIdentityOptionsBundle(): Bundle? =
if (supportsTrustedWireShareCaller()) {
ActivityOptions.makeBasic()
.setShareIdentityEnabled(true)
.toBundle()
} else {
null
}

fun Context.fileProviderSharedCacheFile(fileName: String): File {
val shareDirectory = fileProviderSharedCacheDirectory()
deleteStaleFileProviderSharedCacheFiles(shareDirectory)
return File(shareDirectory, findFirstUniqueName(shareDirectory, fileName.ifBlank { ATTACHMENT_FILENAME }))
}

fun Context.shareableFileProviderUri(sourceFile: File, displayName: String? = null): Uri {
val shareFile = when {
sourceFile.isInDirectory(fileProviderSharedCacheDirectory()) -> sourceFile
else -> linkOrCopyToFileProviderSharedCache(sourceFile, displayName ?: sourceFile.name)
}
return fileProviderUri(shareFile, displayName)
}

private fun Context.linkOrCopyToFileProviderSharedCache(sourceFile: File, displayName: String): File {
val shareFile = fileProviderSharedCacheFile(displayName)
runCatching {
Files.createLink(shareFile.toPath(), sourceFile.toPath())
}.recoverCatching {
sourceFile.copyTo(shareFile, overwrite = true)
}.getOrThrow()
return shareFile
}

private fun Context.fileProviderSharedCacheDirectory(): File =
File(cacheDir, FILE_PROVIDER_SHARED_CACHE_DIRECTORY).apply { mkdirs() }

private fun Context.fileProviderUri(file: File, displayName: String? = null): Uri =
FileProvider.getUriForFile(this, getProviderAuthority(), file, displayName ?: file.name)

private fun File.isInDirectory(directory: File): Boolean {
val directoryPath = directory.canonicalFile.toPath()
return canonicalFile.toPath().startsWith(directoryPath)
}

private fun deleteStaleFileProviderSharedCacheFiles(directory: File) {
val oldestAllowedTimestamp = System.currentTimeMillis() - FILE_PROVIDER_SHARED_CACHE_MAX_AGE_MILLIS
directory.listFiles()
?.filter { it.isFile && it.lastModified() < oldestAllowedTimestamp }
?.forEach(File::delete)
}

private fun PackageManager.queryIntentActivitiesCompat(intent: Intent) =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
queryIntentActivities(intent, PackageManager.ResolveInfoFlags.of(PackageManager.MATCH_DEFAULT_ONLY.toLong()))
} else {
@Suppress("DEPRECATION")
queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY)
}

inline fun <reified T : Parcelable> Intent.parcelable(key: String): T? = when {
Build.VERSION.SDK_INT >= SDK_VERSION -> getParcelableExtra(key, T::class.java)
else -> @Suppress("DEPRECATION") getParcelableExtra(key) as? T
Expand Down Expand Up @@ -498,5 +584,8 @@ fun getAudioLengthInMs(dataPath: Path, mimeType: String): Long =

private const val ATTACHMENT_FILENAME = "attachment"
private const val DATA_COPY_BUFFER_SIZE = 2048
const val FILE_PROVIDER_SHARED_FILES_ROOT = "shared_files"
private const val FILE_PROVIDER_SHARED_CACHE_DIRECTORY = "file-provider-shares"
private const val FILE_PROVIDER_SHARED_CACHE_MAX_AGE_MILLIS = 24 * 60 * 60 * 1000L
const val SDK_VERSION = 33
const val SUPPORTED_AUDIO_MIME_TYPE = "audio/wav"
21 changes: 17 additions & 4 deletions app/src/main/res/xml/provider_paths.xml
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,21 @@

<paths>
<cache-path
name="cached_files"
path="." />
<files-path name="files" path="." />
<external-path name="external_files" path="."/>
name="shared_files"
path="file-provider-shares/" />
<external-path
name="external_downloads"
path="Download/" />
<external-path
name="external_pictures"
path="Pictures/" />
<external-path
name="external_movies"
path="Movies/" />
<external-path
name="external_music"
path="Music/" />
<external-path
name="external_documents"
path="Documents/" />
</paths>
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@
package com.wire.android.feature.cells.util

import android.content.ActivityNotFoundException
import android.content.ComponentName
import android.content.ContentValues
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Environment
import android.provider.MediaStore
Expand Down Expand Up @@ -116,7 +118,9 @@ class FileHelper @Inject constructor(
putExtra(Intent.EXTRA_STREAM, assetUri)
}
val chooserIntent = Intent.createChooser(intent, null).apply {
excludeOwnShareTargets(intent)
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
context.startActivity(chooserIntent)
} catch (e: java.lang.IllegalArgumentException) {
Expand All @@ -134,6 +138,7 @@ class FileHelper @Inject constructor(
putExtra(Intent.EXTRA_TEXT, url)
}
val chooserIntent = Intent.createChooser(intent, null).apply {
excludeOwnShareTargets(intent)
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
context.startActivity(chooserIntent)
Expand All @@ -156,4 +161,22 @@ class FileHelper @Inject constructor(

private fun Context.pathToUri(assetDataPath: Path, assetName: String?): Uri =
FileProvider.getUriForFile(this, getProviderAuthority(), assetDataPath.toFile(), assetName ?: assetDataPath.name)

private fun Intent.excludeOwnShareTargets(sendIntent: Intent) {
val ownShareComponents = context.packageManager.queryIntentActivitiesCompat(sendIntent)
.map { ComponentName(it.activityInfo.packageName, it.activityInfo.name) }
.filter { it.packageName == context.packageName }
.toTypedArray()
if (ownShareComponents.isNotEmpty()) {
putExtra(Intent.EXTRA_EXCLUDE_COMPONENTS, ownShareComponents)
}
}

private fun PackageManager.queryIntentActivitiesCompat(intent: Intent) =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
queryIntentActivities(intent, PackageManager.ResolveInfoFlags.of(PackageManager.MATCH_DEFAULT_ONLY.toLong()))
} else {
@Suppress("DEPRECATION")
queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY)
}
}
Loading