mirror of
https://github.com/bitwarden/android.git
synced 2024-11-27 20:10:33 +03:00
Add initial file chooser (#639)
This commit is contained in:
parent
ebd9628b02
commit
10bf584c90
10 changed files with 295 additions and 17 deletions
|
@ -46,6 +46,16 @@
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
<provider
|
||||||
|
android:name="androidx.core.content.FileProvider"
|
||||||
|
android:authorities="${applicationId}.fileprovider"
|
||||||
|
android:exported="false"
|
||||||
|
android:grantUriPermissions="true">
|
||||||
|
<meta-data
|
||||||
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
|
android:resource="@xml/file_paths" />
|
||||||
|
</provider>
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".data.autofill.BitwardenAutofillService"
|
android:name=".data.autofill.BitwardenAutofillService"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
|
@ -69,4 +79,10 @@
|
||||||
</service>
|
</service>
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
|
<queries>
|
||||||
|
<intent>
|
||||||
|
<action android:name="android.media.action.IMAGE_CAPTURE" />
|
||||||
|
</intent>
|
||||||
|
</queries>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|
|
@ -2,6 +2,9 @@ package com.x8bit.bitwarden.ui.platform.manager.intent
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import androidx.activity.compose.ManagedActivityResultLauncher
|
||||||
|
import androidx.activity.result.ActivityResult
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A manager class for simplifying the handling of Android Intents within a given context.
|
* A manager class for simplifying the handling of Android Intents within a given context.
|
||||||
|
@ -28,8 +31,36 @@ interface IntentManager {
|
||||||
*/
|
*/
|
||||||
fun launchUri(uri: Uri)
|
fun launchUri(uri: Uri)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start an activity using the provided [Intent] and provides a callback, via [onResult], for
|
||||||
|
* retrieving the [ActivityResult].
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun launchActivityForResult(
|
||||||
|
onResult: (ActivityResult) -> Unit,
|
||||||
|
): ManagedActivityResultLauncher<Intent, ActivityResult>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Launches the share sheet with the given [text].
|
* Launches the share sheet with the given [text].
|
||||||
*/
|
*/
|
||||||
fun shareText(text: String)
|
fun shareText(text: String)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Processes the [activityResult] and attempts to get the relevant file data from it.
|
||||||
|
*/
|
||||||
|
fun getFileDataFromIntent(activityResult: ActivityResult): FileData?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an intent for choosing a file saved to disk.
|
||||||
|
*/
|
||||||
|
fun createFileChooserIntent(withCameraIntents: Boolean): Intent
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents file information.
|
||||||
|
*/
|
||||||
|
data class FileData(
|
||||||
|
val fileName: String,
|
||||||
|
val uri: Uri,
|
||||||
|
val sizeBytes: Long,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +1,53 @@
|
||||||
package com.x8bit.bitwarden.ui.platform.manager.intent
|
package com.x8bit.bitwarden.ui.platform.manager.intent
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.ComponentName
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.provider.MediaStore
|
||||||
|
import androidx.activity.compose.ManagedActivityResultLauncher
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.activity.result.ActivityResult
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.browser.customtabs.CustomTabsIntent
|
import androidx.browser.customtabs.CustomTabsIntent
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.content.FileProvider
|
||||||
|
import com.x8bit.bitwarden.BuildConfig
|
||||||
|
import com.x8bit.bitwarden.R
|
||||||
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
|
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
|
||||||
|
import com.x8bit.bitwarden.ui.platform.util.toFormattedPattern
|
||||||
|
import java.io.File
|
||||||
|
import java.time.Clock
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The authority used for pulling in photos from the camera.
|
||||||
|
*
|
||||||
|
* Note: This must match the file provider authority in the manifest.
|
||||||
|
*/
|
||||||
|
private const val FILE_PROVIDER_AUTHORITY: String = "${BuildConfig.APPLICATION_ID}.fileprovider"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Temporary file name for a camera image.
|
||||||
|
*/
|
||||||
|
private const val TEMP_CAMERA_IMAGE_NAME: String = "temp_camera_image.jpg"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This directory must also be declared in file_paths.xml
|
||||||
|
*/
|
||||||
|
private const val TEMP_CAMERA_IMAGE_DIR: String = "camera_temp"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The default implementation of the [IntentManager] for simplifying the handling of Android
|
* The default implementation of the [IntentManager] for simplifying the handling of Android
|
||||||
* Intents within a given context.
|
* Intents within a given context.
|
||||||
*/
|
*/
|
||||||
|
@Suppress("TooManyFunctions")
|
||||||
@OmitFromCoverage
|
@OmitFromCoverage
|
||||||
class IntentManagerImpl(
|
class IntentManagerImpl(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
|
private val clock: Clock = Clock.systemDefaultZone(),
|
||||||
) : IntentManager {
|
) : IntentManager {
|
||||||
|
|
||||||
override fun exitApplication() {
|
override fun exitApplication() {
|
||||||
|
@ -28,6 +63,15 @@ class IntentManagerImpl(
|
||||||
context.startActivity(intent)
|
context.startActivity(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun launchActivityForResult(
|
||||||
|
onResult: (ActivityResult) -> Unit,
|
||||||
|
): ManagedActivityResultLauncher<Intent, ActivityResult> =
|
||||||
|
rememberLauncherForActivityResult(
|
||||||
|
contract = ActivityResultContracts.StartActivityForResult(),
|
||||||
|
onResult = onResult,
|
||||||
|
)
|
||||||
|
|
||||||
override fun startCustomTabsActivity(uri: Uri) {
|
override fun startCustomTabsActivity(uri: Uri) {
|
||||||
CustomTabsIntent
|
CustomTabsIntent
|
||||||
.Builder()
|
.Builder()
|
||||||
|
@ -51,4 +95,98 @@ class IntentManagerImpl(
|
||||||
}
|
}
|
||||||
startActivity(Intent.createChooser(sendIntent, null))
|
startActivity(Intent.createChooser(sendIntent, null))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getFileDataFromIntent(activityResult: ActivityResult): IntentManager.FileData? {
|
||||||
|
if (activityResult.resultCode != Activity.RESULT_OK) return null
|
||||||
|
val uri = activityResult.data?.data
|
||||||
|
return if (uri != null) getLocalFileData(uri) else getCameraFileData()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createFileChooserIntent(withCameraIntents: Boolean): Intent {
|
||||||
|
val chooserIntent = Intent.createChooser(
|
||||||
|
Intent(Intent.ACTION_OPEN_DOCUMENT)
|
||||||
|
.addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
|
.setType("*/*"),
|
||||||
|
ContextCompat.getString(context, R.string.file_source),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (withCameraIntents) {
|
||||||
|
val tmpDir = File(context.filesDir, TEMP_CAMERA_IMAGE_DIR)
|
||||||
|
val file = File(tmpDir, TEMP_CAMERA_IMAGE_NAME)
|
||||||
|
if (!file.exists()) {
|
||||||
|
file.parentFile?.mkdirs()
|
||||||
|
file.createNewFile()
|
||||||
|
}
|
||||||
|
val outputFileUri = FileProvider.getUriForFile(
|
||||||
|
context,
|
||||||
|
FILE_PROVIDER_AUTHORITY,
|
||||||
|
file,
|
||||||
|
)
|
||||||
|
|
||||||
|
chooserIntent.putExtra(
|
||||||
|
Intent.EXTRA_INITIAL_INTENTS,
|
||||||
|
getCameraIntents(outputFileUri).toTypedArray(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return chooserIntent
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getCameraFileData(): IntentManager.FileData {
|
||||||
|
val tmpDir = File(context.filesDir, TEMP_CAMERA_IMAGE_DIR)
|
||||||
|
val file = File(tmpDir, TEMP_CAMERA_IMAGE_NAME)
|
||||||
|
val uri = FileProvider.getUriForFile(context, FILE_PROVIDER_AUTHORITY, file)
|
||||||
|
val fileName = "photo_${clock.instant().toFormattedPattern(pattern = "yyyyMMddHHmmss")}.jpg"
|
||||||
|
return IntentManager.FileData(
|
||||||
|
fileName = fileName,
|
||||||
|
uri = uri,
|
||||||
|
sizeBytes = file.length(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getLocalFileData(uri: Uri): IntentManager.FileData? =
|
||||||
|
context
|
||||||
|
.contentResolver
|
||||||
|
.query(
|
||||||
|
uri,
|
||||||
|
arrayOf(
|
||||||
|
MediaStore.MediaColumns.DISPLAY_NAME,
|
||||||
|
MediaStore.MediaColumns.SIZE,
|
||||||
|
),
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
)
|
||||||
|
?.use { cursor ->
|
||||||
|
if (!cursor.moveToFirst()) return@use null
|
||||||
|
val fileName = cursor
|
||||||
|
.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME)
|
||||||
|
.takeIf { it >= 0 }
|
||||||
|
?.let { cursor.getString(it) }
|
||||||
|
val fileSize = cursor
|
||||||
|
.getColumnIndex(MediaStore.MediaColumns.SIZE)
|
||||||
|
.takeIf { it >= 0 }
|
||||||
|
?.let { cursor.getLong(it) }
|
||||||
|
if (fileName == null || fileSize == null) return@use null
|
||||||
|
IntentManager.FileData(
|
||||||
|
fileName = fileName,
|
||||||
|
uri = uri,
|
||||||
|
sizeBytes = fileSize,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getCameraIntents(outputUri: Uri): List<Intent> {
|
||||||
|
val captureIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
|
||||||
|
return context
|
||||||
|
.packageManager
|
||||||
|
.queryIntentActivities(captureIntent, PackageManager.MATCH_ALL)
|
||||||
|
.map {
|
||||||
|
val packageName = it.activityInfo.packageName
|
||||||
|
Intent(captureIntent).apply {
|
||||||
|
component = ComponentName(packageName, it.activityInfo.name)
|
||||||
|
setPackage(packageName)
|
||||||
|
putExtra(MediaStore.EXTRA_OUTPUT, outputUri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package com.x8bit.bitwarden.ui.tools.feature.send.addsend
|
package com.x8bit.bitwarden.ui.tools.feature.send.addsend
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
import androidx.compose.animation.fadeOut
|
import androidx.compose.animation.fadeOut
|
||||||
|
@ -42,6 +43,7 @@ import com.x8bit.bitwarden.ui.platform.components.BitwardenTextButton
|
||||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenTextField
|
import com.x8bit.bitwarden.ui.platform.components.BitwardenTextField
|
||||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenWideSwitch
|
import com.x8bit.bitwarden.ui.platform.components.BitwardenWideSwitch
|
||||||
import com.x8bit.bitwarden.ui.platform.components.SegmentedButtonState
|
import com.x8bit.bitwarden.ui.platform.components.SegmentedButtonState
|
||||||
|
import com.x8bit.bitwarden.ui.platform.manager.permissions.PermissionsManager
|
||||||
import com.x8bit.bitwarden.ui.platform.theme.LocalNonMaterialTypography
|
import com.x8bit.bitwarden.ui.platform.theme.LocalNonMaterialTypography
|
||||||
import com.x8bit.bitwarden.ui.tools.feature.send.addsend.handlers.AddSendHandlers
|
import com.x8bit.bitwarden.ui.tools.feature.send.addsend.handlers.AddSendHandlers
|
||||||
|
|
||||||
|
@ -54,8 +56,13 @@ fun AddSendContent(
|
||||||
state: AddSendState.ViewState.Content,
|
state: AddSendState.ViewState.Content,
|
||||||
isAddMode: Boolean,
|
isAddMode: Boolean,
|
||||||
addSendHandlers: AddSendHandlers,
|
addSendHandlers: AddSendHandlers,
|
||||||
|
permissionsManager: PermissionsManager,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
|
val chooseFileCameraPermissionLauncher = permissionsManager.getLauncher { isGranted ->
|
||||||
|
addSendHandlers.onChooseFileClick(isGranted)
|
||||||
|
}
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.verticalScroll(rememberScrollState()),
|
.verticalScroll(rememberScrollState()),
|
||||||
|
@ -118,7 +125,13 @@ fun AddSendContent(
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
BitwardenFilledTonalButton(
|
BitwardenFilledTonalButton(
|
||||||
label = stringResource(id = R.string.choose_file),
|
label = stringResource(id = R.string.choose_file),
|
||||||
onClick = addSendHandlers.onChooseFileCLick,
|
onClick = {
|
||||||
|
if (permissionsManager.checkPermission(Manifest.permission.CAMERA)) {
|
||||||
|
addSendHandlers.onChooseFileClick(true)
|
||||||
|
} else {
|
||||||
|
chooseFileCameraPermissionLauncher.launch(Manifest.permission.CAMERA)
|
||||||
|
}
|
||||||
|
},
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 16.dp),
|
.padding(horizontal = 16.dp),
|
||||||
|
|
|
@ -35,7 +35,9 @@ import com.x8bit.bitwarden.ui.platform.components.BitwardenTwoButtonDialog
|
||||||
import com.x8bit.bitwarden.ui.platform.components.LoadingDialogState
|
import com.x8bit.bitwarden.ui.platform.components.LoadingDialogState
|
||||||
import com.x8bit.bitwarden.ui.platform.components.OverflowMenuItemData
|
import com.x8bit.bitwarden.ui.platform.components.OverflowMenuItemData
|
||||||
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
||||||
|
import com.x8bit.bitwarden.ui.platform.manager.permissions.PermissionsManager
|
||||||
import com.x8bit.bitwarden.ui.platform.theme.LocalIntentManager
|
import com.x8bit.bitwarden.ui.platform.theme.LocalIntentManager
|
||||||
|
import com.x8bit.bitwarden.ui.platform.theme.LocalPermissionsManager
|
||||||
import com.x8bit.bitwarden.ui.platform.util.persistentListOfNotNull
|
import com.x8bit.bitwarden.ui.platform.util.persistentListOfNotNull
|
||||||
import com.x8bit.bitwarden.ui.tools.feature.send.addsend.handlers.AddSendHandlers
|
import com.x8bit.bitwarden.ui.tools.feature.send.addsend.handlers.AddSendHandlers
|
||||||
|
|
||||||
|
@ -48,6 +50,7 @@ import com.x8bit.bitwarden.ui.tools.feature.send.addsend.handlers.AddSendHandler
|
||||||
fun AddSendScreen(
|
fun AddSendScreen(
|
||||||
viewModel: AddSendViewModel = hiltViewModel(),
|
viewModel: AddSendViewModel = hiltViewModel(),
|
||||||
intentManager: IntentManager = LocalIntentManager.current,
|
intentManager: IntentManager = LocalIntentManager.current,
|
||||||
|
permissionsManager: PermissionsManager = LocalPermissionsManager.current,
|
||||||
onNavigateBack: () -> Unit,
|
onNavigateBack: () -> Unit,
|
||||||
) {
|
) {
|
||||||
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
|
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
|
||||||
|
@ -55,9 +58,22 @@ fun AddSendScreen(
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val resources = context.resources
|
val resources = context.resources
|
||||||
|
|
||||||
|
val fileChooserLauncher = intentManager.launchActivityForResult { activityResult ->
|
||||||
|
intentManager.getFileDataFromIntent(activityResult)?.let {
|
||||||
|
viewModel.trySendAction(AddSendAction.FileChoose(it))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
EventsEffect(viewModel = viewModel) { event ->
|
EventsEffect(viewModel = viewModel) { event ->
|
||||||
when (event) {
|
when (event) {
|
||||||
is AddSendEvent.NavigateBack -> onNavigateBack()
|
is AddSendEvent.NavigateBack -> onNavigateBack()
|
||||||
|
|
||||||
|
is AddSendEvent.ShowChooserSheet -> {
|
||||||
|
fileChooserLauncher.launch(
|
||||||
|
intentManager.createFileChooserIntent(event.withCameraOption),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
is AddSendEvent.ShowShareSheet -> {
|
is AddSendEvent.ShowShareSheet -> {
|
||||||
intentManager.shareText(event.message)
|
intentManager.shareText(event.message)
|
||||||
}
|
}
|
||||||
|
@ -159,6 +175,7 @@ fun AddSendScreen(
|
||||||
state = viewState,
|
state = viewState,
|
||||||
isAddMode = state.isAddMode,
|
isAddMode = state.isAddMode,
|
||||||
addSendHandlers = remember(viewModel) { AddSendHandlers.create(viewModel) },
|
addSendHandlers = remember(viewModel) { AddSendHandlers.create(viewModel) },
|
||||||
|
permissionsManager = permissionsManager,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -21,6 +21,7 @@ import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
|
||||||
import com.x8bit.bitwarden.ui.platform.base.util.Text
|
import com.x8bit.bitwarden.ui.platform.base.util.Text
|
||||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||||
import com.x8bit.bitwarden.ui.platform.base.util.concat
|
import com.x8bit.bitwarden.ui.platform.base.util.concat
|
||||||
|
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
||||||
import com.x8bit.bitwarden.ui.tools.feature.send.addsend.model.AddSendType
|
import com.x8bit.bitwarden.ui.tools.feature.send.addsend.model.AddSendType
|
||||||
import com.x8bit.bitwarden.ui.tools.feature.send.addsend.util.toSendView
|
import com.x8bit.bitwarden.ui.tools.feature.send.addsend.util.toSendView
|
||||||
import com.x8bit.bitwarden.ui.tools.feature.send.addsend.util.toViewState
|
import com.x8bit.bitwarden.ui.tools.feature.send.addsend.util.toViewState
|
||||||
|
@ -119,6 +120,7 @@ class AddSendViewModel @Inject constructor(
|
||||||
override fun handleAction(action: AddSendAction): Unit = when (action) {
|
override fun handleAction(action: AddSendAction): Unit = when (action) {
|
||||||
AddSendAction.CopyLinkClick -> handleCopyLinkClick()
|
AddSendAction.CopyLinkClick -> handleCopyLinkClick()
|
||||||
AddSendAction.DeleteClick -> handleDeleteClick()
|
AddSendAction.DeleteClick -> handleDeleteClick()
|
||||||
|
is AddSendAction.FileChoose -> handeFileChose(action)
|
||||||
AddSendAction.RemovePasswordClick -> handleRemovePasswordClick()
|
AddSendAction.RemovePasswordClick -> handleRemovePasswordClick()
|
||||||
AddSendAction.ShareLinkClick -> handleShareLinkClick()
|
AddSendAction.ShareLinkClick -> handleShareLinkClick()
|
||||||
is AddSendAction.CloseClick -> handleCloseClick()
|
is AddSendAction.CloseClick -> handleCloseClick()
|
||||||
|
@ -129,7 +131,7 @@ class AddSendViewModel @Inject constructor(
|
||||||
is AddSendAction.SaveClick -> handleSaveClick()
|
is AddSendAction.SaveClick -> handleSaveClick()
|
||||||
is AddSendAction.FileTypeClick -> handleFileTypeClick()
|
is AddSendAction.FileTypeClick -> handleFileTypeClick()
|
||||||
is AddSendAction.TextTypeClick -> handleTextTypeClick()
|
is AddSendAction.TextTypeClick -> handleTextTypeClick()
|
||||||
is AddSendAction.ChooseFileClick -> handleChooseFileClick()
|
is AddSendAction.ChooseFileClick -> handleChooseFileClick(action)
|
||||||
is AddSendAction.NameChange -> handleNameChange(action)
|
is AddSendAction.NameChange -> handleNameChange(action)
|
||||||
is AddSendAction.MaxAccessCountChange -> handleMaxAccessCountChange(action)
|
is AddSendAction.MaxAccessCountChange -> handleMaxAccessCountChange(action)
|
||||||
is AddSendAction.TextChange -> handleTextChange(action)
|
is AddSendAction.TextChange -> handleTextChange(action)
|
||||||
|
@ -354,6 +356,11 @@ class AddSendViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handeFileChose(action: AddSendAction.FileChoose) {
|
||||||
|
// TODO: Process the chosen file (BIT-493)
|
||||||
|
sendEvent(AddSendEvent.ShowToast("Not Yet Implemented".asText()))
|
||||||
|
}
|
||||||
|
|
||||||
private fun handleRemovePasswordClick() {
|
private fun handleRemovePasswordClick() {
|
||||||
onEdit {
|
onEdit {
|
||||||
mutableStateFlow.update {
|
mutableStateFlow.update {
|
||||||
|
@ -511,9 +518,8 @@ class AddSendViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleChooseFileClick() {
|
private fun handleChooseFileClick(action: AddSendAction.ChooseFileClick) {
|
||||||
// TODO: allow for file upload: BIT-1085
|
sendEvent(AddSendEvent.ShowChooserSheet(action.isCameraPermissionGranted))
|
||||||
sendEvent(AddSendEvent.ShowToast("Not Implemented: File Upload".asText()))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleMaxAccessCountChange(action: AddSendAction.MaxAccessCountChange) {
|
private fun handleMaxAccessCountChange(action: AddSendAction.MaxAccessCountChange) {
|
||||||
|
@ -721,6 +727,11 @@ sealed class AddSendEvent {
|
||||||
*/
|
*/
|
||||||
data object NavigateBack : AddSendEvent()
|
data object NavigateBack : AddSendEvent()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show file chooser sheet.
|
||||||
|
*/
|
||||||
|
data class ShowChooserSheet(val withCameraOption: Boolean) : AddSendEvent()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show share sheet.
|
* Show share sheet.
|
||||||
*/
|
*/
|
||||||
|
@ -737,6 +748,11 @@ sealed class AddSendEvent {
|
||||||
*/
|
*/
|
||||||
sealed class AddSendAction {
|
sealed class AddSendAction {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User has chosen a file to be part of the send.
|
||||||
|
*/
|
||||||
|
data class FileChoose(val fileData: IntentManager.FileData) : AddSendAction()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* User clicked the remove password button.
|
* User clicked the remove password button.
|
||||||
*/
|
*/
|
||||||
|
@ -805,7 +821,9 @@ sealed class AddSendAction {
|
||||||
/**
|
/**
|
||||||
* User clicked the choose file button.
|
* User clicked the choose file button.
|
||||||
*/
|
*/
|
||||||
data object ChooseFileClick : AddSendAction()
|
data class ChooseFileClick(
|
||||||
|
val isCameraPermissionGranted: Boolean,
|
||||||
|
) : AddSendAction()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* User toggled the "hide text by default" toggle.
|
* User toggled the "hide text by default" toggle.
|
||||||
|
|
|
@ -12,7 +12,7 @@ data class AddSendHandlers(
|
||||||
val onNamChange: (String) -> Unit,
|
val onNamChange: (String) -> Unit,
|
||||||
val onFileTypeSelect: () -> Unit,
|
val onFileTypeSelect: () -> Unit,
|
||||||
val onTextTypeSelect: () -> Unit,
|
val onTextTypeSelect: () -> Unit,
|
||||||
val onChooseFileCLick: () -> Unit,
|
val onChooseFileClick: (hasPermission: Boolean) -> Unit,
|
||||||
val onTextChange: (String) -> Unit,
|
val onTextChange: (String) -> Unit,
|
||||||
val onIsHideByDefaultToggle: (Boolean) -> Unit,
|
val onIsHideByDefaultToggle: (Boolean) -> Unit,
|
||||||
val onMaxAccessCountChange: (Int) -> Unit,
|
val onMaxAccessCountChange: (Int) -> Unit,
|
||||||
|
@ -36,7 +36,7 @@ data class AddSendHandlers(
|
||||||
onNamChange = { viewModel.trySendAction(AddSendAction.NameChange(it)) },
|
onNamChange = { viewModel.trySendAction(AddSendAction.NameChange(it)) },
|
||||||
onFileTypeSelect = { viewModel.trySendAction(AddSendAction.FileTypeClick) },
|
onFileTypeSelect = { viewModel.trySendAction(AddSendAction.FileTypeClick) },
|
||||||
onTextTypeSelect = { viewModel.trySendAction(AddSendAction.TextTypeClick) },
|
onTextTypeSelect = { viewModel.trySendAction(AddSendAction.TextTypeClick) },
|
||||||
onChooseFileCLick = { viewModel.trySendAction(AddSendAction.ChooseFileClick) },
|
onChooseFileClick = { viewModel.trySendAction(AddSendAction.ChooseFileClick(it)) },
|
||||||
onTextChange = { viewModel.trySendAction(AddSendAction.TextChange(it)) },
|
onTextChange = { viewModel.trySendAction(AddSendAction.TextChange(it)) },
|
||||||
onIsHideByDefaultToggle = {
|
onIsHideByDefaultToggle = {
|
||||||
viewModel.trySendAction(AddSendAction.HideByDefaultToggle(it))
|
viewModel.trySendAction(AddSendAction.HideByDefaultToggle(it))
|
||||||
|
|
9
app/src/main/res/xml/file_paths.xml
Normal file
9
app/src/main/res/xml/file_paths.xml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<paths>
|
||||||
|
<cache-path
|
||||||
|
name="cache"
|
||||||
|
path="." />
|
||||||
|
<files-path
|
||||||
|
name="temp_camera_images"
|
||||||
|
path="camera_temp/" />
|
||||||
|
</paths>
|
|
@ -24,6 +24,7 @@ import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFl
|
||||||
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
|
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
|
||||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||||
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
||||||
|
import com.x8bit.bitwarden.ui.platform.manager.permissions.FakePermissionManager
|
||||||
import com.x8bit.bitwarden.ui.tools.feature.send.addsend.model.AddSendType
|
import com.x8bit.bitwarden.ui.tools.feature.send.addsend.model.AddSendType
|
||||||
import com.x8bit.bitwarden.ui.util.isEditableText
|
import com.x8bit.bitwarden.ui.util.isEditableText
|
||||||
import com.x8bit.bitwarden.ui.util.isProgressBar
|
import com.x8bit.bitwarden.ui.util.isProgressBar
|
||||||
|
@ -44,7 +45,8 @@ class AddSendScreenTest : BaseComposeTest() {
|
||||||
|
|
||||||
private var onNavigateBackCalled = false
|
private var onNavigateBackCalled = false
|
||||||
|
|
||||||
private val intentManager: IntentManager = mockk {
|
private val permissionsManager = FakePermissionManager()
|
||||||
|
private val intentManager: IntentManager = mockk(relaxed = true) {
|
||||||
every { shareText(any()) } just runs
|
every { shareText(any()) } just runs
|
||||||
}
|
}
|
||||||
private val mutableEventFlow = bufferedMutableSharedFlow<AddSendEvent>()
|
private val mutableEventFlow = bufferedMutableSharedFlow<AddSendEvent>()
|
||||||
|
@ -60,6 +62,7 @@ class AddSendScreenTest : BaseComposeTest() {
|
||||||
AddSendScreen(
|
AddSendScreen(
|
||||||
viewModel = viewModel,
|
viewModel = viewModel,
|
||||||
intentManager = intentManager,
|
intentManager = intentManager,
|
||||||
|
permissionsManager = permissionsManager,
|
||||||
onNavigateBack = { onNavigateBackCalled = true },
|
onNavigateBack = { onNavigateBackCalled = true },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -329,7 +332,8 @@ class AddSendScreenTest : BaseComposeTest() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `Choose file button click should send ChooseFileClick`() {
|
fun `Choose file button click with permission should send ChooseFileClick`() {
|
||||||
|
permissionsManager.checkPermissionResult = true
|
||||||
mutableStateFlow.value = DEFAULT_STATE.copy(
|
mutableStateFlow.value = DEFAULT_STATE.copy(
|
||||||
viewState = DEFAULT_VIEW_STATE.copy(
|
viewState = DEFAULT_VIEW_STATE.copy(
|
||||||
selectedType = AddSendState.ViewState.Content.SendType.File,
|
selectedType = AddSendState.ViewState.Content.SendType.File,
|
||||||
|
@ -339,7 +343,32 @@ class AddSendScreenTest : BaseComposeTest() {
|
||||||
.onNodeWithText("Choose file")
|
.onNodeWithText("Choose file")
|
||||||
.performScrollTo()
|
.performScrollTo()
|
||||||
.performClick()
|
.performClick()
|
||||||
verify { viewModel.trySendAction(AddSendAction.ChooseFileClick) }
|
verify {
|
||||||
|
viewModel.trySendAction(
|
||||||
|
AddSendAction.ChooseFileClick(isCameraPermissionGranted = true),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("MaxLineLength")
|
||||||
|
@Test
|
||||||
|
fun `Choose file button click without permission should request permission and send ChooseFileClick`() {
|
||||||
|
permissionsManager.checkPermissionResult = false
|
||||||
|
permissionsManager.getPermissionsResult = false
|
||||||
|
mutableStateFlow.value = DEFAULT_STATE.copy(
|
||||||
|
viewState = DEFAULT_VIEW_STATE.copy(
|
||||||
|
selectedType = AddSendState.ViewState.Content.SendType.File,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithText("Choose file")
|
||||||
|
.performScrollTo()
|
||||||
|
.performClick()
|
||||||
|
verify {
|
||||||
|
viewModel.trySendAction(
|
||||||
|
AddSendAction.ChooseFileClick(isCameraPermissionGranted = false),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
@ -581,14 +581,21 @@ class AddSendViewModelTest : BaseViewModelTest() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `ChooseFileClick should emit ShowToast`() = runTest {
|
fun `FileChose should emit ShowToast`() = runTest {
|
||||||
val viewModel = createViewModel()
|
val viewModel = createViewModel()
|
||||||
viewModel.eventFlow.test {
|
viewModel.eventFlow.test {
|
||||||
viewModel.trySendAction(AddSendAction.ChooseFileClick)
|
viewModel.trySendAction(AddSendAction.FileChoose(fileData = mockk()))
|
||||||
assertEquals(
|
assertEquals(AddSendEvent.ShowToast("Not Yet Implemented".asText()), awaitItem())
|
||||||
AddSendEvent.ShowToast("Not Implemented: File Upload".asText()),
|
}
|
||||||
awaitItem(),
|
}
|
||||||
)
|
|
||||||
|
@Test
|
||||||
|
fun `ChooseFileClick should emit ShowToast`() = runTest {
|
||||||
|
val arePermissionsGranted = true
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
viewModel.eventFlow.test {
|
||||||
|
viewModel.trySendAction(AddSendAction.ChooseFileClick(arePermissionsGranted))
|
||||||
|
assertEquals(AddSendEvent.ShowChooserSheet(arePermissionsGranted), awaitItem())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue