From 235bce8e2a4d50e86ca81ba0b4fd3bdb2ebdda31 Mon Sep 17 00:00:00 2001 From: JRoberts <8711996+jr-simplex@users.noreply.github.com> Date: Sat, 7 May 2022 16:25:04 +0400 Subject: [PATCH] android: remove unnecessary READ_EXTERNAL_STORAGE permission request, open image picker in Gallery; IMG timestamp format (#610) --- apps/android/app/src/main/AndroidManifest.xml | 1 - .../java/chat/simplex/app/model/ChatModel.kt | 7 +- .../simplex/app/views/chat/SendMsgView.kt | 4 +- .../simplex/app/views/chat/item/CIFileView.kt | 2 +- .../app/views/chat/item/CIImageView.kt | 4 +- .../app/views/chat/item/ChatItemView.kt | 2 +- .../simplex/app/views/helpers/GetImageView.kt | 67 ++++++------------- .../chat/simplex/app/views/helpers/Share.kt | 2 +- .../chat/simplex/app/views/helpers/Util.kt | 13 ++-- apps/ios/Shared/FileUtils.swift | 21 ++++-- apps/ios/Shared/Model/Shared/ChatTypes.swift | 7 +- apps/ios/Shared/Views/Chat/ChatView.swift | 6 +- .../ios/Shared/Views/Helpers/CIFileView.swift | 2 +- .../Shared/Views/Helpers/CIImageView.swift | 2 +- 14 files changed, 67 insertions(+), 73 deletions(-) diff --git a/apps/android/app/src/main/AndroidManifest.xml b/apps/android/app/src/main/AndroidManifest.xml index 3c2b3d945..113ffa93e 100644 --- a/apps/android/app/src/main/AndroidManifest.xml +++ b/apps/android/app/src/main/AndroidManifest.xml @@ -8,7 +8,6 @@ - diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt index 56e69929b..acf8469c8 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt @@ -851,13 +851,16 @@ class CIFile( val filePath: String? = null, val fileStatus: CIFileStatus ) { - val stored: Boolean = when (fileStatus) { + val loaded: Boolean = when (fileStatus) { CIFileStatus.SndStored -> true CIFileStatus.SndTransfer -> true CIFileStatus.SndComplete -> true CIFileStatus.SndCancelled -> true + CIFileStatus.RcvInvitation -> false + CIFileStatus.RcvAccepted -> false + CIFileStatus.RcvTransfer -> false + CIFileStatus.RcvCancelled -> false CIFileStatus.RcvComplete -> true - else -> false } companion object { diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/SendMsgView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/SendMsgView.kt index 74fee4740..08f2672a1 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/SendMsgView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/SendMsgView.kt @@ -73,7 +73,7 @@ fun SendMsgView( .size(36.dp) .padding(4.dp), color = HighOrLowlight, - strokeWidth = 4.dp + strokeWidth = 3.dp ) } else { Icon( @@ -149,7 +149,7 @@ fun PreviewSendMsgViewEditing() { fun PreviewSendMsgViewInProgress() { val smallFont = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onBackground) val textStyle = remember { mutableStateOf(smallFont) } - val composeStateInProgress = ComposeState(inProgress = true) + val composeStateInProgress = ComposeState(preview = ComposePreview.FilePreview("test.txt"), inProgress = true) SimpleXTheme { SendMsgView( composeState = remember { mutableStateOf(composeStateInProgress) }, diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIFileView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIFileView.kt index 35a0c22a7..6553a2d7f 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIFileView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIFileView.kt @@ -89,7 +89,7 @@ fun CIFileView( String.format(generalGetString(R.string.file_will_be_received_when_contact_is_online), MAX_FILE_SIZE) ) CIFileStatus.RcvComplete -> { - val filePath = getStoredFilePath(context, file) + val filePath = getLoadedFilePath(context, file) if (filePath != null) { saveFileLauncher.launch(file.fileName) } else { diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIImageView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIImageView.kt index 918c429ea..e3f342dea 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIImageView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIImageView.kt @@ -86,10 +86,10 @@ fun CIImageView( Box(contentAlignment = Alignment.TopEnd) { val context = LocalContext.current - val imageBitmap: Bitmap? = getStoredImage(context, file) + val imageBitmap: Bitmap? = getLoadedImage(context, file) if (imageBitmap != null) { imageView(imageBitmap, onClick = { - if (getStoredFilePath(context, file) != null) { + if (getLoadedFilePath(context, file) != null) { ModalManager.shared.showCustomModal { close -> ImageFullScreenView(imageBitmap, close) } } }) diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/ChatItemView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/ChatItemView.kt index 1d43b0afd..8aed21baf 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/ChatItemView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/ChatItemView.kt @@ -88,7 +88,7 @@ fun ChatItemView( showMenu.value = false }) if (cItem.content.msgContent is MsgContent.MCImage || cItem.content.msgContent is MsgContent.MCFile) { - val filePath = getStoredFilePath(context, cItem.file) + val filePath = getLoadedFilePath(context, cItem.file) if (filePath != null) { ItemAction(stringResource(R.string.save_verb), Icons.Outlined.SaveAlt, onClick = { saveFileLauncher.launch(cItem.file?.fileName) diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/GetImageView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/GetImageView.kt index 095c3ef2e..14b7adbb8 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/GetImageView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/GetImageView.kt @@ -19,7 +19,8 @@ import androidx.annotation.CallSuper import androidx.compose.foundation.layout.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.* -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState import androidx.compose.ui.Modifier import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.platform.LocalContext @@ -126,19 +127,17 @@ class CustomTakePicturePreview: ActivityResultContract() { } } } -//class GetGalleryContent: ActivityResultContracts.GetContent() { -// override fun createIntent(context: Context, input: String): Intent { -// return super.createIntent(context, input).apply { -// Log.e(TAG, "########################################################### in GetGalleryContent") -// uri = DocumentsContract.buildDocumentUriUsingTree(uri, DocumentsContract.getTreeDocumentId(uri)) -// putExtra(DocumentsContract.EXTRA_INITIAL_URI, Environment.DIRECTORY_PICTURES) -// } -// } -//} + +class GetGalleryContent: ActivityResultContracts.GetContent() { + override fun createIntent(context: Context, input: String): Intent { + super.createIntent(context, input) + return Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI) + } +} + @Composable -fun rememberGetContentLauncher(cb: (Uri?) -> Unit): ManagedActivityResultLauncher = -// rememberLauncherForActivityResult(contract = GetGalleryContent(), cb) - rememberLauncherForActivityResult(contract = ActivityResultContracts.GetContent(), cb) +fun rememberGalleryLauncher(cb: (Uri?) -> Unit): ManagedActivityResultLauncher = + rememberLauncherForActivityResult(contract = GetGalleryContent(), cb) @Composable fun rememberCameraLauncher(cb: (Bitmap?) -> Unit): ManagedActivityResultLauncher = @@ -148,6 +147,10 @@ fun rememberCameraLauncher(cb: (Bitmap?) -> Unit): ManagedActivityResultLauncher fun rememberPermissionLauncher(cb: (Boolean) -> Unit): ManagedActivityResultLauncher = rememberLauncherForActivityResult(contract = ActivityResultContracts.RequestPermission(), cb) +@Composable +fun rememberGetContentLauncher(cb: (Uri?) -> Unit): ManagedActivityResultLauncher = + rememberLauncherForActivityResult(contract = ActivityResultContracts.GetContent(), cb) + @Composable fun GetImageBottomSheet( imageBitmap: MutableState, @@ -157,8 +160,7 @@ fun GetImageBottomSheet( hideBottomSheet: () -> Unit ) { val context = LocalContext.current - val isCameraSelected = remember { mutableStateOf(false) } - val galleryLauncher = rememberGetContentLauncher { uri: Uri? -> + val galleryLauncher = rememberGalleryLauncher { uri: Uri? -> if (uri != null) { val source = ImageDecoder.createSource(context.contentResolver, uri) val bitmap = ImageDecoder.decodeBitmap(source) @@ -174,8 +176,7 @@ fun GetImageBottomSheet( } val permissionLauncher = rememberPermissionLauncher { isGranted: Boolean -> if (isGranted) { - if (isCameraSelected.value) cameraLauncher.launch(null) - else galleryLauncher.launch("image/*") + cameraLauncher.launch(null) hideBottomSheet() } else { Toast.makeText(context, generalGetString(R.string.toast_permission_denied), Toast.LENGTH_SHORT).show() @@ -195,14 +196,6 @@ fun GetImageBottomSheet( } } } - val filesPermissionLauncher = rememberPermissionLauncher { isGranted: Boolean -> - if (isGranted) { - filesLauncher.launch("*/*") - hideBottomSheet() - } else { - Toast.makeText(context, generalGetString(R.string.toast_permission_denied), Toast.LENGTH_SHORT).show() - } - } Box( modifier = Modifier @@ -225,34 +218,18 @@ fun GetImageBottomSheet( hideBottomSheet() } else -> { - isCameraSelected.value = true permissionLauncher.launch(Manifest.permission.CAMERA) } } } ActionButton(null, stringResource(R.string.from_gallery_button), icon = Icons.Outlined.Collections) { - when (PackageManager.PERMISSION_GRANTED) { - ContextCompat.checkSelfPermission(context, Manifest.permission.READ_EXTERNAL_STORAGE) -> { - galleryLauncher.launch("image/*") - hideBottomSheet() - } - else -> { - isCameraSelected.value = false - permissionLauncher.launch(Manifest.permission.READ_EXTERNAL_STORAGE) - } - } + galleryLauncher.launch("image/*") + hideBottomSheet() } if (fileUri != null && onFileChange != null) { ActionButton(null, stringResource(R.string.choose_file), icon = Icons.Outlined.InsertDriveFile) { - when (PackageManager.PERMISSION_GRANTED) { - ContextCompat.checkSelfPermission(context, Manifest.permission.READ_EXTERNAL_STORAGE) -> { - filesLauncher.launch("*/*") - hideBottomSheet() - } - else -> { - filesPermissionLauncher.launch(Manifest.permission.READ_EXTERNAL_STORAGE) - } - } + filesLauncher.launch("*/*") + hideBottomSheet() } } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Share.kt b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Share.kt index e05633a1a..6c66b2bbb 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Share.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Share.kt @@ -34,7 +34,7 @@ fun rememberSaveFileLauncher(cxt: Context, ciFile: CIFile?): ManagedActivityResu contract = ActivityResultContracts.CreateDocument(), onResult = { destination -> if (destination != null) { - val filePath = getStoredFilePath(cxt, ciFile) + val filePath = getLoadedFilePath(cxt, ciFile) if (filePath != null) { val contentResolver = cxt.contentResolver val file = File(filePath) diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Util.kt b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Util.kt index 17c307f7d..478bc7b5f 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Util.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Util.kt @@ -29,6 +29,8 @@ import chat.simplex.app.model.CIFile import kotlinx.coroutines.* import java.io.File import java.io.FileOutputStream +import java.text.SimpleDateFormat +import java.util.* import kotlin.math.log2 import kotlin.math.pow @@ -224,8 +226,8 @@ fun getAppFilePath(context: Context, fileName: String): String { return "${getAppFilesDirectory(context)}/$fileName" } -fun getStoredFilePath(context: Context, file: CIFile?): String? { - return if (file?.filePath != null && file.stored) { +fun getLoadedFilePath(context: Context, file: CIFile?): String? { + return if (file?.filePath != null && file.loaded) { val filePath = getAppFilePath(context, file.filePath) if (File(filePath).exists()) filePath else null } else { @@ -234,8 +236,8 @@ fun getStoredFilePath(context: Context, file: CIFile?): String? { } // https://developer.android.com/training/data-storage/shared/documents-files#bitmap -fun getStoredImage(context: Context, file: CIFile?): Bitmap? { - val filePath = getStoredFilePath(context, file) +fun getLoadedImage(context: Context, file: CIFile?): Bitmap? { + val filePath = getLoadedFilePath(context, file) return if (filePath != null) { try { val uri = FileProvider.getUriForFile(context, "${BuildConfig.APPLICATION_ID}.provider", File(filePath)) @@ -271,7 +273,8 @@ fun getFileSize(context: Context, uri: Uri): Long? { fun saveImage(context: Context, image: Bitmap): String? { return try { val dataResized = resizeImageToDataSize(image, maxDataSize = MAX_IMAGE_SIZE) - val fileToSave = uniqueCombine(context, "image_${System.currentTimeMillis()}.jpg") + val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date()) + val fileToSave = uniqueCombine(context, "IMG_${timestamp}.jpg") val file = File(getAppFilePath(context, fileToSave)) val output = FileOutputStream(file) dataResized.writeTo(output) diff --git a/apps/ios/Shared/FileUtils.swift b/apps/ios/Shared/FileUtils.swift index 8978ad7cc..abb150965 100644 --- a/apps/ios/Shared/FileUtils.swift +++ b/apps/ios/Shared/FileUtils.swift @@ -26,17 +26,17 @@ func getAppFilePath(_ fileName: String) -> URL { getAppFilesDirectory().appendingPathComponent(fileName) } -func getStoredFilePath(_ file: CIFile?) -> String? { +func getLoadedFilePath(_ file: CIFile?) -> String? { if let file = file, - file.stored, + file.loaded, let savedFile = file.filePath { return getAppFilePath(savedFile).path } return nil } -func getStoredImage(_ file: CIFile?) -> UIImage? { - if let filePath = getStoredFilePath(file) { +func getLoadedImage(_ file: CIFile?) -> UIImage? { + if let filePath = getLoadedFilePath(file) { return UIImage(contentsOfFile: filePath) } return nil @@ -63,13 +63,22 @@ func saveFileFromURL(_ url: URL) -> String? { func saveImage(_ uiImage: UIImage) -> String? { if let imageDataResized = resizeImageToDataSize(uiImage, maxDataSize: maxImageSize) { - let millisecondsSince1970 = Int64((Date().timeIntervalSince1970 * 1000.0).rounded()) - let fileName = uniqueCombine("image_\(millisecondsSince1970).jpg") + let timestamp = Date().getFormattedDate("yyyyMMdd_HHmmss") + let fileName = uniqueCombine("IMG_\(timestamp).jpg") return saveFile(imageDataResized, fileName) } return nil } +extension Date { + func getFormattedDate(_ format: String) -> String { + let df = DateFormatter() + df.dateFormat = format + df.locale = Locale(identifier: "US") + return df.string(from: self) + } +} + private func saveFile(_ data: Data, _ fileName: String) -> String? { let filePath = getAppFilePath(fileName) do { diff --git a/apps/ios/Shared/Model/Shared/ChatTypes.swift b/apps/ios/Shared/Model/Shared/ChatTypes.swift index 3ab6dc2c8..6f3981b69 100644 --- a/apps/ios/Shared/Model/Shared/ChatTypes.swift +++ b/apps/ios/Shared/Model/Shared/ChatTypes.swift @@ -655,15 +655,18 @@ struct CIFile: Decodable { CIFile(fileId: fileId, fileName: fileName, fileSize: fileSize, filePath: filePath, fileStatus: fileStatus) } - var stored: Bool { + var loaded: Bool { get { switch self.fileStatus { case .sndStored: return true case .sndTransfer: return true case .sndComplete: return true case .sndCancelled: return true + case .rcvInvitation: return false + case .rcvAccepted: return false + case .rcvTransfer: return false + case .rcvCancelled: return false case .rcvComplete: return true - default: return false } } } diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift index ec691f0cf..21fc16c5b 100644 --- a/apps/ios/Shared/Views/Chat/ChatView.swift +++ b/apps/ios/Shared/Views/Chat/ChatView.swift @@ -148,7 +148,7 @@ struct ChatView: View { } label: { Label("Reply", systemImage: "arrowshape.turn.up.left") } Button { var shareItems: [Any] = [ci.content.text] - if case .image = ci.content.msgContent, let image = getStoredImage(ci.file) { + if case .image = ci.content.msgContent, let image = getLoadedImage(ci.file) { shareItems.append(image) } showShareSheet(items: shareItems) @@ -156,14 +156,14 @@ struct ChatView: View { Button { if case let .image(text, _) = ci.content.msgContent, text == "", - let image = getStoredImage(ci.file) { + let image = getLoadedImage(ci.file) { UIPasteboard.general.image = image } else { UIPasteboard.general.string = ci.content.text } } label: { Label("Copy", systemImage: "doc.on.doc") } if case .image = ci.content.msgContent, - let image = getStoredImage(ci.file) { + let image = getLoadedImage(ci.file) { Button { UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil) } label: { Label("Save", systemImage: "square.and.arrow.down") } diff --git a/apps/ios/Shared/Views/Helpers/CIFileView.swift b/apps/ios/Shared/Views/Helpers/CIFileView.swift index cebdfa64d..f6ced00a1 100644 --- a/apps/ios/Shared/Views/Helpers/CIFileView.swift +++ b/apps/ios/Shared/Views/Helpers/CIFileView.swift @@ -78,7 +78,7 @@ struct CIFileView: View { ) case .rcvComplete: logger.debug("CIFileView processFile - in .rcvComplete") - if let filePath = getStoredFilePath(file){ + if let filePath = getLoadedFilePath(file){ let url = URL(fileURLWithPath: filePath) showShareSheet(items: [url]) } diff --git a/apps/ios/Shared/Views/Helpers/CIImageView.swift b/apps/ios/Shared/Views/Helpers/CIImageView.swift index ec15883cb..c4af213e6 100644 --- a/apps/ios/Shared/Views/Helpers/CIImageView.swift +++ b/apps/ios/Shared/Views/Helpers/CIImageView.swift @@ -18,7 +18,7 @@ struct CIImageView: View { var body: some View { VStack(alignment: .center, spacing: 6) { - if let uiImage = getStoredImage(file) { + if let uiImage = getLoadedImage(file) { imageView(uiImage) .fullScreenCover(isPresented: $showFullScreenImage) { ZStack {