multiplatform: split common/android/desktop (#2672)

* multiplatform: relocated code to its new place

* code becomes better

* renamed file

* fixes for BASE64 and images, and changes for appFileUri

* different Base64 for both platforms

* fix file saving on long click

* platformCallbacks refactoring

* renamed callbacks to platform

* eol

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
This commit is contained in:
Stanislav Dmitrenko 2023-07-12 16:42:10 +03:00 committed by GitHub
parent ff7c22e114
commit 38f40fec3d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
251 changed files with 4290 additions and 2469 deletions

View File

@ -7,10 +7,6 @@ plugins {
id("org.jetbrains.kotlin.plugin.serialization") id("org.jetbrains.kotlin.plugin.serialization")
} }
repositories {
maven("https://jitpack.io")
}
android { android {
compileSdkVersion(33) compileSdkVersion(33)
@ -124,9 +120,16 @@ dependencies {
//implementation("androidx.compose.ui:ui:${rootProject.extra["compose.version"] as String}") //implementation("androidx.compose.ui:ui:${rootProject.extra["compose.version"] as String}")
//implementation("androidx.compose.material:material:$compose_version") //implementation("androidx.compose.material:material:$compose_version")
//implementation("androidx.compose.ui:ui-tooling-preview:$compose_version") //implementation("androidx.compose.ui:ui-tooling-preview:$compose_version")
implementation("androidx.appcompat:appcompat:1.5.1")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.4.1") implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.4.1")
implementation("androidx.lifecycle:lifecycle-process:2.4.1") implementation("androidx.lifecycle:lifecycle-process:2.4.1")
implementation("androidx.activity:activity-compose:1.5.0") implementation("androidx.activity:activity-compose:1.5.0")
val work_version = "2.7.1"
implementation("androidx.work:work-runtime-ktx:$work_version")
implementation("androidx.work:work-multiprocess:$work_version")
implementation("com.jakewharton:process-phoenix:2.1.2")
//implementation("androidx.compose.material:material-icons-extended:$compose_version") //implementation("androidx.compose.material:material-icons-extended:$compose_version")
//implementation("androidx.compose.ui:ui-util:$compose_version") //implementation("androidx.compose.ui:ui-util:$compose_version")

View File

@ -3,8 +3,8 @@ package chat.simplex.app
import android.app.backup.BackupAgentHelper import android.app.backup.BackupAgentHelper
import android.app.backup.FullBackupDataOutput import android.app.backup.FullBackupDataOutput
import android.content.Context import android.content.Context
import chat.simplex.app.model.AppPreferences import chat.simplex.common.model.AppPreferences
import chat.simplex.app.model.AppPreferences.Companion.SHARED_PREFS_PRIVACY_FULL_BACKUP import chat.simplex.common.model.AppPreferences.Companion.SHARED_PREFS_PRIVACY_FULL_BACKUP
class BackupAgent: BackupAgentHelper() { class BackupAgent: BackupAgentHelper() {
override fun onFullBackup(data: FullBackupDataOutput?) { override fun onFullBackup(data: FullBackupDataOutput?) {

View File

@ -1,51 +1,41 @@
package chat.simplex.app package chat.simplex.app
import android.app.Application
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.* import android.os.*
import android.os.SystemClock.elapsedRealtime
import android.util.Log
import android.view.WindowManager import android.view.WindowManager
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.compose.animation.core.*
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.* import chat.simplex.app.model.NtfManager
import chat.simplex.app.helpers.applyAppLocale
import chat.simplex.app.model.*
import chat.simplex.app.model.NtfManager.getUserIdFromIntent import chat.simplex.app.model.NtfManager.getUserIdFromIntent
import chat.simplex.app.platform.mainActivity import chat.simplex.common.*
import chat.simplex.app.ui.theme.* import chat.simplex.common.helpers.*
import chat.simplex.app.views.chatlist.* import chat.simplex.common.model.*
import chat.simplex.app.views.helpers.* import chat.simplex.common.ui.theme.*
import chat.simplex.app.views.newchat.* import chat.simplex.common.views.chatlist.*
import chat.simplex.app.views.onboarding.* import chat.simplex.common.views.helpers.*
import chat.simplex.app.views.usersettings.* import chat.simplex.common.views.onboarding.*
import chat.simplex.common.platform.*
import chat.simplex.res.MR import chat.simplex.res.MR
import kotlinx.coroutines.* import kotlinx.coroutines.*
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
import java.net.URI
class MainActivity: FragmentActivity() { class MainActivity: FragmentActivity() {
private val vm by viewModels<SimplexViewModel>()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
mainActivity = WeakReference(this)
// testJson() // testJson()
val m = vm.chatModel mainActivity = WeakReference(this)
applyAppLocale(m.controller.appPrefs.appLanguage) applyAppLocale(ChatModel.controller.appPrefs.appLanguage)
// When call ended and orientation changes, it re-process old intent, it's unneeded. // When call ended and orientation changes, it re-process old intent, it's unneeded.
// Only needed to be processed on first creation of activity // Only needed to be processed on first creation of activity
if (savedInstanceState == null) { if (savedInstanceState == null) {
processNotificationIntent(intent, m) processNotificationIntent(intent)
processIntent(intent, m) processIntent(intent)
processExternalIntent(intent, m) processExternalIntent(intent)
} }
if (m.controller.appPrefs.privacyProtectScreen.get()) { if (ChatController.appPrefs.privacyProtectScreen.get()) {
Log.d(TAG, "onCreate: set FLAG_SECURE") Log.d(TAG, "onCreate: set FLAG_SECURE")
window.setFlags( window.setFlags(
WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE,
@ -54,17 +44,7 @@ class MainActivity: FragmentActivity() {
} }
setContent { setContent {
SimpleXTheme { SimpleXTheme {
Surface(color = MaterialTheme.colors.background) { AppScreen()
MainPage(
m,
AppLock.userAuthorized,
AppLock.laFailed,
AppLock.destroyedAfterBackPress,
{ AppLock.runAuthenticate() },
{ AppLock.setPerformLA(it) },
showLANotice = { AppLock.showLANotice(m.controller.appPrefs.laNoticeShown) }
)
}
} }
} }
SimplexApp.context.schedulePeriodicServiceRestartWorker() SimplexApp.context.schedulePeriodicServiceRestartWorker()
@ -73,22 +53,13 @@ class MainActivity: FragmentActivity() {
override fun onNewIntent(intent: Intent?) { override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent) super.onNewIntent(intent)
processIntent(intent, vm.chatModel) processIntent(intent)
processExternalIntent(intent, vm.chatModel) processExternalIntent(intent)
} }
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
val enteredBackgroundVal = AppLock.enteredBackground.value AppLock.recheckAuthState()
val delay = vm.chatModel.controller.appPrefs.laLockDelay.get()
if (enteredBackgroundVal == null || elapsedRealtime() - enteredBackgroundVal >= delay * 1000) {
if (AppLock.userAuthorized.value != false) {
/** [runAuthenticate] will be called in [MainPage] if needed. Making like this prevents double showing of passcode on start */
AppLock.setAuthState()
} else if (!vm.chatModel.activeCallViewIsVisible.value) {
AppLock.runAuthenticate()
}
}
} }
override fun onPause() { override fun onPause() {
@ -98,13 +69,13 @@ class MainActivity: FragmentActivity() {
* recreation but receives onStop after recreation. So using both (onPause and onStop) to prevent * recreation but receives onStop after recreation. So using both (onPause and onStop) to prevent
* unwanted multiple auth dialogs from [runAuthenticate] * unwanted multiple auth dialogs from [runAuthenticate]
* */ * */
AppLock.enteredBackground.value = elapsedRealtime() AppLock.appWasHidden()
} }
override fun onStop() { override fun onStop() {
super.onStop() super.onStop()
VideoPlayer.stopAll() VideoPlayer.stopAll()
AppLock.enteredBackground.value = elapsedRealtime() AppLock.appWasHidden()
} }
override fun onBackPressed() { override fun onBackPressed() {
@ -117,7 +88,7 @@ class MainActivity: FragmentActivity() {
super.onBackPressed() super.onBackPressed()
} }
if (!onBackPressedDispatcher.hasEnabledCallbacks() && vm.chatModel.controller.appPrefs.performLA.get()) { if (!onBackPressedDispatcher.hasEnabledCallbacks() && ChatController.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 // When pressed Back and there is no one wants to process the back event, clear auth state to force re-auth on launch
AppLock.clearAuthState() AppLock.clearAuthState()
AppLock.laFailed.value = true AppLock.laFailed.value = true
@ -130,12 +101,7 @@ class MainActivity: FragmentActivity() {
} }
} }
class SimplexViewModel(application: Application): AndroidViewModel(application) { fun processNotificationIntent(intent: Intent?) {
val app = getApplication<SimplexApp>()
val chatModel = app.chatModel
}
fun processNotificationIntent(intent: Intent?, chatModel: ChatModel) {
val userId = getUserIdFromIntent(intent) val userId = getUserIdFromIntent(intent)
when (intent?.action) { when (intent?.action) {
NtfManager.OpenChatAction -> { NtfManager.OpenChatAction -> {
@ -179,16 +145,16 @@ fun processNotificationIntent(intent: Intent?, chatModel: ChatModel) {
} }
} }
fun processIntent(intent: Intent?, chatModel: ChatModel) { fun processIntent(intent: Intent?) {
when (intent?.action) { when (intent?.action) {
"android.intent.action.VIEW" -> { "android.intent.action.VIEW" -> {
val uri = intent.data val uri = intent.data
if (uri != null) connectIfOpenedViaUri(uri, chatModel) if (uri != null) connectIfOpenedViaUri(uri.toURI(), ChatModel)
} }
} }
} }
fun processExternalIntent(intent: Intent?, chatModel: ChatModel) { fun processExternalIntent(intent: Intent?) {
when (intent?.action) { when (intent?.action) {
Intent.ACTION_SEND -> { Intent.ACTION_SEND -> {
// Close active chat and show a list of chats // Close active chat and show a list of chats
@ -204,13 +170,13 @@ fun processExternalIntent(intent: Intent?, chatModel: ChatModel) {
isMediaIntent(intent) -> { isMediaIntent(intent) -> {
val uri = intent.getParcelableExtra<Parcelable>(Intent.EXTRA_STREAM) as? Uri val uri = intent.getParcelableExtra<Parcelable>(Intent.EXTRA_STREAM) as? Uri
if (uri != null) { if (uri != null) {
chatModel.sharedContent.value = SharedContent.Media(intent.getStringExtra(Intent.EXTRA_TEXT) ?: "", listOf(uri)) chatModel.sharedContent.value = SharedContent.Media(intent.getStringExtra(Intent.EXTRA_TEXT) ?: "", listOf(uri.toURI()))
} // All other mime types } // All other mime types
} }
else -> { else -> {
val uri = intent.getParcelableExtra<Parcelable>(Intent.EXTRA_STREAM) as? Uri val uri = intent.getParcelableExtra<Parcelable>(Intent.EXTRA_STREAM) as? Uri
if (uri != null) { if (uri != null) {
chatModel.sharedContent.value = SharedContent.File(intent.getStringExtra(Intent.EXTRA_TEXT) ?: "", uri) chatModel.sharedContent.value = SharedContent.File(intent.getStringExtra(Intent.EXTRA_TEXT) ?: "", uri.toURI())
} }
} }
} }
@ -224,7 +190,7 @@ fun processExternalIntent(intent: Intent?, chatModel: ChatModel) {
isMediaIntent(intent) -> { isMediaIntent(intent) -> {
val uris = intent.getParcelableArrayListExtra<Parcelable>(Intent.EXTRA_STREAM) as? List<Uri> val uris = intent.getParcelableArrayListExtra<Parcelable>(Intent.EXTRA_STREAM) as? List<Uri>
if (uris != null) { if (uris != null) {
chatModel.sharedContent.value = SharedContent.Media(intent.getStringExtra(Intent.EXTRA_TEXT) ?: "", uris) chatModel.sharedContent.value = SharedContent.Media(intent.getStringExtra(Intent.EXTRA_TEXT) ?: "", uris.map { it.toURI() })
} // All other mime types } // All other mime types
} }
else -> {} else -> {}

View File

@ -1,11 +1,13 @@
package chat.simplex.app.views.helpers package chat.simplex.app
import android.content.Context import android.content.Context
import android.util.Log import android.util.Log
import androidx.work.* import androidx.work.*
import chat.simplex.app.* import chat.simplex.app.*
import chat.simplex.app.SimplexService.Companion.showPassphraseNotification import chat.simplex.app.SimplexService.Companion.showPassphraseNotification
import chat.simplex.app.model.ChatController import chat.simplex.common.model.ChatController
import chat.simplex.common.views.helpers.DBMigrationResult
import chat.simplex.app.BuildConfig
import kotlinx.coroutines.* import kotlinx.coroutines.*
import java.util.Date import java.util.Date
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit

View File

@ -1,24 +1,23 @@
package chat.simplex.app package chat.simplex.app
import android.app.Application import android.app.Application
import android.net.LocalServerSocket import chat.simplex.common.platform.Log
import android.util.Log
import androidx.lifecycle.* import androidx.lifecycle.*
import androidx.work.* import androidx.work.*
import chat.simplex.app.model.* import chat.simplex.app.model.NtfManager
import chat.simplex.app.platform.* import chat.simplex.common.helpers.APPLICATION_ID
import chat.simplex.app.ui.theme.DefaultTheme import chat.simplex.common.helpers.requiresIgnoringBattery
import chat.simplex.app.views.helpers.* import chat.simplex.common.model.*
import chat.simplex.app.views.onboarding.OnboardingStage import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.onboarding.OnboardingStage
import chat.simplex.common.platform.*
import chat.simplex.common.views.call.RcvCallInvitation
import com.jakewharton.processphoenix.ProcessPhoenix import com.jakewharton.processphoenix.ProcessPhoenix
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.serialization.decodeFromString
import java.io.* import java.io.*
import java.lang.ref.WeakReference
import java.util.* import java.util.*
import java.util.concurrent.Semaphore
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import kotlin.concurrent.thread
const val TAG = "SIMPLEX" const val TAG = "SIMPLEX"
@ -26,6 +25,8 @@ class SimplexApp: Application(), LifecycleEventObserver {
val chatModel: ChatModel val chatModel: ChatModel
get() = chatController.chatModel get() = chatController.chatModel
val chatController: ChatController = ChatController
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
if (ProcessPhoenix.isPhoenixProcess(this)) { if (ProcessPhoenix.isPhoenixProcess(this)) {
@ -33,7 +34,8 @@ class SimplexApp: Application(), LifecycleEventObserver {
} }
context = this context = this
initHaskell() initHaskell()
context.getDir("temp", MODE_PRIVATE).deleteRecursively() initMultiplatform()
tmpDir.deleteRecursively()
withBGApi { withBGApi {
initChatController() initChatController()
runMigrations() runMigrations()
@ -77,7 +79,7 @@ class SimplexApp: Application(), LifecycleEventObserver {
* */ * */
if (chatModel.chatRunning.value != false && if (chatModel.chatRunning.value != false &&
chatModel.onboardingStage.value == OnboardingStage.OnboardingComplete && chatModel.onboardingStage.value == OnboardingStage.OnboardingComplete &&
appPreferences.notificationsMode.get() == NotificationsMode.SERVICE.name appPrefs.notificationsMode.get() == NotificationsMode.SERVICE
) { ) {
SimplexService.start() SimplexService.start()
} }
@ -88,12 +90,12 @@ class SimplexApp: Application(), LifecycleEventObserver {
} }
fun allowToStartServiceAfterAppExit() = with(chatModel.controller) { fun allowToStartServiceAfterAppExit() = with(chatModel.controller) {
appPrefs.notificationsMode.get() == NotificationsMode.SERVICE.name && appPrefs.notificationsMode.get() == NotificationsMode.SERVICE &&
(!NotificationsMode.SERVICE.requiresIgnoringBattery || SimplexService.isIgnoringBatteryOptimizations()) (!NotificationsMode.SERVICE.requiresIgnoringBattery || SimplexService.isIgnoringBatteryOptimizations())
} }
private fun allowToStartPeriodically() = with(chatModel.controller) { private fun allowToStartPeriodically() = with(chatModel.controller) {
appPrefs.notificationsMode.get() == NotificationsMode.PERIODIC.name && appPrefs.notificationsMode.get() == NotificationsMode.PERIODIC &&
(!NotificationsMode.PERIODIC.requiresIgnoringBattery || SimplexService.isIgnoringBatteryOptimizations()) (!NotificationsMode.PERIODIC.requiresIgnoringBattery || SimplexService.isIgnoringBatteryOptimizations())
} }
@ -131,4 +133,73 @@ class SimplexApp: Application(), LifecycleEventObserver {
companion object { companion object {
lateinit var context: SimplexApp private set lateinit var context: SimplexApp private set
} }
private fun initMultiplatform() {
androidAppContext = this
APPLICATION_ID = BuildConfig.APPLICATION_ID
ntfManager = object : chat.simplex.common.platform.NtfManager() {
override fun notifyContactConnected(user: User, contact: Contact) = NtfManager.notifyContactConnected(user, contact)
override fun notifyContactRequestReceived(user: User, cInfo: ChatInfo.ContactRequest) = NtfManager.notifyContactRequestReceived(user, cInfo)
override fun notifyMessageReceived(user: User, cInfo: ChatInfo, cItem: ChatItem) = NtfManager.notifyMessageReceived(user, cInfo, cItem)
override fun notifyCallInvitation(invitation: RcvCallInvitation) = NtfManager.notifyCallInvitation(invitation)
override fun hasNotificationsForChat(chatId: String): Boolean = NtfManager.hasNotificationsForChat(chatId)
override fun cancelNotificationsForChat(chatId: String) = NtfManager.cancelNotificationsForChat(chatId)
override fun displayNotification(user: User, chatId: String, displayName: String, msgText: String, image: String?, actions: List<NotificationAction>) = NtfManager.displayNotification(user, chatId, displayName, msgText, image, actions)
override fun createNtfChannelsMaybeShowAlert() = NtfManager.createNtfChannelsMaybeShowAlert()
override fun cancelCallNotification() = NtfManager.cancelCallNotification()
override fun cancelAllNotifications() = NtfManager.cancelAllNotifications()
}
platform = object : PlatformInterface {
override suspend fun androidServiceStart() {
SimplexService.start()
}
override fun androidServiceSafeStop() {
SimplexService.safeStopService()
}
override fun androidNotificationsModeChanged(mode: NotificationsMode) {
if (mode.requiresIgnoringBattery && !SimplexService.isIgnoringBatteryOptimizations()) {
appPrefs.backgroundServiceNoticeShown.set(false)
}
SimplexService.StartReceiver.toggleReceiver(mode == NotificationsMode.SERVICE)
CoroutineScope(Dispatchers.Default).launch {
if (mode == NotificationsMode.SERVICE)
SimplexService.start()
else
SimplexService.safeStopService()
}
if (mode != NotificationsMode.PERIODIC) {
MessagesFetcherWorker.cancelAll()
}
SimplexService.showBackgroundServiceNoticeIfNeeded()
}
override fun androidChatStartedAfterBeingOff() {
SimplexService.cancelPassphraseNotification()
when (appPrefs.notificationsMode.get()) {
NotificationsMode.SERVICE -> CoroutineScope(Dispatchers.Default).launch { platform.androidServiceStart() }
NotificationsMode.PERIODIC -> SimplexApp.context.schedulePeriodicWakeUp()
NotificationsMode.OFF -> {}
}
}
override fun androidChatStopped() {
SimplexService.safeStopService()
MessagesFetcherWorker.cancelAll()
}
override fun androidChatInitializedAndStarted() {
// Prevents from showing "Enable notifications" alert when onboarding wasn't complete yet
if (chatModel.onboardingStage.value == OnboardingStage.OnboardingComplete) {
SimplexService.showBackgroundServiceNoticeIfNeeded()
if (appPrefs.notificationsMode.get() == NotificationsMode.SERVICE)
withBGApi {
platform.androidServiceStart()
}
}
}
}
}
} }

View File

@ -7,21 +7,26 @@ import android.content.pm.PackageManager
import android.net.Uri import android.net.Uri
import android.os.* import android.os.*
import android.provider.Settings import android.provider.Settings
import android.util.Log
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import chat.simplex.common.platform.Log
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.work.* import androidx.work.*
import chat.simplex.app.model.* import chat.simplex.common.AppLock
import chat.simplex.app.views.helpers.* import chat.simplex.common.AppLock.clearAuthState
import chat.simplex.common.helpers.requiresIgnoringBattery
import chat.simplex.common.model.ChatController
import chat.simplex.common.model.NotificationsMode
import chat.simplex.common.platform.androidAppContext
import chat.simplex.common.views.helpers.*
import kotlinx.coroutines.*
import chat.simplex.res.MR import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource import dev.icerock.moko.resources.compose.stringResource
import kotlinx.coroutines.*
// based on: // based on:
// https://robertohuertas.com/2019/06/29/android_foreground_services/ // https://robertohuertas.com/2019/06/29/android_foreground_services/
@ -103,7 +108,7 @@ class SimplexService: Service() {
if (chatDbStatus != DBMigrationResult.OK) { if (chatDbStatus != DBMigrationResult.OK) {
Log.w(chat.simplex.app.TAG, "SimplexService: problem with the database: $chatDbStatus") Log.w(chat.simplex.app.TAG, "SimplexService: problem with the database: $chatDbStatus")
showPassphraseNotification(chatDbStatus) showPassphraseNotification(chatDbStatus)
safeStopService(self) safeStopService()
return@withApi return@withApi
} }
saveServiceState(self, ServiceState.STARTED) saveServiceState(self, ServiceState.STARTED)
@ -263,9 +268,9 @@ class SimplexService: Service() {
* If there is a need to stop the service, use this function only. It makes sure that the service will be stopped without an * If there is a need to stop the service, use this function only. It makes sure that the service will be stopped without an
* exception related to foreground services lifecycle * exception related to foreground services lifecycle
* */ * */
fun safeStopService(context: Context) { fun safeStopService() {
if (isServiceStarted) { if (isServiceStarted) {
context.stopService(Intent(context, SimplexService::class.java)) androidAppContext.stopService(Intent(androidAppContext, SimplexService::class.java))
} else { } else {
stopAfterStart = true stopAfterStart = true
} }
@ -274,9 +279,9 @@ class SimplexService: Service() {
private suspend fun serviceAction(action: Action) { private suspend fun serviceAction(action: Action) {
Log.d(TAG, "SimplexService serviceAction: ${action.name}") Log.d(TAG, "SimplexService serviceAction: ${action.name}")
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
Intent(SimplexApp.context, SimplexService::class.java).also { Intent(androidAppContext, SimplexService::class.java).also {
it.action = action.name it.action = action.name
ContextCompat.startForegroundService(SimplexApp.context, it) ContextCompat.startForegroundService(androidAppContext, it)
} }
} }
} }
@ -350,7 +355,7 @@ class SimplexService: Service() {
fun showBackgroundServiceNoticeIfNeeded() { fun showBackgroundServiceNoticeIfNeeded() {
val appPrefs = ChatController.appPrefs val appPrefs = ChatController.appPrefs
val mode = NotificationsMode.valueOf(appPrefs.notificationsMode.get()!!) val mode = appPrefs.notificationsMode.get()
Log.d(TAG, "showBackgroundServiceNoticeIfNeeded") Log.d(TAG, "showBackgroundServiceNoticeIfNeeded")
// Nothing to do if mode is OFF. Can be selected on on-boarding stage // Nothing to do if mode is OFF. Can be selected on on-boarding stage
if (mode == NotificationsMode.OFF) return if (mode == NotificationsMode.OFF) return
@ -371,11 +376,10 @@ class SimplexService: Service() {
if (appPrefs.backgroundServiceBatteryNoticeShown.get()) { if (appPrefs.backgroundServiceBatteryNoticeShown.get()) {
// users have been presented with battery notice before - they did not allow ignoring optimizations -> disable service // users have been presented with battery notice before - they did not allow ignoring optimizations -> disable service
showDisablingServiceNotice(mode) showDisablingServiceNotice(mode)
appPrefs.notificationsMode.set(NotificationsMode.OFF.name) appPrefs.notificationsMode.set(NotificationsMode.OFF)
ChatModel.notificationsMode.value = NotificationsMode.OFF StartReceiver.toggleReceiver(false)
SimplexService.StartReceiver.toggleReceiver(false)
MessagesFetcherWorker.cancelAll() MessagesFetcherWorker.cancelAll()
SimplexService.safeStopService(SimplexApp.context) safeStopService()
} else { } else {
// show battery optimization notice // show battery optimization notice
showBGServiceNoticeIgnoreOptimization(mode) showBGServiceNoticeIgnoreOptimization(mode)
@ -487,18 +491,18 @@ class SimplexService: Service() {
} }
fun isIgnoringBatteryOptimizations(): Boolean { fun isIgnoringBatteryOptimizations(): Boolean {
val powerManager = SimplexApp.context.getSystemService(Application.POWER_SERVICE) as PowerManager val powerManager = androidAppContext.getSystemService(Application.POWER_SERVICE) as PowerManager
return powerManager.isIgnoringBatteryOptimizations(SimplexApp.context.packageName) return powerManager.isIgnoringBatteryOptimizations(androidAppContext.packageName)
} }
private fun askAboutIgnoringBatteryOptimization() { private fun askAboutIgnoringBatteryOptimization() {
Intent().apply { Intent().apply {
@SuppressLint("BatteryLife") @SuppressLint("BatteryLife")
action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
data = Uri.parse("package:${SimplexApp.context.packageName}") data = Uri.parse("package:${androidAppContext.packageName}")
// This flag is needed when you start a new activity from non-Activity context // This flag is needed when you start a new activity from non-Activity context
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
SimplexApp.context.startActivity(this) androidAppContext.startActivity(this)
} }
} }
} }

View File

@ -1,6 +1,5 @@
package chat.simplex.app.model package chat.simplex.app.model
import android.Manifest
import android.app.* import android.app.*
import android.app.TaskStackBuilder import android.app.TaskStackBuilder
import android.content.* import android.content.*
@ -9,18 +8,21 @@ import android.graphics.BitmapFactory
import android.hardware.display.DisplayManager import android.hardware.display.DisplayManager
import android.media.AudioAttributes import android.media.AudioAttributes
import android.net.Uri import android.net.Uri
import android.util.Log
import android.view.Display import android.view.Display
import androidx.compose.ui.graphics.asAndroidBitmap
import androidx.core.app.* import androidx.core.app.*
import chat.simplex.app.* import chat.simplex.app.*
import chat.simplex.app.platform.base64ToBitmap import chat.simplex.app.TAG
import chat.simplex.app.platform.isAppOnForeground import chat.simplex.app.views.call.IncomingCallActivity
import chat.simplex.app.views.call.* import chat.simplex.app.views.call.getKeyguardManager
import chat.simplex.app.views.chatlist.acceptContactRequest import chat.simplex.common.views.chatlist.acceptContactRequest
import chat.simplex.app.views.helpers.* import chat.simplex.common.views.helpers.*
import chat.simplex.app.views.usersettings.NotificationPreviewMode import chat.simplex.common.model.*
import chat.simplex.res.MR import chat.simplex.common.platform.*
import chat.simplex.common.views.call.CallMediaType
import chat.simplex.common.views.call.RcvCallInvitation
import kotlinx.datetime.Clock import kotlinx.datetime.Clock
import chat.simplex.res.MR
object NtfManager { object NtfManager {
const val MessageChannel: String = "chat.simplex.app.MESSAGE_NOTIFICATION" const val MessageChannel: String = "chat.simplex.app.MESSAGE_NOTIFICATION"
@ -35,7 +37,7 @@ object NtfManager {
const val CallNotificationId: Int = -1 const val CallNotificationId: Int = -1
private const val UserIdKey: String = "userId" private const val UserIdKey: String = "userId"
private const val ChatIdKey: String = "chatId" private const val ChatIdKey: String = "chatId"
private val appPreferences: AppPreferences by lazy { ChatController.appPrefs } private val appPreferences: AppPreferences = ChatController.appPrefs
private val context: Context private val context: Context
get() = SimplexApp.context get() = SimplexApp.context
@ -44,7 +46,7 @@ object NtfManager {
return if (userId == -1L || userId == null) null else userId return if (userId == -1L || userId == null) null else userId
} }
private val manager: NotificationManager = SimplexApp.context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager private val manager: NotificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
private var prevNtfTime = mutableMapOf<String, Long>() private var prevNtfTime = mutableMapOf<String, Long>()
private val msgNtfTimeoutMs = 30000L private val msgNtfTimeoutMs = 30000L
@ -52,10 +54,6 @@ object NtfManager {
if (manager.areNotificationsEnabled()) createNtfChannelsMaybeShowAlert() if (manager.areNotificationsEnabled()) createNtfChannelsMaybeShowAlert()
} }
enum class NotificationAction {
ACCEPT_CONTACT_REQUEST
}
private fun callNotificationChannel(channelId: String, channelName: String): NotificationChannel { private fun callNotificationChannel(channelId: String, channelName: String): NotificationChannel {
val callChannel = NotificationChannel(channelId, channelName, NotificationManager.IMPORTANCE_HIGH) val callChannel = NotificationChannel(channelId, channelName, NotificationManager.IMPORTANCE_HIGH)
val attrs = AudioAttributes.Builder() val attrs = AudioAttributes.Builder()
@ -121,7 +119,7 @@ object NtfManager {
val largeIcon = when { val largeIcon = when {
actions.isEmpty() -> null actions.isEmpty() -> null
image == null || previewMode == NotificationPreviewMode.HIDDEN.name -> BitmapFactory.decodeResource(context.resources, R.drawable.icon) image == null || previewMode == NotificationPreviewMode.HIDDEN.name -> BitmapFactory.decodeResource(context.resources, R.drawable.icon)
else -> base64ToBitmap(image) else -> base64ToBitmap(image).asAndroidBitmap()
} }
val builder = NotificationCompat.Builder(context, MessageChannel) val builder = NotificationCompat.Builder(context, MessageChannel)
.setContentTitle(title) .setContentTitle(title)
@ -160,7 +158,7 @@ object NtfManager {
with(NotificationManagerCompat.from(context)) { with(NotificationManagerCompat.from(context)) {
// using cInfo.id only shows one notification per chat and updates it when the message arrives // using cInfo.id only shows one notification per chat and updates it when the message arrives
if (ActivityCompat.checkSelfPermission(SimplexApp.context, Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED) { if (ActivityCompat.checkSelfPermission(SimplexApp.context, android.Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED) {
notify(chatId.hashCode(), builder.build()) notify(chatId.hashCode(), builder.build())
notify(0, summary) notify(0, summary)
} }
@ -214,7 +212,7 @@ object NtfManager {
val largeIcon = if (image == null || previewMode == NotificationPreviewMode.HIDDEN.name) val largeIcon = if (image == null || previewMode == NotificationPreviewMode.HIDDEN.name)
BitmapFactory.decodeResource(context.resources, R.drawable.icon) BitmapFactory.decodeResource(context.resources, R.drawable.icon)
else else
base64ToBitmap(image) base64ToBitmap(image).asAndroidBitmap()
ntfBuilder = ntfBuilder ntfBuilder = ntfBuilder
.setContentTitle(title) .setContentTitle(title)
@ -229,7 +227,7 @@ object NtfManager {
// This makes notification sound and vibration repeat endlessly // This makes notification sound and vibration repeat endlessly
notification.flags = notification.flags or NotificationCompat.FLAG_INSISTENT notification.flags = notification.flags or NotificationCompat.FLAG_INSISTENT
with(NotificationManagerCompat.from(context)) { with(NotificationManagerCompat.from(context)) {
if (ActivityCompat.checkSelfPermission(SimplexApp.context, Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED) { if (ActivityCompat.checkSelfPermission(SimplexApp.context, android.Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED) {
notify(CallNotificationId, notification) notify(CallNotificationId, notification)
} }
} }

View File

@ -1,30 +0,0 @@
package chat.simplex.app.platform
import chat.simplex.app.BuildConfig
import chat.simplex.app.model.ChatController
import chat.simplex.app.ui.theme.DefaultTheme
import java.util.*
class FifoQueue<E>(private var capacity: Int) : LinkedList<E>() {
override fun add(element: E): Boolean {
if(size > capacity) removeFirst()
return super.add(element)
}
}
fun runMigrations() {
val lastMigration = ChatController.appPrefs.lastMigratedVersionCode
if (lastMigration.get() < BuildConfig.VERSION_CODE) {
while (true) {
if (lastMigration.get() < 117) {
if (ChatController.appPrefs.currentTheme.get() == DefaultTheme.DARK.name) {
ChatController.appPrefs.currentTheme.set(DefaultTheme.SIMPLEX.name)
}
lastMigration.set(117)
} else {
lastMigration.set(BuildConfig.VERSION_CODE)
break
}
}
}
}

View File

@ -1,30 +0,0 @@
package chat.simplex.app.platform
import chat.simplex.app.SimplexApp
import chat.simplex.app.model.CIFile
import java.io.File
fun getFilesDirectory(): String {
return SimplexApp.context.filesDir.toString()
}
fun getTempFilesDirectory(): String {
return "${getFilesDirectory()}/temp_files"
}
fun getAppFilesDirectory(): String {
return "${getFilesDirectory()}/app_files"
}
fun getAppFilePath(fileName: String): String {
return "${getAppFilesDirectory()}/$fileName"
}
fun getLoadedFilePath(file: CIFile?): String? {
return if (file?.filePath != null && file.loaded) {
val filePath = getAppFilePath(file.filePath)
if (File(filePath).exists()) filePath else null
} else {
null
}
}

View File

@ -1,9 +0,0 @@
package chat.simplex.app.platform
import android.app.UiModeManager
import android.content.Context
import chat.simplex.app.SimplexApp
// Non-@Composable implementation
fun isInNightMode() =
(SimplexApp.context.getSystemService(Context.UI_MODE_SERVICE) as UiModeManager).nightMode == UiModeManager.MODE_NIGHT_YES

View File

@ -4,16 +4,14 @@ import android.app.Activity
import android.app.KeyguardManager import android.app.KeyguardManager
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.res.Configuration
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.util.Log import chat.simplex.common.platform.Log
import android.view.WindowManager import android.view.WindowManager
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.viewModels import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.* import androidx.compose.material.*
@ -24,27 +22,27 @@ import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import dev.icerock.moko.resources.compose.painterResource import androidx.compose.ui.res.painterResource
import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import chat.simplex.app.* import chat.simplex.app.*
import chat.simplex.app.R import chat.simplex.app.R
import chat.simplex.app.model.* import chat.simplex.common.model.*
import chat.simplex.app.model.NtfManager.OpenChatAction import chat.simplex.app.model.NtfManager.OpenChatAction
import chat.simplex.app.ui.theme.* import chat.simplex.common.platform.ntfManager
import chat.simplex.app.views.helpers.ProfileImage import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.call.*
import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.stringResource
import kotlinx.datetime.Clock import kotlinx.datetime.Clock
class IncomingCallActivity: ComponentActivity() { class IncomingCallActivity: ComponentActivity() {
private val vm by viewModels<SimplexViewModel>()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContent { IncomingCallActivityView(vm.chatModel) } setContent { IncomingCallActivityView(ChatModel) }
unlockForIncomingCall() unlockForIncomingCall()
} }
@ -103,7 +101,7 @@ fun IncomingCallActivityView(m: ChatModel) {
) { ) {
if (showCallView) { if (showCallView) {
Box { Box {
ActiveCallView(m) ActiveCallView()
if (invitation != null) IncomingCallAlertView(invitation, m) if (invitation != null) IncomingCallAlertView(invitation, m)
} }
} else if (invitation != null) { } else if (invitation != null) {
@ -121,7 +119,7 @@ fun IncomingCallLockScreenAlert(invitation: RcvCallInvitation, chatModel: ChatMo
DisposableEffect(Unit) { DisposableEffect(Unit) {
onDispose { onDispose {
// Cancel notification whatever happens next since otherwise sound from notification and from inside the app can co-exist // Cancel notification whatever happens next since otherwise sound from notification and from inside the app can co-exist
chatModel.controller.ntfManager.cancelCallNotification() ntfManager.cancelCallNotification()
} }
} }
IncomingCallLockScreenAlertLayout( IncomingCallLockScreenAlertLayout(
@ -131,7 +129,7 @@ fun IncomingCallLockScreenAlert(invitation: RcvCallInvitation, chatModel: ChatMo
rejectCall = { cm.endCall(invitation = invitation) }, rejectCall = { cm.endCall(invitation = invitation) },
ignoreCall = { ignoreCall = {
chatModel.activeCallInvitation.value = null chatModel.activeCallInvitation.value = null
chatModel.controller.ntfManager.cancelCallNotification() ntfManager.cancelCallNotification()
}, },
acceptCall = { cm.acceptIncomingCall(invitation = invitation) }, acceptCall = { cm.acceptIncomingCall(invitation = invitation) },
openApp = { openApp = {
@ -171,18 +169,18 @@ fun IncomingCallLockScreenAlertLayout(
Text(invitation.contact.chatViewName, style = MaterialTheme.typography.h2) Text(invitation.contact.chatViewName, style = MaterialTheme.typography.h2)
Spacer(Modifier.fillMaxHeight().weight(1f)) Spacer(Modifier.fillMaxHeight().weight(1f))
Row { Row {
LockScreenCallButton(stringResource(MR.strings.reject), painterResource(MR.images.ic_call_end_filled), Color.Red, rejectCall) LockScreenCallButton(stringResource(MR.strings.reject), painterResource(R.drawable.ic_call_end_filled), Color.Red, rejectCall)
Spacer(Modifier.size(48.dp)) Spacer(Modifier.size(48.dp))
LockScreenCallButton(stringResource(MR.strings.ignore), painterResource(MR.images.ic_close), MaterialTheme.colors.primary, ignoreCall) LockScreenCallButton(stringResource(MR.strings.ignore), painterResource(R.drawable.ic_close), MaterialTheme.colors.primary, ignoreCall)
Spacer(Modifier.size(48.dp)) Spacer(Modifier.size(48.dp))
LockScreenCallButton(stringResource(MR.strings.accept), painterResource(MR.images.ic_check_filled), SimplexGreen, acceptCall) LockScreenCallButton(stringResource(MR.strings.accept), painterResource(R.drawable.ic_check_filled), SimplexGreen, acceptCall)
} }
} else if (callOnLockScreen == CallOnLockScreen.SHOW) { } else if (callOnLockScreen == CallOnLockScreen.SHOW) {
SimpleXLogo() SimpleXLogo()
Text(stringResource(MR.strings.open_simplex_chat_to_accept_call), textAlign = TextAlign.Center, lineHeight = 22.sp) Text(stringResource(MR.strings.open_simplex_chat_to_accept_call), textAlign = TextAlign.Center, lineHeight = 22.sp)
Text(stringResource(MR.strings.allow_accepting_calls_from_lock_screen), textAlign = TextAlign.Center, style = MaterialTheme.typography.body2, lineHeight = 22.sp) Text(stringResource(MR.strings.allow_accepting_calls_from_lock_screen), textAlign = TextAlign.Center, style = MaterialTheme.typography.body2, lineHeight = 22.sp)
Spacer(Modifier.fillMaxHeight().weight(1f)) Spacer(Modifier.fillMaxHeight().weight(1f))
SimpleButton(text = stringResource(MR.strings.open_verb), icon = painterResource(MR.images.ic_check_filled), click = openApp) SimpleButton(text = stringResource(MR.strings.open_verb), icon = painterResource(R.drawable.ic_check_filled), click = openApp)
} }
} }
} }
@ -190,7 +188,7 @@ fun IncomingCallLockScreenAlertLayout(
@Composable @Composable
private fun SimpleXLogo() { private fun SimpleXLogo() {
Image( Image(
painter = painterResource(if (isInDarkTheme()) MR.images.logo_light else MR.images.logo), painter = painterResource(if (isInDarkTheme()) R.drawable.logo_light else R.drawable.logo),
contentDescription = stringResource(MR.strings.image_descr_simplex_logo), contentDescription = stringResource(MR.strings.image_descr_simplex_logo),
modifier = Modifier modifier = Modifier
.padding(vertical = DEFAULT_PADDING) .padding(vertical = DEFAULT_PADDING)
@ -219,10 +217,10 @@ private fun LockScreenCallButton(text: String, icon: Painter, color: Color, acti
} }
} }
@Preview( @Preview/*(
uiMode = Configuration.UI_MODE_NIGHT_YES, uiMode = Configuration.UI_MODE_NIGHT_YES,
showBackground = true showBackground = true
) )*/
@Composable @Composable
fun PreviewIncomingCallLockScreenAlert() { fun PreviewIncomingCallLockScreenAlert() {
SimpleXTheme(true) { SimpleXTheme(true) {

View File

@ -1,17 +0,0 @@
package chat.simplex.app.views.newchat
import android.graphics.Bitmap
fun Bitmap.replaceColor(from: Int, to: Int): Bitmap {
val pixels = IntArray(width * height)
getPixels(pixels, 0, width, 0, 0, width, height)
var i = 0
while (i < pixels.size) {
if (pixels[i] == from) {
pixels[i] = to
}
i++
}
setPixels(pixels, 0, width, 0, 0, width, height)
return this
}

View File

@ -55,7 +55,6 @@ allprojects {
google() google()
mavenCentral() mavenCentral()
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
maven("https://jitpack.io")
} }
} }

View File

@ -31,7 +31,6 @@ kotlin {
} }
val commonMain by getting { val commonMain by getting {
kotlin.srcDir("./build/generated/moko/commonMain/src/")
dependencies { dependencies {
api(compose.runtime) api(compose.runtime)
api(compose.foundation) api(compose.foundation)
@ -57,49 +56,39 @@ kotlin {
implementation(kotlin("test")) implementation(kotlin("test"))
} }
} }
// LALAL CHANGE TO IMPLEMENTATION
val androidMain by getting { val androidMain by getting {
kotlin.srcDir("./build/generated/moko/commonMain/src/")
dependencies { dependencies {
api("androidx.appcompat:appcompat:1.5.1") implementation("androidx.activity:activity-compose:1.5.0")
api("androidx.core:core-ktx:1.9.0")
api("androidx.activity:activity-compose:1.5.0")
val work_version = "2.7.1" val work_version = "2.7.1"
api("androidx.work:work-runtime-ktx:$work_version") implementation("androidx.work:work-runtime-ktx:$work_version")
api("androidx.work:work-multiprocess:$work_version") implementation("com.google.accompanist:accompanist-insets:0.23.0")
api("com.google.accompanist:accompanist-insets:0.23.0") implementation("dev.icerock.moko:resources:0.22.3")
api("dev.icerock.moko:resources:0.22.3")
// Video support // Video support
api("com.google.android.exoplayer:exoplayer:2.17.1") implementation("com.google.android.exoplayer:exoplayer:2.17.1")
// Biometric authentication // Biometric authentication
api("androidx.biometric:biometric:1.2.0-alpha04") implementation("androidx.biometric:biometric:1.2.0-alpha04")
//Barcode //Barcode
api("org.boofcv:boofcv-android:0.40.1") implementation("org.boofcv:boofcv-android:0.40.1")
//Camera Permission //Camera Permission
api("com.google.accompanist:accompanist-permissions:0.23.0") implementation("com.google.accompanist:accompanist-permissions:0.23.0")
api("androidx.webkit:webkit:1.4.0") implementation("androidx.webkit:webkit:1.4.0")
// GIFs support // GIFs support
api("io.coil-kt:coil-compose:2.1.0") implementation("io.coil-kt:coil-compose:2.1.0")
api("io.coil-kt:coil-gif:2.1.0") implementation("io.coil-kt:coil-gif:2.1.0")
api("com.jakewharton:process-phoenix:2.1.2") implementation("com.jakewharton:process-phoenix:2.1.2")
val camerax_version = "1.1.0-beta01" val camerax_version = "1.1.0-beta01"
api("androidx.camera:camera-core:${camerax_version}") implementation("androidx.camera:camera-core:${camerax_version}")
api("androidx.camera:camera-camera2:${camerax_version}") implementation("androidx.camera:camera-camera2:${camerax_version}")
api("androidx.camera:camera-lifecycle:${camerax_version}") implementation("androidx.camera:camera-lifecycle:${camerax_version}")
api("androidx.camera:camera-view:${camerax_version}") implementation("androidx.camera:camera-view:${camerax_version}")
// LALAL REMOVE
api("org.jsoup:jsoup:1.13.1")
api("com.godaddy.android.colorpicker:compose-color-picker-jvm:0.7.0")
api("androidx.compose.ui:ui-tooling-preview:${extra["compose.version"]}")
} }
} }
val desktopMain by getting { val desktopMain by getting {

View File

@ -0,0 +1,22 @@
package chat.simplex.common.helpers
import android.net.Uri
import android.os.Build
import chat.simplex.common.model.NotificationsMode
import java.net.URI
val NotificationsMode.requiresIgnoringBatterySinceSdk: Int get() = when(this) {
NotificationsMode.OFF -> Int.MAX_VALUE
NotificationsMode.PERIODIC -> Build.VERSION_CODES.M
NotificationsMode.SERVICE -> Build.VERSION_CODES.S
/*INSTANT -> Int.MAX_VALUE - for Firebase notifications */
}
val NotificationsMode.requiresIgnoringBattery
get() = requiresIgnoringBatterySinceSdk <= Build.VERSION.SDK_INT
lateinit var APPLICATION_ID: String
fun Uri.toURI(): URI = URI(toString())
fun URI.toUri(): Uri = Uri.parse(toString())

View File

@ -1,22 +1,22 @@
package chat.simplex.app.helpers package chat.simplex.common.helpers
import android.app.Activity import android.app.Activity
import android.content.res.Configuration import android.content.res.Configuration
import chat.simplex.app.SimplexApp import chat.simplex.common.model.SharedPreference
import chat.simplex.app.model.SharedPreference import chat.simplex.common.platform.androidAppContext
import chat.simplex.app.platform.defaultLocale import chat.simplex.common.platform.defaultLocale
import java.util.* import java.util.*
fun saveAppLocale(pref: SharedPreference<String?>, activity: Activity, languageCode: String? = null) { fun Activity.saveAppLocale(pref: SharedPreference<String?>, languageCode: String? = null) {
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { // if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
// val localeManager = SimplexApp.context.getSystemService(LocaleManager::class.java) // val localeManager = SimplexApp.context.getSystemService(LocaleManager::class.java)
// localeManager.applicationLocales = LocaleList(Locale.forLanguageTag(languageCode ?: return)) // localeManager.applicationLocales = LocaleList(Locale.forLanguageTag(languageCode ?: return))
// } else { // } else {
pref.set(languageCode) pref.set(languageCode)
if (languageCode == null) { if (languageCode == null) {
activity.applyLocale(defaultLocale) applyLocale(defaultLocale)
} }
activity.recreate() recreate()
// } // }
} }
@ -30,10 +30,10 @@ fun Activity.applyAppLocale(pref: SharedPreference<String?>) {
private fun Activity.applyLocale(locale: Locale) { private fun Activity.applyLocale(locale: Locale) {
Locale.setDefault(locale) Locale.setDefault(locale)
val appConf = Configuration(SimplexApp.context.resources.configuration).apply { setLocale(locale) } val appConf = Configuration(androidAppContext.resources.configuration).apply { setLocale(locale) }
val activityConf = Configuration(resources.configuration).apply { setLocale(locale) } val activityConf = Configuration(resources.configuration).apply { setLocale(locale) }
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
SimplexApp.context.resources.updateConfiguration(appConf, resources.displayMetrics) androidAppContext.resources.updateConfiguration(appConf, resources.displayMetrics)
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
resources.updateConfiguration(activityConf, resources.displayMetrics) resources.updateConfiguration(activityConf, resources.displayMetrics)
} }

View File

@ -1,22 +1,22 @@
package chat.simplex.app.views.call package chat.simplex.common.helpers
import android.content.Context
import android.media.* import android.media.*
import android.net.Uri import android.net.Uri
import android.os.VibrationEffect import android.os.VibrationEffect
import android.os.Vibrator import android.os.Vibrator
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import chat.simplex.app.R import chat.simplex.common.R
import chat.simplex.app.SimplexApp import chat.simplex.common.platform.SoundPlayerInterface
import chat.simplex.app.views.helpers.withScope import chat.simplex.common.platform.androidAppContext
import chat.simplex.common.views.helpers.withScope
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
class SoundPlayer { object SoundPlayer: SoundPlayerInterface {
private var player: MediaPlayer? = null private var player: MediaPlayer? = null
var playing = false var playing = false
fun start(scope: CoroutineScope, sound: Boolean) { override fun start(scope: CoroutineScope, sound: Boolean) {
player?.reset() player?.reset()
player = MediaPlayer().apply { player = MediaPlayer().apply {
setAudioAttributes( setAudioAttributes(
@ -25,10 +25,10 @@ class SoundPlayer {
.setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE) .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
.build() .build()
) )
setDataSource(SimplexApp.context, Uri.parse("android.resource://" + SimplexApp.context.packageName + "/" + R.raw.ring_once)) setDataSource(androidAppContext, Uri.parse("android.resource://" + androidAppContext.packageName + "/" + R.raw.ring_once))
prepare() prepare()
} }
val vibrator = ContextCompat.getSystemService(SimplexApp.context, Vibrator::class.java) val vibrator = ContextCompat.getSystemService(androidAppContext, Vibrator::class.java)
val effect = VibrationEffect.createOneShot(250, VibrationEffect.DEFAULT_AMPLITUDE) val effect = VibrationEffect.createOneShot(250, VibrationEffect.DEFAULT_AMPLITUDE)
playing = true playing = true
withScope(scope) { withScope(scope) {
@ -40,12 +40,8 @@ class SoundPlayer {
} }
} }
fun stop() { override fun stop() {
playing = false playing = false
player?.stop() player?.stop()
} }
companion object {
val shared = SoundPlayer()
}
} }

View File

@ -1,23 +1,32 @@
package chat.simplex.app.platform package chat.simplex.common.platform
import android.annotation.SuppressLint
import android.content.Context
import android.net.LocalServerSocket import android.net.LocalServerSocket
import android.util.Log import android.util.Log
import chat.simplex.app.* import androidx.fragment.app.FragmentActivity
import chat.simplex.common.*
import chat.simplex.common.platform.*
import java.io.* import java.io.*
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
import java.util.* import java.util.*
import java.util.concurrent.Semaphore import java.util.concurrent.Semaphore
import kotlin.concurrent.thread import kotlin.concurrent.thread
import kotlin.random.Random
actual val appPlatform = AppPlatform.ANDROID
var isAppOnForeground: Boolean = false var isAppOnForeground: Boolean = false
@Suppress("ConstantLocale") @Suppress("ConstantLocale")
val defaultLocale: Locale = Locale.getDefault() val defaultLocale: Locale = Locale.getDefault()
var mainActivity: WeakReference<MainActivity> = WeakReference(null) @SuppressLint("StaticFieldLeak")
lateinit var androidAppContext: Context
lateinit var mainActivity: WeakReference<FragmentActivity>
fun initHaskell() { actual fun initHaskell() {
val socketName = BuildConfig.APPLICATION_ID + ".local.socket.address.listen.native.cmd2" val socketName = "chat.simplex.app.local.socket.address.listen.native.cmd2" + Random.nextLong(100000)
val s = Semaphore(0) val s = Semaphore(0)
thread(name="stdout/stderr pipe") { thread(name="stdout/stderr pipe") {
Log.d(TAG, "starting server") Log.d(TAG, "starting server")

View File

@ -0,0 +1,9 @@
package chat.simplex.common.platform
import androidx.compose.runtime.*
@SuppressWarnings("MissingJvmstatic")
@Composable
actual fun BackHandler(enabled: Boolean, onBack: () -> Unit) {
androidx.activity.compose.BackHandler(enabled, onBack)
}

View File

@ -1,24 +1,23 @@
package chat.simplex.app.views.usersettings package chat.simplex.common.platform
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.security.keystore.KeyGenParameterSpec import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties import android.security.keystore.KeyProperties
import android.util.Log import chat.simplex.common.views.helpers.AlertManager
import chat.simplex.app.R import chat.simplex.common.views.helpers.generalGetString
import chat.simplex.app.TAG
import chat.simplex.app.views.helpers.AlertManager
import chat.simplex.app.views.helpers.generalGetString
import chat.simplex.res.MR import chat.simplex.res.MR
import java.security.KeyStore import java.security.KeyStore
import javax.crypto.* import javax.crypto.*
import javax.crypto.spec.GCMParameterSpec import javax.crypto.spec.GCMParameterSpec
actual val cryptor: CryptorInterface = Cryptor()
@SuppressLint("ObsoleteSdkInt") @SuppressLint("ObsoleteSdkInt")
internal class Cryptor { internal class Cryptor: CryptorInterface {
private var keyStore: KeyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) } private var keyStore: KeyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
private var warningShown = false private var warningShown = false
fun decryptData(data: ByteArray, iv: ByteArray, alias: String): String? { override fun decryptData(data: ByteArray, iv: ByteArray, alias: String): String? {
val secretKey = getSecretKey(alias) val secretKey = getSecretKey(alias)
if (secretKey == null) { if (secretKey == null) {
if (!warningShown) { if (!warningShown) {
@ -37,13 +36,13 @@ internal class Cryptor {
return runCatching { String(cipher.doFinal(data))}.onFailure { Log.e(TAG, "doFinal: ${it.stackTraceToString()}") }.getOrNull() return runCatching { String(cipher.doFinal(data))}.onFailure { Log.e(TAG, "doFinal: ${it.stackTraceToString()}") }.getOrNull()
} }
fun encryptText(text: String, alias: String): Pair<ByteArray, ByteArray> { override fun encryptText(text: String, alias: String): Pair<ByteArray, ByteArray> {
val cipher: Cipher = Cipher.getInstance(TRANSFORMATION) val cipher: Cipher = Cipher.getInstance(TRANSFORMATION)
cipher.init(Cipher.ENCRYPT_MODE, createSecretKey(alias)) cipher.init(Cipher.ENCRYPT_MODE, createSecretKey(alias))
return Pair(cipher.doFinal(text.toByteArray(charset("UTF-8"))), cipher.iv) return Pair(cipher.doFinal(text.toByteArray(charset("UTF-8"))), cipher.iv)
} }
fun deleteKey(alias: String) { override fun deleteKey(alias: String) {
if (!keyStore.containsAlias(alias)) return if (!keyStore.containsAlias(alias)) return
keyStore.deleteEntry(alias) keyStore.deleteEntry(alias)
} }

View File

@ -0,0 +1,40 @@
package chat.simplex.common.platform
import android.app.Application
import android.net.Uri
import androidx.activity.compose.ManagedActivityResultLauncher
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.runtime.Composable
import chat.simplex.common.helpers.toURI
import chat.simplex.common.helpers.toUri
import java.io.*
import java.net.URI
actual val dataDir: File = androidAppContext.dataDir
actual val tmpDir: File = androidAppContext.getDir("temp", Application.MODE_PRIVATE)
actual val cacheDir: File = androidAppContext.cacheDir
@Composable
actual fun rememberFileChooserLauncher(getContent: Boolean, onResult: (URI?) -> Unit): FileChooserLauncher {
val launcher = rememberLauncherForActivityResult(
contract = if (getContent) ActivityResultContracts.GetContent() else ActivityResultContracts.CreateDocument(),
onResult = { onResult(it?.toURI()) }
)
return FileChooserLauncher(launcher)
}
actual class FileChooserLauncher actual constructor() {
private lateinit var launcher: ManagedActivityResultLauncher<String, Uri?>
constructor(launcher: ManagedActivityResultLauncher<String, Uri?>): this() {
this.launcher = launcher
}
actual suspend fun launch(input: String) {
launcher.launch(input)
}
}
actual fun URI.inputStream(): InputStream? = androidAppContext.contentResolver.openInputStream(toUri())
actual fun URI.outputStream(): OutputStream = androidAppContext.contentResolver.openOutputStream(toUri())!!

View File

@ -1,36 +1,40 @@
package chat.simplex.app.platform package chat.simplex.common.platform
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.graphics.drawable.AnimatedImageDrawable import android.graphics.drawable.AnimatedImageDrawable
import android.net.Uri
import android.os.Build import android.os.Build
import android.util.Base64 import android.util.Base64
import android.util.Log
import android.webkit.MimeTypeMap import android.webkit.MimeTypeMap
import androidx.compose.ui.graphics.*
import androidx.core.graphics.applyCanvas import androidx.core.graphics.applyCanvas
import androidx.core.graphics.drawable.toBitmap import androidx.core.graphics.drawable.toBitmap
import chat.simplex.app.* import androidx.core.graphics.scale
import chat.simplex.app.views.helpers.errorBitmap import boofcv.android.ConvertBitmap
import chat.simplex.app.views.helpers.getFileName import boofcv.struct.image.GrayU8
import chat.simplex.common.R
import chat.simplex.common.views.helpers.errorBitmap
import chat.simplex.common.views.helpers.getFileName
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.io.InputStream
import java.net.URI
import kotlin.math.min import kotlin.math.min
import kotlin.math.sqrt import kotlin.math.sqrt
fun base64ToBitmap(base64ImageString: String): Bitmap { actual fun base64ToBitmap(base64ImageString: String): ImageBitmap {
val imageString = base64ImageString val imageString = base64ImageString
.removePrefix("data:image/png;base64,") .removePrefix("data:image/png;base64,")
.removePrefix("data:image/jpg;base64,") .removePrefix("data:image/jpg;base64,")
try { return try {
val imageBytes = Base64.decode(imageString, Base64.NO_WRAP) val imageBytes = Base64.decode(imageString, Base64.NO_WRAP)
return BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size) BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size).asImageBitmap()
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "base64ToBitmap error: $e") Log.e(TAG, "base64ToBitmap error: $e")
return errorBitmap errorBitmap.asImageBitmap()
} }
} }
fun resizeImageToStrSize(image: Bitmap, maxDataSize: Long): String { actual fun resizeImageToStrSize(image: ImageBitmap, maxDataSize: Long): String {
var img = image var img = image
var str = compressImageStr(img) var str = compressImageStr(img)
while (str.length > maxDataSize) { while (str.length > maxDataSize) {
@ -38,14 +42,14 @@ fun resizeImageToStrSize(image: Bitmap, maxDataSize: Long): String {
val clippedRatio = min(ratio, 2.0) val clippedRatio = min(ratio, 2.0)
val width = (img.width.toDouble() / clippedRatio).toInt() val width = (img.width.toDouble() / clippedRatio).toInt()
val height = img.height * width / img.width val height = img.height * width / img.width
img = Bitmap.createScaledBitmap(img, width, height, true) img = Bitmap.createScaledBitmap(img.asAndroidBitmap(), width, height, true).asImageBitmap()
str = compressImageStr(img) str = compressImageStr(img)
} }
return str return str
} }
// Inspired by https://github.com/MakeItEasyDev/Jetpack-Compose-Capture-Image-Or-Choose-from-Gallery // Inspired by https://github.com/MakeItEasyDev/Jetpack-Compose-Capture-Image-Or-Choose-from-Gallery
fun cropToSquare(image: Bitmap): Bitmap { actual fun cropToSquare(image: ImageBitmap): ImageBitmap {
var xOffset = 0 var xOffset = 0
var yOffset = 0 var yOffset = 0
val side = min(image.height, image.width) val side = min(image.height, image.width)
@ -54,22 +58,22 @@ fun cropToSquare(image: Bitmap): Bitmap {
} else { } else {
yOffset = (image.height - side) / 2 yOffset = (image.height - side) / 2
} }
return Bitmap.createBitmap(image, xOffset, yOffset, side, side) return Bitmap.createBitmap(image.asAndroidBitmap(), xOffset, yOffset, side, side).asImageBitmap()
} }
private fun compressImageStr(bitmap: Bitmap): String { actual fun compressImageStr(bitmap: ImageBitmap): String {
val usePng = bitmap.hasAlpha() val usePng = bitmap.hasAlpha
val ext = if (usePng) "png" else "jpg" val ext = if (usePng) "png" else "jpg"
return "data:image/$ext;base64," + Base64.encodeToString(compressImageData(bitmap, usePng).toByteArray(), Base64.NO_WRAP) return "data:image/$ext;base64," + Base64.encodeToString(compressImageData(bitmap, usePng).toByteArray(), Base64.NO_WRAP)
} }
private fun compressImageData(bitmap: Bitmap, usePng: Boolean): ByteArrayOutputStream { actual fun compressImageData(bitmap: ImageBitmap, usePng: Boolean): ByteArrayOutputStream {
val stream = ByteArrayOutputStream() val stream = ByteArrayOutputStream()
bitmap.compress(if (!usePng) Bitmap.CompressFormat.JPEG else Bitmap.CompressFormat.PNG, 85, stream) bitmap.asAndroidBitmap().compress(if (!usePng) Bitmap.CompressFormat.JPEG else Bitmap.CompressFormat.PNG, 85, stream)
return stream return stream
} }
fun resizeImageToDataSize(image: Bitmap, usePng: Boolean, maxDataSize: Long): ByteArrayOutputStream { actual fun resizeImageToDataSize(image: ImageBitmap, usePng: Boolean, maxDataSize: Long): ByteArrayOutputStream {
var img = image var img = image
var stream = compressImageData(img, usePng) var stream = compressImageData(img, usePng)
while (stream.size() > maxDataSize) { while (stream.size() > maxDataSize) {
@ -77,30 +81,36 @@ fun resizeImageToDataSize(image: Bitmap, usePng: Boolean, maxDataSize: Long): By
val clippedRatio = min(ratio, 2.0) val clippedRatio = min(ratio, 2.0)
val width = (img.width.toDouble() / clippedRatio).toInt() val width = (img.width.toDouble() / clippedRatio).toInt()
val height = img.height * width / img.width val height = img.height * width / img.width
img = Bitmap.createScaledBitmap(img, width, height, true) img = Bitmap.createScaledBitmap(img.asAndroidBitmap(), width, height, true).asImageBitmap()
stream = compressImageData(img, usePng) stream = compressImageData(img, usePng)
} }
return stream return stream
} }
fun Bitmap.addLogo(): Bitmap = applyCanvas { actual fun GrayU8.toImageBitmap(): ImageBitmap = ConvertBitmap.grayToBitmap(this, Bitmap.Config.RGB_565).asImageBitmap()
actual fun ImageBitmap.addLogo(): ImageBitmap = asAndroidBitmap().applyCanvas {
val radius = (width * 0.16f) / 2 val radius = (width * 0.16f) / 2
val paint = android.graphics.Paint() val paint = android.graphics.Paint()
paint.color = android.graphics.Color.WHITE paint.color = android.graphics.Color.WHITE
drawCircle(width / 2f, height / 2f, radius, paint) drawCircle(width / 2f, height / 2f, radius, paint)
val logo = SimplexApp.context.resources.getDrawable(R.mipmap.icon_foreground, null).toBitmap() val logo = androidAppContext.resources.getDrawable(R.drawable.icon_foreground_android_common, null).toBitmap()
val logoSize = (width * 0.24).toInt() val logoSize = (width * 0.24).toInt()
translate((width - logoSize) / 2f, (height - logoSize) / 2f) translate((width - logoSize) / 2f, (height - logoSize) / 2f)
drawBitmap(logo, null, android.graphics.Rect(0, 0, logoSize, logoSize), null) drawBitmap(logo, null, android.graphics.Rect(0, 0, logoSize, logoSize), null)
} }.asImageBitmap()
fun isImage(uri: Uri): Boolean = actual fun ImageBitmap.scale(width: Int, height: Int): ImageBitmap = asAndroidBitmap().scale(width, height).asImageBitmap()
actual fun isImage(uri: URI): Boolean =
MimeTypeMap.getSingleton().getMimeTypeFromExtension(getFileName(uri)?.split(".")?.last())?.contains("image/") == true MimeTypeMap.getSingleton().getMimeTypeFromExtension(getFileName(uri)?.split(".")?.last())?.contains("image/") == true
actual fun isAnimImage(uri: URI, drawable: Any?): Boolean {
fun isAnimImage(uri: Uri, drawable: Any?): Boolean {
val isAnimNewApi = Build.VERSION.SDK_INT >= 28 && drawable is AnimatedImageDrawable val isAnimNewApi = Build.VERSION.SDK_INT >= 28 && drawable is AnimatedImageDrawable
val isAnimOldApi = Build.VERSION.SDK_INT < 28 && val isAnimOldApi = Build.VERSION.SDK_INT < 28 &&
(getFileName(uri)?.endsWith(".gif") == true || getFileName(uri)?.endsWith(".webp") == true) (getFileName(uri)?.endsWith(".gif") == true || getFileName(uri)?.endsWith(".webp") == true)
return isAnimNewApi || isAnimOldApi return isAnimNewApi || isAnimOldApi
} }
actual fun loadImageBitmap(inputStream: InputStream): ImageBitmap =
BitmapFactory.decodeStream(inputStream).asImageBitmap()

View File

@ -0,0 +1,10 @@
package chat.simplex.common.platform
import android.util.Log
actual object Log {
actual fun d(tag: String, text: String) = Log.d(tag, text).run{}
actual fun e(tag: String, text: String) = Log.e(tag, text).run{}
actual fun i(tag: String, text: String) = Log.i(tag, text).run{}
actual fun w(tag: String, text: String) = Log.w(tag, text).run{}
}

View File

@ -0,0 +1,16 @@
package chat.simplex.common.platform
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.google.accompanist.insets.navigationBarsWithImePadding
actual fun Modifier.navigationBarsWithImePadding(): Modifier = navigationBarsWithImePadding()
@Composable
actual fun ProvideWindowInsets(
consumeWindowInsets: Boolean,
windowInsetsAnimationsEnabled: Boolean,
content: @Composable () -> Unit
) {
com.google.accompanist.insets.ProvideWindowInsets(content = content)
}

View File

@ -0,0 +1,3 @@
package chat.simplex.common.platform
actual fun allowedToShowNotification(): Boolean = !isAppOnForeground

View File

@ -1,4 +1,4 @@
package chat.simplex.app.platform package chat.simplex.common.platform
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context import android.content.Context
@ -27,20 +27,22 @@ import androidx.core.view.inputmethod.EditorInfoCompat
import androidx.core.view.inputmethod.InputConnectionCompat import androidx.core.view.inputmethod.InputConnectionCompat
import androidx.core.widget.doAfterTextChanged import androidx.core.widget.doAfterTextChanged
import androidx.core.widget.doOnTextChanged import androidx.core.widget.doOnTextChanged
import chat.simplex.app.* import chat.simplex.common.*
import chat.simplex.app.R import chat.simplex.common.R
import chat.simplex.app.model.ChatModel import chat.simplex.common.helpers.toURI
import chat.simplex.app.ui.theme.CurrentColors import chat.simplex.common.model.ChatModel
import chat.simplex.app.views.chat.* import chat.simplex.common.ui.theme.CurrentColors
import chat.simplex.app.views.helpers.SharedContent import chat.simplex.common.views.chat.*
import chat.simplex.app.views.helpers.generalGetString import chat.simplex.common.views.helpers.SharedContent
import chat.simplex.common.views.helpers.generalGetString
import chat.simplex.res.MR import chat.simplex.res.MR
import dev.icerock.moko.resources.StringResource import dev.icerock.moko.resources.StringResource
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import java.lang.reflect.Field import java.lang.reflect.Field
import java.net.URI
@Composable @Composable
fun NativeKeyboard( actual fun PlatformTextField(
composeState: MutableState<ComposeState>, composeState: MutableState<ComposeState>,
textStyle: MutableState<TextStyle>, textStyle: MutableState<TextStyle>,
showDeleteTextButton: MutableState<Boolean>, showDeleteTextButton: MutableState<Boolean>,
@ -85,7 +87,7 @@ fun NativeKeyboard(
} catch (e: Exception) { } catch (e: Exception) {
return@OnCommitContentListener false return@OnCommitContentListener false
} }
ChatModel.sharedContent.value = SharedContent.Media("", listOf(inputContentInfo.contentUri)) ChatModel.sharedContent.value = SharedContent.Media("", listOf(inputContentInfo.contentUri.toURI()))
true true
} }
return InputConnectionCompat.createWrapper(connection, editorInfo, onCommit) return InputConnectionCompat.createWrapper(connection, editorInfo, onCommit)
@ -96,7 +98,7 @@ fun NativeKeyboard(
editText.inputType = InputType.TYPE_TEXT_FLAG_CAP_SENTENCES or editText.inputType editText.inputType = InputType.TYPE_TEXT_FLAG_CAP_SENTENCES or editText.inputType
editText.setTextColor(textColor.toArgb()) editText.setTextColor(textColor.toArgb())
editText.textSize = textStyle.value.fontSize.value editText.textSize = textStyle.value.fontSize.value
val drawable = it.getDrawable(R.drawable.send_msg_view_background)!! val drawable = androidAppContext.getDrawable(R.drawable.send_msg_view_background)!!
DrawableCompat.setTint(drawable, tintColor.toArgb()) DrawableCompat.setTint(drawable, tintColor.toArgb())
editText.background = drawable editText.background = drawable
editText.setPadding(paddingStart, paddingTop, paddingEnd, paddingBottom) editText.setPadding(paddingStart, paddingTop, paddingEnd, paddingBottom)
@ -134,7 +136,7 @@ fun NativeKeyboard(
} }
if (showKeyboard) { if (showKeyboard) {
it.requestFocus() it.requestFocus()
val imm: InputMethodManager = SimplexApp.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager val imm: InputMethodManager = androidAppContext.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.showSoftInput(it, InputMethodManager.SHOW_IMPLICIT) imm.showSoftInput(it, InputMethodManager.SHOW_IMPLICIT)
showKeyboard = false showKeyboard = false
} }

View File

@ -1,4 +1,4 @@
package chat.simplex.app.views.helpers package chat.simplex.common.platform
import android.app.Application import android.app.Application
import android.content.Context import android.content.Context
@ -7,34 +7,22 @@ import android.media.AudioManager.AudioPlaybackCallback
import android.media.MediaRecorder.MEDIA_RECORDER_INFO_MAX_DURATION_REACHED import android.media.MediaRecorder.MEDIA_RECORDER_INFO_MAX_DURATION_REACHED
import android.media.MediaRecorder.MEDIA_RECORDER_INFO_MAX_FILESIZE_REACHED import android.media.MediaRecorder.MEDIA_RECORDER_INFO_MAX_FILESIZE_REACHED
import android.os.Build import android.os.Build
import android.util.Log
import androidx.compose.runtime.* import androidx.compose.runtime.*
import chat.simplex.app.*
import chat.simplex.app.R
import chat.simplex.app.model.ChatItem
import chat.simplex.app.views.helpers.AudioPlayer.duration
import chat.simplex.res.MR import chat.simplex.res.MR
import chat.simplex.common.model.ChatItem
import chat.simplex.common.platform.AudioPlayer.duration
import chat.simplex.common.views.helpers.*
import kotlinx.coroutines.* import kotlinx.coroutines.*
import java.io.* import java.io.*
interface Recorder { actual class RecorderNative: RecorderInterface {
fun start(onProgressUpdate: (position: Int?, finished: Boolean) -> Unit): String
fun stop(): Int
}
class RecorderNative(): Recorder {
companion object {
// Allows to stop the recorder from outside without having the recorder in a variable
var stopRecording: (() -> Unit)? = null
const val extension = "m4a"
}
private var recorder: MediaRecorder? = null private var recorder: MediaRecorder? = null
private var progressJob: Job? = null private var progressJob: Job? = null
private var filePath: String? = null private var filePath: String? = null
private var recStartedAt: Long? = null private var recStartedAt: Long? = null
private fun initRecorder() = private fun initRecorder() =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
MediaRecorder(SimplexApp.context) MediaRecorder(androidAppContext)
} else { } else {
MediaRecorder() MediaRecorder()
} }
@ -51,8 +39,7 @@ class RecorderNative(): Recorder {
rec.setAudioSamplingRate(16000) rec.setAudioSamplingRate(16000)
rec.setAudioEncodingBitRate(32000) rec.setAudioEncodingBitRate(32000)
rec.setMaxDuration(MAX_VOICE_MILLIS_FOR_SENDING) rec.setMaxDuration(MAX_VOICE_MILLIS_FOR_SENDING)
val tmpDir = SimplexApp.context.getDir("temp", Application.MODE_PRIVATE) val fileToSave = File.createTempFile(generateNewFileName("voice", "${RecorderInterface.extension}_"), ".tmp", tmpDir)
val fileToSave = File.createTempFile(generateNewFileName("voice", "${extension}_"), ".tmp", tmpDir)
fileToSave.deleteOnExit() fileToSave.deleteOnExit()
val path = fileToSave.absolutePath val path = fileToSave.absolutePath
filePath = path filePath = path
@ -75,13 +62,13 @@ class RecorderNative(): Recorder {
stop() stop()
} }
} }
stopRecording = { stop() } RecorderInterface.stopRecording = { stop() }
return path return path
} }
override fun stop(): Int { override fun stop(): Int {
val path = filePath ?: return 0 val path = filePath ?: return 0
stopRecording = null RecorderInterface.stopRecording = null
runCatching { runCatching {
recorder?.stop() recorder?.stop()
} }
@ -110,7 +97,7 @@ class RecorderNative(): Recorder {
private fun realDuration(path: String): Int? = duration(path) ?: progress() private fun realDuration(path: String): Int? = duration(path) ?: progress()
} }
object AudioPlayer { actual object AudioPlayer: AudioPlayerInterface {
private val player = MediaPlayer().apply { private val player = MediaPlayer().apply {
setAudioAttributes( setAudioAttributes(
AudioAttributes.Builder() AudioAttributes.Builder()
@ -118,13 +105,13 @@ object AudioPlayer {
.setUsage(AudioAttributes.USAGE_MEDIA) .setUsage(AudioAttributes.USAGE_MEDIA)
.build() .build()
) )
(SimplexApp.context.getSystemService(Context.AUDIO_SERVICE) as AudioManager) (androidAppContext.getSystemService(Context.AUDIO_SERVICE) as AudioManager)
.registerAudioPlaybackCallback(object: AudioPlaybackCallback() { .registerAudioPlaybackCallback(object: AudioPlaybackCallback() {
override fun onPlaybackConfigChanged(configs: MutableList<AudioPlaybackConfiguration>?) { override fun onPlaybackConfigChanged(configs: MutableList<AudioPlaybackConfiguration>?) {
if (configs?.any { it.audioAttributes.usage == AudioAttributes.USAGE_VOICE_COMMUNICATION } == true) { if (configs?.any { it.audioAttributes.usage == AudioAttributes.USAGE_VOICE_COMMUNICATION } == true) {
// In a process of making a call // In a process of making a call
RecorderNative.stopRecording?.invoke() RecorderInterface.stopRecording?.invoke()
stop() AudioPlayer.stop()
} }
super.onPlaybackConfigChanged(configs) super.onPlaybackConfigChanged(configs)
} }
@ -154,7 +141,7 @@ object AudioPlayer {
} }
VideoPlayer.stopAll() VideoPlayer.stopAll()
RecorderNative.stopRecording?.invoke() RecorderInterface.stopRecording?.invoke()
val current = currentlyPlaying.value val current = currentlyPlaying.value
if (current == null || current.first != filePath) { if (current == null || current.first != filePath) {
stopListener() stopListener()
@ -208,16 +195,16 @@ object AudioPlayer {
return player.currentPosition return player.currentPosition
} }
fun stop() { override fun stop() {
if (currentlyPlaying.value == null) return if (currentlyPlaying.value == null) return
player.stop() player.stop()
stopListener() stopListener()
} }
fun stop(item: ChatItem) = stop(item.file?.fileName) override fun stop(item: ChatItem) = stop(item.file?.fileName)
// FileName or filePath are ok // FileName or filePath are ok
fun stop(fileName: String?) { override fun stop(fileName: String?) {
if (fileName != null && currentlyPlaying.value?.first?.endsWith(fileName) == true) { if (fileName != null && currentlyPlaying.value?.first?.endsWith(fileName) == true) {
stop() stop()
} }
@ -241,7 +228,7 @@ object AudioPlayer {
progressJob = null progressJob = null
} }
fun play( override fun play(
filePath: String?, filePath: String?,
audioPlaying: MutableState<Boolean>, audioPlaying: MutableState<Boolean>,
progress: MutableState<Int>, progress: MutableState<Int>,
@ -269,19 +256,19 @@ object AudioPlayer {
realDuration?.let { duration.value = it } realDuration?.let { duration.value = it }
} }
fun pause(audioPlaying: MutableState<Boolean>, pro: MutableState<Int>) { override fun pause(audioPlaying: MutableState<Boolean>, pro: MutableState<Int>) {
pro.value = pause() pro.value = pause()
audioPlaying.value = false audioPlaying.value = false
} }
fun seekTo(ms: Int, pro: MutableState<Int>, filePath: String?) { override fun seekTo(ms: Int, pro: MutableState<Int>, filePath: String?) {
pro.value = ms pro.value = ms
if (this.currentlyPlaying.value?.first == filePath) { if (currentlyPlaying.value?.first == filePath) {
player.seekTo(ms) player.seekTo(ms)
} }
} }
fun duration(filePath: String): Int? { override fun duration(filePath: String): Int? {
var res: Int? = null var res: Int? = null
kotlin.runCatching { kotlin.runCatching {
helperPlayer.setDataSource(filePath) helperPlayer.setDataSource(filePath)
@ -294,3 +281,5 @@ object AudioPlayer {
return res return res
} }
} }
actual typealias SoundPlayer = chat.simplex.common.helpers.SoundPlayer

View File

@ -0,0 +1,51 @@
package chat.simplex.common.platform
import android.annotation.SuppressLint
import android.app.UiModeManager
import android.content.Context
import android.content.SharedPreferences
import android.content.res.Configuration
import android.text.BidiFormatter
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import chat.simplex.common.model.AppPreferences
import com.russhwolf.settings.Settings
import com.russhwolf.settings.SharedPreferencesSettings
import dev.icerock.moko.resources.StringResource
import dev.icerock.moko.resources.desc.desc
@SuppressLint("DiscouragedApi")
@Composable
actual fun font(name: String, res: String, weight: FontWeight, style: FontStyle): Font {
val context = LocalContext.current
val id = context.resources.getIdentifier(res, "font", context.packageName)
return Font(id, weight, style)
}
actual fun StringResource.localized(): String = desc().toString(context = androidAppContext)
actual fun isInNightMode() =
(androidAppContext.getSystemService(Context.UI_MODE_SERVICE) as UiModeManager).nightMode == UiModeManager.MODE_NIGHT_YES
private val sharedPreferences: SharedPreferences by lazy { androidAppContext.getSharedPreferences(AppPreferences.SHARED_PREFS_ID, Context.MODE_PRIVATE) }
private val sharedPreferencesThemes: SharedPreferences by lazy { androidAppContext.getSharedPreferences(AppPreferences.SHARED_PREFS_THEMES_ID, Context.MODE_PRIVATE) }
actual val settings: Settings by lazy { SharedPreferencesSettings(sharedPreferences) }
actual val settingsThemes: Settings by lazy { SharedPreferencesSettings(sharedPreferencesThemes) }
actual fun screenOrientation(): ScreenOrientation = when (mainActivity.get()?.resources?.configuration?.orientation) {
Configuration.ORIENTATION_PORTRAIT -> ScreenOrientation.PORTRAIT
Configuration.ORIENTATION_LANDSCAPE -> ScreenOrientation.LANDSCAPE
else -> ScreenOrientation.UNDEFINED
}
@Composable
actual fun screenWidth(): Dp = LocalConfiguration.current.screenWidthDp.dp
actual fun isRtl(text: CharSequence): Boolean = BidiFormatter.getInstance().isRtl(text)

View File

@ -1,35 +1,35 @@
package chat.simplex.app.platform package chat.simplex.common.platform
import android.Manifest import android.Manifest
import android.content.* import android.content.*
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
import android.net.Uri import android.net.Uri
import android.provider.MediaStore import android.provider.MediaStore
import android.util.Log
import android.webkit.MimeTypeMap import android.webkit.MimeTypeMap
import android.widget.Toast import androidx.compose.ui.platform.ClipboardManager
import androidx.core.content.ContextCompat import androidx.compose.ui.platform.UriHandler
import androidx.core.content.FileProvider import chat.simplex.common.helpers.toUri
import chat.simplex.app.* import chat.simplex.common.model.CIFile
import chat.simplex.app.model.CIFile import chat.simplex.common.views.helpers.generalGetString
import chat.simplex.app.views.helpers.generalGetString import chat.simplex.common.views.helpers.getAppFileUri
import chat.simplex.res.MR
import java.io.BufferedOutputStream import java.io.BufferedOutputStream
import java.io.File import java.io.File
import chat.simplex.res.MR
fun shareText(text: String) { actual fun ClipboardManager.shareText(text: String) {
val sendIntent: Intent = Intent().apply { val sendIntent: Intent = Intent().apply {
action = Intent.ACTION_SEND action = Intent.ACTION_SEND
putExtra(Intent.EXTRA_TEXT, text) putExtra(Intent.EXTRA_TEXT, text)
type = "text/plain" type = "text/plain"
flags = FLAG_ACTIVITY_NEW_TASK
} }
val shareIntent = Intent.createChooser(sendIntent, null) val shareIntent = Intent.createChooser(sendIntent, null)
// This flag is needed when you start a new activity from non-Activity context shareIntent.addFlags(FLAG_ACTIVITY_NEW_TASK)
shareIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) androidAppContext.startActivity(shareIntent)
SimplexApp.context.startActivity(shareIntent)
} }
fun shareFile(text: String, filePath: String) { actual fun shareFile(text: String, filePath: String) {
val uri = FileProvider.getUriForFile(SimplexApp.context, "${BuildConfig.APPLICATION_ID}.provider", File(filePath)) val uri = getAppFileUri(filePath.substringAfterLast(File.separator))
val ext = filePath.substringAfterLast(".") val ext = filePath.substringAfterLast(".")
val mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext) ?: return val mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext) ?: return
val sendIntent: Intent = Intent().apply { val sendIntent: Intent = Intent().apply {
@ -37,28 +37,22 @@ fun shareFile(text: String, filePath: String) {
/*if (text.isNotEmpty()) { /*if (text.isNotEmpty()) {
putExtra(Intent.EXTRA_TEXT, text) putExtra(Intent.EXTRA_TEXT, text)
}*/ }*/
putExtra(Intent.EXTRA_STREAM, uri) putExtra(Intent.EXTRA_STREAM, uri.toUri())
type = mimeType type = mimeType
flags = Intent.FLAG_ACTIVITY_NEW_TASK
} }
val shareIntent = Intent.createChooser(sendIntent, null) val shareIntent = Intent.createChooser(sendIntent, null)
// This flag is needed when you start a new activity from non-Activity context shareIntent.addFlags(FLAG_ACTIVITY_NEW_TASK)
shareIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) androidAppContext.startActivity(shareIntent)
SimplexApp.context.startActivity(shareIntent)
} }
fun copyText(text: String) { actual fun UriHandler.sendEmail(subject: String, body: CharSequence) {
val clipboard = ContextCompat.getSystemService(SimplexApp.context, ClipboardManager::class.java)
clipboard?.setPrimaryClip(ClipData.newPlainText("text", text))
}
fun sendEmail(subject: String, body: CharSequence) {
val emailIntent = Intent(Intent.ACTION_SENDTO, Uri.parse("mailto:")) val emailIntent = Intent(Intent.ACTION_SENDTO, Uri.parse("mailto:"))
emailIntent.putExtra(Intent.EXTRA_SUBJECT, subject) emailIntent.putExtra(Intent.EXTRA_SUBJECT, subject)
emailIntent.putExtra(Intent.EXTRA_TEXT, body) emailIntent.putExtra(Intent.EXTRA_TEXT, body)
// This flag is needed when you start a new activity from non-Activity context
emailIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) emailIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
try { try {
SimplexApp.context.startActivity(emailIntent) androidAppContext.startActivity(emailIntent)
} catch (e: ActivityNotFoundException) { } catch (e: ActivityNotFoundException) {
Log.e(TAG, "No activity was found for handling email intent") Log.e(TAG, "No activity was found for handling email intent")
} }
@ -78,7 +72,6 @@ fun imageMimeType(fileName: String): String {
/** Before calling, make sure the user allows to write to external storage [Manifest.permission.WRITE_EXTERNAL_STORAGE] */ /** Before calling, make sure the user allows to write to external storage [Manifest.permission.WRITE_EXTERNAL_STORAGE] */
fun saveImage(ciFile: CIFile?) { fun saveImage(ciFile: CIFile?) {
val cxt = SimplexApp.context
val filePath = getLoadedFilePath(ciFile) val filePath = getLoadedFilePath(ciFile)
val fileName = ciFile?.fileName val fileName = ciFile?.fileName
if (filePath != null && fileName != null) { if (filePath != null && fileName != null) {
@ -87,16 +80,16 @@ fun saveImage(ciFile: CIFile?) {
values.put(MediaStore.Images.Media.MIME_TYPE, imageMimeType(fileName)) values.put(MediaStore.Images.Media.MIME_TYPE, imageMimeType(fileName))
values.put(MediaStore.MediaColumns.DISPLAY_NAME, fileName) values.put(MediaStore.MediaColumns.DISPLAY_NAME, fileName)
values.put(MediaStore.MediaColumns.TITLE, fileName) values.put(MediaStore.MediaColumns.TITLE, fileName)
val uri = cxt.contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values) val uri = androidAppContext.contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)
uri?.let { uri?.let {
cxt.contentResolver.openOutputStream(uri)?.let { stream -> androidAppContext.contentResolver.openOutputStream(uri)?.let { stream ->
val outputStream = BufferedOutputStream(stream) val outputStream = BufferedOutputStream(stream)
File(filePath).inputStream().use { it.copyTo(outputStream) } File(filePath).inputStream().use { it.copyTo(outputStream) }
outputStream.close() outputStream.close()
Toast.makeText(cxt, generalGetString(MR.strings.image_saved), Toast.LENGTH_SHORT).show() showToast(generalGetString(MR.strings.image_saved))
} }
} }
} else { } else {
Toast.makeText(cxt, generalGetString(MR.strings.file_not_found), Toast.LENGTH_SHORT).show() showToast(generalGetString(MR.strings.file_not_found))
} }
} }

View File

@ -1,4 +1,4 @@
package chat.simplex.app.platform package chat.simplex.common.platform
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
@ -7,17 +7,19 @@ import android.graphics.Rect
import android.os.Build import android.os.Build
import android.view.* import android.view.*
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import android.widget.Toast
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.LocalView
import chat.simplex.app.SimplexApp import chat.simplex.common.views.helpers.KeyboardState
import chat.simplex.app.views.helpers.KeyboardState import androidx.compose.ui.platform.LocalContext as LocalContext1
actual fun showToast(text: String, timeout: Long) = Toast.makeText(androidAppContext, text, Toast.LENGTH_SHORT).show()
@Composable @Composable
fun LockToCurrentOrientationUntilDispose() { actual fun LockToCurrentOrientationUntilDispose() {
val context = LocalContext.current val context = LocalContext1.current
DisposableEffect(Unit) { DisposableEffect(Unit) {
val activity = context as Activity val activity = (context as Activity?) ?: return@DisposableEffect onDispose {}
val manager = context.getSystemService(Activity.WINDOW_SERVICE) as WindowManager val manager = context.getSystemService(Activity.WINDOW_SERVICE) as WindowManager
val rotation = if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) manager.defaultDisplay.rotation else activity.display?.rotation val rotation = if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) manager.defaultDisplay.rotation else activity.display?.rotation
activity.requestedOrientation = when (rotation) { activity.requestedOrientation = when (rotation) {
@ -32,7 +34,10 @@ fun LockToCurrentOrientationUntilDispose() {
} }
@Composable @Composable
fun getKeyboardState(): State<KeyboardState> { actual fun LocalMultiplatformView(): Any? = LocalView.current
@Composable
actual fun getKeyboardState(): State<KeyboardState> {
val keyboardState = remember { mutableStateOf(KeyboardState.Closed) } val keyboardState = remember { mutableStateOf(KeyboardState.Closed) }
val view = LocalView.current val view = LocalView.current
DisposableEffect(view) { DisposableEffect(view) {
@ -57,5 +62,10 @@ fun getKeyboardState(): State<KeyboardState> {
return keyboardState return keyboardState
} }
fun hideKeyboard(view: View) = actual fun hideKeyboard(view: Any?) {
(SimplexApp.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager).hideSoftInputFromWindow(view.windowToken, 0) // LALAL
// LocalSoftwareKeyboardController.current?.hide()
if (view is View) {
(androidAppContext.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager).hideSoftInputFromWindow(view.windowToken, 0)
}
}

View File

@ -1,45 +1,43 @@
package chat.simplex.app.views.helpers package chat.simplex.common.platform
import android.content.Context
import android.graphics.Bitmap
import android.media.session.PlaybackState import android.media.session.PlaybackState
import android.net.Uri import android.net.Uri
import android.util.Log
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import chat.simplex.app.* import androidx.compose.ui.graphics.ImageBitmap
import chat.simplex.app.R import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR
import com.google.android.exoplayer2.* import com.google.android.exoplayer2.*
import com.google.android.exoplayer2.C.* import com.google.android.exoplayer2.C.*
import com.google.android.exoplayer2.audio.AudioAttributes import com.google.android.exoplayer2.audio.AudioAttributes
import com.google.android.exoplayer2.source.ProgressiveMediaSource import com.google.android.exoplayer2.source.ProgressiveMediaSource
import com.google.android.exoplayer2.upstream.DefaultDataSource import com.google.android.exoplayer2.upstream.DefaultDataSource
import com.google.android.exoplayer2.upstream.DefaultHttpDataSource import com.google.android.exoplayer2.upstream.DefaultHttpDataSource
import chat.simplex.res.MR
import kotlinx.coroutines.* import kotlinx.coroutines.*
import java.io.File import java.io.File
import java.net.URI
class VideoPlayer private constructor( actual class VideoPlayer private constructor(
private val uri: Uri, private val uri: URI,
private val gallery: Boolean, private val gallery: Boolean,
private val defaultPreview: Bitmap, private val defaultPreview: ImageBitmap,
defaultDuration: Long, defaultDuration: Long,
soundEnabled: Boolean, soundEnabled: Boolean
) { ): VideoPlayerInterface {
companion object { actual companion object {
private val players: MutableMap<Pair<Uri, Boolean>, VideoPlayer> = mutableMapOf() private val players: MutableMap<Pair<URI, Boolean>, VideoPlayer> = mutableMapOf()
private val previewsAndDurations: MutableMap<Uri, PreviewAndDuration> = mutableMapOf() private val previewsAndDurations: MutableMap<URI, VideoPlayerInterface.PreviewAndDuration> = mutableMapOf()
fun getOrCreate( actual fun getOrCreate(
uri: Uri, uri: URI,
gallery: Boolean, gallery: Boolean,
defaultPreview: Bitmap, defaultPreview: ImageBitmap,
defaultDuration: Long, defaultDuration: Long,
soundEnabled: Boolean, soundEnabled: Boolean
): VideoPlayer = ): VideoPlayer =
players.getOrPut(uri to gallery) { VideoPlayer(uri, gallery, defaultPreview, defaultDuration, soundEnabled) } players.getOrPut(uri to gallery) { VideoPlayer(uri, gallery, defaultPreview, defaultDuration, soundEnabled) }
fun enableSound(enable: Boolean, fileName: String?, gallery: Boolean): Boolean = actual fun enableSound(enable: Boolean, fileName: String?, gallery: Boolean): Boolean =
player(fileName, gallery)?.enableSound(enable) == true player(fileName, gallery)?.enableSound(enable) == true
private fun player(fileName: String?, gallery: Boolean): VideoPlayer? { private fun player(fileName: String?, gallery: Boolean): VideoPlayer? {
@ -47,36 +45,34 @@ class VideoPlayer private constructor(
return players.values.firstOrNull { player -> player.uri.path?.endsWith(fileName) == true && player.gallery == gallery } return players.values.firstOrNull { player -> player.uri.path?.endsWith(fileName) == true && player.gallery == gallery }
} }
fun release(uri: Uri, gallery: Boolean, remove: Boolean) = actual fun release(uri: URI, gallery: Boolean, remove: Boolean) =
player(uri.path, gallery)?.release(remove) player(uri.path, gallery)?.release(remove).run { }
fun stopAll() { actual fun stopAll() {
players.values.forEach { it.stop() } players.values.forEach { it.stop() }
} }
fun releaseAll() { actual fun releaseAll() {
players.values.forEach { it.release(false) } players.values.forEach { it.release(false) }
players.clear() players.clear()
previewsAndDurations.clear() previewsAndDurations.clear()
} }
} }
data class PreviewAndDuration(val preview: Bitmap?, val duration: Long?, val timestamp: Long)
private val currentVolume: Float private val currentVolume: Float
val soundEnabled: MutableState<Boolean> = mutableStateOf(soundEnabled) override val soundEnabled: MutableState<Boolean> = mutableStateOf(soundEnabled)
val brokenVideo: MutableState<Boolean> = mutableStateOf(false) override val brokenVideo: MutableState<Boolean> = mutableStateOf(false)
val videoPlaying: MutableState<Boolean> = mutableStateOf(false) override val videoPlaying: MutableState<Boolean> = mutableStateOf(false)
val progress: MutableState<Long> = mutableStateOf(0L) override val progress: MutableState<Long> = mutableStateOf(0L)
val duration: MutableState<Long> = mutableStateOf(defaultDuration) override val duration: MutableState<Long> = mutableStateOf(defaultDuration)
val preview: MutableState<Bitmap> = mutableStateOf(defaultPreview) override val preview: MutableState<ImageBitmap> = mutableStateOf(defaultPreview)
init { init {
setPreviewAndDuration() setPreviewAndDuration()
} }
val player = ExoPlayer.Builder(SimplexApp.context, val player = ExoPlayer.Builder(androidAppContext,
DefaultRenderersFactory(SimplexApp.context)) DefaultRenderersFactory(androidAppContext))
/*.setLoadControl(DefaultLoadControl.Builder() /*.setLoadControl(DefaultLoadControl.Builder()
.setPrioritizeTimeOverSizeThresholds(false) // Could probably save some megabytes in memory in case it will be needed .setPrioritizeTimeOverSizeThresholds(false) // Could probably save some megabytes in memory in case it will be needed
.createDefaultLoadControl())*/ .createDefaultLoadControl())*/
@ -84,20 +80,20 @@ class VideoPlayer private constructor(
.setSeekForwardIncrementMs(10_000) .setSeekForwardIncrementMs(10_000)
.build() .build()
.apply { .apply {
// Repeat the same track endlessly // Repeat the same track endlessly
repeatMode = Player.REPEAT_MODE_ONE repeatMode = Player.REPEAT_MODE_ONE
currentVolume = volume currentVolume = volume
if (!soundEnabled) { if (!soundEnabled) {
volume = 0f volume = 0f
}
setAudioAttributes(
AudioAttributes.Builder()
.setContentType(CONTENT_TYPE_MUSIC)
.setUsage(USAGE_MEDIA)
.build(),
true // disallow to play multiple instances simultaneously
)
} }
setAudioAttributes(
AudioAttributes.Builder()
.setContentType(CONTENT_TYPE_MUSIC)
.setUsage(USAGE_MEDIA)
.build(),
true // disallow to play multiple instances simultaneously
)
}
private val listener: MutableState<((position: Long?, state: TrackState) -> Unit)?> = mutableStateOf(null) private val listener: MutableState<((position: Long?, state: TrackState) -> Unit)?> = mutableStateOf(null)
private var progressJob: Job? = null private var progressJob: Job? = null
@ -115,14 +111,14 @@ class VideoPlayer private constructor(
} }
if (soundEnabled.value) { if (soundEnabled.value) {
RecorderNative.stopRecording?.invoke() RecorderInterface.stopRecording?.invoke()
} }
AudioPlayer.stop() AudioPlayer.stop()
stopAll() stopAll()
if (listener.value == null) { if (listener.value == null) {
runCatching { runCatching {
val dataSourceFactory = DefaultDataSource.Factory(SimplexApp.context, DefaultHttpDataSource.Factory()) val dataSourceFactory = DefaultDataSource.Factory(androidAppContext, DefaultHttpDataSource.Factory())
val source = ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(MediaItem.fromUri(uri)) val source = ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(MediaItem.fromUri(Uri.parse(uri.toString())))
player.setMediaSource(source, seek ?: 0L) player.setMediaSource(source, seek ?: 0L)
}.onFailure { }.onFailure {
Log.e(TAG, it.stackTraceToString()) Log.e(TAG, it.stackTraceToString())
@ -170,14 +166,14 @@ class VideoPlayer private constructor(
override fun onIsPlayingChanged(isPlaying: Boolean) { override fun onIsPlayingChanged(isPlaying: Boolean) {
super.onIsPlayingChanged(isPlaying) super.onIsPlayingChanged(isPlaying)
// Produce non-ideal transition from stopped to playing state while showing preview image in ChatView // Produce non-ideal transition from stopped to playing state while showing preview image in ChatView
// videoPlaying.value = isPlaying // videoPlaying.value = isPlaying
} }
}) })
return true return true
} }
fun stop() { override fun stop() {
player.stop() player.stop()
stopListener() stopListener()
} }
@ -199,7 +195,7 @@ class VideoPlayer private constructor(
progressJob = null progressJob = null
} }
fun play(resetOnEnd: Boolean) { override fun play(resetOnEnd: Boolean) {
if (progress.value == duration.value) { if (progress.value == duration.value) {
progress.value = 0 progress.value = 0
} }
@ -218,14 +214,14 @@ class VideoPlayer private constructor(
} }
} }
fun enableSound(enable: Boolean): Boolean { override fun enableSound(enable: Boolean): Boolean {
if (soundEnabled.value == enable) return false if (soundEnabled.value == enable) return false
soundEnabled.value = enable soundEnabled.value = enable
player.volume = if (enable) currentVolume else 0f player.volume = if (enable) currentVolume else 0f
return true return true
} }
fun release(remove: Boolean) { override fun release(remove: Boolean) {
player.release() player.release()
if (remove) { if (remove) {
players.remove(uri to gallery) players.remove(uri to gallery)

View File

@ -1,10 +1,9 @@
package chat.simplex.app.ui.theme package chat.simplex.common.ui.theme
import androidx.compose.ui.text.font.* import androidx.compose.ui.text.font.*
import chat.simplex.res.MR import chat.simplex.res.MR
// https://github.com/rsms/inter actual val Inter: FontFamily = FontFamily(
val Inter: FontFamily = FontFamily(
Font(MR.fonts.Inter.regular.fontResourceId), Font(MR.fonts.Inter.regular.fontResourceId),
Font(MR.fonts.Inter.italic.fontResourceId, style = FontStyle.Italic), Font(MR.fonts.Inter.italic.fontResourceId, style = FontStyle.Italic),
Font(MR.fonts.Inter.bold.fontResourceId, FontWeight.Bold), Font(MR.fonts.Inter.bold.fontResourceId, FontWeight.Bold),

View File

@ -1,4 +1,4 @@
package chat.simplex.app.views.call package chat.simplex.common.views.call
import android.Manifest import android.Manifest
import android.annotation.SuppressLint import android.annotation.SuppressLint
@ -9,10 +9,9 @@ import android.media.*
import android.os.Build import android.os.Build
import android.os.PowerManager import android.os.PowerManager
import android.os.PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK import android.os.PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK
import android.util.Log
import android.view.ViewGroup import android.view.ViewGroup
import android.webkit.* import android.webkit.*
import androidx.activity.compose.BackHandler import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
@ -26,20 +25,21 @@ import androidx.compose.ui.platform.LocalLifecycleOwner
import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.LifecycleEventObserver
import androidx.webkit.WebViewAssetLoader import androidx.webkit.WebViewAssetLoader
import androidx.webkit.WebViewClientCompat import androidx.webkit.WebViewClientCompat
import chat.simplex.app.* import chat.simplex.common.model.*
import chat.simplex.app.model.* import chat.simplex.common.ui.theme.*
import chat.simplex.app.ui.theme.* import chat.simplex.common.views.helpers.ProfileImage
import chat.simplex.app.views.helpers.ProfileImage import chat.simplex.common.views.helpers.withApi
import chat.simplex.app.views.helpers.withApi import chat.simplex.common.model.ChatModel
import com.google.accompanist.permissions.rememberMultiplePermissionsState import chat.simplex.common.model.Contact
import chat.simplex.common.platform.*
import chat.simplex.res.MR import chat.simplex.res.MR
import com.google.accompanist.permissions.rememberMultiplePermissionsState
import dev.icerock.moko.resources.StringResource import dev.icerock.moko.resources.StringResource
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.serialization.decodeFromString import kotlinx.serialization.decodeFromString
@ -47,20 +47,21 @@ import kotlinx.serialization.encodeToString
@SuppressLint("SourceLockedOrientationActivity") @SuppressLint("SourceLockedOrientationActivity")
@Composable @Composable
fun ActiveCallView(chatModel: ChatModel) { actual fun ActiveCallView() {
val chatModel = ChatModel
BackHandler(onBack = { BackHandler(onBack = {
val call = chatModel.activeCall.value val call = chatModel.activeCall.value
if (call != null) withApi { chatModel.callManager.endCall(call) } if (call != null) withApi { chatModel.callManager.endCall(call) }
}) })
val audioViaBluetooth = rememberSaveable { mutableStateOf(false) } val audioViaBluetooth = rememberSaveable { mutableStateOf(false) }
val ntfModeService = remember { chatModel.controller.appPrefs.notificationsMode.get() == NotificationsMode.SERVICE.name } val ntfModeService = remember { chatModel.controller.appPrefs.notificationsMode.get() == NotificationsMode.SERVICE }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
// Start service when call happening since it's not already started. // Start service when call happening since it's not already started.
// It's needed to prevent Android from shutting down a microphone after a minute or so when screen is off // It's needed to prevent Android from shutting down a microphone after a minute or so when screen is off
if (!ntfModeService) SimplexService.start() if (!ntfModeService) platform.androidServiceStart()
} }
DisposableEffect(Unit) { DisposableEffect(Unit) {
val am = SimplexApp.context.getSystemService(Context.AUDIO_SERVICE) as AudioManager val am = androidAppContext.getSystemService(Context.AUDIO_SERVICE) as AudioManager
var btDeviceCount = 0 var btDeviceCount = 0
val audioCallback = object: AudioDeviceCallback() { val audioCallback = object: AudioDeviceCallback() {
override fun onAudioDevicesAdded(addedDevices: Array<out AudioDeviceInfo>) { override fun onAudioDevicesAdded(addedDevices: Array<out AudioDeviceInfo>) {
@ -87,16 +88,16 @@ fun ActiveCallView(chatModel: ChatModel) {
} }
} }
am.registerAudioDeviceCallback(audioCallback, null) am.registerAudioDeviceCallback(audioCallback, null)
val pm = (SimplexApp.context.getSystemService(Context.POWER_SERVICE) as PowerManager) val pm = (androidAppContext.getSystemService(Context.POWER_SERVICE) as PowerManager)
val proximityLock = if (pm.isWakeLockLevelSupported(PROXIMITY_SCREEN_OFF_WAKE_LOCK)) { val proximityLock = if (pm.isWakeLockLevelSupported(PROXIMITY_SCREEN_OFF_WAKE_LOCK)) {
pm.newWakeLock(PROXIMITY_SCREEN_OFF_WAKE_LOCK, SimplexApp.context.packageName + ":proximityLock") pm.newWakeLock(PROXIMITY_SCREEN_OFF_WAKE_LOCK, androidAppContext.packageName + ":proximityLock")
} else { } else {
null null
} }
proximityLock?.acquire() proximityLock?.acquire()
onDispose { onDispose {
// Stop it when call ended // Stop it when call ended
if (!ntfModeService) SimplexService.safeStopService(SimplexApp.context) if (!ntfModeService) platform.androidServiceSafeStop()
dropAudioManagerOverrides() dropAudioManagerOverrides()
am.unregisterAudioDeviceCallback(audioCallback) am.unregisterAudioDeviceCallback(audioCallback)
proximityLock?.release() proximityLock?.release()
@ -215,7 +216,7 @@ private fun ActiveCallOverlay(call: Call, chatModel: ChatModel, audioViaBluetoot
} }
private fun setCallSound(speaker: Boolean, audioViaBluetooth: MutableState<Boolean>) { private fun setCallSound(speaker: Boolean, audioViaBluetooth: MutableState<Boolean>) {
val am = SimplexApp.context.getSystemService(Context.AUDIO_SERVICE) as AudioManager val am = androidAppContext.getSystemService(Context.AUDIO_SERVICE) as AudioManager
Log.d(TAG, "setCallSound: set audio mode, speaker enabled: $speaker") Log.d(TAG, "setCallSound: set audio mode, speaker enabled: $speaker")
am.mode = AudioManager.MODE_IN_COMMUNICATION am.mode = AudioManager.MODE_IN_COMMUNICATION
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
@ -241,7 +242,7 @@ private fun setCallSound(speaker: Boolean, audioViaBluetooth: MutableState<Boole
} }
private fun dropAudioManagerOverrides() { private fun dropAudioManagerOverrides() {
val am = SimplexApp.context.getSystemService(Context.AUDIO_SERVICE) as AudioManager val am = androidAppContext.getSystemService(Context.AUDIO_SERVICE) as AudioManager
am.mode = AudioManager.MODE_NORMAL am.mode = AudioManager.MODE_NORMAL
// Clear selected communication device to default value after we changed it in call // Clear selected communication device to default value after we changed it in call
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
@ -351,7 +352,7 @@ fun CallInfoView(call: Call, alignment: Alignment.Horizontal) {
InfoText(call.callState.text) InfoText(call.callState.text)
val connInfo = call.connectionInfo val connInfo = call.connectionInfo
// val connInfoText = if (connInfo == null) "" else " (${connInfo.text}, ${connInfo.protocolText})" // val connInfoText = if (connInfo == null) "" else " (${connInfo.text}, ${connInfo.protocolText})"
val connInfoText = if (connInfo == null) "" else " (${connInfo.text})" val connInfoText = if (connInfo == null) "" else " (${connInfo.text})"
InfoText(call.encryptionStatus + connInfoText) InfoText(call.encryptionStatus + connInfoText)
} }

View File

@ -1,32 +1,32 @@
package chat.simplex.app.views.chat package chat.simplex.common.views.chat
import android.Manifest import android.Manifest
import android.content.ActivityNotFoundException import android.content.ActivityNotFoundException
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.net.Uri import android.net.Uri
import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.graphics.ImageBitmap
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import chat.simplex.app.SimplexApp import chat.simplex.common.helpers.toURI
import chat.simplex.app.platform.resizeImageToStrSize import chat.simplex.common.platform.*
import chat.simplex.app.views.helpers.* import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR import chat.simplex.res.MR
import java.net.URI
@Composable @Composable
fun AttachmentSelection( actual fun AttachmentSelection(
composeState: MutableState<ComposeState>, composeState: MutableState<ComposeState>,
attachmentOption: MutableState<AttachmentOption?>, attachmentOption: MutableState<AttachmentOption?>,
processPickedFile: (Uri?, String?) -> Unit, processPickedFile: (URI?, String?) -> Unit,
processPickedMedia: (List<Uri>, String?) -> Unit processPickedMedia: (List<URI>, String?) -> Unit
) { ) {
val cameraLauncher = rememberCameraLauncher { uri: Uri? -> val cameraLauncher = rememberCameraLauncher { uri: Uri? ->
if (uri != null) { if (uri != null) {
val bitmap: Bitmap? = getBitmapFromUri(uri) val bitmap: ImageBitmap? = getBitmapFromUri(uri.toURI())
if (bitmap != null) { if (bitmap != null) {
val imagePreview = resizeImageToStrSize(bitmap, maxDataSize = 14000) val imagePreview = resizeImageToStrSize(bitmap, maxDataSize = 14000)
composeState.value = composeState.value.copy(preview = ComposePreview.MediaPreview(listOf(imagePreview), listOf(UploadContent.SimpleImage(uri)))) composeState.value = composeState.value.copy(preview = ComposePreview.MediaPreview(listOf(imagePreview), listOf(UploadContent.SimpleImage(uri.toURI()))))
} }
} }
} }
@ -34,20 +34,19 @@ fun AttachmentSelection(
if (isGranted) { if (isGranted) {
cameraLauncher.launchWithFallback() cameraLauncher.launchWithFallback()
} else { } else {
Toast.makeText(SimplexApp.context, generalGetString(MR.strings.toast_permission_denied), Toast.LENGTH_SHORT).show() showToast(generalGetString(MR.strings.toast_permission_denied))
} }
} }
val galleryImageLauncher = rememberLauncherForActivityResult(contract = PickMultipleImagesFromGallery()) { processPickedMedia(it, null) } val galleryImageLauncher = rememberLauncherForActivityResult(contract = PickMultipleImagesFromGallery()) { processPickedMedia(it.map { it.toURI() }, null) }
val galleryImageLauncherFallback = rememberGetMultipleContentsLauncher { processPickedMedia(it, null) } val galleryImageLauncherFallback = rememberGetMultipleContentsLauncher { processPickedMedia(it.map { it.toURI() }, null) }
val galleryVideoLauncher = rememberLauncherForActivityResult(contract = PickMultipleVideosFromGallery()) { processPickedMedia(it, null) } val galleryVideoLauncher = rememberLauncherForActivityResult(contract = PickMultipleVideosFromGallery()) { processPickedMedia(it.map { it.toURI() }, null) }
val galleryVideoLauncherFallback = rememberGetMultipleContentsLauncher { processPickedMedia(it, null) } val galleryVideoLauncherFallback = rememberGetMultipleContentsLauncher { processPickedMedia(it.map { it.toURI() }, null) }
val filesLauncher = rememberGetContentLauncher { processPickedFile(it?.toURI(), null) }
val filesLauncher = rememberGetContentLauncher { processPickedFile(it, null) }
LaunchedEffect(attachmentOption.value) { LaunchedEffect(attachmentOption.value) {
when (attachmentOption.value) { when (attachmentOption.value) {
AttachmentOption.CameraPhoto -> { AttachmentOption.CameraPhoto -> {
when (PackageManager.PERMISSION_GRANTED) { when (PackageManager.PERMISSION_GRANTED) {
ContextCompat.checkSelfPermission(SimplexApp.context, Manifest.permission.CAMERA) -> { ContextCompat.checkSelfPermission(androidAppContext, Manifest.permission.CAMERA) -> {
cameraLauncher.launchWithFallback() cameraLauncher.launchWithFallback()
} }
else -> { else -> {

View File

@ -1,12 +1,13 @@
package chat.simplex.app.views.chat package chat.simplex.common.views.chat
import android.Manifest import android.Manifest
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import chat.simplex.common.views.chat.ScanCodeLayout
import com.google.accompanist.permissions.rememberPermissionState import com.google.accompanist.permissions.rememberPermissionState
@Composable @Composable
fun ScanCodeView(verifyCode: (String?, cb: (Boolean) -> Unit) -> Unit, close: () -> Unit) { actual fun ScanCodeView(verifyCode: (String?, cb: (Boolean) -> Unit) -> Unit, close: () -> Unit) {
val cameraPermissionState = rememberPermissionState(permission = Manifest.permission.CAMERA) val cameraPermissionState = rememberPermissionState(permission = Manifest.permission.CAMERA)
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
cameraPermissionState.launchPermissionRequest() cameraPermissionState.launchPermissionRequest()

View File

@ -1,17 +1,17 @@
package chat.simplex.app.views.chat package chat.simplex.common.views.chat
import android.Manifest import android.Manifest
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import com.google.accompanist.permissions.rememberMultiplePermissionsState import com.google.accompanist.permissions.rememberMultiplePermissionsState
@Composable @Composable
fun allowedToRecordVoiceByPlatform(): Boolean { actual fun allowedToRecordVoiceByPlatform(): Boolean {
val permissionsState = rememberMultiplePermissionsState(listOf(Manifest.permission.RECORD_AUDIO)) val permissionsState = rememberMultiplePermissionsState(listOf(Manifest.permission.RECORD_AUDIO))
return permissionsState.allPermissionsGranted return permissionsState.allPermissionsGranted
} }
@Composable @Composable
fun VoiceButtonWithoutPermissionByPlatform() { actual fun VoiceButtonWithoutPermissionByPlatform() {
val permissionsState = rememberMultiplePermissionsState(listOf(Manifest.permission.RECORD_AUDIO)) val permissionsState = rememberMultiplePermissionsState(listOf(Manifest.permission.RECORD_AUDIO))
VoiceButtonWithoutPermission { permissionsState.launchMultiplePermissionRequest() } VoiceButtonWithoutPermission { permissionsState.launchMultiplePermissionRequest() }
} }

View File

@ -1,37 +1,37 @@
package chat.simplex.app.views.chat.item package chat.simplex.common.views.chat.item
import android.net.Uri import android.os.Build.VERSION.SDK_INT
import android.os.Build import androidx.compose.runtime.Composable
import androidx.compose.runtime.* import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.*
import androidx.compose.ui.graphics.painter.BitmapPainter import androidx.compose.ui.graphics.painter.BitmapPainter
import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.* import androidx.compose.ui.platform.LocalContext
import chat.simplex.app.* import chat.simplex.common.helpers.toUri
import chat.simplex.app.model.* import chat.simplex.common.model.CIFile
import chat.simplex.app.platform.getLoadedFilePath import chat.simplex.common.platform.*
import chat.simplex.app.platform.hideKeyboard import chat.simplex.common.views.helpers.ModalManager
import chat.simplex.app.views.helpers.*
import coil.ImageLoader import coil.ImageLoader
import coil.compose.rememberAsyncImagePainter import coil.compose.rememberAsyncImagePainter
import coil.decode.GifDecoder import coil.decode.GifDecoder
import coil.decode.ImageDecoderDecoder import coil.decode.ImageDecoderDecoder
import coil.request.ImageRequest import coil.request.ImageRequest
import java.net.URI
@Composable @Composable
fun SimpleAndAnimatedImageView( actual fun SimpleAndAnimatedImageView(
uri: Uri, uri: URI,
imageBitmap: ImageBitmap, imageBitmap: ImageBitmap,
file: CIFile?, file: CIFile?,
imageProvider: () -> ImageGalleryProvider, imageProvider: () -> ImageGalleryProvider,
ImageView: @Composable (painter: Painter, onClick: () -> Unit) -> Unit ImageView: @Composable (painter: Painter, onClick: () -> Unit) -> Unit
) { ) {
val context = LocalContext.current
val imagePainter = rememberAsyncImagePainter( val imagePainter = rememberAsyncImagePainter(
ImageRequest.Builder(SimplexApp.context).data(data = uri).size(coil.size.Size.ORIGINAL).build(), ImageRequest.Builder(context).data(data = uri.toUri()).size(coil.size.Size.ORIGINAL).build(),
placeholder = BitmapPainter(imageBitmap), // show original image while it's still loading by coil placeholder = BitmapPainter(imageBitmap), // show original image while it's still loading by coil
imageLoader = imageLoader imageLoader = imageLoader
) )
val view = LocalView.current val view = LocalMultiplatformView()
ImageView(imagePainter) { ImageView(imagePainter) {
hideKeyboard(view) hideKeyboard(view)
if (getLoadedFilePath(file) != null) { if (getLoadedFilePath(file) != null) {
@ -42,9 +42,9 @@ fun SimpleAndAnimatedImageView(
} }
} }
private val imageLoader = ImageLoader.Builder(SimplexApp.context) private val imageLoader = ImageLoader.Builder(androidAppContext)
.components { .components {
if (Build.VERSION.SDK_INT >= 28) { if (SDK_INT >= 28) {
add(ImageDecoderDecoder.Factory()) add(ImageDecoderDecoder.Factory())
} else { } else {
add(GifDecoder.Factory()) add(GifDecoder.Factory())

View File

@ -1,20 +1,22 @@
package chat.simplex.app.views.chat.item package chat.simplex.common.views.chat.item
import android.graphics.Rect import android.graphics.Rect
import androidx.compose.foundation.* import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.width
import androidx.compose.runtime.* import androidx.compose.runtime.Composable
import androidx.compose.ui.* import androidx.compose.runtime.remember
import androidx.compose.ui.platform.* import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.* import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import chat.simplex.app.views.helpers.* import chat.simplex.common.platform.VideoPlayer
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MODE_FIXED_WIDTH import com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MODE_FIXED_WIDTH
import com.google.android.exoplayer2.ui.StyledPlayerView import com.google.android.exoplayer2.ui.StyledPlayerView
@Composable @Composable
fun PlayerView(player: VideoPlayer, width: Dp, onClick: () -> Unit, onLongClick: () -> Unit, stop: () -> Unit) { actual fun PlayerView(player: VideoPlayer, width: Dp, onClick: () -> Unit, onLongClick: () -> Unit, stop: () -> Unit) {
AndroidView( AndroidView(
factory = { ctx -> factory = { ctx ->
StyledPlayerView(ctx).apply { StyledPlayerView(ctx).apply {
@ -32,7 +34,8 @@ fun PlayerView(player: VideoPlayer, width: Dp, onClick: () -> Unit, onLongClick:
) )
} }
@Composable fun LocalWindowWidth(): Dp { @Composable
actual fun LocalWindowWidth(): Dp {
val view = LocalView.current val view = LocalView.current
val density = LocalDensity.current.density val density = LocalDensity.current.density
return remember { return remember {

View File

@ -1,20 +1,21 @@
package chat.simplex.app.views.chat.item package chat.simplex.common.views.chat.item
import android.Manifest import android.Manifest
import android.os.Build import android.os.Build
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import chat.simplex.app.model.ChatItem import chat.simplex.common.model.ChatItem
import chat.simplex.app.model.MsgContent import chat.simplex.common.model.MsgContent
import chat.simplex.app.platform.saveImage import chat.simplex.common.platform.FileChooserLauncher
import chat.simplex.common.platform.saveImage
import chat.simplex.common.views.helpers.withApi
import chat.simplex.res.MR import chat.simplex.res.MR
import com.google.accompanist.permissions.rememberPermissionState import com.google.accompanist.permissions.rememberPermissionState
import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource import dev.icerock.moko.resources.compose.stringResource
@Composable @Composable
fun SaveContentItemAction(cItem: ChatItem, showMenu: MutableState<Boolean>) { actual fun SaveContentItemAction(cItem: ChatItem, saveFileLauncher: FileChooserLauncher, showMenu: MutableState<Boolean>) {
val saveFileLauncher = rememberSaveFileLauncher(ciFile = cItem.file)
val writePermissionState = rememberPermissionState(permission = Manifest.permission.WRITE_EXTERNAL_STORAGE) val writePermissionState = rememberPermissionState(permission = Manifest.permission.WRITE_EXTERNAL_STORAGE)
ItemAction(stringResource(MR.strings.save_verb), painterResource(MR.images.ic_download), onClick = { ItemAction(stringResource(MR.strings.save_verb), painterResource(MR.images.ic_download), onClick = {
when (cItem.content.msgContent) { when (cItem.content.msgContent) {
@ -25,7 +26,7 @@ fun SaveContentItemAction(cItem: ChatItem, showMenu: MutableState<Boolean>) {
writePermissionState.launchPermissionRequest() writePermissionState.launchPermissionRequest()
} }
} }
is MsgContent.MCFile, is MsgContent.MCVoice, is MsgContent.MCVideo -> saveFileLauncher.launch(cItem.file?.fileName) is MsgContent.MCFile, is MsgContent.MCVoice, is MsgContent.MCVideo -> withApi { saveFileLauncher.launch(cItem.file?.fileName ?: "") }
else -> {} else -> {}
} }
showMenu.value = false showMenu.value = false

View File

@ -1,7 +1,5 @@
package chat.simplex.app.views.chat.item package chat.simplex.common.views.chat.item
import android.graphics.Bitmap
import android.net.Uri
import android.os.Build import android.os.Build
import android.view.View import android.view.View
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
@ -13,7 +11,8 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.view.isVisible import androidx.core.view.isVisible
import chat.simplex.app.views.helpers.VideoPlayer import chat.simplex.common.helpers.toUri
import chat.simplex.common.platform.VideoPlayer
import chat.simplex.res.MR import chat.simplex.res.MR
import coil.ImageLoader import coil.ImageLoader
import coil.compose.rememberAsyncImagePainter import coil.compose.rememberAsyncImagePainter
@ -21,13 +20,13 @@ import coil.decode.GifDecoder
import coil.decode.ImageDecoderDecoder import coil.decode.ImageDecoderDecoder
import coil.request.ImageRequest import coil.request.ImageRequest
import coil.size.Size import coil.size.Size
import com.google.android.exoplayer2.R
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout import com.google.android.exoplayer2.ui.AspectRatioFrameLayout
import com.google.android.exoplayer2.ui.StyledPlayerView import com.google.android.exoplayer2.ui.StyledPlayerView
import dev.icerock.moko.resources.compose.stringResource import dev.icerock.moko.resources.compose.stringResource
import java.net.URI
@Composable @Composable
fun FullScreenImageView(modifier: Modifier, uri: Uri, imageBitmap: Bitmap) { actual fun FullScreenImageView(modifier: Modifier, uri: URI, imageBitmap: ImageBitmap) {
// I'm making a new instance of imageLoader here because if I use one instance in multiple places // I'm making a new instance of imageLoader here because if I use one instance in multiple places
// after end of composition here a GIF from the first instance will be paused automatically which isn't what I want // after end of composition here a GIF from the first instance will be paused automatically which isn't what I want
val imageLoader = ImageLoader.Builder(LocalContext.current) val imageLoader = ImageLoader.Builder(LocalContext.current)
@ -41,8 +40,8 @@ fun FullScreenImageView(modifier: Modifier, uri: Uri, imageBitmap: Bitmap) {
.build() .build()
Image( Image(
rememberAsyncImagePainter( rememberAsyncImagePainter(
ImageRequest.Builder(LocalContext.current).data(data = uri).size(Size.ORIGINAL).build(), ImageRequest.Builder(LocalContext.current).data(data = uri.toUri()).size(Size.ORIGINAL).build(),
placeholder = BitmapPainter(imageBitmap.asImageBitmap()), // show original image while it's still loading by coil placeholder = BitmapPainter(imageBitmap), // show original image while it's still loading by coil
imageLoader = imageLoader imageLoader = imageLoader
), ),
contentDescription = stringResource(MR.strings.image_descr), contentDescription = stringResource(MR.strings.image_descr),
@ -52,7 +51,7 @@ fun FullScreenImageView(modifier: Modifier, uri: Uri, imageBitmap: Bitmap) {
} }
@Composable @Composable
fun FullScreenVideoView(player: VideoPlayer, modifier: Modifier) { actual fun FullScreenVideoView(player: VideoPlayer, modifier: Modifier) {
AndroidView( AndroidView(
factory = { ctx -> factory = { ctx ->
StyledPlayerView(ctx).apply { StyledPlayerView(ctx).apply {
@ -66,8 +65,8 @@ fun FullScreenVideoView(player: VideoPlayer, modifier: Modifier) {
setShowSubtitleButton(false) setShowSubtitleButton(false)
setShowVrButton(false) setShowVrButton(false)
controllerAutoShow = false controllerAutoShow = false
findViewById<View>(R.id.exo_controls_background).setBackgroundColor(Color.Black.copy(alpha = 0.3f).toArgb()) findViewById<View>(com.google.android.exoplayer2.R.id.exo_controls_background).setBackgroundColor(Color.Black.copy(alpha = 0.3f).toArgb())
findViewById<View>(R.id.exo_settings).isVisible = false findViewById<View>(com.google.android.exoplayer2.R.id.exo_settings).isVisible = false
this.player = player.player this.player = player.player
} }
}, },

View File

@ -0,0 +1,16 @@
package chat.simplex.common.views.helpers
import androidx.compose.runtime.Composable
import androidx.compose.ui.window.Dialog
@Composable
actual fun DefaultDialog(
onDismissRequest: () -> Unit,
content: @Composable () -> Unit
) {
Dialog(
onDismissRequest = onDismissRequest
) {
content()
}
}

View File

@ -0,0 +1,46 @@
package chat.simplex.common.views.helpers
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.PopupProperties
actual interface DefaultExposedDropdownMenuBoxScope {
@Composable
actual fun DefaultExposedDropdownMenu(
expanded: Boolean,
onDismissRequest: () -> Unit,
modifier: Modifier,
content: @Composable ColumnScope.() -> Unit
) {
DropdownMenu(expanded, onDismissRequest, modifier, content = content)
}
@Composable
fun DropdownMenu(
expanded: Boolean,
onDismissRequest: () -> Unit,
modifier: Modifier = Modifier,
offset: DpOffset = DpOffset(0.dp, 0.dp),
properties: PopupProperties = PopupProperties(focusable = true),
content: @Composable ColumnScope.() -> Unit
) {
androidx.compose.material.DropdownMenu(expanded, onDismissRequest, modifier, offset, properties, content)
}
}
@Composable
actual fun DefaultExposedDropdownMenuBox(
expanded: Boolean,
onExpandedChange: (Boolean) -> Unit,
modifier: Modifier,
content: @Composable DefaultExposedDropdownMenuBoxScope.() -> Unit
) {
val scope = remember { object : DefaultExposedDropdownMenuBoxScope {} }
androidx.compose.material.ExposedDropdownMenuBox(expanded, onExpandedChange, modifier, content = {
scope.content()
})
}

View File

@ -1,4 +1,4 @@
package chat.simplex.app.views.helpers package chat.simplex.common.views.helpers
import android.Manifest import android.Manifest
import android.app.Activity import android.app.Activity
@ -9,8 +9,6 @@ import android.graphics.*
import android.net.Uri import android.net.Uri
import android.provider.MediaStore import android.provider.MediaStore
import android.util.Base64 import android.util.Base64
import android.util.Log
import android.widget.Toast
import androidx.activity.compose.ManagedActivityResultLauncher import androidx.activity.compose.ManagedActivityResultLauncher
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContract import androidx.activity.result.contract.ActivityResultContract
@ -22,19 +20,22 @@ import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import chat.simplex.app.* import chat.simplex.common.helpers.APPLICATION_ID
import chat.simplex.app.model.json import chat.simplex.common.helpers.toURI
import chat.simplex.app.platform.getAppFilesDirectory import chat.simplex.common.model.json
import chat.simplex.app.views.newchat.ActionButton import chat.simplex.common.platform.*
import chat.simplex.common.views.newchat.ActionButton
import chat.simplex.res.MR import chat.simplex.res.MR
import kotlinx.serialization.builtins.* import kotlinx.serialization.builtins.*
import java.io.File import java.io.File
import java.net.URI
val errorBitmapBytes = Base64.decode("iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAAKVJREFUeF7t1kENACEUQ0FQhnVQ9lfGO+xggITQdvbMzArPey+8fa3tAfwAEdABZQspQStgBssEcgAIkSAJkiAJljtEgiRIgmUCSZAESZAESZAEyx0iQRIkwTKBJEiCv5fgvTd1wDmn7QAP4AeIgA4oW0gJWgEzWCZwbQ7gAA7ggLKFOIADOKBMIAeAEAmSIAmSYLlDJEiCJFgmkARJkARJ8N8S/ADTZUewBvnTOQAAAABJRU5ErkJggg==", Base64.NO_WRAP) val errorBitmapBytes = Base64.decode("iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAAKVJREFUeF7t1kENACEUQ0FQhnVQ9lfGO+xggITQdvbMzArPey+8fa3tAfwAEdABZQspQStgBssEcgAIkSAJkiAJljtEgiRIgmUCSZAESZAESZAEyx0iQRIkwTKBJEiCv5fgvTd1wDmn7QAP4AeIgA4oW0gJWgEzWCZwbQ7gAA7ggLKFOIADOKBMIAeAEAmSIAmSYLlDJEiCJFgmkARJkARJ8N8S/ADTZUewBvnTOQAAAABJRU5ErkJggg==", Base64.NO_WRAP)
val errorBitmap: Bitmap = BitmapFactory.decodeByteArray(errorBitmapBytes, 0, errorBitmapBytes.size) val errorBitmap: Bitmap = BitmapFactory.decodeByteArray(errorBitmapBytes, 0, errorBitmapBytes.size)
@ -45,7 +46,7 @@ class CustomTakePicturePreview(var uri: Uri?, var tmpFile: File?): ActivityResul
tmpFile = File.createTempFile("image", ".bmp", File(getAppFilesDirectory())) tmpFile = File.createTempFile("image", ".bmp", File(getAppFilesDirectory()))
// Since the class should return Uri, the file should be deleted somewhere else. And in order to be sure, delegate this to system // Since the class should return Uri, the file should be deleted somewhere else. And in order to be sure, delegate this to system
tmpFile?.deleteOnExit() tmpFile?.deleteOnExit()
uri = FileProvider.getUriForFile(context, "${BuildConfig.APPLICATION_ID}.provider", tmpFile!!) uri = FileProvider.getUriForFile(context, "$APPLICATION_ID.provider", tmpFile!!)
return Intent(MediaStore.ACTION_IMAGE_CAPTURE) return Intent(MediaStore.ACTION_IMAGE_CAPTURE)
.putExtra(MediaStore.EXTRA_OUTPUT, uri) .putExtra(MediaStore.EXTRA_OUTPUT, uri)
} }
@ -127,7 +128,7 @@ fun ManagedActivityResultLauncher<Void?, Uri?>.launchWithFallback() {
try { try {
// Try to open any camera just to capture an image, will not be returned like with previous intent // Try to open any camera just to capture an image, will not be returned like with previous intent
SimplexApp.context.startActivity(Intent(MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA).also { it.addFlags(FLAG_ACTIVITY_NEW_TASK) }) androidAppContext.startActivity(Intent(MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA).also { it.addFlags(FLAG_ACTIVITY_NEW_TASK) })
} catch (e: ActivityNotFoundException) { } catch (e: ActivityNotFoundException) {
// No camera apps available at all // No camera apps available at all
Log.e(TAG, "Camera launcher2: " + e.stackTraceToString()) Log.e(TAG, "Camera launcher2: " + e.stackTraceToString())
@ -136,14 +137,15 @@ fun ManagedActivityResultLauncher<Void?, Uri?>.launchWithFallback() {
} }
@Composable @Composable
fun GetImageBottomSheet( actual fun GetImageBottomSheet(
imageBitmap: MutableState<Uri?>, imageBitmap: MutableState<URI?>,
onImageChange: (Bitmap) -> Unit, onImageChange: (ImageBitmap) -> Unit,
hideBottomSheet: () -> Unit hideBottomSheet: () -> Unit
) { ) {
val context = LocalContext.current val context = LocalContext.current
val processPickedImage = { uri: Uri? -> val processPickedImage = { uri: Uri? ->
if (uri != null) { if (uri != null) {
val uri = uri.toURI()
val bitmap = getBitmapFromUri(uri) val bitmap = getBitmapFromUri(uri)
if (bitmap != null) { if (bitmap != null) {
imageBitmap.value = uri imageBitmap.value = uri
@ -159,7 +161,7 @@ fun GetImageBottomSheet(
cameraLauncher.launchWithFallback() cameraLauncher.launchWithFallback()
hideBottomSheet() hideBottomSheet()
} else { } else {
Toast.makeText(context, generalGetString(MR.strings.toast_permission_denied), Toast.LENGTH_SHORT).show() showToast(generalGetString(MR.strings.toast_permission_denied))
} }
} }

View File

@ -1,4 +1,4 @@
package chat.simplex.app.views.helpers package chat.simplex.common.views.helpers
import android.os.Build.VERSION.SDK_INT import android.os.Build.VERSION.SDK_INT
import androidx.biometric.BiometricManager import androidx.biometric.BiometricManager
@ -6,15 +6,14 @@ import androidx.biometric.BiometricManager.Authenticators.*
import androidx.biometric.BiometricPrompt import androidx.biometric.BiometricPrompt
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import chat.simplex.app.SimplexApp import chat.simplex.common.platform.mainActivity
import chat.simplex.app.platform.mainActivity import chat.simplex.common.views.usersettings.LAMode
import chat.simplex.app.views.usersettings.LAMode
fun authenticate( actual fun authenticate(
promptTitle: String, promptTitle: String,
promptSubtitle: String, promptSubtitle: String,
selfDestruct: Boolean = false, selfDestruct: Boolean,
usingLAMode: LAMode = SimplexApp.context.chatModel.controller.appPrefs.laMode.get(), usingLAMode: LAMode,
completed: (LAResult) -> Unit completed: (LAResult) -> Unit
) { ) {
val activity = mainActivity.get() ?: return completed(LAResult.Error("")) val activity = mainActivity.get() ?: return completed(LAResult.Error(""))

View File

@ -1,20 +1,20 @@
package chat.simplex.app.views.helpers package chat.simplex.common.views.helpers
import android.app.Application import android.app.Application
//import android.app.LocaleManager
import android.content.res.Resources import android.content.res.Resources
import android.graphics.* import android.graphics.*
import android.graphics.Typeface import android.graphics.Typeface
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.media.MediaMetadataRetriever import android.media.MediaMetadataRetriever
import android.net.Uri
import android.os.* import android.os.*
import android.provider.OpenableColumns import android.provider.OpenableColumns
import android.text.Spanned import android.text.Spanned
import android.text.SpannedString import android.text.SpannedString
import android.text.style.* import android.text.style.*
import android.util.Log import android.util.Base64
import androidx.compose.runtime.* import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.*
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.* import androidx.compose.ui.platform.*
import androidx.compose.ui.text.* import androidx.compose.ui.text.*
@ -24,12 +24,13 @@ import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.* import androidx.compose.ui.unit.*
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import androidx.core.text.HtmlCompat import androidx.core.text.HtmlCompat
import chat.simplex.app.* import chat.simplex.common.helpers.*
import chat.simplex.app.model.* import chat.simplex.common.model.*
import chat.simplex.app.platform.getLoadedFilePath import chat.simplex.common.platform.*
import chat.simplex.res.MR import chat.simplex.res.MR
import dev.icerock.moko.resources.StringResource import dev.icerock.moko.resources.StringResource
import java.io.* import java.io.*
import java.net.URI
fun Spanned.toHtmlWithoutParagraphs(): String { fun Spanned.toHtmlWithoutParagraphs(): String {
return HtmlCompat.toHtml(this, HtmlCompat.TO_HTML_PARAGRAPH_LINES_CONSECUTIVE) return HtmlCompat.toHtml(this, HtmlCompat.TO_HTML_PARAGRAPH_LINES_CONSECUTIVE)
@ -46,19 +47,10 @@ fun Resources.getText(id: StringResource, vararg args: Any): CharSequence {
return HtmlCompat.fromHtml(formattedHtml, HtmlCompat.FROM_HTML_MODE_LEGACY) return HtmlCompat.fromHtml(formattedHtml, HtmlCompat.FROM_HTML_MODE_LEGACY)
} }
fun escapedHtmlToAnnotatedString(text: String, density: Density): AnnotatedString { actual fun escapedHtmlToAnnotatedString(text: String, density: Density): AnnotatedString {
return spannableStringToAnnotatedString(HtmlCompat.fromHtml(text, HtmlCompat.FROM_HTML_MODE_LEGACY), density) return spannableStringToAnnotatedString(HtmlCompat.fromHtml(text, HtmlCompat.FROM_HTML_MODE_LEGACY), density)
} }
@Composable
fun annotatedStringResource(id: StringResource): AnnotatedString {
val density = LocalDensity.current
return remember(id) {
val text = id.getString(SimplexApp.context)
escapedHtmlToAnnotatedString(text, density)
}
}
private fun spannableStringToAnnotatedString( private fun spannableStringToAnnotatedString(
text: CharSequence, text: CharSequence,
density: Density, density: Density,
@ -163,17 +155,20 @@ private fun spannableStringToAnnotatedString(
} }
} }
actual fun getAppFileUri(fileName: String): URI =
FileProvider.getUriForFile(androidAppContext, "$APPLICATION_ID.provider", File(getAppFilePath(fileName))).toURI()
// https://developer.android.com/training/data-storage/shared/documents-files#bitmap // https://developer.android.com/training/data-storage/shared/documents-files#bitmap
fun getLoadedImage(file: CIFile?): Bitmap? { actual fun getLoadedImage(file: CIFile?): ImageBitmap? {
val filePath = getLoadedFilePath(file) val filePath = getLoadedFilePath(file)
return if (filePath != null) { return if (filePath != null) {
try { try {
val uri = FileProvider.getUriForFile(SimplexApp.context, "${BuildConfig.APPLICATION_ID}.provider", File(filePath)) val uri = getAppFileUri(filePath.substringAfterLast(File.separator))
val parcelFileDescriptor = SimplexApp.context.contentResolver.openFileDescriptor(uri, "r") val parcelFileDescriptor = androidAppContext.contentResolver.openFileDescriptor(uri.toUri(), "r")
val fileDescriptor = parcelFileDescriptor?.fileDescriptor val fileDescriptor = parcelFileDescriptor?.fileDescriptor
val image = decodeSampledBitmapFromFileDescriptor(fileDescriptor, 1000, 1000) val image = decodeSampledBitmapFromFileDescriptor(fileDescriptor, 1000, 1000)
parcelFileDescriptor?.close() parcelFileDescriptor?.close()
image image.asImageBitmap()
} catch (e: Exception) { } catch (e: Exception) {
null null
} }
@ -215,33 +210,33 @@ private fun calculateInSampleSize(options: BitmapFactory.Options, reqWidth: Int,
return inSampleSize return inSampleSize
} }
fun getFileName(uri: Uri): String? { actual fun getFileName(uri: URI): String? {
return SimplexApp.context.contentResolver.query(uri, null, null, null, null)?.use { cursor -> return androidAppContext.contentResolver.query(uri.toUri(), null, null, null, null)?.use { cursor ->
val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME) val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
cursor.moveToFirst() cursor.moveToFirst()
cursor.getString(nameIndex) cursor.getString(nameIndex)
} }
} }
fun getAppFilePath(uri: Uri): String? { actual fun getAppFilePath(uri: URI): String? {
return SimplexApp.context.contentResolver.query(uri, null, null, null, null)?.use { cursor -> return androidAppContext.contentResolver.query(uri.toUri(), null, null, null, null)?.use { cursor ->
val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME) val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
cursor.moveToFirst() cursor.moveToFirst()
chat.simplex.app.platform.getAppFilePath(cursor.getString(nameIndex)) getAppFilePath(cursor.getString(nameIndex))
} }
} }
fun getFileSize(uri: Uri): Long? { actual fun getFileSize(uri: URI): Long? {
return SimplexApp.context.contentResolver.query(uri, null, null, null, null)?.use { cursor -> return androidAppContext.contentResolver.query(uri.toUri(), null, null, null, null)?.use { cursor ->
val sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE) val sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE)
cursor.moveToFirst() cursor.moveToFirst()
cursor.getLong(sizeIndex) cursor.getLong(sizeIndex)
} }
} }
fun getBitmapFromUri(uri: Uri, withAlertOnException: Boolean = true): Bitmap? { actual fun getBitmapFromUri(uri: URI, withAlertOnException: Boolean): ImageBitmap? {
return if (Build.VERSION.SDK_INT >= 28) { return if (Build.VERSION.SDK_INT >= 28) {
val source = ImageDecoder.createSource(SimplexApp.context.contentResolver, uri) val source = ImageDecoder.createSource(androidAppContext.contentResolver, uri.toUri())
try { try {
ImageDecoder.decodeBitmap(source) ImageDecoder.decodeBitmap(source)
} catch (e: android.graphics.ImageDecoder.DecodeException) { } catch (e: android.graphics.ImageDecoder.DecodeException) {
@ -256,12 +251,12 @@ fun getBitmapFromUri(uri: Uri, withAlertOnException: Boolean = true): Bitmap? {
} }
} else { } else {
BitmapFactory.decodeFile(getAppFilePath(uri)) BitmapFactory.decodeFile(getAppFilePath(uri))
} }?.asImageBitmap()
} }
fun getDrawableFromUri(uri: Uri, withAlertOnException: Boolean = true): Drawable? { actual fun getDrawableFromUri(uri: URI, withAlertOnException: Boolean): Any? {
return if (Build.VERSION.SDK_INT >= 28) { return if (Build.VERSION.SDK_INT >= 28) {
val source = ImageDecoder.createSource(SimplexApp.context.contentResolver, uri) val source = ImageDecoder.createSource(androidAppContext.contentResolver, uri.toUri())
try { try {
ImageDecoder.decodeDrawable(source) ImageDecoder.decodeDrawable(source)
} catch (e: android.graphics.ImageDecoder.DecodeException) { } catch (e: android.graphics.ImageDecoder.DecodeException) {
@ -279,17 +274,16 @@ fun getDrawableFromUri(uri: Uri, withAlertOnException: Boolean = true): Drawable
} }
} }
fun saveTempImageUncompressed(image: Bitmap, asPng: Boolean): File? { actual suspend fun saveTempImageUncompressed(image: ImageBitmap, asPng: Boolean): File? {
return try { return try {
val ext = if (asPng) "png" else "jpg" val ext = if (asPng) "png" else "jpg"
val tmpDir = SimplexApp.context.getDir("temp", Application.MODE_PRIVATE) return File(getTempFilesDirectory() + File.separator + generateNewFileName("IMG", ext)).apply {
return File(tmpDir.absolutePath + File.separator + generateNewFileName("IMG", ext)).apply {
outputStream().use { out -> outputStream().use { out ->
image.compress(if (asPng) Bitmap.CompressFormat.PNG else Bitmap.CompressFormat.JPEG, 85, out) image.asAndroidBitmap().compress(if (asPng) Bitmap.CompressFormat.PNG else Bitmap.CompressFormat.JPEG, 85, out)
out.flush() out.flush()
} }
deleteOnExit() deleteOnExit()
SimplexApp.context.chatModel.filesToDelete.add(this) ChatModel.filesToDelete.add(this)
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Util.kt saveTempImageUncompressed error: ${e.message}") Log.e(TAG, "Util.kt saveTempImageUncompressed error: ${e.message}")
@ -297,9 +291,9 @@ fun saveTempImageUncompressed(image: Bitmap, asPng: Boolean): File? {
} }
} }
fun getBitmapFromVideo(uri: Uri, timestamp: Long? = null, random: Boolean = true): VideoPlayer.PreviewAndDuration { actual fun getBitmapFromVideo(uri: URI, timestamp: Long?, random: Boolean): VideoPlayerInterface.PreviewAndDuration {
val mmr = MediaMetadataRetriever() val mmr = MediaMetadataRetriever()
mmr.setDataSource(SimplexApp.context, uri) mmr.setDataSource(androidAppContext, uri.toUri())
val durationMs = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toLong() val durationMs = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toLong()
val image = when { val image = when {
timestamp != null -> mmr.getFrameAtTime(timestamp * 1000, MediaMetadataRetriever.OPTION_CLOSEST) timestamp != null -> mmr.getFrameAtTime(timestamp * 1000, MediaMetadataRetriever.OPTION_CLOSEST)
@ -307,5 +301,9 @@ fun getBitmapFromVideo(uri: Uri, timestamp: Long? = null, random: Boolean = true
else -> mmr.getFrameAtTime(0) else -> mmr.getFrameAtTime(0)
} }
mmr.release() mmr.release()
return VideoPlayer.PreviewAndDuration(image, durationMs, timestamp ?: 0) return VideoPlayerInterface.PreviewAndDuration(image?.asImageBitmap(), durationMs, timestamp ?: 0)
} }
actual fun ByteArray.toBase64StringForPassphrase(): String = Base64.encodeToString(this, Base64.DEFAULT)
actual fun String.toByteArrayFromBase64ForPassphrase(): ByteArray = Base64.decode(this, Base64.DEFAULT)

View File

@ -0,0 +1,18 @@
package chat.simplex.common.views.newchat
import androidx.compose.ui.graphics.*
actual fun ImageBitmap.replaceColor(from: Int, to: Int): ImageBitmap {
val pixels = IntArray(width * height)
val bitmap = this.asAndroidBitmap()
bitmap.getPixels(pixels, 0, width, 0, 0, width, height)
var i = 0
while (i < pixels.size) {
if (pixels[i] == from) {
pixels[i] = to
}
i++
}
bitmap.setPixels(pixels, 0, width, 0, 0, width, height)
return bitmap.asImageBitmap()
}

View File

@ -1,4 +1,4 @@
package chat.simplex.app.views.newchat package chat.simplex.common.views.newchat
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.util.Log import android.util.Log
@ -7,6 +7,8 @@ import androidx.camera.core.*
import androidx.camera.lifecycle.ProcessCameraProvider import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.view.PreviewView import androidx.camera.view.PreviewView
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
@ -16,14 +18,14 @@ import boofcv.alg.color.ColorFormat
import boofcv.android.ConvertCameraImage import boofcv.android.ConvertCameraImage
import boofcv.factory.fiducial.FactoryFiducial import boofcv.factory.fiducial.FactoryFiducial
import boofcv.struct.image.GrayU8 import boofcv.struct.image.GrayU8
import chat.simplex.app.TAG import chat.simplex.common.platform.TAG
import com.google.common.util.concurrent.ListenableFuture import com.google.common.util.concurrent.ListenableFuture
import java.util.concurrent.* import java.util.concurrent.*
// Adapted from learntodroid - https://gist.github.com/learntodroid/8f839be0b29d0378f843af70607bd7f5 // Adapted from learntodroid - https://gist.github.com/learntodroid/8f839be0b29d0378f843af70607bd7f5
@Composable @Composable
fun QRCodeScanner(onBarcode: (String) -> Unit) { actual fun QRCodeScanner(onBarcode: (String) -> Unit) {
val context = LocalContext.current val context = LocalContext.current
val lifecycleOwner = LocalLifecycleOwner.current val lifecycleOwner = LocalLifecycleOwner.current
var preview by remember { mutableStateOf<Preview?>(null) } var preview by remember { mutableStateOf<Preview?>(null) }
@ -50,7 +52,8 @@ fun QRCodeScanner(onBarcode: (String) -> Unit) {
) )
implementationMode = PreviewView.ImplementationMode.COMPATIBLE implementationMode = PreviewView.ImplementationMode.COMPATIBLE
} }
} },
modifier = Modifier.clipToBounds()
) { previewView -> ) { previewView ->
val cameraSelector: CameraSelector = CameraSelector.Builder() val cameraSelector: CameraSelector = CameraSelector.Builder()
.requireLensFacing(CameraSelector.LENS_FACING_BACK) .requireLensFacing(CameraSelector.LENS_FACING_BACK)

View File

@ -1,13 +1,13 @@
package chat.simplex.app.views.newchat package chat.simplex.common.views.newchat
import android.Manifest import android.Manifest
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import chat.simplex.app.model.ChatModel import chat.simplex.common.model.ChatModel
import com.google.accompanist.permissions.rememberPermissionState import com.google.accompanist.permissions.rememberPermissionState
@Composable @Composable
fun ScanToConnectView(chatModel: ChatModel, close: () -> Unit) { actual fun ScanToConnectView(chatModel: ChatModel, close: () -> Unit) {
val cameraPermissionState = rememberPermissionState(permission = Manifest.permission.CAMERA) val cameraPermissionState = rememberPermissionState(permission = Manifest.permission.CAMERA)
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
cameraPermissionState.launchPermissionRequest() cameraPermissionState.launchPermissionRequest()

View File

@ -1,26 +1,26 @@
package chat.simplex.app.views.onboarding package chat.simplex.common.views.onboarding
import android.Manifest import android.Manifest
import android.os.Build import android.os.Build
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import chat.simplex.app.SimplexApp import chat.simplex.common.platform.ntfManager
import com.google.accompanist.permissions.rememberPermissionState import com.google.accompanist.permissions.rememberPermissionState
@Composable @Composable
fun SetNotificationsModeAdditions() { actual fun SetNotificationsModeAdditions() {
if (Build.VERSION.SDK_INT >= 33) { if (Build.VERSION.SDK_INT >= 33) {
val notificationsPermissionState = rememberPermissionState(Manifest.permission.POST_NOTIFICATIONS) val notificationsPermissionState = rememberPermissionState(Manifest.permission.POST_NOTIFICATIONS)
LaunchedEffect(notificationsPermissionState.hasPermission) { LaunchedEffect(notificationsPermissionState.hasPermission) {
if (notificationsPermissionState.hasPermission) { if (notificationsPermissionState.hasPermission) {
SimplexApp.context.chatModel.controller.ntfManager.createNtfChannelsMaybeShowAlert() ntfManager.createNtfChannelsMaybeShowAlert()
} else { } else {
notificationsPermissionState.launchPermissionRequest() notificationsPermissionState.launchPermissionRequest()
} }
} }
} else { } else {
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
SimplexApp.context.chatModel.controller.ntfManager.createNtfChannelsMaybeShowAlert() ntfManager.createNtfChannelsMaybeShowAlert()
} }
} }
} }

View File

@ -1,78 +1,60 @@
package chat.simplex.app.views.usersettings package chat.simplex.common.views.usersettings
import SectionBottomSpacer import SectionBottomSpacer
import SectionDividerSpaced import SectionDividerSpaced
import SectionItemView
import SectionItemViewSpaceBetween
import SectionSpacer
import SectionView import SectionView
import android.app.Activity import android.app.Activity
import android.content.ComponentName import android.content.ComponentName
import android.content.Context
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DEFAULT import android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DEFAULT
import android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_ENABLED import android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_ENABLED
import android.net.Uri import androidx.compose.desktop.ui.tooling.preview.Preview
import android.util.Log
import android.widget.Toast
import androidx.activity.compose.ManagedActivityResultLauncher
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.* import androidx.compose.foundation.*
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.material.*
import androidx.compose.material.MaterialTheme.colors import androidx.compose.material.MaterialTheme.colors
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.* import androidx.compose.ui.graphics.*
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.toBitmap import androidx.core.graphics.drawable.toBitmap
import chat.simplex.app.* import chat.simplex.common.R
import chat.simplex.app.R import chat.simplex.common.model.*
import chat.simplex.app.helpers.saveAppLocale import chat.simplex.common.ui.theme.*
import chat.simplex.app.model.* import chat.simplex.common.views.helpers.*
import chat.simplex.app.ui.theme.* import chat.simplex.common.model.ChatModel
import chat.simplex.app.views.helpers.* import chat.simplex.common.platform.*
import chat.simplex.app.views.usersettings.AppearanceScope.ColorEditor import chat.simplex.common.helpers.APPLICATION_ID
import chat.simplex.app.views.usersettings.AppearanceScope.LangSelector import chat.simplex.common.helpers.saveAppLocale
import chat.simplex.app.views.usersettings.AppearanceScope.ThemesSection import chat.simplex.common.views.usersettings.AppearanceScope.ColorEditor
import com.godaddy.android.colorpicker.*
import chat.simplex.res.MR import chat.simplex.res.MR
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.serialization.encodeToString
import java.io.BufferedOutputStream
import java.util.*
import kotlin.collections.ArrayList
enum class AppIcon(val resId: Int) { enum class AppIcon(val resId: Int) {
DEFAULT(R.mipmap.icon), DEFAULT(R.drawable.icon_round_common),
DARK_BLUE(R.mipmap.icon_dark_blue), DARK_BLUE(R.drawable.icon_dark_blue_round_common),
} }
@Composable @Composable
fun AppearanceView(m: ChatModel, showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit)) { actual fun AppearanceView(m: ChatModel, showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit)) {
val appIcon = remember { mutableStateOf(findEnabledIcon()) } val appIcon = remember { mutableStateOf(findEnabledIcon()) }
fun setAppIcon(newIcon: AppIcon) { fun setAppIcon(newIcon: AppIcon) {
if (appIcon.value == newIcon) return if (appIcon.value == newIcon) return
val newComponent = ComponentName(BuildConfig.APPLICATION_ID, "chat.simplex.app.MainActivity_${newIcon.name.lowercase()}") val newComponent = ComponentName(APPLICATION_ID, "chat.simplex.app.MainActivity_${newIcon.name.lowercase()}")
val oldComponent = ComponentName(BuildConfig.APPLICATION_ID, "chat.simplex.app.MainActivity_${appIcon.value.name.lowercase()}") val oldComponent = ComponentName(APPLICATION_ID, "chat.simplex.app.MainActivity_${appIcon.value.name.lowercase()}")
SimplexApp.context.packageManager.setComponentEnabledSetting( androidAppContext.packageManager.setComponentEnabledSetting(
newComponent, newComponent,
COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP
) )
SimplexApp.context.packageManager.setComponentEnabledSetting( androidAppContext.packageManager.setComponentEnabledSetting(
oldComponent, oldComponent,
PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP
) )
@ -80,7 +62,7 @@ fun AppearanceView(m: ChatModel, showSettingsModal: (@Composable (ChatModel) ->
appIcon.value = newIcon appIcon.value = newIcon
} }
AppearanceLayout( AppearanceScope.AppearanceLayout(
appIcon, appIcon,
m.controller.appPrefs.appLanguage, m.controller.appPrefs.appLanguage,
m.controller.appPrefs.systemDarkTheme, m.controller.appPrefs.systemDarkTheme,
@ -94,7 +76,8 @@ fun AppearanceView(m: ChatModel, showSettingsModal: (@Composable (ChatModel) ->
) )
} }
@Composable fun AppearanceLayout( @Composable
fun AppearanceScope.AppearanceLayout(
icon: MutableState<AppIcon>, icon: MutableState<AppIcon>,
languagePref: SharedPreference<String?>, languagePref: SharedPreference<String?>,
systemDarkTheme: SharedPreference<String?>, systemDarkTheme: SharedPreference<String?>,
@ -108,14 +91,14 @@ fun AppearanceView(m: ChatModel, showSettingsModal: (@Composable (ChatModel) ->
AppBarTitle(stringResource(MR.strings.appearance_settings)) AppBarTitle(stringResource(MR.strings.appearance_settings))
SectionView(stringResource(MR.strings.settings_section_title_language), padding = PaddingValues()) { SectionView(stringResource(MR.strings.settings_section_title_language), padding = PaddingValues()) {
val context = LocalContext.current val context = LocalContext.current
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { // if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
// SectionItemWithValue( // SectionItemWithValue(
// generalGetString(MR.strings.settings_section_title_language).lowercase().replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.US) else it.toString() }, // generalGetString(MR.strings.settings_section_title_language).lowercase().replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.US) else it.toString() },
// remember { mutableStateOf("system") }, // remember { mutableStateOf("system") },
// listOf(ValueTitleDesc("system", generalGetString(MR.strings.change_verb), "")), // listOf(ValueTitleDesc("system", generalGetString(MR.strings.change_verb), "")),
// onSelected = { openSystemLangPicker(context as? Activity ?: return@SectionItemWithValue) } // onSelected = { openSystemLangPicker(context as? Activity ?: return@SectionItemWithValue) }
// ) // )
// } else { // } else {
val state = rememberSaveable { mutableStateOf(languagePref.get() ?: "system") } val state = rememberSaveable { mutableStateOf(languagePref.get() ?: "system") }
LangSelector(state) { LangSelector(state) {
state.value = it state.value = it
@ -124,14 +107,14 @@ fun AppearanceView(m: ChatModel, showSettingsModal: (@Composable (ChatModel) ->
val activity = context as? Activity val activity = context as? Activity
if (activity != null) { if (activity != null) {
if (it == "system") { if (it == "system") {
saveAppLocale(languagePref, activity) activity.saveAppLocale(languagePref)
} else { } else {
saveAppLocale(languagePref, activity, it) activity.saveAppLocale(languagePref, it)
} }
} }
} }
} }
// } // }
} }
SectionDividerSpaced() SectionDividerSpaced()
@ -165,16 +148,16 @@ fun AppearanceView(m: ChatModel, showSettingsModal: (@Composable (ChatModel) ->
} }
private fun findEnabledIcon(): AppIcon = AppIcon.values().first { icon -> private fun findEnabledIcon(): AppIcon = AppIcon.values().first { icon ->
SimplexApp.context.packageManager.getComponentEnabledSetting( androidAppContext.packageManager.getComponentEnabledSetting(
ComponentName(BuildConfig.APPLICATION_ID, "chat.simplex.app.MainActivity_${icon.name.lowercase()}") ComponentName(APPLICATION_ID, "chat.simplex.app.MainActivity_${icon.name.lowercase()}")
).let { it == COMPONENT_ENABLED_STATE_DEFAULT || it == COMPONENT_ENABLED_STATE_ENABLED } ).let { it == COMPONENT_ENABLED_STATE_DEFAULT || it == COMPONENT_ENABLED_STATE_ENABLED }
} }
@Preview(showBackground = true) @Preview
@Composable @Composable
fun PreviewAppearanceSettings() { fun PreviewAppearanceSettings() {
SimpleXTheme { SimpleXTheme {
AppearanceLayout( AppearanceScope.AppearanceLayout(
icon = remember { mutableStateOf(AppIcon.DARK_BLUE) }, icon = remember { mutableStateOf(AppIcon.DARK_BLUE) },
languagePref = SharedPreference({ null }, {}), languagePref = SharedPreference({ null }, {}),
systemDarkTheme = SharedPreference({ null }, {}), systemDarkTheme = SharedPreference({ null }, {}),

View File

@ -1,25 +1,24 @@
package chat.simplex.app.views.usersettings package chat.simplex.common.views.usersettings
import SectionView import SectionView
import android.view.WindowManager import android.view.WindowManager
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import chat.simplex.app.model.ChatController import chat.simplex.common.model.ChatModel
import chat.simplex.app.model.ChatModel
import chat.simplex.res.MR import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource import dev.icerock.moko.resources.compose.stringResource
@Composable @Composable
fun PrivacyDeviceSection( actual fun PrivacyDeviceSection(
showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
setPerformLA: (Boolean) -> Unit, setPerformLA: (Boolean) -> Unit,
) { ) {
SectionView(stringResource(MR.strings.settings_section_title_device)) { SectionView(stringResource(MR.strings.settings_section_title_device)) {
ChatLockItem(ChatModel, showSettingsModal, setPerformLA) ChatLockItem(showSettingsModal, setPerformLA)
val context = LocalContext.current val context = LocalContext.current
SettingsPreferenceItem(painterResource(MR.images.ic_visibility_off), stringResource(MR.strings.protect_app_screen), ChatController.appPrefs.privacyProtectScreen) { on -> SettingsPreferenceItem(painterResource(MR.images.ic_visibility_off), stringResource(MR.strings.protect_app_screen), ChatModel.controller.appPrefs.privacyProtectScreen) { on ->
if (on) { if (on) {
(context as? FragmentActivity)?.window?.setFlags( (context as? FragmentActivity)?.window?.setFlags(
WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE,

View File

@ -1,13 +1,13 @@
package chat.simplex.app.views.usersettings package chat.simplex.common.views.usersettings
import android.Manifest import android.Manifest
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import chat.simplex.app.model.ServerCfg import chat.simplex.common.model.ServerCfg
import com.google.accompanist.permissions.rememberPermissionState import com.google.accompanist.permissions.rememberPermissionState
@Composable @Composable
fun ScanProtocolServer(onNext: (ServerCfg) -> Unit) { actual fun ScanProtocolServer(onNext: (ServerCfg) -> Unit) {
val cameraPermissionState = rememberPermissionState(permission = Manifest.permission.CAMERA) val cameraPermissionState = rememberPermissionState(permission = Manifest.permission.CAMERA)
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
cameraPermissionState.launchPermissionRequest() cameraPermissionState.launchPermissionRequest()

View File

@ -1,20 +1,19 @@
package chat.simplex.app.views.usersettings package chat.simplex.common.views.usersettings
import SectionView import SectionView
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.work.WorkManager import androidx.work.WorkManager
import chat.simplex.app.SimplexApp import chat.simplex.common.model.ChatModel
import chat.simplex.app.SimplexService import chat.simplex.common.platform.*
import chat.simplex.app.model.ChatModel import chat.simplex.common.views.helpers.AlertManager
import chat.simplex.app.views.helpers.AlertManager import chat.simplex.common.views.helpers.generalGetString
import chat.simplex.app.views.helpers.generalGetString
import chat.simplex.res.MR import chat.simplex.res.MR
import com.jakewharton.processphoenix.ProcessPhoenix import com.jakewharton.processphoenix.ProcessPhoenix
import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource import dev.icerock.moko.resources.compose.stringResource
@Composable @Composable
fun SettingsSectionApp( actual fun SettingsSectionApp(
showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
showCustomModal: (@Composable (ChatModel, () -> Unit) -> Unit) -> (() -> Unit), showCustomModal: (@Composable (ChatModel, () -> Unit) -> Unit) -> (() -> Unit),
showVersion: () -> Unit, showVersion: () -> Unit,
@ -28,14 +27,15 @@ fun SettingsSectionApp(
} }
} }
private fun restartApp() { private fun restartApp() {
ProcessPhoenix.triggerRebirth(SimplexApp.context) ProcessPhoenix.triggerRebirth(androidAppContext)
shutdownApp() shutdownApp()
} }
private fun shutdownApp() { private fun shutdownApp() {
WorkManager.getInstance(SimplexApp.context).cancelAllWork() WorkManager.getInstance(androidAppContext).cancelAllWork()
SimplexService.safeStopService(SimplexApp.context) platform.androidServiceSafeStop()
Runtime.getRuntime().exit(0) Runtime.getRuntime().exit(0)
} }

View File

@ -19,7 +19,7 @@ extern void __rel_iplt_start(void){};
extern void reallocarray(void){}; extern void reallocarray(void){};
JNIEXPORT jint JNICALL JNIEXPORT jint JNICALL
Java_chat_simplex_app_platform_Backend_1commonKt_pipeStdOutToSocket(JNIEnv *env, __unused jclass clazz, jstring socket_name) { Java_chat_simplex_common_platform_CoreKt_pipeStdOutToSocket(JNIEnv *env, __unused jclass clazz, jstring socket_name) {
const char *name = (*env)->GetStringUTFChars(env, socket_name, JNI_FALSE); const char *name = (*env)->GetStringUTFChars(env, socket_name, JNI_FALSE);
int ret = pipe_std_to_socket(name); int ret = pipe_std_to_socket(name);
(*env)->ReleaseStringUTFChars(env, socket_name, name); (*env)->ReleaseStringUTFChars(env, socket_name, name);
@ -27,7 +27,7 @@ Java_chat_simplex_app_platform_Backend_1commonKt_pipeStdOutToSocket(JNIEnv *env,
} }
JNIEXPORT void JNICALL JNIEXPORT void JNICALL
Java_chat_simplex_app_platform_Backend_1commonKt_initHS(__unused JNIEnv *env, __unused jclass clazz) { Java_chat_simplex_common_platform_CoreKt_initHS(__unused JNIEnv *env, __unused jclass clazz) {
hs_init(NULL, NULL); hs_init(NULL, NULL);
setLineBuffering(); setLineBuffering();
} }
@ -44,7 +44,7 @@ extern char *chat_parse_server(const char *str);
extern char *chat_password_hash(const char *pwd, const char *salt); extern char *chat_password_hash(const char *pwd, const char *salt);
JNIEXPORT jobjectArray JNICALL JNIEXPORT jobjectArray JNICALL
Java_chat_simplex_app_platform_Backend_1commonKt_chatMigrateInit(JNIEnv *env, __unused jclass clazz, jstring dbPath, jstring dbKey, jstring confirm) { Java_chat_simplex_common_platform_CoreKt_chatMigrateInit(JNIEnv *env, __unused jclass clazz, jstring dbPath, jstring dbKey, jstring confirm) {
const char *_dbPath = (*env)->GetStringUTFChars(env, dbPath, JNI_FALSE); const char *_dbPath = (*env)->GetStringUTFChars(env, dbPath, JNI_FALSE);
const char *_dbKey = (*env)->GetStringUTFChars(env, dbKey, JNI_FALSE); const char *_dbKey = (*env)->GetStringUTFChars(env, dbKey, JNI_FALSE);
const char *_confirm = (*env)->GetStringUTFChars(env, confirm, JNI_FALSE); const char *_confirm = (*env)->GetStringUTFChars(env, confirm, JNI_FALSE);
@ -67,7 +67,7 @@ Java_chat_simplex_app_platform_Backend_1commonKt_chatMigrateInit(JNIEnv *env, __
} }
JNIEXPORT jstring JNICALL JNIEXPORT jstring JNICALL
Java_chat_simplex_app_platform_Backend_1commonKt_chatSendCmd(JNIEnv *env, __unused jclass clazz, jlong controller, jstring msg) { Java_chat_simplex_common_platform_CoreKt_chatSendCmd(JNIEnv *env, __unused jclass clazz, jlong controller, jstring msg) {
const char *_msg = (*env)->GetStringUTFChars(env, msg, JNI_FALSE); const char *_msg = (*env)->GetStringUTFChars(env, msg, JNI_FALSE);
jstring res = (*env)->NewStringUTF(env, chat_send_cmd((void*)controller, _msg)); jstring res = (*env)->NewStringUTF(env, chat_send_cmd((void*)controller, _msg));
(*env)->ReleaseStringUTFChars(env, msg, _msg); (*env)->ReleaseStringUTFChars(env, msg, _msg);
@ -75,17 +75,17 @@ Java_chat_simplex_app_platform_Backend_1commonKt_chatSendCmd(JNIEnv *env, __unus
} }
JNIEXPORT jstring JNICALL JNIEXPORT jstring JNICALL
Java_chat_simplex_app_platform_Backend_1commonKt_chatRecvMsg(JNIEnv *env, __unused jclass clazz, jlong controller) { Java_chat_simplex_common_platform_CoreKt_chatRecvMsg(JNIEnv *env, __unused jclass clazz, jlong controller) {
return (*env)->NewStringUTF(env, chat_recv_msg((void*)controller)); return (*env)->NewStringUTF(env, chat_recv_msg((void*)controller));
} }
JNIEXPORT jstring JNICALL JNIEXPORT jstring JNICALL
Java_chat_simplex_app_platform_Backend_1commonKt_chatRecvMsgWait(JNIEnv *env, __unused jclass clazz, jlong controller, jint wait) { Java_chat_simplex_common_platform_CoreKt_chatRecvMsgWait(JNIEnv *env, __unused jclass clazz, jlong controller, jint wait) {
return (*env)->NewStringUTF(env, chat_recv_msg_wait((void*)controller, wait)); return (*env)->NewStringUTF(env, chat_recv_msg_wait((void*)controller, wait));
} }
JNIEXPORT jstring JNICALL JNIEXPORT jstring JNICALL
Java_chat_simplex_app_platform_Backend_1commonKt_chatParseMarkdown(JNIEnv *env, __unused jclass clazz, jstring str) { Java_chat_simplex_common_platform_CoreKt_chatParseMarkdown(JNIEnv *env, __unused jclass clazz, jstring str) {
const char *_str = (*env)->GetStringUTFChars(env, str, JNI_FALSE); const char *_str = (*env)->GetStringUTFChars(env, str, JNI_FALSE);
jstring res = (*env)->NewStringUTF(env, chat_parse_markdown(_str)); jstring res = (*env)->NewStringUTF(env, chat_parse_markdown(_str));
(*env)->ReleaseStringUTFChars(env, str, _str); (*env)->ReleaseStringUTFChars(env, str, _str);
@ -93,7 +93,7 @@ Java_chat_simplex_app_platform_Backend_1commonKt_chatParseMarkdown(JNIEnv *env,
} }
JNIEXPORT jstring JNICALL JNIEXPORT jstring JNICALL
Java_chat_simplex_app_platform_Backend_1commonKt_chatParseServer(JNIEnv *env, __unused jclass clazz, jstring str) { Java_chat_simplex_common_platform_CoreKt_chatParseServer(JNIEnv *env, __unused jclass clazz, jstring str) {
const char *_str = (*env)->GetStringUTFChars(env, str, JNI_FALSE); const char *_str = (*env)->GetStringUTFChars(env, str, JNI_FALSE);
jstring res = (*env)->NewStringUTF(env, chat_parse_server(_str)); jstring res = (*env)->NewStringUTF(env, chat_parse_server(_str));
(*env)->ReleaseStringUTFChars(env, str, _str); (*env)->ReleaseStringUTFChars(env, str, _str);
@ -101,7 +101,7 @@ Java_chat_simplex_app_platform_Backend_1commonKt_chatParseServer(JNIEnv *env, __
} }
JNIEXPORT jstring JNICALL JNIEXPORT jstring JNICALL
Java_chat_simplex_app_platform_Backend_1commonKt_chatPasswordHash(JNIEnv *env, __unused jclass clazz, jstring pwd, jstring salt) { Java_chat_simplex_common_platform_CoreKt_chatPasswordHash(JNIEnv *env, __unused jclass clazz, jstring pwd, jstring salt) {
const char *_pwd = (*env)->GetStringUTFChars(env, pwd, JNI_FALSE); const char *_pwd = (*env)->GetStringUTFChars(env, pwd, JNI_FALSE);
const char *_salt = (*env)->GetStringUTFChars(env, salt, JNI_FALSE); const char *_salt = (*env)->GetStringUTFChars(env, salt, JNI_FALSE);
jstring res = (*env)->NewStringUTF(env, chat_password_hash(_pwd, _salt)); jstring res = (*env)->NewStringUTF(env, chat_password_hash(_pwd, _salt));

View File

@ -15,7 +15,7 @@ void hs_init(int * argc, char **argv[]);
//extern void reallocarray(void){}; //extern void reallocarray(void){};
JNIEXPORT void JNICALL JNIEXPORT void JNICALL
Java_chat_simplex_common_platform_BackendKt_initHS(JNIEnv *env, jclass clazz) { Java_chat_simplex_common_platform_CoreKt_initHS(JNIEnv *env, jclass clazz) {
hs_init(NULL, NULL); hs_init(NULL, NULL);
} }
@ -31,7 +31,7 @@ extern char *chat_parse_server(const char *str);
extern char *chat_password_hash(const char *pwd, const char *salt); extern char *chat_password_hash(const char *pwd, const char *salt);
JNIEXPORT jobjectArray JNICALL JNIEXPORT jobjectArray JNICALL
Java_chat_simplex_common_platform_BackendKt_chatMigrateInit(JNIEnv *env, jclass clazz, jstring dbPath, jstring dbKey, jstring confirm) { Java_chat_simplex_common_platform_CoreKt_chatMigrateInit(JNIEnv *env, jclass clazz, jstring dbPath, jstring dbKey, jstring confirm) {
const char *_dbPath = (*env)->GetStringUTFChars(env, dbPath, JNI_FALSE); const char *_dbPath = (*env)->GetStringUTFChars(env, dbPath, JNI_FALSE);
const char *_dbKey = (*env)->GetStringUTFChars(env, dbKey, JNI_FALSE); const char *_dbKey = (*env)->GetStringUTFChars(env, dbKey, JNI_FALSE);
const char *_confirm = (*env)->GetStringUTFChars(env, confirm, JNI_FALSE); const char *_confirm = (*env)->GetStringUTFChars(env, confirm, JNI_FALSE);
@ -54,7 +54,7 @@ Java_chat_simplex_common_platform_BackendKt_chatMigrateInit(JNIEnv *env, jclass
} }
JNIEXPORT jstring JNICALL JNIEXPORT jstring JNICALL
Java_chat_simplex_common_platform_BackendKt_chatSendCmd(JNIEnv *env, jclass clazz, jlong controller, jstring msg) { Java_chat_simplex_common_platform_CoreKt_chatSendCmd(JNIEnv *env, jclass clazz, jlong controller, jstring msg) {
const char *_msg = (*env)->GetStringUTFChars(env, msg, JNI_FALSE); const char *_msg = (*env)->GetStringUTFChars(env, msg, JNI_FALSE);
jstring res = (*env)->NewStringUTF(env, chat_send_cmd((void*)controller, _msg)); jstring res = (*env)->NewStringUTF(env, chat_send_cmd((void*)controller, _msg));
(*env)->ReleaseStringUTFChars(env, msg, _msg); (*env)->ReleaseStringUTFChars(env, msg, _msg);
@ -62,17 +62,17 @@ Java_chat_simplex_common_platform_BackendKt_chatSendCmd(JNIEnv *env, jclass claz
} }
JNIEXPORT jstring JNICALL JNIEXPORT jstring JNICALL
Java_chat_simplex_common_platform_BackendKt_chatRecvMsg(JNIEnv *env, jclass clazz, jlong controller) { Java_chat_simplex_common_platform_CoreKt_chatRecvMsg(JNIEnv *env, jclass clazz, jlong controller) {
return (*env)->NewStringUTF(env, chat_recv_msg((void*)controller)); return (*env)->NewStringUTF(env, chat_recv_msg((void*)controller));
} }
JNIEXPORT jstring JNICALL JNIEXPORT jstring JNICALL
Java_chat_simplex_common_platform_BackendKt_chatRecvMsgWait(JNIEnv *env, jclass clazz, jlong controller, jint wait) { Java_chat_simplex_common_platform_CoreKt_chatRecvMsgWait(JNIEnv *env, jclass clazz, jlong controller, jint wait) {
return (*env)->NewStringUTF(env, chat_recv_msg_wait((void*)controller, wait)); return (*env)->NewStringUTF(env, chat_recv_msg_wait((void*)controller, wait));
} }
JNIEXPORT jstring JNICALL JNIEXPORT jstring JNICALL
Java_chat_simplex_common_platform_BackendKt_chatParseMarkdown(JNIEnv *env, jclass clazz, jstring str) { Java_chat_simplex_common_platform_CoreKt_chatParseMarkdown(JNIEnv *env, jclass clazz, jstring str) {
const char *_str = (*env)->GetStringUTFChars(env, str, JNI_FALSE); const char *_str = (*env)->GetStringUTFChars(env, str, JNI_FALSE);
jstring res = (*env)->NewStringUTF(env, chat_parse_markdown(_str)); jstring res = (*env)->NewStringUTF(env, chat_parse_markdown(_str));
(*env)->ReleaseStringUTFChars(env, str, _str); (*env)->ReleaseStringUTFChars(env, str, _str);
@ -80,7 +80,7 @@ Java_chat_simplex_common_platform_BackendKt_chatParseMarkdown(JNIEnv *env, jclas
} }
JNIEXPORT jstring JNICALL JNIEXPORT jstring JNICALL
Java_chat_simplex_common_platform_BackendKt_chatParseServer(JNIEnv *env, jclass clazz, jstring str) { Java_chat_simplex_common_platform_CoreKt_chatParseServer(JNIEnv *env, jclass clazz, jstring str) {
const char *_str = (*env)->GetStringUTFChars(env, str, JNI_FALSE); const char *_str = (*env)->GetStringUTFChars(env, str, JNI_FALSE);
jstring res = (*env)->NewStringUTF(env, chat_parse_server(_str)); jstring res = (*env)->NewStringUTF(env, chat_parse_server(_str));
(*env)->ReleaseStringUTFChars(env, str, _str); (*env)->ReleaseStringUTFChars(env, str, _str);
@ -88,7 +88,7 @@ Java_chat_simplex_common_platform_BackendKt_chatParseServer(JNIEnv *env, jclass
} }
JNIEXPORT jstring JNICALL JNIEXPORT jstring JNICALL
Java_chat_simplex_common_platform_BackendKt_chatPasswordHash(JNIEnv *env, jclass clazz, jstring pwd, jstring salt) { Java_chat_simplex_common_platform_CoreKt_chatPasswordHash(JNIEnv *env, jclass clazz, jstring pwd, jstring salt) {
const char *_pwd = (*env)->GetStringUTFChars(env, pwd, JNI_FALSE); const char *_pwd = (*env)->GetStringUTFChars(env, pwd, JNI_FALSE);
const char *_salt = (*env)->GetStringUTFChars(env, salt, JNI_FALSE); const char *_salt = (*env)->GetStringUTFChars(env, salt, JNI_FALSE);
jstring res = (*env)->NewStringUTF(env, chat_password_hash(_pwd, _salt)); jstring res = (*env)->NewStringUTF(env, chat_password_hash(_pwd, _salt));

View File

@ -1,6 +1,5 @@
package chat.simplex.app package chat.simplex.common
import android.os.SystemClock
import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.Animatable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.material.* import androidx.compose.material.*
@ -10,44 +9,44 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import chat.simplex.app.model.ChatModel import chat.simplex.common.model.*
import chat.simplex.app.ui.theme.DEFAULT_PADDING import chat.simplex.common.platform.*
import chat.simplex.app.ui.theme.SimpleButton import chat.simplex.common.ui.theme.DEFAULT_PADDING
import chat.simplex.app.views.SplashView import chat.simplex.common.views.helpers.SimpleButton
import chat.simplex.app.views.call.ActiveCallView import chat.simplex.common.views.SplashView
import chat.simplex.app.views.call.IncomingCallAlertView import chat.simplex.common.views.call.ActiveCallView
import chat.simplex.app.views.chat.ChatView import chat.simplex.common.views.call.IncomingCallAlertView
import chat.simplex.app.views.chatlist.ChatListView import chat.simplex.common.views.chat.ChatView
import chat.simplex.app.views.chatlist.ShareListView import chat.simplex.common.views.chatlist.ChatListView
import chat.simplex.app.views.database.DatabaseErrorView import chat.simplex.common.views.chatlist.ShareListView
import chat.simplex.app.views.helpers.* import chat.simplex.common.views.database.DatabaseErrorView
import chat.simplex.app.views.onboarding.* import chat.simplex.common.views.helpers.*
import chat.simplex.app.views.usersettings.LAMode import chat.simplex.common.views.onboarding.*
import chat.simplex.app.views.usersettings.laUnavailableInstructionAlert import chat.simplex.common.views.usersettings.*
import chat.simplex.res.MR import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource import dev.icerock.moko.resources.compose.stringResource
import kotlinx.coroutines.delay import kotlinx.coroutines.*
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.launch
@Composable @Composable
fun MainPage( fun AppScreen() {
chatModel: ChatModel, ProvideWindowInsets(windowInsetsAnimationsEnabled = true) {
userAuthorized: MutableState<Boolean?>, Surface(color = MaterialTheme.colors.background) {
laFailed: MutableState<Boolean>, MainScreen()
destroyedAfterBackPress: MutableState<Boolean>, }
runAuthenticate: () -> Unit, }
setPerformLA: (Boolean) -> Unit, }
showLANotice: () -> Unit
) { @Composable
fun MainScreen() {
val chatModel = ChatModel
var showChatDatabaseError by rememberSaveable { var showChatDatabaseError by rememberSaveable {
mutableStateOf(chatModel.chatDbStatus.value != DBMigrationResult.OK && chatModel.chatDbStatus.value != null) mutableStateOf(chatModel.chatDbStatus.value != DBMigrationResult.OK && chatModel.chatDbStatus.value != null)
} }
LaunchedEffect(chatModel.chatDbStatus.value) { LaunchedEffect(chatModel.chatDbStatus.value) {
showChatDatabaseError = chatModel.chatDbStatus.value != DBMigrationResult.OK && chatModel.chatDbStatus.value != null showChatDatabaseError = chatModel.chatDbStatus.value != DBMigrationResult.OK && chatModel.chatDbStatus.value != null
} }
var showAdvertiseLAAlert by remember { mutableStateOf(false) } var showAdvertiseLAAlert by remember { mutableStateOf(false) }
LaunchedEffect(showAdvertiseLAAlert) { LaunchedEffect(showAdvertiseLAAlert) {
if ( if (
@ -57,8 +56,7 @@ fun MainPage(
&& chatModel.chats.isNotEmpty() && chatModel.chats.isNotEmpty()
&& chatModel.activeCallInvitation.value == null && chatModel.activeCallInvitation.value == null
) { ) {
showLANotice() AppLock.showLANotice(ChatModel.controller.appPrefs.laNoticeShown) }
}
} }
LaunchedEffect(chatModel.showAdvertiseLAUnavailableAlert.value) { LaunchedEffect(chatModel.showAdvertiseLAUnavailableAlert.value) {
if (chatModel.showAdvertiseLAUnavailableAlert.value) { if (chatModel.showAdvertiseLAUnavailableAlert.value) {
@ -83,8 +81,8 @@ fun MainPage(
stringResource(MR.strings.auth_unlock), stringResource(MR.strings.auth_unlock),
icon = painterResource(MR.images.ic_lock), icon = painterResource(MR.images.ic_lock),
click = { click = {
laFailed.value = false AppLock.laFailed.value = false
runAuthenticate() AppLock.runAuthenticate()
} }
) )
} }
@ -117,7 +115,7 @@ fun MainPage(
) { ) {
val stopped = chatModel.chatRunning.value == false val stopped = chatModel.chatRunning.value == false
if (chatModel.sharedContent.value == null) if (chatModel.sharedContent.value == null)
ChatListView(chatModel, setPerformLA, stopped) ChatListView(chatModel, AppLock::setPerformLA, stopped)
else else
ShareListView(chatModel, stopped) ShareListView(chatModel, stopped)
} }
@ -143,7 +141,7 @@ fun MainPage(
} }
} }
} }
Box (Modifier.graphicsLayer { translationX = maxWidth.toPx() - offset.value.dp.toPx() }) Box2@ { Box(Modifier.graphicsLayer { translationX = maxWidth.toPx() - offset.value.dp.toPx() }) Box2@{
currentChatId?.let { currentChatId?.let {
ChatView(it, chatModel, onComposed) ChatView(it, chatModel, onComposed)
} }
@ -157,22 +155,23 @@ fun MainPage(
onboarding == OnboardingStage.Step4_SetNotificationsMode -> SetNotificationsMode(chatModel) onboarding == OnboardingStage.Step4_SetNotificationsMode -> SetNotificationsMode(chatModel)
} }
ModalManager.shared.showInView() ModalManager.shared.showInView()
val unauthorized = remember { derivedStateOf { userAuthorized.value != true } }
val unauthorized = remember { derivedStateOf { AppLock.userAuthorized.value != true } }
if (unauthorized.value && !(chatModel.activeCallViewIsVisible.value && chatModel.showCallView.value)) { if (unauthorized.value && !(chatModel.activeCallViewIsVisible.value && chatModel.showCallView.value)) {
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
// With these constrains when user presses back button while on ChatList, activity destroys and shows auth request // With these constrains when user presses back button while on ChatList, activity destroys and shows auth request
// while the screen moves to a launcher. Detect it and prevent showing the auth // while the screen moves to a launcher. Detect it and prevent showing the auth
if (!(destroyedAfterBackPress.value && chatModel.controller.appPrefs.laMode.get() == LAMode.SYSTEM)) { if (!(AppLock.destroyedAfterBackPress.value && chatModel.controller.appPrefs.laMode.get() == LAMode.SYSTEM)) {
runAuthenticate() AppLock.runAuthenticate()
} }
} }
if (chatModel.controller.appPrefs.performLA.get() && laFailed.value) { if (chatModel.controller.appPrefs.performLA.get() && AppLock.laFailed.value) {
AuthView() AuthView()
} else { } else {
SplashView() SplashView()
} }
} else if (chatModel.showCallView.value) { } else if (chatModel.showCallView.value) {
ActiveCallView(chatModel) ActiveCallView()
} }
ModalManager.shared.showPasscodeInView() ModalManager.shared.showPasscodeInView()
val invitation = chatModel.activeCallInvitation.value val invitation = chatModel.activeCallInvitation.value
@ -191,13 +190,13 @@ fun MainPage(
// When using lock delay = 0 and screen rotates, the app will be locked which is not useful. // When using lock delay = 0 and screen rotates, the app will be locked which is not useful.
// Let's prolong the unlocked period to 3 sec for screen rotation to take place // Let's prolong the unlocked period to 3 sec for screen rotation to take place
if (chatModel.controller.appPrefs.laLockDelay.get() == 0) { if (chatModel.controller.appPrefs.laLockDelay.get() == 0) {
AppLock.enteredBackground.value = SystemClock.elapsedRealtime() + 3000 AppLock.enteredBackground.value = AppLock.elapsedRealtime() + 3000
} }
} }
} }
@Composable @Composable
private fun InitializationView() { fun InitializationView() {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Column(horizontalAlignment = Alignment.CenterHorizontally) { Column(horizontalAlignment = Alignment.CenterHorizontally) {
CircularProgressIndicator( CircularProgressIndicator(

View File

@ -1,16 +1,16 @@
package chat.simplex.app package chat.simplex.common
import android.util.Log
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface import androidx.compose.material.Surface
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import chat.simplex.app.model.ChatModel import chat.simplex.common.model.*
import chat.simplex.app.model.SharedPreference import chat.simplex.common.platform.Log
import chat.simplex.app.views.helpers.* import chat.simplex.common.platform.TAG
import chat.simplex.app.views.localauth.SetAppPasscodeView import chat.simplex.common.views.helpers.*
import chat.simplex.app.views.usersettings.* import chat.simplex.common.views.localauth.SetAppPasscodeView
import chat.simplex.common.views.usersettings.*
import chat.simplex.res.MR import chat.simplex.res.MR
import kotlinx.coroutines.* import kotlinx.coroutines.*
@ -70,8 +70,8 @@ object AppLock {
private fun initialEnableLA() { private fun initialEnableLA() {
val m = ChatModel val m = ChatModel
val appPrefs = m.controller.appPrefs val appPrefs = ChatController.appPrefs
m.controller.appPrefs.laMode.set(LAMode.SYSTEM) appPrefs.laMode.set(LAMode.SYSTEM)
authenticate( authenticate(
generalGetString(MR.strings.auth_enable_simplex_lock), generalGetString(MR.strings.auth_enable_simplex_lock),
generalGetString(MR.strings.auth_confirm_credential), generalGetString(MR.strings.auth_confirm_credential),
@ -99,19 +99,18 @@ object AppLock {
} }
private fun setPasscode() { private fun setPasscode() {
val chatModel = ChatModel val appPrefs = ChatController.appPrefs
val appPrefs = chatModel.controller.appPrefs
ModalManager.shared.showCustomModal { close -> ModalManager.shared.showCustomModal { close ->
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background) { Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
SetAppPasscodeView( SetAppPasscodeView(
submit = { submit = {
chatModel.performLA.value = true ChatModel.performLA.value = true
appPrefs.performLA.set(true) appPrefs.performLA.set(true)
appPrefs.laMode.set(LAMode.PASSCODE) appPrefs.laMode.set(LAMode.PASSCODE)
laTurnedOnAlert() laTurnedOnAlert()
}, },
cancel = { cancel = {
chatModel.performLA.value = false ChatModel.performLA.value = false
appPrefs.performLA.set(false) appPrefs.performLA.set(false)
laPasscodeNotSetAlert() laPasscodeNotSetAlert()
}, },
@ -122,7 +121,7 @@ object AppLock {
} }
fun setAuthState() { fun setAuthState() {
userAuthorized.value = !ChatModel.controller.appPrefs.performLA.get() userAuthorized.value = !ChatController.appPrefs.performLA.get()
} }
fun runAuthenticate() { fun runAuthenticate() {
@ -169,7 +168,7 @@ object AppLock {
} }
fun setPerformLA(on: Boolean) { fun setPerformLA(on: Boolean) {
ChatModel.controller.appPrefs.laNoticeShown.set(true) ChatController.appPrefs.laNoticeShown.set(true)
if (on) { if (on) {
enableLA() enableLA()
} else { } else {
@ -249,4 +248,22 @@ object AppLock {
} }
) )
} }
fun elapsedRealtime(): Long = System.nanoTime() / 1_000_000
fun recheckAuthState() {
val enteredBackgroundVal = enteredBackground.value
val delay = ChatController.appPrefs.laLockDelay.get()
if (enteredBackgroundVal == null || elapsedRealtime() - enteredBackgroundVal >= delay * 1000) {
if (userAuthorized.value != false) {
/** [runAuthenticate] will be called in [MainScreen] if needed. Making like this prevents double showing of passcode on start */
setAuthState()
} else if (!ChatModel.activeCallViewIsVisible.value) {
runAuthenticate()
}
}
}
fun appWasHidden() {
enteredBackground.value = elapsedRealtime()
}
} }

View File

@ -1,19 +1,17 @@
package chat.simplex.app.model package chat.simplex.common.model
import android.net.Uri
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.font.* import androidx.compose.ui.text.font.*
import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.style.TextDecoration
import chat.simplex.app.R import chat.simplex.common.ui.theme.*
import chat.simplex.app.ui.theme.* import chat.simplex.common.views.call.*
import chat.simplex.app.views.call.* import chat.simplex.common.views.chat.ComposeState
import chat.simplex.app.views.chat.ComposeState import chat.simplex.common.views.helpers.*
import chat.simplex.app.views.helpers.* import chat.simplex.common.views.onboarding.OnboardingStage
import chat.simplex.app.views.onboarding.OnboardingStage import chat.simplex.common.platform.AudioPlayer
import chat.simplex.app.views.usersettings.NotificationPreviewMode
import chat.simplex.res.MR import chat.simplex.res.MR
import dev.icerock.moko.resources.ImageResource import dev.icerock.moko.resources.ImageResource
import dev.icerock.moko.resources.StringResource import dev.icerock.moko.resources.StringResource
@ -26,6 +24,7 @@ import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.* import kotlinx.serialization.json.*
import java.io.File import java.io.File
import java.net.URI
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle import java.time.format.FormatStyle
import java.util.* import java.util.*
@ -65,14 +64,21 @@ object ChatModel {
val clearOverlays = mutableStateOf<Boolean>(false) val clearOverlays = mutableStateOf<Boolean>(false)
// set when app is opened via contact or invitation URI // set when app is opened via contact or invitation URI
val appOpenUrl = mutableStateOf<Uri?>(null) val appOpenUrl = mutableStateOf<URI?>(null)
// preferences // preferences
val notificationsMode by lazy { mutableStateOf(NotificationsMode.values().firstOrNull { it.name == controller.appPrefs.notificationsMode.get() } ?: NotificationsMode.default) } val notificationPreviewMode by lazy {
val notificationPreviewMode by lazy { mutableStateOf(NotificationPreviewMode.values().firstOrNull { it.name == controller.appPrefs.notificationPreviewMode.get() } ?: NotificationPreviewMode.default) } mutableStateOf(
val performLA by lazy { mutableStateOf(controller.appPrefs.performLA.get()) } try {
NotificationPreviewMode.valueOf(controller.appPrefs.notificationPreviewMode.get()!!)
} catch (e: Exception) {
NotificationPreviewMode.default
}
)
}
val performLA by lazy { mutableStateOf(ChatController.appPrefs.performLA.get()) }
val showAdvertiseLAUnavailableAlert = mutableStateOf(false) val showAdvertiseLAUnavailableAlert = mutableStateOf(false)
val incognito by lazy { mutableStateOf(controller.appPrefs.incognito.get()) } val incognito by lazy { mutableStateOf(ChatController.appPrefs.incognito.get()) }
// current WebRTC call // current WebRTC call
val callManager = CallManager(this) val callManager = CallManager(this)
@ -94,7 +100,7 @@ object ChatModel {
val sharedContent = mutableStateOf(null as SharedContent?) val sharedContent = mutableStateOf(null as SharedContent?)
val filesToDelete = mutableSetOf<File>() val filesToDelete = mutableSetOf<File>()
val simplexLinkMode by lazy { mutableStateOf(controller.appPrefs.simplexLinkMode.get()) } val simplexLinkMode by lazy { mutableStateOf(ChatController.appPrefs.simplexLinkMode.get()) }
fun getUser(userId: Long): User? = if (currentUser.value?.userId == userId) { fun getUser(userId: Long): User? = if (currentUser.value?.userId == userId) {
currentUser.value currentUser.value
@ -2409,3 +2415,11 @@ data class ChatItemVersion(
val itemVersionTs: Instant, val itemVersionTs: Instant,
val createdAt: Instant, val createdAt: Instant,
) )
enum class NotificationPreviewMode {
MESSAGE, CONTACT, HIDDEN;
companion object {
val default: NotificationPreviewMode = MESSAGE
}
}

View File

@ -1,23 +1,21 @@
package chat.simplex.app.model package chat.simplex.common.model
import android.content.* import chat.simplex.common.views.helpers.*
import android.os.Build
import android.util.Log
import chat.simplex.app.views.helpers.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.painter.Painter
import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.painterResource
import chat.simplex.app.* import chat.simplex.common.*
import chat.simplex.app.platform.* import chat.simplex.common.platform.*
import chat.simplex.app.ui.theme.* import chat.simplex.common.ui.theme.*
import chat.simplex.app.views.call.* import chat.simplex.common.views.call.*
import chat.simplex.app.views.newchat.ConnectViaLinkTab import chat.simplex.common.views.newchat.ConnectViaLinkTab
import chat.simplex.app.views.onboarding.OnboardingStage import chat.simplex.common.views.onboarding.OnboardingStage
import chat.simplex.app.views.usersettings.* import chat.simplex.common.views.usersettings.*
import com.charleskorn.kaml.Yaml import com.charleskorn.kaml.Yaml
import com.charleskorn.kaml.YamlConfiguration import com.charleskorn.kaml.YamlConfiguration
import chat.simplex.res.MR import chat.simplex.res.MR
import com.russhwolf.settings.Settings
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.datetime.Clock import kotlinx.datetime.Clock
import kotlinx.datetime.Instant import kotlinx.datetime.Instant
@ -45,19 +43,17 @@ enum class SimplexLinkMode {
BROWSER; BROWSER;
companion object { companion object {
val default = SimplexLinkMode.DESCRIPTION val default = DESCRIPTION
} }
} }
class AppPreferences { class AppPreferences {
private val sharedPreferences: SharedPreferences = SimplexApp.context.getSharedPreferences(SHARED_PREFS_ID, Context.MODE_PRIVATE)
private val sharedPreferencesThemes: SharedPreferences = SimplexApp.context.getSharedPreferences(SHARED_PREFS_THEMES_ID, Context.MODE_PRIVATE)
// deprecated, remove in 2024 // deprecated, remove in 2024
private val runServiceInBackground = mkBoolPreference(SHARED_PREFS_RUN_SERVICE_IN_BACKGROUND, true) private val runServiceInBackground = mkBoolPreference(SHARED_PREFS_RUN_SERVICE_IN_BACKGROUND, true)
val notificationsMode = mkStrPreference(SHARED_PREFS_NOTIFICATIONS_MODE, val notificationsMode = mkEnumPreference(
if (!runServiceInBackground.get()) NotificationsMode.OFF.name else NotificationsMode.default.name SHARED_PREFS_NOTIFICATIONS_MODE,
) if (!runServiceInBackground.get()) NotificationsMode.OFF else NotificationsMode.default
) { NotificationsMode.values().firstOrNull { it.name == this } }
val notificationPreviewMode = mkStrPreference(SHARED_PREFS_NOTIFICATION_PREVIEW_MODE, NotificationPreviewMode.default.name) val notificationPreviewMode = mkStrPreference(SHARED_PREFS_NOTIFICATION_PREVIEW_MODE, NotificationPreviewMode.default.name)
val backgroundServiceNoticeShown = mkBoolPreference(SHARED_PREFS_SERVICE_NOTICE_SHOWN, false) val backgroundServiceNoticeShown = mkBoolPreference(SHARED_PREFS_SERVICE_NOTICE_SHOWN, false)
val backgroundServiceBatteryNoticeShown = mkBoolPreference(SHARED_PREFS_SERVICE_BATTERY_NOTICE_SHOWN, false) val backgroundServiceBatteryNoticeShown = mkBoolPreference(SHARED_PREFS_SERVICE_BATTERY_NOTICE_SHOWN, false)
@ -143,7 +139,7 @@ class AppPreferences {
val initializationVectorAppPassphrase = mkStrPreference(SHARED_PREFS_INITIALIZATION_VECTOR_APP_PASSPHRASE, null) val initializationVectorAppPassphrase = mkStrPreference(SHARED_PREFS_INITIALIZATION_VECTOR_APP_PASSPHRASE, null)
val encryptedSelfDestructPassphrase = mkStrPreference(SHARED_PREFS_ENCRYPTED_SELF_DESTRUCT_PASSPHRASE, null) val encryptedSelfDestructPassphrase = mkStrPreference(SHARED_PREFS_ENCRYPTED_SELF_DESTRUCT_PASSPHRASE, null)
val initializationVectorSelfDestructPassphrase = mkStrPreference(SHARED_PREFS_INITIALIZATION_VECTOR_SELF_DESTRUCT_PASSPHRASE, null) val initializationVectorSelfDestructPassphrase = mkStrPreference(SHARED_PREFS_INITIALIZATION_VECTOR_SELF_DESTRUCT_PASSPHRASE, null)
val encryptionStartedAt = mkDatePreference(SHARED_PREFS_ENCRYPTION_STARTED_AT, null, true) val encryptionStartedAt = mkDatePreference(SHARED_PREFS_ENCRYPTION_STARTED_AT, null)
val confirmDBUpgrades = mkBoolPreference(SHARED_PREFS_CONFIRM_DB_UPGRADES, false) val confirmDBUpgrades = mkBoolPreference(SHARED_PREFS_CONFIRM_DB_UPGRADES, false)
val selfDestruct = mkBoolPreference(SHARED_PREFS_SELF_DESTRUCT, false) val selfDestruct = mkBoolPreference(SHARED_PREFS_SELF_DESTRUCT, false)
val selfDestructDisplayName = mkStrPreference(SHARED_PREFS_SELF_DESTRUCT_DISPLAY_NAME, null) val selfDestructDisplayName = mkStrPreference(SHARED_PREFS_SELF_DESTRUCT_DISPLAY_NAME, null)
@ -154,7 +150,7 @@ class AppPreferences {
json.encodeToString(MapSerializer(String.serializer(), ThemeOverrides.serializer()), it) json.encodeToString(MapSerializer(String.serializer(), ThemeOverrides.serializer()), it)
}, decode = { }, decode = {
json.decodeFromString(MapSerializer(String.serializer(), ThemeOverrides.serializer()), it) json.decodeFromString(MapSerializer(String.serializer(), ThemeOverrides.serializer()), it)
}, sharedPreferencesThemes) }, settingsThemes)
val whatsNewVersion = mkStrPreference(SHARED_PREFS_WHATS_NEW_VERSION, null) val whatsNewVersion = mkStrPreference(SHARED_PREFS_WHATS_NEW_VERSION, null)
val lastMigratedVersionCode = mkIntPreference(SHARED_PREFS_LAST_MIGRATED_VERSION_CODE, 0) val lastMigratedVersionCode = mkIntPreference(SHARED_PREFS_LAST_MIGRATED_VERSION_CODE, 0)
@ -162,65 +158,73 @@ class AppPreferences {
private fun mkIntPreference(prefName: String, default: Int) = private fun mkIntPreference(prefName: String, default: Int) =
SharedPreference( SharedPreference(
get = fun() = sharedPreferences.getInt(prefName, default), get = fun() = settings.getInt(prefName, default),
set = fun(value) = sharedPreferences.edit().putInt(prefName, value).apply() set = fun(value) = settings.putInt(prefName, value)
) )
private fun mkLongPreference(prefName: String, default: Long) = private fun mkLongPreference(prefName: String, default: Long) =
SharedPreference( SharedPreference(
get = fun() = sharedPreferences.getLong(prefName, default), get = fun() = settings.getLong(prefName, default),
set = fun(value) = sharedPreferences.edit().putLong(prefName, value).apply() set = fun(value) = settings.putLong(prefName, value)
) )
private fun mkTimeoutPreference(prefName: String, default: Long, proxyDefault: Long): SharedPreference<Long> { private fun mkTimeoutPreference(prefName: String, default: Long, proxyDefault: Long): SharedPreference<Long> {
val d = if (networkUseSocksProxy.get()) proxyDefault else default val d = if (networkUseSocksProxy.get()) proxyDefault else default
return SharedPreference( return SharedPreference(
get = fun() = sharedPreferences.getLong(prefName, d), get = fun() = settings.getLong(prefName, d),
set = fun(value) = sharedPreferences.edit().putLong(prefName, value).apply() set = fun(value) = settings.putLong(prefName, value)
) )
} }
private fun mkBoolPreference(prefName: String, default: Boolean) = private fun mkBoolPreference(prefName: String, default: Boolean) =
SharedPreference( SharedPreference(
get = fun() = sharedPreferences.getBoolean(prefName, default), get = fun() = settings.getBoolean(prefName, default),
set = fun(value) = sharedPreferences.edit().putBoolean(prefName, value).apply() set = fun(value) = settings.putBoolean(prefName, value)
) )
private fun mkStrPreference(prefName: String, default: String?): SharedPreference<String?> = private fun mkStrPreference(prefName: String, default: String?): SharedPreference<String?> =
SharedPreference( SharedPreference(
get = fun() = sharedPreferences.getString(prefName, default), get = {
set = fun(value) = sharedPreferences.edit().putString(prefName, value).apply() val nullValue = "----------------------"
val pref = settings.getString(prefName, default ?: nullValue)
if (pref != nullValue) {
pref
} else {
null
}
},
set = fun(value) = if (value != null) settings.putString(prefName, value) else settings.remove(prefName)
) )
private fun <T> mkEnumPreference(prefName: String, default: T, construct: String.() -> T?): SharedPreference<T> = private fun <T> mkEnumPreference(prefName: String, default: T, construct: String.() -> T?): SharedPreference<T> =
SharedPreference( SharedPreference(
get = fun() = sharedPreferences.getString(prefName, default.toString())?.construct() ?: default, get = fun() = settings.getString(prefName, default.toString()).construct() ?: default,
set = fun(value) = sharedPreferences.edit().putString(prefName, value.toString()).apply() set = fun(value) = settings.putString(prefName, value.toString())
) )
/** // LALAL
* Provide `[commit] = true` to save preferences right now, not after some unknown period of time. private fun mkDatePreference(prefName: String, default: Instant?): SharedPreference<Instant?> =
* So in case of a crash this value will be saved 100%
* */
private fun mkDatePreference(prefName: String, default: Instant?, commit: Boolean = false): SharedPreference<Instant?> =
SharedPreference( SharedPreference(
get = { get = {
val pref = sharedPreferences.getString(prefName, default?.toEpochMilliseconds()?.toString()) val nullValue = "----------------------"
pref?.let { Instant.fromEpochMilliseconds(pref.toLong()) } val pref = settings.getString(prefName, default?.toEpochMilliseconds()?.toString() ?: nullValue)
if (pref != nullValue) {
Instant.fromEpochMilliseconds(pref.toLong())
} else {
null
}
}, },
set = fun(value) = sharedPreferences.edit().putString(prefName, value?.toEpochMilliseconds()?.toString()).let { set = fun(value) = if (value?.toEpochMilliseconds() != null) settings.putString(prefName, value.toEpochMilliseconds().toString()) else settings.remove(prefName)
if (commit) it.commit() else it.apply()
}
) )
private fun <K, V> mkMapPreference(prefName: String, default: Map<K, V>, encode: (Map<K, V>) -> String, decode: (String) -> Map<K, V>, prefs: SharedPreferences = sharedPreferences): SharedPreference<Map<K,V>> = private fun <K, V> mkMapPreference(prefName: String, default: Map<K, V>, encode: (Map<K, V>) -> String, decode: (String) -> Map<K, V>, prefs: Settings = settings): SharedPreference<Map<K,V>> =
SharedPreference( SharedPreference(
get = fun() = decode(prefs.getString(prefName, encode(default))!!), get = fun() = decode(prefs.getString(prefName, encode(default))),
set = fun(value) = prefs.edit().putString(prefName, encode(value)).apply() set = fun(value) = prefs.putString(prefName, encode(value))
) )
companion object { companion object {
internal const val SHARED_PREFS_ID = "chat.simplex.app.SIMPLEX_APP_PREFS" const val SHARED_PREFS_ID = "chat.simplex.app.SIMPLEX_APP_PREFS"
internal const val SHARED_PREFS_THEMES_ID = "chat.simplex.app.THEMES" internal const val SHARED_PREFS_THEMES_ID = "chat.simplex.app.THEMES"
private const val SHARED_PREFS_AUTO_RESTART_WORKER_VERSION = "AutoRestartWorkerVersion" private const val SHARED_PREFS_AUTO_RESTART_WORKER_VERSION = "AutoRestartWorkerVersion"
private const val SHARED_PREFS_RUN_SERVICE_IN_BACKGROUND = "RunServiceInBackground" private const val SHARED_PREFS_RUN_SERVICE_IN_BACKGROUND = "RunServiceInBackground"
@ -240,7 +244,7 @@ class AppPreferences {
private const val SHARED_PREFS_PRIVACY_TRANSFER_IMAGES_INLINE = "PrivacyTransferImagesInline" private const val SHARED_PREFS_PRIVACY_TRANSFER_IMAGES_INLINE = "PrivacyTransferImagesInline"
private const val SHARED_PREFS_PRIVACY_LINK_PREVIEWS = "PrivacyLinkPreviews" private const val SHARED_PREFS_PRIVACY_LINK_PREVIEWS = "PrivacyLinkPreviews"
private const val SHARED_PREFS_PRIVACY_SIMPLEX_LINK_MODE = "PrivacySimplexLinkMode" private const val SHARED_PREFS_PRIVACY_SIMPLEX_LINK_MODE = "PrivacySimplexLinkMode"
internal const val SHARED_PREFS_PRIVACY_FULL_BACKUP = "FullBackup" const val SHARED_PREFS_PRIVACY_FULL_BACKUP = "FullBackup"
private const val SHARED_PREFS_EXPERIMENTAL_CALLS = "ExperimentalCalls" private const val SHARED_PREFS_EXPERIMENTAL_CALLS = "ExperimentalCalls"
private const val SHARED_PREFS_SHOW_UNREAD_AND_FAVORITES = "ShowUnreadAndFavorites" private const val SHARED_PREFS_SHOW_UNREAD_AND_FAVORITES = "ShowUnreadAndFavorites"
private const val SHARED_PREFS_CHAT_ARCHIVE_NAME = "ChatArchiveName" private const val SHARED_PREFS_CHAT_ARCHIVE_NAME = "ChatArchiveName"
@ -294,7 +298,6 @@ private const val MESSAGE_TIMEOUT: Int = 15_000_000
object ChatController { object ChatController {
var ctrl: ChatCtrl? = -1 var ctrl: ChatCtrl? = -1
val appPrefs: AppPreferences by lazy { AppPreferences() } val appPrefs: AppPreferences by lazy { AppPreferences() }
val ntfManager by lazy { NtfManager }
val chatModel = ChatModel val chatModel = ChatModel
private var receiverStarted = false private var receiverStarted = false
@ -328,7 +331,7 @@ object ChatController {
chatModel.userCreated.value = true chatModel.userCreated.value = true
apiSetIncognito(chatModel.incognito.value) apiSetIncognito(chatModel.incognito.value)
getUserChatData() getUserChatData()
chatModel.controller.appPrefs.chatLastStart.set(Clock.System.now()) appPrefs.chatLastStart.set(Clock.System.now())
chatModel.chatRunning.value = true chatModel.chatRunning.value = true
startReceiver() startReceiver()
Log.d(TAG, "startChat: started") Log.d(TAG, "startChat: started")
@ -940,7 +943,8 @@ object ChatController {
val r = sendCmd(CC.ApiShowMyAddress(userId)) val r = sendCmd(CC.ApiShowMyAddress(userId))
if (r is CR.UserContactLink) return r.contactLink if (r is CR.UserContactLink) return r.contactLink
if (r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorStore if (r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorStore
&& r.chatError.storeError is StoreError.UserContactLinkNotFound) { && r.chatError.storeError is StoreError.UserContactLinkNotFound
) {
return null return null
} }
Log.e(TAG, "apiGetUserAddress bad response: ${r.responseType} ${r.details}") Log.e(TAG, "apiGetUserAddress bad response: ${r.responseType} ${r.details}")
@ -952,7 +956,8 @@ object ChatController {
val r = sendCmd(CC.ApiAddressAutoAccept(userId, autoAccept)) val r = sendCmd(CC.ApiAddressAutoAccept(userId, autoAccept))
if (r is CR.UserContactLinkUpdated) return r.contactLink if (r is CR.UserContactLinkUpdated) return r.contactLink
if (r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorStore if (r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorStore
&& r.chatError.storeError is StoreError.UserContactLinkNotFound) { && r.chatError.storeError is StoreError.UserContactLinkNotFound
) {
return null return null
} }
Log.e(TAG, "userAddressAutoAccept bad response: ${r.responseType} ${r.details}") Log.e(TAG, "userAddressAutoAccept bad response: ${r.responseType} ${r.details}")
@ -1381,7 +1386,7 @@ object ChatController {
|| (mc is MsgContent.MCVoice && file.fileSize <= MAX_VOICE_SIZE_AUTO_RCV && file.fileStatus !is CIFileStatus.RcvAccepted))) { || (mc is MsgContent.MCVoice && file.fileSize <= MAX_VOICE_SIZE_AUTO_RCV && file.fileStatus !is CIFileStatus.RcvAccepted))) {
withApi { receiveFile(r.user, file.fileId) } withApi { receiveFile(r.user, file.fileId) }
} }
if (cItem.showNotification && (!isAppOnForeground || chatModel.chatId.value != cInfo.id)) { if (cItem.showNotification && (allowedToShowNotification() || chatModel.chatId.value != cInfo.id)) {
ntfManager.notifyMessageReceived(r.user, cInfo, cItem) ntfManager.notifyMessageReceived(r.user, cInfo, cItem)
} }
} }
@ -1534,7 +1539,7 @@ object ChatController {
// TODO check encryption is compatible // TODO check encryption is compatible
withCall(r, r.contact) { call -> withCall(r, r.contact) { call ->
chatModel.activeCall.value = call.copy(callState = CallState.OfferReceived, peerMedia = r.callType.media, sharedKey = r.sharedKey) chatModel.activeCall.value = call.copy(callState = CallState.OfferReceived, peerMedia = r.callType.media, sharedKey = r.sharedKey)
val useRelay = chatModel.controller.appPrefs.webrtcPolicyRelay.get() val useRelay = appPrefs.webrtcPolicyRelay.get()
val iceServers = getIceServers() val iceServers = getIceServers()
Log.d(TAG, ".callOffer iceServers $iceServers") Log.d(TAG, ".callOffer iceServers $iceServers")
chatModel.callCommand.value = WCallCommand.Offer( chatModel.callCommand.value = WCallCommand.Offer(
@ -3836,11 +3841,9 @@ sealed class ArchiveError {
@Serializable @SerialName("importFile") class ArchiveErrorImportFile(val file: String, val chatError: ChatError): ArchiveError() @Serializable @SerialName("importFile") class ArchiveErrorImportFile(val file: String, val chatError: ChatError): ArchiveError()
} }
enum class NotificationsMode(private val requiresIgnoringBatterySinceSdk: Int) {
OFF(Int.MAX_VALUE), PERIODIC(Build.VERSION_CODES.M), SERVICE(Build.VERSION_CODES.S), /*INSTANT(Int.MAX_VALUE) - for Firebase notifications */;
val requiresIgnoringBattery enum class NotificationsMode() {
get() = requiresIgnoringBatterySinceSdk <= Build.VERSION.SDK_INT OFF, PERIODIC, SERVICE, /*INSTANT - for Firebase notifications */;
companion object { companion object {
val default: NotificationsMode = SERVICE val default: NotificationsMode = SERVICE

View File

@ -0,0 +1,47 @@
package chat.simplex.common.platform
import chat.simplex.common.BuildConfigCommon
import chat.simplex.common.model.ChatController
import chat.simplex.common.ui.theme.DefaultTheme
import java.util.*
enum class AppPlatform {
ANDROID, DESKTOP;
val isAndroid: Boolean
get() = this == ANDROID
}
expect val appPlatform: AppPlatform
val appVersionInfo: Pair<String, Int?> = if (appPlatform == AppPlatform.ANDROID)
BuildConfigCommon.ANDROID_VERSION_NAME to BuildConfigCommon.ANDROID_VERSION_CODE
else
BuildConfigCommon.DESKTOP_VERSION_NAME to null
expect fun initHaskell()
class FifoQueue<E>(private var capacity: Int) : LinkedList<E>() {
override fun add(element: E): Boolean {
if(size > capacity) removeFirst()
return super.add(element)
}
}
// LALAL VERSION CODE
fun runMigrations() {
val lastMigration = ChatController.appPrefs.lastMigratedVersionCode
if (lastMigration.get() < BuildConfigCommon.ANDROID_VERSION_CODE) {
while (true) {
if (lastMigration.get() < 117) {
if (ChatController.appPrefs.currentTheme.get() == DefaultTheme.DARK.name) {
ChatController.appPrefs.currentTheme.set(DefaultTheme.SIMPLEX.name)
}
lastMigration.set(117)
} else {
lastMigration.set(BuildConfigCommon.ANDROID_VERSION_CODE)
break
}
}
}
}

View File

@ -0,0 +1,7 @@
package chat.simplex.common.platform
import androidx.compose.runtime.*
@SuppressWarnings("MissingJvmstatic")
@Composable
expect fun BackHandler(enabled: Boolean = true, onBack: () -> Unit)

View File

@ -1,11 +1,8 @@
package chat.simplex.app.platform package chat.simplex.common.platform
import android.util.Log import chat.simplex.common.model.*
import chat.simplex.app.SimplexService import chat.simplex.common.views.helpers.*
import chat.simplex.app.TAG import chat.simplex.common.views.onboarding.OnboardingStage
import chat.simplex.app.model.*
import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.onboarding.OnboardingStage
import kotlinx.serialization.decodeFromString import kotlinx.serialization.decodeFromString
// ghc's rts // ghc's rts
@ -31,7 +28,6 @@ val appPreferences: AppPreferences
val chatController: ChatController = ChatController val chatController: ChatController = ChatController
suspend fun initChatController(useKey: String? = null, confirmMigrations: MigrationConfirmation? = null, startChat: Boolean = true) { suspend fun initChatController(useKey: String? = null, confirmMigrations: MigrationConfirmation? = null, startChat: Boolean = true) {
val dbKey = useKey ?: DatabaseUtils.useDatabaseKey() val dbKey = useKey ?: DatabaseUtils.useDatabaseKey()
val dbAbsolutePathPrefix = getFilesDirectory() val dbAbsolutePathPrefix = getFilesDirectory()
@ -65,12 +61,7 @@ suspend fun initChatController(useKey: String? = null, confirmMigrations: Migrat
savedOnboardingStage savedOnboardingStage
} }
chatController.startChat(user) chatController.startChat(user)
// Prevents from showing "Enable notifications" alert when onboarding wasn't complete yet platform.androidChatInitializedAndStarted()
if (chatModel.onboardingStage.value == OnboardingStage.OnboardingComplete) {
SimplexService.showBackgroundServiceNoticeIfNeeded()
if (appPreferences.notificationsMode.get() == NotificationsMode.SERVICE.name)
SimplexService.start()
}
} }
} }
} }

View File

@ -0,0 +1,9 @@
package chat.simplex.common.platform
interface CryptorInterface {
fun decryptData(data: ByteArray, iv: ByteArray, alias: String): String?
fun encryptText(text: String, alias: String): Pair<ByteArray, ByteArray>
fun deleteKey(alias: String)
}
expect val cryptor: CryptorInterface

View File

@ -0,0 +1,80 @@
package chat.simplex.common.platform
import androidx.compose.runtime.Composable
import chat.simplex.common.model.CIFile
import chat.simplex.common.views.helpers.generalGetString
import chat.simplex.res.MR
import java.io.*
import java.net.URI
expect val dataDir: File
expect val tmpDir: File
expect val cacheDir: File
fun copyFileToFile(from: File, to: URI, finally: () -> Unit) {
try {
to.outputStream().use { stream ->
BufferedOutputStream(stream).use { outputStream ->
from.inputStream().use { it.copyTo(outputStream) }
}
}
showToast(generalGetString(MR.strings.file_saved))
} catch (e: Error) {
showToast(generalGetString(MR.strings.error_saving_file))
Log.e(TAG, "copyFileToFile error saving file $e")
} finally {
finally()
}
}
fun copyBytesToFile(bytes: ByteArrayInputStream, to: URI, finally: () -> Unit) {
try {
to.outputStream().use { stream ->
BufferedOutputStream(stream).use { outputStream ->
bytes.use { it.copyTo(outputStream) }
}
}
showToast(generalGetString(MR.strings.file_saved))
} catch (e: Error) {
showToast(generalGetString(MR.strings.error_saving_file))
Log.e(TAG, "copyBytesToFile error saving file $e")
} finally {
finally()
}
}
fun getFilesDirectory(): String {
return dataDir.absolutePath + File.separator + "files"
}
// LALAL
fun getTempFilesDirectory(): String {
return getFilesDirectory() + File.separator + "temp_files"
}
fun getAppFilesDirectory(): String {
return getFilesDirectory() + File.separator + "app_files"
}
fun getAppFilePath(fileName: String): String {
return getAppFilesDirectory() + File.separator + fileName
}
fun getLoadedFilePath(file: CIFile?): String? {
return if (file?.filePath != null && file.loaded) {
val filePath = getAppFilePath(file.filePath)
if (File(filePath).exists()) filePath else null
} else {
null
}
}
@Composable
expect fun rememberFileChooserLauncher(getContent: Boolean, onResult: (URI?) -> Unit): FileChooserLauncher
expect class FileChooserLauncher() {
suspend fun launch(input: String)
}
expect fun URI.inputStream(): InputStream?
expect fun URI.outputStream(): OutputStream

View File

@ -0,0 +1,24 @@
package chat.simplex.common.platform
import androidx.compose.ui.graphics.ImageBitmap
import boofcv.struct.image.GrayU8
import java.io.ByteArrayOutputStream
import java.io.InputStream
import java.net.URI
expect fun base64ToBitmap(base64ImageString: String): ImageBitmap
expect fun resizeImageToStrSize(image: ImageBitmap, maxDataSize: Long): String
expect fun resizeImageToDataSize(image: ImageBitmap, usePng: Boolean, maxDataSize: Long): ByteArrayOutputStream
expect fun cropToSquare(image: ImageBitmap): ImageBitmap
expect fun compressImageStr(bitmap: ImageBitmap): String
expect fun compressImageData(bitmap: ImageBitmap, usePng: Boolean): ByteArrayOutputStream
expect fun GrayU8.toImageBitmap(): ImageBitmap
expect fun ImageBitmap.addLogo(): ImageBitmap
expect fun ImageBitmap.scale(width: Int, height: Int): ImageBitmap
expect fun isImage(uri: URI): Boolean
expect fun isAnimImage(uri: URI, drawable: Any?): Boolean
expect fun loadImageBitmap(inputStream: InputStream): ImageBitmap

View File

@ -0,0 +1,10 @@
package chat.simplex.common.platform
const val TAG = "SIMPLEX"
expect object Log {
fun d(tag: String, text: String)
fun e(tag: String, text: String)
fun i(tag: String, text: String)
fun w(tag: String, text: String)
}

View File

@ -0,0 +1,13 @@
package chat.simplex.common.platform
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
expect fun Modifier.navigationBarsWithImePadding(): Modifier
@Composable
expect fun ProvideWindowInsets(
consumeWindowInsets: Boolean = true,
windowInsetsAnimationsEnabled: Boolean = true,
content: @Composable () -> Unit
)

View File

@ -0,0 +1,3 @@
package chat.simplex.common.platform
expect fun allowedToShowNotification(): Boolean

View File

@ -0,0 +1,23 @@
package chat.simplex.common.platform
import chat.simplex.common.model.*
import chat.simplex.common.views.call.RcvCallInvitation
enum class NotificationAction {
ACCEPT_CONTACT_REQUEST
}
lateinit var ntfManager: NtfManager
abstract class NtfManager {
abstract fun notifyContactConnected(user: User, contact: Contact)
abstract fun notifyContactRequestReceived(user: User, cInfo: ChatInfo.ContactRequest)
abstract fun notifyMessageReceived(user: User, cInfo: ChatInfo, cItem: ChatItem)
abstract fun notifyCallInvitation(invitation: RcvCallInvitation)
abstract fun hasNotificationsForChat(chatId: String): Boolean
abstract fun cancelNotificationsForChat(chatId: String)
abstract fun displayNotification(user: User, chatId: String, displayName: String, msgText: String, image: String? = null, actions: List<NotificationAction> = emptyList())
abstract fun createNtfChannelsMaybeShowAlert()
abstract fun cancelCallNotification()
abstract fun cancelAllNotifications()
}

View File

@ -0,0 +1,26 @@
package chat.simplex.common.platform
import chat.simplex.common.model.NotificationsMode
interface PlatformInterface {
suspend fun androidServiceStart() {}
fun androidServiceSafeStop() {}
fun androidNotificationsModeChanged(mode: NotificationsMode) {}
fun androidChatStartedAfterBeingOff() {}
fun androidChatStopped() {}
fun androidChatInitializedAndStarted() {}
}
/**
* Multiplatform project has separate directories per platform + common directory that contains directories per platform + common for all of them.
* This means that we can not call code from `android` directory via code from `common/androidMain` directory. So this is a way to do it:
* - we specify interface that should be implemented by platforms
* - platforms made its implementation by assigning it to this variable at runtime
* - common code calls this variable and everything works as expected.
*
* Functions that expected to be used on only one platform, should be prefixed with platform name, like androidSomething. It helps
* to identify it's use-case. Easy to understand that it is only needed on one specific platform. Functions without prefixes are used on
* more than one platform.
*
* See [SimplexApp] and [AppCommon.desktop] for re-assigning of this var
* */
var platform: PlatformInterface = object : PlatformInterface {}

View File

@ -0,0 +1,15 @@
package chat.simplex.common.platform
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.ui.text.TextStyle
import chat.simplex.common.views.chat.ComposeState
@Composable
expect fun PlatformTextField(
composeState: MutableState<ComposeState>,
textStyle: MutableState<TextStyle>,
showDeleteTextButton: MutableState<Boolean>,
userIsObserver: Boolean,
onMessageChange: (String) -> Unit
)

View File

@ -0,0 +1,42 @@
package chat.simplex.common.platform
import androidx.compose.runtime.MutableState
import chat.simplex.common.model.ChatItem
import kotlinx.coroutines.CoroutineScope
interface RecorderInterface {
companion object {
// Allows to stop the recorder from outside without having the recorder in a variable
var stopRecording: (() -> Unit)? = null
val extension: String = "m4a"
}
fun start(onProgressUpdate: (position: Int?, finished: Boolean) -> Unit): String
fun stop(): Int
}
expect class RecorderNative: RecorderInterface
interface AudioPlayerInterface {
fun play(
filePath: String?,
audioPlaying: MutableState<Boolean>,
progress: MutableState<Int>,
duration: MutableState<Int>,
resetOnEnd: Boolean,
)
fun stop()
fun stop(item: ChatItem)
fun stop(fileName: String?)
fun pause(audioPlaying: MutableState<Boolean>, pro: MutableState<Int>)
fun seekTo(ms: Int, pro: MutableState<Int>, filePath: String?)
fun duration(filePath: String): Int?
}
expect object AudioPlayer: AudioPlayerInterface
interface SoundPlayerInterface {
fun start(scope: CoroutineScope, sound: Boolean)
fun stop()
}
expect object SoundPlayer: SoundPlayerInterface

View File

@ -0,0 +1,31 @@
package chat.simplex.common.platform
import androidx.compose.runtime.Composable
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.Dp
import com.russhwolf.settings.Settings
import dev.icerock.moko.resources.StringResource
@Composable
expect fun font(name: String, res: String, weight: FontWeight = FontWeight.Normal, style: FontStyle = FontStyle.Normal): Font
expect fun StringResource.localized(): String
// Non-@Composable implementation
expect fun isInNightMode(): Boolean
expect val settings: Settings
expect val settingsThemes: Settings
enum class ScreenOrientation {
UNDEFINED, PORTRAIT, LANDSCAPE
}
expect fun screenOrientation(): ScreenOrientation
@Composable
expect fun screenWidth(): Dp
expect fun isRtl(text: CharSequence): Boolean

View File

@ -0,0 +1,9 @@
package chat.simplex.common.platform
import androidx.compose.ui.platform.ClipboardManager
import androidx.compose.ui.platform.UriHandler
expect fun UriHandler.sendEmail(subject: String, body: CharSequence)
expect fun ClipboardManager.shareText(text: String)
expect fun shareFile(text: String, filePath: String)

View File

@ -0,0 +1,16 @@
package chat.simplex.common.platform
import androidx.compose.runtime.*
import chat.simplex.common.views.helpers.KeyboardState
expect fun showToast(text: String, timeout: Long = 2500L)
@Composable
expect fun LockToCurrentOrientationUntilDispose()
@Composable
expect fun LocalMultiplatformView(): Any?
@Composable
expect fun getKeyboardState(): State<KeyboardState>
expect fun hideKeyboard(view: Any?)

View File

@ -0,0 +1,37 @@
package chat.simplex.common.platform
import androidx.compose.runtime.MutableState
import androidx.compose.ui.graphics.ImageBitmap
import java.net.URI
interface VideoPlayerInterface {
data class PreviewAndDuration(val preview: ImageBitmap?, val duration: Long?, val timestamp: Long)
val soundEnabled: MutableState<Boolean>
val brokenVideo: MutableState<Boolean>
val videoPlaying: MutableState<Boolean>
val progress: MutableState<Long>
val duration: MutableState<Long>
val preview: MutableState<ImageBitmap>
fun stop()
fun play(resetOnEnd: Boolean)
fun enableSound(enable: Boolean): Boolean
fun release(remove: Boolean)
}
expect class VideoPlayer: VideoPlayerInterface {
companion object {
fun getOrCreate(
uri: URI,
gallery: Boolean,
defaultPreview: ImageBitmap,
defaultDuration: Long,
soundEnabled: Boolean
): VideoPlayer
fun enableSound(enable: Boolean, fileName: String?, gallery: Boolean): Boolean
fun release(uri: URI, gallery: Boolean, remove: Boolean)
fun stopAll()
fun releaseAll()
}
}

View File

@ -1,4 +1,4 @@
package chat.simplex.app.ui.theme package chat.simplex.common.ui.theme
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color

View File

@ -1,4 +1,4 @@
package chat.simplex.app.ui.theme package chat.simplex.common.ui.theme
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Shapes import androidx.compose.material.Shapes

View File

@ -1,4 +1,4 @@
package chat.simplex.app.ui.theme package chat.simplex.common.ui.theme
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
@ -8,13 +8,13 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.* import androidx.compose.ui.graphics.*
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import chat.simplex.app.SimplexApp import chat.simplex.common.model.ChatController
import chat.simplex.app.platform.isInNightMode import chat.simplex.common.platform.isInNightMode
import chat.simplex.app.views.helpers.* import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import chat.simplex.res.MR
enum class DefaultTheme { enum class DefaultTheme {
SYSTEM, LIGHT, DARK, SIMPLEX; SYSTEM, LIGHT, DARK, SIMPLEX;
@ -264,7 +264,7 @@ fun SimpleXTheme(darkTheme: Boolean? = null, content: @Composable () -> Unit) {
} }
val systemDark = isSystemInDarkTheme() val systemDark = isSystemInDarkTheme()
LaunchedEffect(systemDark) { LaunchedEffect(systemDark) {
if (SimplexApp.context.chatModel.controller.appPrefs.currentTheme.get() == DefaultTheme.SYSTEM.name && CurrentColors.value.colors.isLight == systemDark) { if (ChatController.appPrefs.currentTheme.get() == DefaultTheme.SYSTEM.name && CurrentColors.value.colors.isLight == systemDark) {
// Change active colors from light to dark and back based on system theme // Change active colors from light to dark and back based on system theme
ThemeManager.applyTheme(DefaultTheme.SYSTEM.name, systemDark) ThemeManager.applyTheme(DefaultTheme.SYSTEM.name, systemDark)
} }

View File

@ -1,18 +1,20 @@
package chat.simplex.app.ui.theme package chat.simplex.common.ui.theme
import androidx.compose.material.Colors import androidx.compose.material.Colors
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.graphics.toArgb
import chat.simplex.app.R import androidx.compose.ui.text.font.FontFamily
import chat.simplex.app.SimplexApp
import chat.simplex.app.model.AppPreferences
import chat.simplex.app.views.helpers.generalGetString
import chat.simplex.res.MR import chat.simplex.res.MR
import chat.simplex.common.model.AppPreferences
import chat.simplex.common.model.ChatController
import chat.simplex.common.views.helpers.generalGetString
// https://github.com/rsms/inter
// I place it here because IDEA shows an error (but still works anyway) when this declaration inside Type.kt
expect val Inter: FontFamily
object ThemeManager { object ThemeManager {
private val appPrefs: AppPreferences by lazy { private val appPrefs: AppPreferences = ChatController.appPrefs
SimplexApp.context.chatModel.controller.appPrefs
}
data class ActiveTheme(val name: String, val base: DefaultTheme, val colors: Colors, val appColors: AppColors) data class ActiveTheme(val name: String, val base: DefaultTheme, val colors: Colors, val appColors: AppColors)

View File

@ -1,8 +1,8 @@
package chat.simplex.app.ui.theme package chat.simplex.common.ui.theme
import androidx.compose.material.Typography import androidx.compose.material.Typography
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.*
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
// Set of Material typography styles to start with // Set of Material typography styles to start with

View File

@ -0,0 +1,7 @@
package androidx.compose.desktop.ui.tooling.preview
@Retention(AnnotationRetention.SOURCE)
@Target(
AnnotationTarget.FUNCTION
)
annotation class Preview

View File

@ -1,4 +1,4 @@
package chat.simplex.app.views package chat.simplex.common.views
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme

View File

@ -1,7 +1,6 @@
package chat.simplex.app.views package chat.simplex.common.views
import android.content.res.Configuration import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.* import androidx.compose.foundation.*
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.* import androidx.compose.foundation.lazy.*
@ -9,20 +8,18 @@ import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import chat.simplex.app.model.* import chat.simplex.common.model.*
import chat.simplex.app.platform.shareText import chat.simplex.common.ui.theme.*
import chat.simplex.app.ui.theme.* import chat.simplex.common.views.chat.*
import chat.simplex.app.views.chat.* import chat.simplex.common.views.helpers.*
import chat.simplex.app.views.helpers.* import chat.simplex.common.model.ChatModel
import com.google.accompanist.insets.ProvideWindowInsets import chat.simplex.common.platform.*
import com.google.accompanist.insets.navigationBarsWithImePadding
@Composable @Composable
fun TerminalView(chatModel: ChatModel, close: () -> Unit) { fun TerminalView(chatModel: ChatModel, close: () -> Unit) {
@ -30,12 +27,12 @@ fun TerminalView(chatModel: ChatModel, close: () -> Unit) {
BackHandler(onBack = { BackHandler(onBack = {
close() close()
}) })
TerminalLayout( TerminalLayout(
remember { chatModel.terminalItems }, remember { chatModel.terminalItems },
composeState, composeState,
sendCommand = { sendCommand(chatModel, composeState) }, sendCommand = { sendCommand(chatModel, composeState) },
close close
) )
} }
private fun sendCommand(chatModel: ChatModel, composeState: MutableState<ComposeState>) { private fun sendCommand(chatModel: ChatModel, composeState: MutableState<ComposeState>) {
@ -118,7 +115,7 @@ fun TerminalLog(terminalItems: List<TerminalItem>) {
onDispose { lazyListState = listState.firstVisibleItemIndex to listState.firstVisibleItemScrollOffset } onDispose { lazyListState = listState.firstVisibleItemIndex to listState.firstVisibleItemScrollOffset }
} }
val reversedTerminalItems by remember { derivedStateOf { terminalItems.reversed().toList() } } val reversedTerminalItems by remember { derivedStateOf { terminalItems.reversed().toList() } }
val context = LocalContext.current val clipboard = LocalClipboardManager.current
LazyColumn(state = listState, reverseLayout = true) { LazyColumn(state = listState, reverseLayout = true) {
items(reversedTerminalItems) { item -> items(reversedTerminalItems) { item ->
Text( Text(
@ -129,7 +126,7 @@ fun TerminalLog(terminalItems: List<TerminalItem>) {
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.clickable { .clickable {
ModalManager.shared.showModal(endButtons = { ShareButton { shareText(item.details) } }) { ModalManager.shared.showModal(endButtons = { ShareButton { clipboard.shareText(item.details) } }) {
SelectionContainer(modifier = Modifier.verticalScroll(rememberScrollState())) { SelectionContainer(modifier = Modifier.verticalScroll(rememberScrollState())) {
Text(item.details, modifier = Modifier.padding(horizontal = DEFAULT_PADDING).padding(bottom = DEFAULT_PADDING)) Text(item.details, modifier = Modifier.padding(horizontal = DEFAULT_PADDING).padding(bottom = DEFAULT_PADDING))
} }
@ -140,12 +137,11 @@ fun TerminalLog(terminalItems: List<TerminalItem>) {
} }
} }
@Preview(showBackground = true) @Preview/*(
@Preview(
uiMode = Configuration.UI_MODE_NIGHT_YES, uiMode = Configuration.UI_MODE_NIGHT_YES,
showBackground = true, showBackground = true,
name = "Dark Mode" name = "Dark Mode"
) )*/
@Composable @Composable
fun PreviewTerminalLayout() { fun PreviewTerminalLayout() {
SimpleXTheme { SimpleXTheme {

View File

@ -1,4 +1,4 @@
package chat.simplex.app.views package chat.simplex.common.views
import androidx.compose.foundation.* import androidx.compose.foundation.*
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
@ -22,17 +22,14 @@ import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.style.* import androidx.compose.ui.text.style.*
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import chat.simplex.app.* import chat.simplex.common.model.ChatModel
import chat.simplex.app.R import chat.simplex.common.model.Profile
import chat.simplex.app.model.ChatModel import chat.simplex.common.platform.navigationBarsWithImePadding
import chat.simplex.app.model.Profile import chat.simplex.common.ui.theme.*
import chat.simplex.app.ui.theme.* import chat.simplex.common.views.helpers.*
import chat.simplex.app.views.helpers.* import chat.simplex.common.views.onboarding.OnboardingStage
import chat.simplex.app.views.onboarding.OnboardingStage import chat.simplex.common.views.onboarding.ReadableText
import chat.simplex.app.views.onboarding.ReadableText
import com.google.accompanist.insets.navigationBarsWithImePadding
import chat.simplex.res.MR import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.stringResource
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged

View File

@ -1,9 +1,8 @@
package chat.simplex.app.views.call package chat.simplex.common.views.call
import android.util.Log import chat.simplex.common.model.ChatModel
import chat.simplex.app.TAG import chat.simplex.common.platform.*
import chat.simplex.app.model.ChatModel import chat.simplex.common.views.helpers.withApi
import chat.simplex.app.views.helpers.withApi
import kotlinx.datetime.Clock import kotlinx.datetime.Clock
import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.minutes
@ -15,10 +14,10 @@ class CallManager(val chatModel: ChatModel) {
if (invitation.user.showNotifications) { if (invitation.user.showNotifications) {
if (Clock.System.now() - invitation.callTs <= 3.minutes) { if (Clock.System.now() - invitation.callTs <= 3.minutes) {
activeCallInvitation.value = invitation activeCallInvitation.value = invitation
controller.ntfManager.notifyCallInvitation(invitation) ntfManager.notifyCallInvitation(invitation)
} else { } else {
val contact = invitation.contact val contact = invitation.contact
controller.ntfManager.displayNotification(user = invitation.user, chatId = contact.id, displayName = contact.displayName, msgText = invitation.callTypeText) ntfManager.displayNotification(user = invitation.user, chatId = contact.id, displayName = contact.displayName, msgText = invitation.callTypeText)
} }
} }
} }
@ -62,7 +61,7 @@ class CallManager(val chatModel: ChatModel) {
callInvitations.remove(invitation.contact.id) callInvitations.remove(invitation.contact.id)
if (invitation.contact.id == activeCallInvitation.value?.contact?.id) { if (invitation.contact.id == activeCallInvitation.value?.contact?.id) {
activeCallInvitation.value = null activeCallInvitation.value = null
controller.ntfManager.cancelCallNotification() ntfManager.cancelCallNotification()
} }
} }
} }
@ -88,7 +87,7 @@ class CallManager(val chatModel: ChatModel) {
callInvitations.remove(invitation.contact.id) callInvitations.remove(invitation.contact.id)
if (invitation.contact.id == activeCallInvitation.value?.contact?.id) { if (invitation.contact.id == activeCallInvitation.value?.contact?.id) {
activeCallInvitation.value = null activeCallInvitation.value = null
controller.ntfManager.cancelCallNotification() ntfManager.cancelCallNotification()
} }
withApi { withApi {
if (!controller.apiRejectCall(invitation.contact)) { if (!controller.apiRejectCall(invitation.contact)) {
@ -101,7 +100,7 @@ class CallManager(val chatModel: ChatModel) {
fun reportCallRemoteEnded(invitation: RcvCallInvitation) { fun reportCallRemoteEnded(invitation: RcvCallInvitation) {
if (chatModel.activeCallInvitation.value?.contact?.id == invitation.contact.id) { if (chatModel.activeCallInvitation.value?.contact?.id == invitation.contact.id) {
chatModel.activeCallInvitation.value = null chatModel.activeCallInvitation.value = null
chatModel.controller.ntfManager.cancelCallNotification() ntfManager.cancelCallNotification()
} }
} }
} }

View File

@ -0,0 +1,6 @@
package chat.simplex.common.views.call
import androidx.compose.runtime.Composable
@Composable
expect fun ActiveCallView()

View File

@ -1,5 +1,6 @@
package chat.simplex.app.views.call package chat.simplex.common.views.call
import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.foundation.* import androidx.compose.foundation.*
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
@ -10,34 +11,31 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.scale import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.LocalContext
import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import chat.simplex.app.R import chat.simplex.common.model.*
import chat.simplex.app.SimplexApp import chat.simplex.common.ui.theme.*
import chat.simplex.app.model.* import chat.simplex.common.views.helpers.ProfileImage
import chat.simplex.app.ui.theme.* import chat.simplex.common.views.usersettings.ProfilePreview
import chat.simplex.app.views.helpers.ProfileImage import chat.simplex.common.platform.ntfManager
import chat.simplex.app.views.usersettings.ProfilePreview import chat.simplex.common.platform.SoundPlayer
import chat.simplex.res.MR import chat.simplex.res.MR
import kotlinx.datetime.Clock import kotlinx.datetime.Clock
@Composable @Composable
fun IncomingCallAlertView(invitation: RcvCallInvitation, chatModel: ChatModel) { fun IncomingCallAlertView(invitation: RcvCallInvitation, chatModel: ChatModel) {
val cm = chatModel.callManager val cm = chatModel.callManager
val cxt = LocalContext.current
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
LaunchedEffect(true) { SoundPlayer.shared.start(scope, sound = !chatModel.showCallView.value) } LaunchedEffect(true) { SoundPlayer.start(scope, sound = !chatModel.showCallView.value) }
DisposableEffect(true) { onDispose { SoundPlayer.shared.stop() } } DisposableEffect(true) { onDispose { SoundPlayer.stop() } }
IncomingCallAlertLayout( IncomingCallAlertLayout(
invitation, invitation,
chatModel, chatModel,
rejectCall = { cm.endCall(invitation = invitation) }, rejectCall = { cm.endCall(invitation = invitation) },
ignoreCall = { ignoreCall = {
chatModel.activeCallInvitation.value = null chatModel.activeCallInvitation.value = null
chatModel.controller.ntfManager.cancelCallNotification() ntfManager.cancelCallNotification()
}, },
acceptCall = { cm.acceptIncomingCall(invitation = invitation) } acceptCall = { cm.acceptIncomingCall(invitation = invitation) }
) )
@ -114,7 +112,7 @@ fun PreviewIncomingCallAlertLayout() {
sharedKey = null, sharedKey = null,
callTs = Clock.System.now() callTs = Clock.System.now()
), ),
chatModel = SimplexApp.context.chatModel, chatModel = ChatModel,
rejectCall = {}, rejectCall = {},
ignoreCall = {}, ignoreCall = {},
acceptCall = {} acceptCall = {}

View File

@ -1,9 +1,9 @@
package chat.simplex.app.views.call package chat.simplex.common.views.call
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import dev.icerock.moko.resources.compose.stringResource import dev.icerock.moko.resources.compose.stringResource
import chat.simplex.app.model.* import chat.simplex.common.views.helpers.generalGetString
import chat.simplex.app.views.helpers.generalGetString import chat.simplex.common.model.*
import chat.simplex.res.MR import chat.simplex.res.MR
import kotlinx.datetime.Instant import kotlinx.datetime.Instant
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName

View File

@ -1,4 +1,4 @@
package chat.simplex.app.views.chat package chat.simplex.common.views.chat
import InfoRow import InfoRow
import InfoRowEllipsis import InfoRowEllipsis
@ -8,8 +8,7 @@ import SectionItemView
import SectionSpacer import SectionSpacer
import SectionTextFooter import SectionTextFooter
import SectionView import SectionView
import android.widget.Toast import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.* import androidx.compose.foundation.*
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardActions
@ -27,15 +26,13 @@ import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import chat.simplex.app.SimplexApp import chat.simplex.common.model.*
import chat.simplex.app.model.* import chat.simplex.common.ui.theme.*
import chat.simplex.app.platform.shareText import chat.simplex.common.views.helpers.*
import chat.simplex.app.ui.theme.* import chat.simplex.common.views.newchat.QRCode
import chat.simplex.app.views.helpers.* import chat.simplex.common.views.usersettings.*
import chat.simplex.app.views.newchat.QRCode import chat.simplex.common.platform.*
import chat.simplex.app.views.usersettings.*
import chat.simplex.res.MR import chat.simplex.res.MR
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
@ -135,7 +132,7 @@ fun deleteContactDialog(chatInfo: ChatInfo, chatModel: ChatModel, close: (() ->
if (r) { if (r) {
chatModel.removeChat(chatInfo.id) chatModel.removeChat(chatInfo.id)
chatModel.chatId.value = null chatModel.chatId.value = null
chatModel.controller.ntfManager.cancelNotificationsForChat(chatInfo.id) ntfManager.cancelNotificationsForChat(chatInfo.id)
close?.invoke() close?.invoke()
} }
} }
@ -154,7 +151,7 @@ fun clearChatDialog(chatInfo: ChatInfo, chatModel: ChatModel, close: (() -> Unit
val updatedChatInfo = chatModel.controller.apiClearChat(chatInfo.chatType, chatInfo.apiId) val updatedChatInfo = chatModel.controller.apiClearChat(chatInfo.chatType, chatInfo.apiId)
if (updatedChatInfo != null) { if (updatedChatInfo != null) {
chatModel.clearChat(updatedChatInfo) chatModel.clearChat(updatedChatInfo)
chatModel.controller.ntfManager.cancelNotificationsForChat(chatInfo.id) ntfManager.cancelNotificationsForChat(chatInfo.id)
close?.invoke() close?.invoke()
} }
} }
@ -214,7 +211,8 @@ fun ChatInfoLayout(
if (contact.contactLink != null) { if (contact.contactLink != null) {
SectionView(stringResource(MR.strings.address_section_title).uppercase()) { SectionView(stringResource(MR.strings.address_section_title).uppercase()) {
QRCode(contact.contactLink, Modifier.padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF).aspectRatio(1f)) QRCode(contact.contactLink, Modifier.padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF).aspectRatio(1f))
ShareAddressButton { shareText(contact.contactLink) } val clipboard = LocalClipboardManager.current
ShareAddressButton { clipboard.shareText(contact.contactLink) }
SectionTextFooter(stringResource(MR.strings.you_can_share_this_address_with_your_contacts).format(contact.displayName)) SectionTextFooter(stringResource(MR.strings.you_can_share_this_address_with_your_contacts).format(contact.displayName))
} }
SectionDividerSpaced() SectionDividerSpaced()
@ -397,7 +395,7 @@ fun SimplexServers(text: String, servers: List<String>) {
val clipboardManager: ClipboardManager = LocalClipboardManager.current val clipboardManager: ClipboardManager = LocalClipboardManager.current
InfoRowEllipsis(text, info) { InfoRowEllipsis(text, info) {
clipboardManager.setText(AnnotatedString(servers.joinToString(separator = ","))) clipboardManager.setText(AnnotatedString(servers.joinToString(separator = ",")))
Toast.makeText(SimplexApp.context, generalGetString(MR.strings.copied), Toast.LENGTH_SHORT).show() showToast(generalGetString(MR.strings.copied))
} }
} }

View File

@ -1,4 +1,4 @@
package chat.simplex.app.views.chat package chat.simplex.common.views.chat
import InfoRow import InfoRow
import SectionBottomSpacer import SectionBottomSpacer
@ -12,20 +12,21 @@ import androidx.compose.material.Text
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.text.AnnotatedString
import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import chat.simplex.app.model.* import chat.simplex.common.model.*
import chat.simplex.app.platform.copyText import chat.simplex.common.ui.theme.CurrentColors
import chat.simplex.app.platform.shareText import chat.simplex.common.ui.theme.DEFAULT_PADDING
import chat.simplex.app.ui.theme.CurrentColors import chat.simplex.common.views.chat.item.ItemAction
import chat.simplex.app.ui.theme.DEFAULT_PADDING import chat.simplex.common.views.chat.item.MarkdownText
import chat.simplex.app.views.chat.item.ItemAction import chat.simplex.common.views.helpers.*
import chat.simplex.app.views.chat.item.MarkdownText import chat.simplex.common.platform.shareText
import chat.simplex.app.views.helpers.*
import chat.simplex.res.MR import chat.simplex.res.MR
@Composable @Composable
@ -81,13 +82,15 @@ fun ChatItemInfoView(ci: ChatItem, ciInfo: ChatItemInfo, devTools: Boolean) {
} }
} }
if (text != "") { if (text != "") {
val clipboard = LocalClipboardManager.current
DefaultDropdownMenu(showMenu) { DefaultDropdownMenu(showMenu) {
ItemAction(stringResource(MR.strings.share_verb), painterResource(MR.images.ic_share), onClick = { ItemAction(stringResource(MR.strings.share_verb), painterResource(MR.images.ic_share), onClick = {
shareText(text) clipboard.shareText(text)
showMenu.value = false showMenu.value = false
}) })
val clipboard = LocalClipboardManager.current
ItemAction(stringResource(MR.strings.copy_verb), painterResource(MR.images.ic_content_copy), onClick = { ItemAction(stringResource(MR.strings.copy_verb), painterResource(MR.images.ic_content_copy), onClick = {
copyText(text) clipboard.setText(AnnotatedString(text))
showMenu.value = false showMenu.value = false
}) })
} }

View File

@ -1,10 +1,6 @@
package chat.simplex.app.views.chat package chat.simplex.common.views.chat
import android.content.res.Configuration import androidx.compose.desktop.ui.tooling.preview.Preview
import android.graphics.Bitmap
import android.net.Uri
import android.util.Log
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.* import androidx.compose.foundation.*
import androidx.compose.foundation.gestures.* import androidx.compose.foundation.gestures.*
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
@ -26,26 +22,24 @@ import androidx.compose.ui.text.capitalize
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.intl.Locale import androidx.compose.ui.text.intl.Locale
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.unit.* import androidx.compose.ui.unit.*
import androidx.core.content.FileProvider import chat.simplex.common.model.*
import chat.simplex.app.* import chat.simplex.common.ui.theme.*
import chat.simplex.app.model.* import chat.simplex.common.views.call.*
import chat.simplex.app.platform.* import chat.simplex.common.views.chat.group.*
import chat.simplex.app.ui.theme.* import chat.simplex.common.views.chat.item.*
import chat.simplex.app.views.call.* import chat.simplex.common.views.chatlist.*
import chat.simplex.app.views.chat.group.* import chat.simplex.common.views.helpers.*
import chat.simplex.app.views.chat.item.* import chat.simplex.common.model.GroupInfo
import chat.simplex.app.views.chatlist.* import chat.simplex.common.platform.*
import chat.simplex.app.views.helpers.* import chat.simplex.common.platform.AudioPlayer
import chat.simplex.app.views.helpers.AppBarHeight
import com.google.accompanist.insets.ProvideWindowInsets
import com.google.accompanist.insets.navigationBarsWithImePadding
import chat.simplex.res.MR import chat.simplex.res.MR
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import kotlinx.datetime.Clock import kotlinx.datetime.Clock
import java.io.File import java.io.File
import java.net.URI
import kotlin.math.sign import kotlin.math.sign
@Composable @Composable
@ -101,8 +95,7 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: () -> Unit) {
.collect { activeChat.value = it } .collect { activeChat.value = it }
} }
} }
val view = LocalView.current val view = LocalMultiplatformView()
val context = LocalContext.current
if (activeChat.value == null || user == null) { if (activeChat.value == null || user == null) {
chatModel.chatId.value = null chatModel.chatId.value = null
} else { } else {
@ -114,6 +107,7 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: () -> Unit) {
chatModel.chats.firstOrNull { chat -> chat.chatInfo.id == chatId }?.chatStats?.unreadCount ?: 0 chatModel.chats.firstOrNull { chat -> chat.chatInfo.id == chatId }?.chatStats?.unreadCount ?: 0
} }
} }
val clipboard = LocalClipboardManager.current
ChatLayout( ChatLayout(
chat, chat,
@ -279,7 +273,7 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: () -> Unit) {
val ciInfo = chatModel.controller.apiGetChatItemInfo(cInfo.chatType, cInfo.apiId, cItem.id) val ciInfo = chatModel.controller.apiGetChatItemInfo(cInfo.chatType, cInfo.apiId, cItem.id)
if (ciInfo != null) { if (ciInfo != null) {
ModalManager.shared.showModal(endButtons = { ShareButton { ModalManager.shared.showModal(endButtons = { ShareButton {
shareText(itemInfoShareText(cItem, ciInfo, chatModel.controller.appPrefs.developerTools.get())) clipboard.shareText(itemInfoShareText(cItem, ciInfo, chatModel.controller.appPrefs.developerTools.get()))
} }) { } }) {
ChatItemInfoView(cItem, ciInfo, devTools = chatModel.controller.appPrefs.developerTools.get()) ChatItemInfoView(cItem, ciInfo, devTools = chatModel.controller.appPrefs.developerTools.get())
} }
@ -297,7 +291,7 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: () -> Unit) {
}, },
markRead = { range, unreadCountAfter -> markRead = { range, unreadCountAfter ->
chatModel.markChatItemsRead(chat.chatInfo, range, unreadCountAfter) chatModel.markChatItemsRead(chat.chatInfo, range, unreadCountAfter)
chatModel.controller.ntfManager.cancelNotificationsForChat(chat.id) ntfManager.cancelNotificationsForChat(chat.id)
withBGApi { withBGApi {
chatModel.controller.apiChatRead( chatModel.controller.apiChatRead(
chat.chatInfo.chatType, chat.chatInfo.chatType,
@ -422,7 +416,7 @@ fun ChatInfoToolbar(
val barButtons = arrayListOf<@Composable RowScope.() -> Unit>() val barButtons = arrayListOf<@Composable RowScope.() -> Unit>()
val menuItems = arrayListOf<@Composable () -> Unit>() val menuItems = arrayListOf<@Composable () -> Unit>()
menuItems.add { menuItems.add {
ItemAction(stringResource(MR.strings.search_verb).capitalize(Locale.current), painterResource(MR.images.ic_search), onClick = { ItemAction(stringResource(MR.strings.search_verb), painterResource(MR.images.ic_search), onClick = {
showMenu.value = false showMenu.value = false
showSearch = true showSearch = true
}) })
@ -602,8 +596,8 @@ fun BoxWithConstraintsScope.ChatItemsList(
.distinctUntilChanged() .distinctUntilChanged()
.filter { !stopListening } .filter { !stopListening }
.collect { .collect {
onComposed() onComposed()
stopListening = true stopListening = true
} }
} }
DisposableEffectOnGone( DisposableEffectOnGone(
@ -966,8 +960,8 @@ private fun markUnreadChatAsRead(activeChat: MutableState<Chat?>, chatModel: Cha
} }
sealed class ProviderMedia { sealed class ProviderMedia {
data class Image(val uri: Uri, val image: Bitmap): ProviderMedia() data class Image(val uri: URI, val image: ImageBitmap): ProviderMedia()
data class Video(val uri: Uri, val preview: String): ProviderMedia() data class Video(val uri: URI, val preview: String): ProviderMedia()
} }
private fun providerForGallery( private fun providerForGallery(
@ -1004,17 +998,17 @@ private fun providerForGallery(
val item = item(internalIndex, initialChatId)?.second ?: return null val item = item(internalIndex, initialChatId)?.second ?: return null
return when (item.content.msgContent) { return when (item.content.msgContent) {
is MsgContent.MCImage -> { is MsgContent.MCImage -> {
val imageBitmap: Bitmap? = getLoadedImage(item.file) val imageBitmap: ImageBitmap? = getLoadedImage(item.file)
val filePath = getLoadedFilePath(item.file) val filePath = getLoadedFilePath(item.file)
if (imageBitmap != null && filePath != null) { if (imageBitmap != null && filePath != null) {
val uri = FileProvider.getUriForFile(SimplexApp.context, "${BuildConfig.APPLICATION_ID}.provider", File(filePath)) val uri = getAppFileUri(filePath.substringAfterLast(File.separator))
ProviderMedia.Image(uri, imageBitmap) ProviderMedia.Image(uri, imageBitmap)
} else null } else null
} }
is MsgContent.MCVideo -> { is MsgContent.MCVideo -> {
val filePath = getLoadedFilePath(item.file) val filePath = getLoadedFilePath(item.file)
if (filePath != null) { if (filePath != null) {
val uri = FileProvider.getUriForFile(SimplexApp.context, "${BuildConfig.APPLICATION_ID}.provider", File(filePath)) val uri = getAppFileUri(filePath.substringAfterLast(File.separator))
ProviderMedia.Video(uri, (item.content.msgContent as MsgContent.MCVideo).image) ProviderMedia.Video(uri, (item.content.msgContent as MsgContent.MCVideo).image)
} else null } else null
} }
@ -1058,12 +1052,11 @@ private fun ViewConfiguration.bigTouchSlop(slop: Float = 50f) = object: ViewConf
override val touchSlop: Float get() = slop override val touchSlop: Float get() = slop
} }
@Preview(showBackground = true) @Preview/*(
@Preview(
uiMode = Configuration.UI_MODE_NIGHT_YES, uiMode = Configuration.UI_MODE_NIGHT_YES,
showBackground = true, showBackground = true,
name = "Dark Mode" name = "Dark Mode"
) )*/
@Composable @Composable
fun PreviewChatLayout() { fun PreviewChatLayout() {
SimpleXTheme { SimpleXTheme {
@ -1125,7 +1118,7 @@ fun PreviewChatLayout() {
} }
} }
@Preview(showBackground = true) @Preview
@Composable @Composable
fun PreviewGroupChatLayout() { fun PreviewGroupChatLayout() {
SimpleXTheme { SimpleXTheme {

View File

@ -1,3 +1,6 @@
package chat.simplex.common.views.chat
import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.material.* import androidx.compose.material.*
@ -7,10 +10,8 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import chat.simplex.app.R import chat.simplex.common.ui.theme.*
import chat.simplex.app.ui.theme.*
import chat.simplex.res.MR import chat.simplex.res.MR
@Composable @Composable

View File

@ -1,4 +1,4 @@
package chat.simplex.app.views.chat package chat.simplex.common.views.chat
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
@ -10,15 +10,13 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.asImageBitmap
import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import chat.simplex.app.R import chat.simplex.common.platform.base64ToBitmap
import chat.simplex.app.platform.base64ToBitmap
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.UploadContent
import chat.simplex.res.MR import chat.simplex.res.MR
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.UploadContent
@Composable @Composable
fun ComposeImageView(media: ComposePreview.MediaPreview, cancelImages: () -> Unit, cancelEnabled: Boolean) { fun ComposeImageView(media: ComposePreview.MediaPreview, cancelImages: () -> Unit, cancelEnabled: Boolean) {
@ -38,7 +36,7 @@ fun ComposeImageView(media: ComposePreview.MediaPreview, cancelImages: () -> Uni
val content = media.content[index] val content = media.content[index]
if (content is UploadContent.Video) { if (content is UploadContent.Video) {
Box(contentAlignment = Alignment.Center) { Box(contentAlignment = Alignment.Center) {
val imageBitmap = base64ToBitmap(item).asImageBitmap() val imageBitmap = base64ToBitmap(item)
Image( Image(
imageBitmap, imageBitmap,
"preview video", "preview video",
@ -53,7 +51,7 @@ fun ComposeImageView(media: ComposePreview.MediaPreview, cancelImages: () -> Uni
) )
} }
} else { } else {
val imageBitmap = base64ToBitmap(item).asImageBitmap() val imageBitmap = base64ToBitmap(item)
Image( Image(
imageBitmap, imageBitmap,
"preview image", "preview image",

View File

@ -1,11 +1,6 @@
@file:UseSerializers(UriSerializer::class) @file:UseSerializers(UriSerializer::class)
package chat.simplex.app.views.chat package chat.simplex.common.views.chat
import ComposeFileView
import ComposeVoiceView
import android.app.Activity
import android.graphics.*
import android.net.Uri
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.* import androidx.compose.material.*
@ -15,19 +10,20 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.graphics.ImageBitmap
import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import chat.simplex.app.model.* import chat.simplex.common.model.*
import chat.simplex.app.platform.* import chat.simplex.common.platform.*
import chat.simplex.app.views.chat.item.* import chat.simplex.common.views.chat.item.*
import chat.simplex.app.views.helpers.* import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR import chat.simplex.res.MR
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.serialization.* import kotlinx.serialization.*
import java.io.File import java.io.File
import java.net.URI
import java.nio.file.Files import java.nio.file.Files
@Serializable @Serializable
@ -36,7 +32,7 @@ sealed class ComposePreview {
@Serializable class CLinkPreview(val linkPreview: LinkPreview?): ComposePreview() @Serializable class CLinkPreview(val linkPreview: LinkPreview?): ComposePreview()
@Serializable class MediaPreview(val images: List<String>, val content: List<UploadContent>): ComposePreview() @Serializable class MediaPreview(val images: List<String>, val content: List<UploadContent>): ComposePreview()
@Serializable data class VoicePreview(val voice: String, val durationMs: Int, val finished: Boolean): ComposePreview() @Serializable data class VoicePreview(val voice: String, val durationMs: Int, val finished: Boolean): ComposePreview()
@Serializable class FilePreview(val fileName: String, val uri: Uri): ComposePreview() @Serializable class FilePreview(val fileName: String, val uri: URI): ComposePreview()
} }
@Serializable @Serializable
@ -151,6 +147,14 @@ fun chatItemPreview(chatItem: ChatItem): ComposePreview {
} }
} }
@Composable
expect fun AttachmentSelection(
composeState: MutableState<ComposeState>,
attachmentOption: MutableState<AttachmentOption?>,
processPickedFile: (URI?, String?) -> Unit,
processPickedMedia: (List<URI>, String?) -> Unit
)
@Composable @Composable
fun ComposeView( fun ComposeView(
chatModel: ChatModel, chatModel: ChatModel,
@ -159,7 +163,6 @@ fun ComposeView(
attachmentOption: MutableState<AttachmentOption?>, attachmentOption: MutableState<AttachmentOption?>,
showChooseAttachment: () -> Unit showChooseAttachment: () -> Unit
) { ) {
val context = LocalContext.current
val linkUrl = rememberSaveable { mutableStateOf<String?>(null) } val linkUrl = rememberSaveable { mutableStateOf<String?>(null) }
val prevLinkUrl = rememberSaveable { mutableStateOf<String?>(null) } val prevLinkUrl = rememberSaveable { mutableStateOf<String?>(null) }
val pendingLinkUrl = rememberSaveable { mutableStateOf<String?>(null) } val pendingLinkUrl = rememberSaveable { mutableStateOf<String?>(null) }
@ -168,11 +171,11 @@ fun ComposeView(
val maxFileSize = getMaxFileSize(FileProtocol.XFTP) val maxFileSize = getMaxFileSize(FileProtocol.XFTP)
val smallFont = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onBackground) val smallFont = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onBackground)
val textStyle = remember { mutableStateOf(smallFont) } val textStyle = remember { mutableStateOf(smallFont) }
val processPickedMedia = { uris: List<Uri>, text: String? -> val processPickedMedia = { uris: List<URI>, text: String? ->
val content = ArrayList<UploadContent>() val content = ArrayList<UploadContent>()
val imagesPreview = ArrayList<String>() val imagesPreview = ArrayList<String>()
uris.forEach { uri -> uris.forEach { uri ->
var bitmap: Bitmap? = null var bitmap: ImageBitmap? = null
when { when {
isImage(uri) -> { isImage(uri) -> {
// Image // Image
@ -210,7 +213,7 @@ fun ComposeView(
composeState.value = composeState.value.copy(message = text ?: composeState.value.message, preview = ComposePreview.MediaPreview(imagesPreview, content)) composeState.value = composeState.value.copy(message = text ?: composeState.value.message, preview = ComposePreview.MediaPreview(imagesPreview, content))
} }
} }
val processPickedFile = { uri: Uri?, text: String? -> val processPickedFile = { uri: URI?, text: String? ->
if (uri != null) { if (uri != null) {
val fileSize = getFileSize(uri) val fileSize = getFileSize(uri)
if (fileSize != null && fileSize <= maxFileSize) { if (fileSize != null && fileSize <= maxFileSize) {
@ -312,6 +315,8 @@ fun ComposeView(
return null return null
} }
suspend fun sendMessageAsync(text: String?, live: Boolean, ttl: Int?): ChatItem? { suspend fun sendMessageAsync(text: String?, live: Boolean, ttl: Int?): ChatItem? {
val cInfo = chat.chatInfo val cInfo = chat.chatInfo
val cs = composeState.value val cs = composeState.value
@ -402,7 +407,7 @@ fun ComposeView(
is ComposePreview.VoicePreview -> { is ComposePreview.VoicePreview -> {
val tmpFile = File(preview.voice) val tmpFile = File(preview.voice)
AudioPlayer.stop(tmpFile.absolutePath) AudioPlayer.stop(tmpFile.absolutePath)
val actualFile = File(getAppFilePath(tmpFile.name.replaceAfter(RecorderNative.extension, ""))) val actualFile = File(getAppFilePath(tmpFile.name.replaceAfter(RecorderInterface.extension, "")))
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
Files.move(tmpFile.toPath(), actualFile.toPath()) Files.move(tmpFile.toPath(), actualFile.toPath())
} }
@ -434,7 +439,7 @@ fun ComposeView(
(cs.preview is ComposePreview.MediaPreview || (cs.preview is ComposePreview.MediaPreview ||
cs.preview is ComposePreview.FilePreview || cs.preview is ComposePreview.FilePreview ||
cs.preview is ComposePreview.VoicePreview) cs.preview is ComposePreview.VoicePreview)
) { ) {
sent = send(cInfo, MsgContent.MCText(msgText), quotedItemId, null, live, ttl) sent = send(cInfo, MsgContent.MCText(msgText), quotedItemId, null, live, ttl)
} }
} }
@ -492,7 +497,7 @@ fun ComposeView(
recState.value = RecordingState.NotStarted recState.value = RecordingState.NotStarted
composeState.value = composeState.value.copy(preview = ComposePreview.NoPreview) composeState.value = composeState.value.copy(preview = ComposePreview.NoPreview)
withBGApi { withBGApi {
RecorderNative.stopRecording?.invoke() RecorderInterface.stopRecording?.invoke()
AudioPlayer.stop(filePath) AudioPlayer.stop(filePath)
filePath?.let { File(it).delete() } filePath?.let { File(it).delete() }
} }
@ -699,32 +704,26 @@ fun ComposeView(
} }
} }
val activity = LocalContext.current as Activity DisposableEffectOnGone {
DisposableEffect(Unit) { val cs = composeState.value
val orientation = activity.resources.configuration.orientation if (cs.liveMessage != null && (cs.message.isNotEmpty() || cs.liveMessage.sent)) {
onDispose { sendMessage(null)
if (orientation == activity.resources.configuration.orientation) { resetLinkPreview()
val cs = composeState.value clearCurrentDraft()
if (cs.liveMessage != null && (cs.message.isNotEmpty() || cs.liveMessage.sent)) { deleteUnusedFiles()
sendMessage(null) } else if (composeState.value.inProgress) {
resetLinkPreview() clearCurrentDraft()
clearCurrentDraft() } else if (!composeState.value.empty) {
deleteUnusedFiles() if (cs.preview is ComposePreview.VoicePreview && !cs.preview.finished) {
} else if (composeState.value.inProgress) { composeState.value = cs.copy(preview = cs.preview.copy(finished = true))
clearCurrentDraft()
} else if (!composeState.value.empty) {
if (cs.preview is ComposePreview.VoicePreview && !cs.preview.finished) {
composeState.value = cs.copy(preview = cs.preview.copy(finished = true))
}
chatModel.draft.value = composeState.value
chatModel.draftChatId.value = chat.id
} else {
clearCurrentDraft()
deleteUnusedFiles()
}
chatModel.removeLiveDummy()
} }
chatModel.draft.value = composeState.value
chatModel.draftChatId.value = chat.id
} else {
clearCurrentDraft()
deleteUnusedFiles()
} }
chatModel.removeLiveDummy()
} }
val timedMessageAllowed = remember(chat.chatInfo) { chat.chatInfo.featureEnabled(ChatFeature.TimedMessages) } val timedMessageAllowed = remember(chat.chatInfo) { chat.chatInfo.featureEnabled(ChatFeature.TimedMessages) }

View File

@ -1,4 +1,7 @@
package chat.simplex.common.views.chat
import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.Animatable
import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.material.* import androidx.compose.material.*
@ -12,13 +15,12 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import chat.simplex.app.R import chat.simplex.common.model.durationText
import chat.simplex.app.model.durationText import chat.simplex.common.ui.theme.*
import chat.simplex.app.ui.theme.* import chat.simplex.common.views.helpers.*
import chat.simplex.app.views.helpers.* import chat.simplex.common.platform.AudioPlayer
import chat.simplex.res.MR import chat.simplex.res.MR
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged

View File

@ -1,4 +1,4 @@
package chat.simplex.app.views.chat package chat.simplex.common.views.chat
import InfoRow import InfoRow
import SectionBottomSpacer import SectionBottomSpacer
@ -15,11 +15,10 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import dev.icerock.moko.resources.compose.stringResource import dev.icerock.moko.resources.compose.stringResource
import chat.simplex.app.R import chat.simplex.common.ui.theme.*
import chat.simplex.app.model.* import chat.simplex.common.views.helpers.*
import chat.simplex.app.ui.theme.* import chat.simplex.common.views.usersettings.PreferenceToggle
import chat.simplex.app.views.helpers.* import chat.simplex.common.model.*
import chat.simplex.app.views.usersettings.PreferenceToggle
import chat.simplex.res.MR import chat.simplex.res.MR
@Composable @Composable

View File

@ -1,4 +1,4 @@
package chat.simplex.app.views.chat package chat.simplex.common.views.chat
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
@ -10,12 +10,11 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import chat.simplex.app.R import chat.simplex.common.ui.theme.*
import chat.simplex.app.model.* import chat.simplex.common.views.chat.item.*
import chat.simplex.app.ui.theme.* import chat.simplex.common.model.*
import chat.simplex.app.views.chat.item.*
import chat.simplex.res.MR import chat.simplex.res.MR
import kotlinx.datetime.Clock import kotlinx.datetime.Clock

View File

@ -1,15 +1,18 @@
package chat.simplex.app.views.chat package chat.simplex.common.views.chat
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import chat.simplex.app.ui.theme.DEFAULT_PADDING import chat.simplex.common.ui.theme.DEFAULT_PADDING
import chat.simplex.app.views.helpers.* import chat.simplex.common.views.helpers.*
import chat.simplex.app.views.newchat.QRCodeScanner import chat.simplex.common.views.newchat.QRCodeScanner
import chat.simplex.res.MR import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.stringResource import dev.icerock.moko.resources.compose.stringResource
@Composable
expect fun ScanCodeView(verifyCode: (String?, cb: (Boolean) -> Unit) -> Unit, close: () -> Unit)
@Composable @Composable
fun ScanCodeLayout(verifyCode: (String?, cb: (Boolean) -> Unit) -> Unit, close: () -> Unit) { fun ScanCodeLayout(verifyCode: (String?, cb: (Boolean) -> Unit) -> Unit, close: () -> Unit) {
Column( Column(

Some files were not shown because too many files have changed in this diff Show More