android: remove unnecessary READ_EXTERNAL_STORAGE permission request, open image picker in Gallery; IMG timestamp format (#610)
This commit is contained in:
@@ -8,7 +8,6 @@
|
||||
<uses-permission android:name="android.permission.VIDEO_CAPTURE" />
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) },
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) }
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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<Void?, Bitmap?>() {
|
||||
}
|
||||
}
|
||||
}
|
||||
//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<String, Uri?> =
|
||||
// rememberLauncherForActivityResult(contract = GetGalleryContent(), cb)
|
||||
rememberLauncherForActivityResult(contract = ActivityResultContracts.GetContent(), cb)
|
||||
fun rememberGalleryLauncher(cb: (Uri?) -> Unit): ManagedActivityResultLauncher<String, Uri?> =
|
||||
rememberLauncherForActivityResult(contract = GetGalleryContent(), cb)
|
||||
|
||||
@Composable
|
||||
fun rememberCameraLauncher(cb: (Bitmap?) -> Unit): ManagedActivityResultLauncher<Void?, Bitmap?> =
|
||||
@@ -148,6 +147,10 @@ fun rememberCameraLauncher(cb: (Bitmap?) -> Unit): ManagedActivityResultLauncher
|
||||
fun rememberPermissionLauncher(cb: (Boolean) -> Unit): ManagedActivityResultLauncher<String, Boolean> =
|
||||
rememberLauncherForActivityResult(contract = ActivityResultContracts.RequestPermission(), cb)
|
||||
|
||||
@Composable
|
||||
fun rememberGetContentLauncher(cb: (Uri?) -> Unit): ManagedActivityResultLauncher<String, Uri?> =
|
||||
rememberLauncherForActivityResult(contract = ActivityResultContracts.GetContent(), cb)
|
||||
|
||||
@Composable
|
||||
fun GetImageBottomSheet(
|
||||
imageBitmap: MutableState<Bitmap?>,
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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") }
|
||||
|
||||
@@ -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])
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user