diff --git a/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt b/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt index 19dbaec96d..22259fc3fa 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt @@ -22,7 +22,6 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.fragment.app.FragmentActivity import androidx.lifecycle.* -import androidx.work.* import chat.simplex.app.model.ChatModel import chat.simplex.app.model.NtfManager import chat.simplex.app.ui.theme.SimpleButton @@ -38,18 +37,29 @@ import chat.simplex.app.views.newchat.connectViaUri import chat.simplex.app.views.newchat.withUriAction import chat.simplex.app.views.onboarding.* import kotlinx.coroutines.delay -import java.util.concurrent.TimeUnit -class MainActivity: FragmentActivity(), LifecycleEventObserver { +class MainActivity: FragmentActivity() { + companion object { + /** + * We don't want these values to be bound to Activity lifecycle since activities are changed often, for example, when a user + * clicks on new message in notification. In this case savedInstanceState will be null (this prevents restoring the values) + * See [SimplexService.onTaskRemoved] for another part of the logic which nullifies the values when app closed by the user + * */ + val userAuthorized = mutableStateOf(null) + val enteredBackground = mutableStateOf(null) + // Remember result and show it after orientation change + private val laFailed = mutableStateOf(false) + + fun clearAuthState() { + userAuthorized.value = null + enteredBackground.value = null + } + } private val vm by viewModels() private val chatController by lazy { (application as SimplexApp).chatController } - private val userAuthorized = mutableStateOf(null) - private val enteredBackground = mutableStateOf(null) - private val laFailed = mutableStateOf(false) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - ProcessLifecycleOwner.get().lifecycle.addObserver(this) // testJson() val m = vm.chatModel // When call ended and orientation changes, it re-process old intent, it's unneeded. @@ -83,20 +93,25 @@ class MainActivity: FragmentActivity(), LifecycleEventObserver { processIntent(intent, vm.chatModel) } - override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) { - withApi { - when (event) { - Lifecycle.Event.ON_STOP -> { - enteredBackground.value = elapsedRealtime() - } - Lifecycle.Event.ON_START -> { - val enteredBackgroundVal = enteredBackground.value - if (enteredBackgroundVal == null || elapsedRealtime() - enteredBackgroundVal >= 30 * 1e+3) { - runAuthenticate() - } - } - else -> {} - } + override fun onStart() { + super.onStart() + val enteredBackgroundVal = enteredBackground.value + if (enteredBackgroundVal == null || elapsedRealtime() - enteredBackgroundVal >= 30 * 1e+3) { + runAuthenticate() + } + } + + override fun onStop() { + super.onStop() + enteredBackground.value = elapsedRealtime() + } + + override fun onBackPressed() { + super.onBackPressed() + if (!onBackPressedDispatcher.hasEnabledCallbacks() && vm.chatModel.controller.appPrefs.performLA.get()) { + // When pressed Back and there is no one wants to process the back event, clear auth state to force re-auth on launch + clearAuthState() + laFailed.value = true } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/SimplexService.kt b/apps/android/app/src/main/java/chat/simplex/app/SimplexService.kt index 134c1591ef..7a4dd8ea71 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/SimplexService.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/SimplexService.kt @@ -151,6 +151,9 @@ class SimplexService: Service() { // re-schedules the task when "Clear recent apps" is pressed override fun onTaskRemoved(rootIntent: Intent) { + // Just to make sure that after restart of the app the user will need to re-authenticate + MainActivity.clearAuthState() + // If private notifications aren't enabled or battery optimization isn't disabled, we shouldn't restart the service if (!SimplexApp.context.allowToStartServiceAfterAppExit()) { return diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/TerminalView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/TerminalView.kt index 46709dd1c8..c0ca2ae479 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/TerminalView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/TerminalView.kt @@ -82,13 +82,9 @@ fun TerminalLayout( @Composable fun TerminalLog(terminalItems: List) { val listState = rememberLazyListState() - val keyboardState by getKeyboardState() - val ciListState = rememberSaveable(stateSaver = CIListStateSaver) { - mutableStateOf(CIListState(false, terminalItems.count(), keyboardState)) - } - val scope = rememberCoroutineScope() - LazyColumn(state = listState) { - items(terminalItems) { item -> + val reversedTerminalItems by remember { derivedStateOf { terminalItems.reversed() } } + LazyColumn(state = listState, reverseLayout = true) { + items(reversedTerminalItems) { item -> Text("${item.date.toString().subSequence(11, 19)} ${item.label}", style = TextStyle(fontFamily = FontFamily.Monospace, fontSize = 18.sp, color = MaterialTheme.colors.primary), maxLines = 1, @@ -104,13 +100,6 @@ fun TerminalLog(terminalItems: List) { } ) } - val len = terminalItems.count() - if (len > 1 && (keyboardState != ciListState.value.keyboardState || !ciListState.value.scrolled || len != ciListState.value.itemCount)) { - scope.launch { - ciListState.value = CIListState(true, len, keyboardState) - listState.animateScrollToItem(len - 1) - } - } } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatInfoView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatInfoView.kt index cd84fb6348..6ed71d2955 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatInfoView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatInfoView.kt @@ -254,22 +254,23 @@ fun ChatInfoHeader(cInfo: ChatInfo, contact: Contact) { @Composable private fun LocalAliasEditor(initialValue: String, updateValue: (String) -> Unit) { var value by rememberSaveable { mutableStateOf(initialValue) } - DefaultBasicTextField( - Modifier.fillMaxWidth().padding(horizontal = 10.dp), - value, - { - Text( - generalGetString(R.string.text_field_set_contact_placeholder), - Modifier.fillMaxWidth(), - textAlign = TextAlign.Center, - color = HighOrLowlight - ) - }, - color = HighOrLowlight, - textStyle = TextStyle.Default.copy(textAlign = TextAlign.Center), - keyboardActions = KeyboardActions(onDone = { updateValue(value) }) - ) { - value = it + Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) { + DefaultBasicTextField( + Modifier.padding(horizontal = 10.dp).widthIn(min = 100.dp), + value, + { + Text( + generalGetString(R.string.text_field_set_contact_placeholder), + textAlign = TextAlign.Center, + color = HighOrLowlight + ) + }, + color = HighOrLowlight, + textStyle = TextStyle.Default.copy(textAlign = if (value.isEmpty()) TextAlign.Start else TextAlign.Center), + keyboardActions = KeyboardActions(onDone = { updateValue(value) }) + ) { + value = it + } } LaunchedEffect(Unit) { snapshotFlow { value } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ComposeView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ComposeView.kt index 0c18eb38aa..d96b8a75ca 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ComposeView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ComposeView.kt @@ -4,8 +4,7 @@ import ComposeFileView import ComposeImageView import android.Manifest import android.app.Activity -import android.content.Context -import android.content.Intent +import android.content.* import android.content.pm.PackageManager import android.graphics.Bitmap import android.graphics.ImageDecoder @@ -19,7 +18,8 @@ import androidx.annotation.CallSuper import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.* +import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.AttachFile import androidx.compose.material.icons.filled.Edit @@ -194,7 +194,16 @@ fun ComposeView( Toast.makeText(context, generalGetString(R.string.toast_permission_denied), Toast.LENGTH_SHORT).show() } } - val galleryLauncher = rememberGetContentLauncher { uri: Uri? -> + val galleryLauncher = rememberLauncherForActivityResult(contract = PickFromGallery()) { uri: Uri? -> + if (uri != null) { + val source = ImageDecoder.createSource(context.contentResolver, uri) + val bitmap = ImageDecoder.decodeBitmap(source) + chosenImage.value = bitmap + val imagePreview = resizeImageToStrSize(bitmap, maxDataSize = 14000) + composeState.value = composeState.value.copy(preview = ComposePreview.ImagePreview(imagePreview)) + } + } + val galleryLauncherFallback = rememberGetContentLauncher { uri: Uri? -> if (uri != null) { val source = ImageDecoder.createSource(context.contentResolver, uri) val bitmap = ImageDecoder.decodeBitmap(source) @@ -235,7 +244,11 @@ fun ComposeView( attachmentOption.value = null } AttachmentOption.PickImage -> { - galleryLauncher.launch("image/*") + try { + galleryLauncher.launch(0) + } catch (e: ActivityNotFoundException) { + galleryLauncherFallback.launch("image/*") + } attachmentOption.value = null } AttachmentOption.PickFile -> { @@ -500,3 +513,9 @@ fun ComposeView( } } } + +class PickFromGallery: ActivityResultContract() { + override fun createIntent(context: Context, input: Int) = Intent(Intent.ACTION_PICK, MediaStore.Images.Media.INTERNAL_CONTENT_URI) + + override fun parseResult(resultCode: Int, intent: Intent?): Uri? = intent?.data +} diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/GestureDetector.kt b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/GestureDetector.kt index e29f2de9c0..e19f00f971 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/GestureDetector.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/GestureDetector.kt @@ -86,11 +86,7 @@ suspend fun PointerInputScope.detectGesture( } else { if (shouldConsume) upOrCancel.consumeDownChange() - // If onLongPress event is needed, cancel short press event - if (onLongPress != null) - pressScope.cancel() - else - pressScope.release() + pressScope.release() } } catch (_: PointerEventTimeoutCancellationException) { onLongPress?.invoke(down.position) @@ -176,7 +172,7 @@ private class PressGestureScopeImpl( if (!isReleased && !isCanceled) { mutex.lock() } - return isCanceled + return isReleased && !isCanceled } } diff --git a/scripts/android/build-android.sh b/scripts/android/build-android.sh index 4edd9d42db..8db48eb551 100755 --- a/scripts/android/build-android.sh +++ b/scripts/android/build-android.sh @@ -4,8 +4,9 @@ set -eu u="$USER" tmp=$(mktemp -d -t) -commit="${1:-nix-android}" -commands="nix git gradle unzip curl" +source="github:simplex-chat/simplex-chat" +commit="$1" +commands="nix git curl gradle zip unzip zipalign" nix_install() { # Pre-setup nix @@ -69,18 +70,26 @@ checks() { build() { # Build simplex lib - nix build "$tmp/simplex-chat/#packages.x86_64-linux.aarch64-android:lib:simplex-chat" + nix build "$source/$commit#hydraJobs.aarch64-android:lib:simplex-chat.x86_64-linux" unzip -o "$PWD/result/pkg-aarch64-android-libsimplex.zip" -d "$tmp/simplex-chat/apps/android/app/src/main/cpp/libs/arm64-v8a" # Build android suppprt lib - nix build "$tmp/simplex-chat/#packages.x86_64-linux.aarch64-android:lib:support" + nix build "$source/$commit#hydraJobs.aarch64-android:lib:support.x86_64-linux" unzip -o "$PWD/result/pkg-aarch64-android-libsupport.zip" -d "$tmp/simplex-chat/apps/android/app/src/main/cpp/libs/arm64-v8a" - gradle -p "$tmp/simplex-chat/apps/android/" clean build + sed -i.bak 's/${extract_native_libs}/true/' "$tmp/simplex-chat/apps/android/app/src/main/AndroidManifest.xml" + + gradle -p "$tmp/simplex-chat/apps/android/" clean build assembleRelease + + mkdir -p "$tmp/android" + unzip -oqd "$tmp/android/" "$tmp/simplex-chat/apps/android/app/build/outputs/apk/release/app-release-unsigned.apk" + + (cd "$tmp/android" && zip -rq5 "$tmp/simplex-chat.apk" . && zip -rq0 "$tmp/simplex-chat.apk" resources.arsc res) + + zipalign -p -f 4 "$tmp/simplex-chat.apk" "$PWD/simplex-chat.apk" } final() { - cp "$tmp/simplex-chat/apps/android/app/build/outputs/apk/release/app-release-unsigned.apk" "$PWD/simplex-chat.apk" printf "Simplex-chat was successfully compiled: %s/simplex-chat.apk\nDelete nix and gradle caches with 'rm -rf /nix && rm \$HOME/.nix* && \$HOME/.gradle/caches' in case if no longer needed.\n" "$PWD" }