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")
}
repositories {
maven("https://jitpack.io")
}
android {
compileSdkVersion(33)
@ -124,9 +120,16 @@ dependencies {
//implementation("androidx.compose.ui:ui:${rootProject.extra["compose.version"] as String}")
//implementation("androidx.compose.material:material:$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-process:2.4.1")
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.ui:ui-util:$compose_version")

View File

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

View File

@ -1,51 +1,41 @@
package chat.simplex.app
import android.app.Application
import android.content.Intent
import android.net.Uri
import android.os.*
import android.os.SystemClock.elapsedRealtime
import android.util.Log
import android.view.WindowManager
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.lifecycle.*
import chat.simplex.app.helpers.applyAppLocale
import chat.simplex.app.model.*
import chat.simplex.app.model.NtfManager
import chat.simplex.app.model.NtfManager.getUserIdFromIntent
import chat.simplex.app.platform.mainActivity
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.chatlist.*
import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.newchat.*
import chat.simplex.app.views.onboarding.*
import chat.simplex.app.views.usersettings.*
import chat.simplex.common.*
import chat.simplex.common.helpers.*
import chat.simplex.common.model.*
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.chatlist.*
import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.onboarding.*
import chat.simplex.common.platform.*
import chat.simplex.res.MR
import kotlinx.coroutines.*
import java.lang.ref.WeakReference
import java.net.URI
class MainActivity: FragmentActivity() {
private val vm by viewModels<SimplexViewModel>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mainActivity = WeakReference(this)
// testJson()
val m = vm.chatModel
applyAppLocale(m.controller.appPrefs.appLanguage)
mainActivity = WeakReference(this)
applyAppLocale(ChatModel.controller.appPrefs.appLanguage)
// When call ended and orientation changes, it re-process old intent, it's unneeded.
// Only needed to be processed on first creation of activity
if (savedInstanceState == null) {
processNotificationIntent(intent, m)
processIntent(intent, m)
processExternalIntent(intent, m)
processNotificationIntent(intent)
processIntent(intent)
processExternalIntent(intent)
}
if (m.controller.appPrefs.privacyProtectScreen.get()) {
if (ChatController.appPrefs.privacyProtectScreen.get()) {
Log.d(TAG, "onCreate: set FLAG_SECURE")
window.setFlags(
WindowManager.LayoutParams.FLAG_SECURE,
@ -54,17 +44,7 @@ class MainActivity: FragmentActivity() {
}
setContent {
SimpleXTheme {
Surface(color = MaterialTheme.colors.background) {
MainPage(
m,
AppLock.userAuthorized,
AppLock.laFailed,
AppLock.destroyedAfterBackPress,
{ AppLock.runAuthenticate() },
{ AppLock.setPerformLA(it) },
showLANotice = { AppLock.showLANotice(m.controller.appPrefs.laNoticeShown) }
)
}
AppScreen()
}
}
SimplexApp.context.schedulePeriodicServiceRestartWorker()
@ -73,22 +53,13 @@ class MainActivity: FragmentActivity() {
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
processIntent(intent, vm.chatModel)
processExternalIntent(intent, vm.chatModel)
processIntent(intent)
processExternalIntent(intent)
}
override fun onResume() {
super.onResume()
val enteredBackgroundVal = AppLock.enteredBackground.value
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()
}
}
AppLock.recheckAuthState()
}
override fun onPause() {
@ -98,13 +69,13 @@ class MainActivity: FragmentActivity() {
* recreation but receives onStop after recreation. So using both (onPause and onStop) to prevent
* unwanted multiple auth dialogs from [runAuthenticate]
* */
AppLock.enteredBackground.value = elapsedRealtime()
AppLock.appWasHidden()
}
override fun onStop() {
super.onStop()
VideoPlayer.stopAll()
AppLock.enteredBackground.value = elapsedRealtime()
AppLock.appWasHidden()
}
override fun onBackPressed() {
@ -117,7 +88,7 @@ class MainActivity: FragmentActivity() {
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
AppLock.clearAuthState()
AppLock.laFailed.value = true
@ -130,12 +101,7 @@ class MainActivity: FragmentActivity() {
}
}
class SimplexViewModel(application: Application): AndroidViewModel(application) {
val app = getApplication<SimplexApp>()
val chatModel = app.chatModel
}
fun processNotificationIntent(intent: Intent?, chatModel: ChatModel) {
fun processNotificationIntent(intent: Intent?) {
val userId = getUserIdFromIntent(intent)
when (intent?.action) {
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) {
"android.intent.action.VIEW" -> {
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) {
Intent.ACTION_SEND -> {
// Close active chat and show a list of chats
@ -204,13 +170,13 @@ fun processExternalIntent(intent: Intent?, chatModel: ChatModel) {
isMediaIntent(intent) -> {
val uri = intent.getParcelableExtra<Parcelable>(Intent.EXTRA_STREAM) as? Uri
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
}
else -> {
val uri = intent.getParcelableExtra<Parcelable>(Intent.EXTRA_STREAM) as? Uri
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) -> {
val uris = intent.getParcelableArrayListExtra<Parcelable>(Intent.EXTRA_STREAM) as? List<Uri>
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
}
else -> {}

View File

@ -1,11 +1,13 @@
package chat.simplex.app.views.helpers
package chat.simplex.app
import android.content.Context
import android.util.Log
import androidx.work.*
import chat.simplex.app.*
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 java.util.Date
import java.util.concurrent.TimeUnit

View File

@ -1,24 +1,23 @@
package chat.simplex.app
import android.app.Application
import android.net.LocalServerSocket
import android.util.Log
import chat.simplex.common.platform.Log
import androidx.lifecycle.*
import androidx.work.*
import chat.simplex.app.model.*
import chat.simplex.app.platform.*
import chat.simplex.app.ui.theme.DefaultTheme
import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.onboarding.OnboardingStage
import chat.simplex.app.model.NtfManager
import chat.simplex.common.helpers.APPLICATION_ID
import chat.simplex.common.helpers.requiresIgnoringBattery
import chat.simplex.common.model.*
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 kotlinx.coroutines.*
import kotlinx.serialization.decodeFromString
import java.io.*
import java.lang.ref.WeakReference
import java.util.*
import java.util.concurrent.Semaphore
import java.util.concurrent.TimeUnit
import kotlin.concurrent.thread
const val TAG = "SIMPLEX"
@ -26,6 +25,8 @@ class SimplexApp: Application(), LifecycleEventObserver {
val chatModel: ChatModel
get() = chatController.chatModel
val chatController: ChatController = ChatController
override fun onCreate() {
super.onCreate()
if (ProcessPhoenix.isPhoenixProcess(this)) {
@ -33,7 +34,8 @@ class SimplexApp: Application(), LifecycleEventObserver {
}
context = this
initHaskell()
context.getDir("temp", MODE_PRIVATE).deleteRecursively()
initMultiplatform()
tmpDir.deleteRecursively()
withBGApi {
initChatController()
runMigrations()
@ -77,7 +79,7 @@ class SimplexApp: Application(), LifecycleEventObserver {
* */
if (chatModel.chatRunning.value != false &&
chatModel.onboardingStage.value == OnboardingStage.OnboardingComplete &&
appPreferences.notificationsMode.get() == NotificationsMode.SERVICE.name
appPrefs.notificationsMode.get() == NotificationsMode.SERVICE
) {
SimplexService.start()
}
@ -88,12 +90,12 @@ class SimplexApp: Application(), LifecycleEventObserver {
}
fun allowToStartServiceAfterAppExit() = with(chatModel.controller) {
appPrefs.notificationsMode.get() == NotificationsMode.SERVICE.name &&
appPrefs.notificationsMode.get() == NotificationsMode.SERVICE &&
(!NotificationsMode.SERVICE.requiresIgnoringBattery || SimplexService.isIgnoringBatteryOptimizations())
}
private fun allowToStartPeriodically() = with(chatModel.controller) {
appPrefs.notificationsMode.get() == NotificationsMode.PERIODIC.name &&
appPrefs.notificationsMode.get() == NotificationsMode.PERIODIC &&
(!NotificationsMode.PERIODIC.requiresIgnoringBattery || SimplexService.isIgnoringBatteryOptimizations())
}
@ -131,4 +133,73 @@ class SimplexApp: Application(), LifecycleEventObserver {
companion object {
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.os.*
import android.provider.Settings
import android.util.Log
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import chat.simplex.common.platform.Log
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import androidx.work.*
import chat.simplex.app.model.*
import chat.simplex.app.views.helpers.*
import chat.simplex.common.AppLock
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 dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import kotlinx.coroutines.*
// based on:
// https://robertohuertas.com/2019/06/29/android_foreground_services/
@ -103,7 +108,7 @@ class SimplexService: Service() {
if (chatDbStatus != DBMigrationResult.OK) {
Log.w(chat.simplex.app.TAG, "SimplexService: problem with the database: $chatDbStatus")
showPassphraseNotification(chatDbStatus)
safeStopService(self)
safeStopService()
return@withApi
}
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
* exception related to foreground services lifecycle
* */
fun safeStopService(context: Context) {
fun safeStopService() {
if (isServiceStarted) {
context.stopService(Intent(context, SimplexService::class.java))
androidAppContext.stopService(Intent(androidAppContext, SimplexService::class.java))
} else {
stopAfterStart = true
}
@ -274,9 +279,9 @@ class SimplexService: Service() {
private suspend fun serviceAction(action: Action) {
Log.d(TAG, "SimplexService serviceAction: ${action.name}")
withContext(Dispatchers.IO) {
Intent(SimplexApp.context, SimplexService::class.java).also {
Intent(androidAppContext, SimplexService::class.java).also {
it.action = action.name
ContextCompat.startForegroundService(SimplexApp.context, it)
ContextCompat.startForegroundService(androidAppContext, it)
}
}
}
@ -350,7 +355,7 @@ class SimplexService: Service() {
fun showBackgroundServiceNoticeIfNeeded() {
val appPrefs = ChatController.appPrefs
val mode = NotificationsMode.valueOf(appPrefs.notificationsMode.get()!!)
val mode = appPrefs.notificationsMode.get()
Log.d(TAG, "showBackgroundServiceNoticeIfNeeded")
// Nothing to do if mode is OFF. Can be selected on on-boarding stage
if (mode == NotificationsMode.OFF) return
@ -371,11 +376,10 @@ class SimplexService: Service() {
if (appPrefs.backgroundServiceBatteryNoticeShown.get()) {
// users have been presented with battery notice before - they did not allow ignoring optimizations -> disable service
showDisablingServiceNotice(mode)
appPrefs.notificationsMode.set(NotificationsMode.OFF.name)
ChatModel.notificationsMode.value = NotificationsMode.OFF
SimplexService.StartReceiver.toggleReceiver(false)
appPrefs.notificationsMode.set(NotificationsMode.OFF)
StartReceiver.toggleReceiver(false)
MessagesFetcherWorker.cancelAll()
SimplexService.safeStopService(SimplexApp.context)
safeStopService()
} else {
// show battery optimization notice
showBGServiceNoticeIgnoreOptimization(mode)
@ -487,18 +491,18 @@ class SimplexService: Service() {
}
fun isIgnoringBatteryOptimizations(): Boolean {
val powerManager = SimplexApp.context.getSystemService(Application.POWER_SERVICE) as PowerManager
return powerManager.isIgnoringBatteryOptimizations(SimplexApp.context.packageName)
val powerManager = androidAppContext.getSystemService(Application.POWER_SERVICE) as PowerManager
return powerManager.isIgnoringBatteryOptimizations(androidAppContext.packageName)
}
private fun askAboutIgnoringBatteryOptimization() {
Intent().apply {
@SuppressLint("BatteryLife")
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
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
SimplexApp.context.startActivity(this)
androidAppContext.startActivity(this)
}
}
}

View File

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

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.content.Context
import android.content.Intent
import android.content.res.Configuration
import android.os.Build
import android.os.Bundle
import android.util.Log
import chat.simplex.common.platform.Log
import android.view.WindowManager
import androidx.activity.ComponentActivity
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.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
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.painter.Painter
import androidx.compose.ui.platform.LocalContext
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.res.painterResource
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.sp
import chat.simplex.app.*
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.ui.theme.*
import chat.simplex.app.views.helpers.ProfileImage
import chat.simplex.common.platform.ntfManager
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.call.*
import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.stringResource
import kotlinx.datetime.Clock
class IncomingCallActivity: ComponentActivity() {
private val vm by viewModels<SimplexViewModel>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent { IncomingCallActivityView(vm.chatModel) }
setContent { IncomingCallActivityView(ChatModel) }
unlockForIncomingCall()
}
@ -103,7 +101,7 @@ fun IncomingCallActivityView(m: ChatModel) {
) {
if (showCallView) {
Box {
ActiveCallView(m)
ActiveCallView()
if (invitation != null) IncomingCallAlertView(invitation, m)
}
} else if (invitation != null) {
@ -121,7 +119,7 @@ fun IncomingCallLockScreenAlert(invitation: RcvCallInvitation, chatModel: ChatMo
DisposableEffect(Unit) {
onDispose {
// 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(
@ -131,7 +129,7 @@ fun IncomingCallLockScreenAlert(invitation: RcvCallInvitation, chatModel: ChatMo
rejectCall = { cm.endCall(invitation = invitation) },
ignoreCall = {
chatModel.activeCallInvitation.value = null
chatModel.controller.ntfManager.cancelCallNotification()
ntfManager.cancelCallNotification()
},
acceptCall = { cm.acceptIncomingCall(invitation = invitation) },
openApp = {
@ -171,18 +169,18 @@ fun IncomingCallLockScreenAlertLayout(
Text(invitation.contact.chatViewName, style = MaterialTheme.typography.h2)
Spacer(Modifier.fillMaxHeight().weight(1f))
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))
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))
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) {
SimpleXLogo()
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)
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
private fun SimpleXLogo() {
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),
modifier = Modifier
.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,
showBackground = true
)
)*/
@Composable
fun PreviewIncomingCallLockScreenAlert() {
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()
mavenCentral()
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 {
kotlin.srcDir("./build/generated/moko/commonMain/src/")
dependencies {
api(compose.runtime)
api(compose.foundation)
@ -57,49 +56,39 @@ kotlin {
implementation(kotlin("test"))
}
}
// LALAL CHANGE TO IMPLEMENTATION
val androidMain by getting {
kotlin.srcDir("./build/generated/moko/commonMain/src/")
dependencies {
api("androidx.appcompat:appcompat:1.5.1")
api("androidx.core:core-ktx:1.9.0")
api("androidx.activity:activity-compose:1.5.0")
implementation("androidx.activity:activity-compose:1.5.0")
val work_version = "2.7.1"
api("androidx.work:work-runtime-ktx:$work_version")
api("androidx.work:work-multiprocess:$work_version")
api("com.google.accompanist:accompanist-insets:0.23.0")
api("dev.icerock.moko:resources:0.22.3")
implementation("androidx.work:work-runtime-ktx:$work_version")
implementation("com.google.accompanist:accompanist-insets:0.23.0")
implementation("dev.icerock.moko:resources:0.22.3")
// Video support
api("com.google.android.exoplayer:exoplayer:2.17.1")
implementation("com.google.android.exoplayer:exoplayer:2.17.1")
// Biometric authentication
api("androidx.biometric:biometric:1.2.0-alpha04")
implementation("androidx.biometric:biometric:1.2.0-alpha04")
//Barcode
api("org.boofcv:boofcv-android:0.40.1")
implementation("org.boofcv:boofcv-android:0.40.1")
//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
api("io.coil-kt:coil-compose:2.1.0")
api("io.coil-kt:coil-gif:2.1.0")
implementation("io.coil-kt:coil-compose: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"
api("androidx.camera:camera-core:${camerax_version}")
api("androidx.camera:camera-camera2:${camerax_version}")
api("androidx.camera:camera-lifecycle:${camerax_version}")
api("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"]}")
implementation("androidx.camera:camera-core:${camerax_version}")
implementation("androidx.camera:camera-camera2:${camerax_version}")
implementation("androidx.camera:camera-lifecycle:${camerax_version}")
implementation("androidx.camera:camera-view:${camerax_version}")
}
}
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.content.res.Configuration
import chat.simplex.app.SimplexApp
import chat.simplex.app.model.SharedPreference
import chat.simplex.app.platform.defaultLocale
import chat.simplex.common.model.SharedPreference
import chat.simplex.common.platform.androidAppContext
import chat.simplex.common.platform.defaultLocale
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) {
// val localeManager = SimplexApp.context.getSystemService(LocaleManager::class.java)
// localeManager.applicationLocales = LocaleList(Locale.forLanguageTag(languageCode ?: return))
// } else {
pref.set(languageCode)
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) {
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) }
@Suppress("DEPRECATION")
SimplexApp.context.resources.updateConfiguration(appConf, resources.displayMetrics)
androidAppContext.resources.updateConfiguration(appConf, resources.displayMetrics)
@Suppress("DEPRECATION")
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.net.Uri
import android.os.VibrationEffect
import android.os.Vibrator
import androidx.core.content.ContextCompat
import chat.simplex.app.R
import chat.simplex.app.SimplexApp
import chat.simplex.app.views.helpers.withScope
import chat.simplex.common.R
import chat.simplex.common.platform.SoundPlayerInterface
import chat.simplex.common.platform.androidAppContext
import chat.simplex.common.views.helpers.withScope
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
class SoundPlayer {
object SoundPlayer: SoundPlayerInterface {
private var player: MediaPlayer? = null
var playing = false
fun start(scope: CoroutineScope, sound: Boolean) {
override fun start(scope: CoroutineScope, sound: Boolean) {
player?.reset()
player = MediaPlayer().apply {
setAudioAttributes(
@ -25,10 +25,10 @@ class SoundPlayer {
.setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
.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()
}
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)
playing = true
withScope(scope) {
@ -40,12 +40,8 @@ class SoundPlayer {
}
}
fun stop() {
override fun stop() {
playing = false
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.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.lang.ref.WeakReference
import java.util.*
import java.util.concurrent.Semaphore
import kotlin.concurrent.thread
import kotlin.random.Random
actual val appPlatform = AppPlatform.ANDROID
var isAppOnForeground: Boolean = false
@Suppress("ConstantLocale")
val defaultLocale: Locale = Locale.getDefault()
var mainActivity: WeakReference<MainActivity> = WeakReference(null)
@SuppressLint("StaticFieldLeak")
lateinit var androidAppContext: Context
lateinit var mainActivity: WeakReference<FragmentActivity>
fun initHaskell() {
val socketName = BuildConfig.APPLICATION_ID + ".local.socket.address.listen.native.cmd2"
actual fun initHaskell() {
val socketName = "chat.simplex.app.local.socket.address.listen.native.cmd2" + Random.nextLong(100000)
val s = Semaphore(0)
thread(name="stdout/stderr pipe") {
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.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import android.util.Log
import chat.simplex.app.R
import chat.simplex.app.TAG
import chat.simplex.app.views.helpers.AlertManager
import chat.simplex.app.views.helpers.generalGetString
import chat.simplex.common.views.helpers.AlertManager
import chat.simplex.common.views.helpers.generalGetString
import chat.simplex.res.MR
import java.security.KeyStore
import javax.crypto.*
import javax.crypto.spec.GCMParameterSpec
actual val cryptor: CryptorInterface = Cryptor()
@SuppressLint("ObsoleteSdkInt")
internal class Cryptor {
internal class Cryptor: CryptorInterface {
private var keyStore: KeyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
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)
if (secretKey == null) {
if (!warningShown) {
@ -37,13 +36,13 @@ internal class Cryptor {
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)
cipher.init(Cipher.ENCRYPT_MODE, createSecretKey(alias))
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
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.BitmapFactory
import android.graphics.drawable.AnimatedImageDrawable
import android.net.Uri
import android.os.Build
import android.util.Base64
import android.util.Log
import android.webkit.MimeTypeMap
import androidx.compose.ui.graphics.*
import androidx.core.graphics.applyCanvas
import androidx.core.graphics.drawable.toBitmap
import chat.simplex.app.*
import chat.simplex.app.views.helpers.errorBitmap
import chat.simplex.app.views.helpers.getFileName
import androidx.core.graphics.scale
import boofcv.android.ConvertBitmap
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.InputStream
import java.net.URI
import kotlin.math.min
import kotlin.math.sqrt
fun base64ToBitmap(base64ImageString: String): Bitmap {
actual fun base64ToBitmap(base64ImageString: String): ImageBitmap {
val imageString = base64ImageString
.removePrefix("data:image/png;base64,")
.removePrefix("data:image/jpg;base64,")
try {
return try {
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) {
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 str = compressImageStr(img)
while (str.length > maxDataSize) {
@ -38,14 +42,14 @@ fun resizeImageToStrSize(image: Bitmap, maxDataSize: Long): String {
val clippedRatio = min(ratio, 2.0)
val width = (img.width.toDouble() / clippedRatio).toInt()
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)
}
return str
}
// 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 yOffset = 0
val side = min(image.height, image.width)
@ -54,22 +58,22 @@ fun cropToSquare(image: Bitmap): Bitmap {
} else {
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 {
val usePng = bitmap.hasAlpha()
actual fun compressImageStr(bitmap: ImageBitmap): String {
val usePng = bitmap.hasAlpha
val ext = if (usePng) "png" else "jpg"
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()
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
}
fun resizeImageToDataSize(image: Bitmap, usePng: Boolean, maxDataSize: Long): ByteArrayOutputStream {
actual fun resizeImageToDataSize(image: ImageBitmap, usePng: Boolean, maxDataSize: Long): ByteArrayOutputStream {
var img = image
var stream = compressImageData(img, usePng)
while (stream.size() > maxDataSize) {
@ -77,30 +81,36 @@ fun resizeImageToDataSize(image: Bitmap, usePng: Boolean, maxDataSize: Long): By
val clippedRatio = min(ratio, 2.0)
val width = (img.width.toDouble() / clippedRatio).toInt()
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)
}
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 paint = android.graphics.Paint()
paint.color = android.graphics.Color.WHITE
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()
translate((width - logoSize) / 2f, (height - logoSize) / 2f)
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
fun isAnimImage(uri: Uri, drawable: Any?): Boolean {
actual fun isAnimImage(uri: URI, drawable: Any?): Boolean {
val isAnimNewApi = Build.VERSION.SDK_INT >= 28 && drawable is AnimatedImageDrawable
val isAnimOldApi = Build.VERSION.SDK_INT < 28 &&
(getFileName(uri)?.endsWith(".gif") == true || getFileName(uri)?.endsWith(".webp") == true)
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.content.Context
@ -27,20 +27,22 @@ import androidx.core.view.inputmethod.EditorInfoCompat
import androidx.core.view.inputmethod.InputConnectionCompat
import androidx.core.widget.doAfterTextChanged
import androidx.core.widget.doOnTextChanged
import chat.simplex.app.*
import chat.simplex.app.R
import chat.simplex.app.model.ChatModel
import chat.simplex.app.ui.theme.CurrentColors
import chat.simplex.app.views.chat.*
import chat.simplex.app.views.helpers.SharedContent
import chat.simplex.app.views.helpers.generalGetString
import chat.simplex.common.*
import chat.simplex.common.R
import chat.simplex.common.helpers.toURI
import chat.simplex.common.model.ChatModel
import chat.simplex.common.ui.theme.CurrentColors
import chat.simplex.common.views.chat.*
import chat.simplex.common.views.helpers.SharedContent
import chat.simplex.common.views.helpers.generalGetString
import chat.simplex.res.MR
import dev.icerock.moko.resources.StringResource
import kotlinx.coroutines.delay
import java.lang.reflect.Field
import java.net.URI
@Composable
fun NativeKeyboard(
actual fun PlatformTextField(
composeState: MutableState<ComposeState>,
textStyle: MutableState<TextStyle>,
showDeleteTextButton: MutableState<Boolean>,
@ -85,7 +87,7 @@ fun NativeKeyboard(
} catch (e: Exception) {
return@OnCommitContentListener false
}
ChatModel.sharedContent.value = SharedContent.Media("", listOf(inputContentInfo.contentUri))
ChatModel.sharedContent.value = SharedContent.Media("", listOf(inputContentInfo.contentUri.toURI()))
true
}
return InputConnectionCompat.createWrapper(connection, editorInfo, onCommit)
@ -96,7 +98,7 @@ fun NativeKeyboard(
editText.inputType = InputType.TYPE_TEXT_FLAG_CAP_SENTENCES or editText.inputType
editText.setTextColor(textColor.toArgb())
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())
editText.background = drawable
editText.setPadding(paddingStart, paddingTop, paddingEnd, paddingBottom)
@ -134,7 +136,7 @@ fun NativeKeyboard(
}
if (showKeyboard) {
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)
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.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_FILESIZE_REACHED
import android.os.Build
import android.util.Log
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.common.model.ChatItem
import chat.simplex.common.platform.AudioPlayer.duration
import chat.simplex.common.views.helpers.*
import kotlinx.coroutines.*
import java.io.*
interface Recorder {
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"
}
actual class RecorderNative: RecorderInterface {
private var recorder: MediaRecorder? = null
private var progressJob: Job? = null
private var filePath: String? = null
private var recStartedAt: Long? = null
private fun initRecorder() =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
MediaRecorder(SimplexApp.context)
MediaRecorder(androidAppContext)
} else {
MediaRecorder()
}
@ -51,8 +39,7 @@ class RecorderNative(): Recorder {
rec.setAudioSamplingRate(16000)
rec.setAudioEncodingBitRate(32000)
rec.setMaxDuration(MAX_VOICE_MILLIS_FOR_SENDING)
val tmpDir = SimplexApp.context.getDir("temp", Application.MODE_PRIVATE)
val fileToSave = File.createTempFile(generateNewFileName("voice", "${extension}_"), ".tmp", tmpDir)
val fileToSave = File.createTempFile(generateNewFileName("voice", "${RecorderInterface.extension}_"), ".tmp", tmpDir)
fileToSave.deleteOnExit()
val path = fileToSave.absolutePath
filePath = path
@ -75,13 +62,13 @@ class RecorderNative(): Recorder {
stop()
}
}
stopRecording = { stop() }
RecorderInterface.stopRecording = { stop() }
return path
}
override fun stop(): Int {
val path = filePath ?: return 0
stopRecording = null
RecorderInterface.stopRecording = null
runCatching {
recorder?.stop()
}
@ -110,7 +97,7 @@ class RecorderNative(): Recorder {
private fun realDuration(path: String): Int? = duration(path) ?: progress()
}
object AudioPlayer {
actual object AudioPlayer: AudioPlayerInterface {
private val player = MediaPlayer().apply {
setAudioAttributes(
AudioAttributes.Builder()
@ -118,13 +105,13 @@ object AudioPlayer {
.setUsage(AudioAttributes.USAGE_MEDIA)
.build()
)
(SimplexApp.context.getSystemService(Context.AUDIO_SERVICE) as AudioManager)
(androidAppContext.getSystemService(Context.AUDIO_SERVICE) as AudioManager)
.registerAudioPlaybackCallback(object: AudioPlaybackCallback() {
override fun onPlaybackConfigChanged(configs: MutableList<AudioPlaybackConfiguration>?) {
if (configs?.any { it.audioAttributes.usage == AudioAttributes.USAGE_VOICE_COMMUNICATION } == true) {
// In a process of making a call
RecorderNative.stopRecording?.invoke()
stop()
RecorderInterface.stopRecording?.invoke()
AudioPlayer.stop()
}
super.onPlaybackConfigChanged(configs)
}
@ -154,7 +141,7 @@ object AudioPlayer {
}
VideoPlayer.stopAll()
RecorderNative.stopRecording?.invoke()
RecorderInterface.stopRecording?.invoke()
val current = currentlyPlaying.value
if (current == null || current.first != filePath) {
stopListener()
@ -208,16 +195,16 @@ object AudioPlayer {
return player.currentPosition
}
fun stop() {
override fun stop() {
if (currentlyPlaying.value == null) return
player.stop()
stopListener()
}
fun stop(item: ChatItem) = stop(item.file?.fileName)
override fun stop(item: ChatItem) = stop(item.file?.fileName)
// FileName or filePath are ok
fun stop(fileName: String?) {
override fun stop(fileName: String?) {
if (fileName != null && currentlyPlaying.value?.first?.endsWith(fileName) == true) {
stop()
}
@ -241,7 +228,7 @@ object AudioPlayer {
progressJob = null
}
fun play(
override fun play(
filePath: String?,
audioPlaying: MutableState<Boolean>,
progress: MutableState<Int>,
@ -269,19 +256,19 @@ object AudioPlayer {
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()
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
if (this.currentlyPlaying.value?.first == filePath) {
if (currentlyPlaying.value?.first == filePath) {
player.seekTo(ms)
}
}
fun duration(filePath: String): Int? {
override fun duration(filePath: String): Int? {
var res: Int? = null
kotlin.runCatching {
helperPlayer.setDataSource(filePath)
@ -294,3 +281,5 @@ object AudioPlayer {
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.content.*
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
import android.net.Uri
import android.provider.MediaStore
import android.util.Log
import android.webkit.MimeTypeMap
import android.widget.Toast
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import chat.simplex.app.*
import chat.simplex.app.model.CIFile
import chat.simplex.app.views.helpers.generalGetString
import chat.simplex.res.MR
import androidx.compose.ui.platform.ClipboardManager
import androidx.compose.ui.platform.UriHandler
import chat.simplex.common.helpers.toUri
import chat.simplex.common.model.CIFile
import chat.simplex.common.views.helpers.generalGetString
import chat.simplex.common.views.helpers.getAppFileUri
import java.io.BufferedOutputStream
import java.io.File
import chat.simplex.res.MR
fun shareText(text: String) {
actual fun ClipboardManager.shareText(text: String) {
val sendIntent: Intent = Intent().apply {
action = Intent.ACTION_SEND
putExtra(Intent.EXTRA_TEXT, text)
type = "text/plain"
flags = FLAG_ACTIVITY_NEW_TASK
}
val shareIntent = Intent.createChooser(sendIntent, null)
// This flag is needed when you start a new activity from non-Activity context
shareIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
SimplexApp.context.startActivity(shareIntent)
shareIntent.addFlags(FLAG_ACTIVITY_NEW_TASK)
androidAppContext.startActivity(shareIntent)
}
fun shareFile(text: String, filePath: String) {
val uri = FileProvider.getUriForFile(SimplexApp.context, "${BuildConfig.APPLICATION_ID}.provider", File(filePath))
actual fun shareFile(text: String, filePath: String) {
val uri = getAppFileUri(filePath.substringAfterLast(File.separator))
val ext = filePath.substringAfterLast(".")
val mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext) ?: return
val sendIntent: Intent = Intent().apply {
@ -37,28 +37,22 @@ fun shareFile(text: String, filePath: String) {
/*if (text.isNotEmpty()) {
putExtra(Intent.EXTRA_TEXT, text)
}*/
putExtra(Intent.EXTRA_STREAM, uri)
putExtra(Intent.EXTRA_STREAM, uri.toUri())
type = mimeType
flags = Intent.FLAG_ACTIVITY_NEW_TASK
}
val shareIntent = Intent.createChooser(sendIntent, null)
// This flag is needed when you start a new activity from non-Activity context
shareIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
SimplexApp.context.startActivity(shareIntent)
shareIntent.addFlags(FLAG_ACTIVITY_NEW_TASK)
androidAppContext.startActivity(shareIntent)
}
fun copyText(text: String) {
val clipboard = ContextCompat.getSystemService(SimplexApp.context, ClipboardManager::class.java)
clipboard?.setPrimaryClip(ClipData.newPlainText("text", text))
}
fun sendEmail(subject: String, body: CharSequence) {
actual fun UriHandler.sendEmail(subject: String, body: CharSequence) {
val emailIntent = Intent(Intent.ACTION_SENDTO, Uri.parse("mailto:"))
emailIntent.putExtra(Intent.EXTRA_SUBJECT, subject)
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)
try {
SimplexApp.context.startActivity(emailIntent)
androidAppContext.startActivity(emailIntent)
} catch (e: ActivityNotFoundException) {
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] */
fun saveImage(ciFile: CIFile?) {
val cxt = SimplexApp.context
val filePath = getLoadedFilePath(ciFile)
val fileName = ciFile?.fileName
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.MediaColumns.DISPLAY_NAME, 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 {
cxt.contentResolver.openOutputStream(uri)?.let { stream ->
androidAppContext.contentResolver.openOutputStream(uri)?.let { stream ->
val outputStream = BufferedOutputStream(stream)
File(filePath).inputStream().use { it.copyTo(outputStream) }
outputStream.close()
Toast.makeText(cxt, generalGetString(MR.strings.image_saved), Toast.LENGTH_SHORT).show()
showToast(generalGetString(MR.strings.image_saved))
}
}
} 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.content.Context
@ -7,17 +7,19 @@ import android.graphics.Rect
import android.os.Build
import android.view.*
import android.view.inputmethod.InputMethodManager
import android.widget.Toast
import androidx.compose.runtime.*
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import chat.simplex.app.SimplexApp
import chat.simplex.app.views.helpers.KeyboardState
import chat.simplex.common.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
fun LockToCurrentOrientationUntilDispose() {
val context = LocalContext.current
actual fun LockToCurrentOrientationUntilDispose() {
val context = LocalContext1.current
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 rotation = if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) manager.defaultDisplay.rotation else activity.display?.rotation
activity.requestedOrientation = when (rotation) {
@ -32,7 +34,10 @@ fun LockToCurrentOrientationUntilDispose() {
}
@Composable
fun getKeyboardState(): State<KeyboardState> {
actual fun LocalMultiplatformView(): Any? = LocalView.current
@Composable
actual fun getKeyboardState(): State<KeyboardState> {
val keyboardState = remember { mutableStateOf(KeyboardState.Closed) }
val view = LocalView.current
DisposableEffect(view) {
@ -57,5 +62,10 @@ fun getKeyboardState(): State<KeyboardState> {
return keyboardState
}
fun hideKeyboard(view: View) =
(SimplexApp.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager).hideSoftInputFromWindow(view.windowToken, 0)
actual fun hideKeyboard(view: Any?) {
// 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.net.Uri
import android.util.Log
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import chat.simplex.app.*
import chat.simplex.app.R
import androidx.compose.ui.graphics.ImageBitmap
import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR
import com.google.android.exoplayer2.*
import com.google.android.exoplayer2.C.*
import com.google.android.exoplayer2.audio.AudioAttributes
import com.google.android.exoplayer2.source.ProgressiveMediaSource
import com.google.android.exoplayer2.upstream.DefaultDataSource
import com.google.android.exoplayer2.upstream.DefaultHttpDataSource
import chat.simplex.res.MR
import kotlinx.coroutines.*
import java.io.File
import java.net.URI
class VideoPlayer private constructor(
private val uri: Uri,
actual class VideoPlayer private constructor(
private val uri: URI,
private val gallery: Boolean,
private val defaultPreview: Bitmap,
private val defaultPreview: ImageBitmap,
defaultDuration: Long,
soundEnabled: Boolean,
) {
companion object {
private val players: MutableMap<Pair<Uri, Boolean>, VideoPlayer> = mutableMapOf()
private val previewsAndDurations: MutableMap<Uri, PreviewAndDuration> = mutableMapOf()
soundEnabled: Boolean
): VideoPlayerInterface {
actual companion object {
private val players: MutableMap<Pair<URI, Boolean>, VideoPlayer> = mutableMapOf()
private val previewsAndDurations: MutableMap<URI, VideoPlayerInterface.PreviewAndDuration> = mutableMapOf()
fun getOrCreate(
uri: Uri,
actual fun getOrCreate(
uri: URI,
gallery: Boolean,
defaultPreview: Bitmap,
defaultPreview: ImageBitmap,
defaultDuration: Long,
soundEnabled: Boolean,
soundEnabled: Boolean
): VideoPlayer =
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
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 }
}
fun release(uri: Uri, gallery: Boolean, remove: Boolean) =
player(uri.path, gallery)?.release(remove)
actual fun release(uri: URI, gallery: Boolean, remove: Boolean) =
player(uri.path, gallery)?.release(remove).run { }
fun stopAll() {
actual fun stopAll() {
players.values.forEach { it.stop() }
}
fun releaseAll() {
actual fun releaseAll() {
players.values.forEach { it.release(false) }
players.clear()
previewsAndDurations.clear()
}
}
data class PreviewAndDuration(val preview: Bitmap?, val duration: Long?, val timestamp: Long)
private val currentVolume: Float
val soundEnabled: MutableState<Boolean> = mutableStateOf(soundEnabled)
val brokenVideo: MutableState<Boolean> = mutableStateOf(false)
val videoPlaying: MutableState<Boolean> = mutableStateOf(false)
val progress: MutableState<Long> = mutableStateOf(0L)
val duration: MutableState<Long> = mutableStateOf(defaultDuration)
val preview: MutableState<Bitmap> = mutableStateOf(defaultPreview)
override val soundEnabled: MutableState<Boolean> = mutableStateOf(soundEnabled)
override val brokenVideo: MutableState<Boolean> = mutableStateOf(false)
override val videoPlaying: MutableState<Boolean> = mutableStateOf(false)
override val progress: MutableState<Long> = mutableStateOf(0L)
override val duration: MutableState<Long> = mutableStateOf(defaultDuration)
override val preview: MutableState<ImageBitmap> = mutableStateOf(defaultPreview)
init {
setPreviewAndDuration()
}
val player = ExoPlayer.Builder(SimplexApp.context,
DefaultRenderersFactory(SimplexApp.context))
val player = ExoPlayer.Builder(androidAppContext,
DefaultRenderersFactory(androidAppContext))
/*.setLoadControl(DefaultLoadControl.Builder()
.setPrioritizeTimeOverSizeThresholds(false) // Could probably save some megabytes in memory in case it will be needed
.createDefaultLoadControl())*/
@ -84,20 +80,20 @@ class VideoPlayer private constructor(
.setSeekForwardIncrementMs(10_000)
.build()
.apply {
// Repeat the same track endlessly
repeatMode = Player.REPEAT_MODE_ONE
currentVolume = volume
if (!soundEnabled) {
volume = 0f
// Repeat the same track endlessly
repeatMode = Player.REPEAT_MODE_ONE
currentVolume = volume
if (!soundEnabled) {
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 var progressJob: Job? = null
@ -115,14 +111,14 @@ class VideoPlayer private constructor(
}
if (soundEnabled.value) {
RecorderNative.stopRecording?.invoke()
RecorderInterface.stopRecording?.invoke()
}
AudioPlayer.stop()
stopAll()
if (listener.value == null) {
runCatching {
val dataSourceFactory = DefaultDataSource.Factory(SimplexApp.context, DefaultHttpDataSource.Factory())
val source = ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(MediaItem.fromUri(uri))
val dataSourceFactory = DefaultDataSource.Factory(androidAppContext, DefaultHttpDataSource.Factory())
val source = ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(MediaItem.fromUri(Uri.parse(uri.toString())))
player.setMediaSource(source, seek ?: 0L)
}.onFailure {
Log.e(TAG, it.stackTraceToString())
@ -170,14 +166,14 @@ class VideoPlayer private constructor(
override fun onIsPlayingChanged(isPlaying: Boolean) {
super.onIsPlayingChanged(isPlaying)
// Produce non-ideal transition from stopped to playing state while showing preview image in ChatView
// videoPlaying.value = isPlaying
// videoPlaying.value = isPlaying
}
})
return true
}
fun stop() {
override fun stop() {
player.stop()
stopListener()
}
@ -199,7 +195,7 @@ class VideoPlayer private constructor(
progressJob = null
}
fun play(resetOnEnd: Boolean) {
override fun play(resetOnEnd: Boolean) {
if (progress.value == duration.value) {
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
soundEnabled.value = enable
player.volume = if (enable) currentVolume else 0f
return true
}
fun release(remove: Boolean) {
override fun release(remove: Boolean) {
player.release()
if (remove) {
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 chat.simplex.res.MR
// https://github.com/rsms/inter
val Inter: FontFamily = FontFamily(
actual val Inter: FontFamily = FontFamily(
Font(MR.fonts.Inter.regular.fontResourceId),
Font(MR.fonts.Inter.italic.fontResourceId, style = FontStyle.Italic),
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.annotation.SuppressLint
@ -9,10 +9,9 @@ import android.media.*
import android.os.Build
import android.os.PowerManager
import android.os.PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK
import android.util.Log
import android.view.ViewGroup
import android.webkit.*
import androidx.activity.compose.BackHandler
import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
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.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.webkit.WebViewAssetLoader
import androidx.webkit.WebViewClientCompat
import chat.simplex.app.*
import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.ProfileImage
import chat.simplex.app.views.helpers.withApi
import com.google.accompanist.permissions.rememberMultiplePermissionsState
import chat.simplex.common.model.*
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.ProfileImage
import chat.simplex.common.views.helpers.withApi
import chat.simplex.common.model.ChatModel
import chat.simplex.common.model.Contact
import chat.simplex.common.platform.*
import chat.simplex.res.MR
import com.google.accompanist.permissions.rememberMultiplePermissionsState
import dev.icerock.moko.resources.StringResource
import kotlinx.coroutines.*
import kotlinx.serialization.decodeFromString
@ -47,20 +47,21 @@ import kotlinx.serialization.encodeToString
@SuppressLint("SourceLockedOrientationActivity")
@Composable
fun ActiveCallView(chatModel: ChatModel) {
actual fun ActiveCallView() {
val chatModel = ChatModel
BackHandler(onBack = {
val call = chatModel.activeCall.value
if (call != null) withApi { chatModel.callManager.endCall(call) }
})
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) {
// 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
if (!ntfModeService) SimplexService.start()
if (!ntfModeService) platform.androidServiceStart()
}
DisposableEffect(Unit) {
val am = SimplexApp.context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
val am = androidAppContext.getSystemService(Context.AUDIO_SERVICE) as AudioManager
var btDeviceCount = 0
val audioCallback = object: AudioDeviceCallback() {
override fun onAudioDevicesAdded(addedDevices: Array<out AudioDeviceInfo>) {
@ -87,16 +88,16 @@ fun ActiveCallView(chatModel: ChatModel) {
}
}
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)) {
pm.newWakeLock(PROXIMITY_SCREEN_OFF_WAKE_LOCK, SimplexApp.context.packageName + ":proximityLock")
pm.newWakeLock(PROXIMITY_SCREEN_OFF_WAKE_LOCK, androidAppContext.packageName + ":proximityLock")
} else {
null
}
proximityLock?.acquire()
onDispose {
// Stop it when call ended
if (!ntfModeService) SimplexService.safeStopService(SimplexApp.context)
if (!ntfModeService) platform.androidServiceSafeStop()
dropAudioManagerOverrides()
am.unregisterAudioDeviceCallback(audioCallback)
proximityLock?.release()
@ -215,7 +216,7 @@ private fun ActiveCallOverlay(call: Call, chatModel: ChatModel, audioViaBluetoot
}
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")
am.mode = AudioManager.MODE_IN_COMMUNICATION
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
@ -241,7 +242,7 @@ private fun setCallSound(speaker: Boolean, audioViaBluetooth: MutableState<Boole
}
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
// Clear selected communication device to default value after we changed it in call
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
@ -351,7 +352,7 @@ fun CallInfoView(call: Call, alignment: Alignment.Horizontal) {
InfoText(call.callState.text)
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})"
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.content.ActivityNotFoundException
import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.net.Uri
import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.compose.runtime.*
import androidx.compose.ui.graphics.ImageBitmap
import androidx.core.content.ContextCompat
import chat.simplex.app.SimplexApp
import chat.simplex.app.platform.resizeImageToStrSize
import chat.simplex.app.views.helpers.*
import chat.simplex.common.helpers.toURI
import chat.simplex.common.platform.*
import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR
import java.net.URI
@Composable
fun AttachmentSelection(
actual fun AttachmentSelection(
composeState: MutableState<ComposeState>,
attachmentOption: MutableState<AttachmentOption?>,
processPickedFile: (Uri?, String?) -> Unit,
processPickedMedia: (List<Uri>, String?) -> Unit
processPickedFile: (URI?, String?) -> Unit,
processPickedMedia: (List<URI>, String?) -> Unit
) {
val cameraLauncher = rememberCameraLauncher { uri: Uri? ->
if (uri != null) {
val bitmap: Bitmap? = getBitmapFromUri(uri)
val bitmap: ImageBitmap? = getBitmapFromUri(uri.toURI())
if (bitmap != null) {
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) {
cameraLauncher.launchWithFallback()
} 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 galleryImageLauncherFallback = rememberGetMultipleContentsLauncher { processPickedMedia(it, null) }
val galleryVideoLauncher = rememberLauncherForActivityResult(contract = PickMultipleVideosFromGallery()) { processPickedMedia(it, null) }
val galleryVideoLauncherFallback = rememberGetMultipleContentsLauncher { processPickedMedia(it, null) }
val filesLauncher = rememberGetContentLauncher { processPickedFile(it, null) }
val galleryImageLauncher = rememberLauncherForActivityResult(contract = PickMultipleImagesFromGallery()) { processPickedMedia(it.map { it.toURI() }, null) }
val galleryImageLauncherFallback = rememberGetMultipleContentsLauncher { processPickedMedia(it.map { it.toURI() }, null) }
val galleryVideoLauncher = rememberLauncherForActivityResult(contract = PickMultipleVideosFromGallery()) { processPickedMedia(it.map { it.toURI() }, null) }
val galleryVideoLauncherFallback = rememberGetMultipleContentsLauncher { processPickedMedia(it.map { it.toURI() }, null) }
val filesLauncher = rememberGetContentLauncher { processPickedFile(it?.toURI(), null) }
LaunchedEffect(attachmentOption.value) {
when (attachmentOption.value) {
AttachmentOption.CameraPhoto -> {
when (PackageManager.PERMISSION_GRANTED) {
ContextCompat.checkSelfPermission(SimplexApp.context, Manifest.permission.CAMERA) -> {
ContextCompat.checkSelfPermission(androidAppContext, Manifest.permission.CAMERA) -> {
cameraLauncher.launchWithFallback()
}
else -> {

View File

@ -1,12 +1,13 @@
package chat.simplex.app.views.chat
package chat.simplex.common.views.chat
import android.Manifest
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import chat.simplex.common.views.chat.ScanCodeLayout
import com.google.accompanist.permissions.rememberPermissionState
@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)
LaunchedEffect(Unit) {
cameraPermissionState.launchPermissionRequest()

View File

@ -1,17 +1,17 @@
package chat.simplex.app.views.chat
package chat.simplex.common.views.chat
import android.Manifest
import androidx.compose.runtime.Composable
import com.google.accompanist.permissions.rememberMultiplePermissionsState
@Composable
fun allowedToRecordVoiceByPlatform(): Boolean {
actual fun allowedToRecordVoiceByPlatform(): Boolean {
val permissionsState = rememberMultiplePermissionsState(listOf(Manifest.permission.RECORD_AUDIO))
return permissionsState.allPermissionsGranted
}
@Composable
fun VoiceButtonWithoutPermissionByPlatform() {
actual fun VoiceButtonWithoutPermissionByPlatform() {
val permissionsState = rememberMultiplePermissionsState(listOf(Manifest.permission.RECORD_AUDIO))
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
import androidx.compose.runtime.*
import androidx.compose.ui.graphics.*
import android.os.Build.VERSION.SDK_INT
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.painter.BitmapPainter
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.*
import chat.simplex.app.*
import chat.simplex.app.model.*
import chat.simplex.app.platform.getLoadedFilePath
import chat.simplex.app.platform.hideKeyboard
import chat.simplex.app.views.helpers.*
import androidx.compose.ui.platform.LocalContext
import chat.simplex.common.helpers.toUri
import chat.simplex.common.model.CIFile
import chat.simplex.common.platform.*
import chat.simplex.common.views.helpers.ModalManager
import coil.ImageLoader
import coil.compose.rememberAsyncImagePainter
import coil.decode.GifDecoder
import coil.decode.ImageDecoderDecoder
import coil.request.ImageRequest
import java.net.URI
@Composable
fun SimpleAndAnimatedImageView(
uri: Uri,
actual fun SimpleAndAnimatedImageView(
uri: URI,
imageBitmap: ImageBitmap,
file: CIFile?,
imageProvider: () -> ImageGalleryProvider,
ImageView: @Composable (painter: Painter, onClick: () -> Unit) -> Unit
) {
val context = LocalContext.current
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
imageLoader = imageLoader
)
val view = LocalView.current
val view = LocalMultiplatformView()
ImageView(imagePainter) {
hideKeyboard(view)
if (getLoadedFilePath(file) != null) {
@ -42,9 +42,9 @@ fun SimpleAndAnimatedImageView(
}
}
private val imageLoader = ImageLoader.Builder(SimplexApp.context)
private val imageLoader = ImageLoader.Builder(androidAppContext)
.components {
if (Build.VERSION.SDK_INT >= 28) {
if (SDK_INT >= 28) {
add(ImageDecoderDecoder.Factory())
} else {
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 androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.runtime.*
import androidx.compose.ui.*
import androidx.compose.ui.platform.*
import androidx.compose.ui.unit.*
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
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 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.StyledPlayerView
@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(
factory = { ctx ->
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 density = LocalDensity.current.density
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.os.Build
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import chat.simplex.app.model.ChatItem
import chat.simplex.app.model.MsgContent
import chat.simplex.app.platform.saveImage
import chat.simplex.common.model.ChatItem
import chat.simplex.common.model.MsgContent
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 com.google.accompanist.permissions.rememberPermissionState
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
@Composable
fun SaveContentItemAction(cItem: ChatItem, showMenu: MutableState<Boolean>) {
val saveFileLauncher = rememberSaveFileLauncher(ciFile = cItem.file)
actual fun SaveContentItemAction(cItem: ChatItem, saveFileLauncher: FileChooserLauncher, showMenu: MutableState<Boolean>) {
val writePermissionState = rememberPermissionState(permission = Manifest.permission.WRITE_EXTERNAL_STORAGE)
ItemAction(stringResource(MR.strings.save_verb), painterResource(MR.images.ic_download), onClick = {
when (cItem.content.msgContent) {
@ -25,7 +26,7 @@ fun SaveContentItemAction(cItem: ChatItem, showMenu: MutableState<Boolean>) {
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 -> {}
}
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.view.View
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.viewinterop.AndroidView
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 coil.ImageLoader
import coil.compose.rememberAsyncImagePainter
@ -21,13 +20,13 @@ import coil.decode.GifDecoder
import coil.decode.ImageDecoderDecoder
import coil.request.ImageRequest
import coil.size.Size
import com.google.android.exoplayer2.R
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout
import com.google.android.exoplayer2.ui.StyledPlayerView
import dev.icerock.moko.resources.compose.stringResource
import java.net.URI
@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
// 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)
@ -41,8 +40,8 @@ fun FullScreenImageView(modifier: Modifier, uri: Uri, imageBitmap: Bitmap) {
.build()
Image(
rememberAsyncImagePainter(
ImageRequest.Builder(LocalContext.current).data(data = uri).size(Size.ORIGINAL).build(),
placeholder = BitmapPainter(imageBitmap.asImageBitmap()), // show original image while it's still loading by coil
ImageRequest.Builder(LocalContext.current).data(data = uri.toUri()).size(Size.ORIGINAL).build(),
placeholder = BitmapPainter(imageBitmap), // show original image while it's still loading by coil
imageLoader = imageLoader
),
contentDescription = stringResource(MR.strings.image_descr),
@ -52,7 +51,7 @@ fun FullScreenImageView(modifier: Modifier, uri: Uri, imageBitmap: Bitmap) {
}
@Composable
fun FullScreenVideoView(player: VideoPlayer, modifier: Modifier) {
actual fun FullScreenVideoView(player: VideoPlayer, modifier: Modifier) {
AndroidView(
factory = { ctx ->
StyledPlayerView(ctx).apply {
@ -66,8 +65,8 @@ fun FullScreenVideoView(player: VideoPlayer, modifier: Modifier) {
setShowSubtitleButton(false)
setShowVrButton(false)
controllerAutoShow = false
findViewById<View>(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_controls_background).setBackgroundColor(Color.Black.copy(alpha = 0.3f).toArgb())
findViewById<View>(com.google.android.exoplayer2.R.id.exo_settings).isVisible = false
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.app.Activity
@ -9,8 +9,6 @@ import android.graphics.*
import android.net.Uri
import android.provider.MediaStore
import android.util.Base64
import android.util.Log
import android.widget.Toast
import androidx.activity.compose.ManagedActivityResultLauncher
import androidx.activity.compose.rememberLauncherForActivityResult
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.ui.Modifier
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.platform.LocalContext
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import chat.simplex.app.*
import chat.simplex.app.model.json
import chat.simplex.app.platform.getAppFilesDirectory
import chat.simplex.app.views.newchat.ActionButton
import chat.simplex.common.helpers.APPLICATION_ID
import chat.simplex.common.helpers.toURI
import chat.simplex.common.model.json
import chat.simplex.common.platform.*
import chat.simplex.common.views.newchat.ActionButton
import chat.simplex.res.MR
import kotlinx.serialization.builtins.*
import java.io.File
import java.net.URI
val errorBitmapBytes = Base64.decode("iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAAKVJREFUeF7t1kENACEUQ0FQhnVQ9lfGO+xggITQdvbMzArPey+8fa3tAfwAEdABZQspQStgBssEcgAIkSAJkiAJljtEgiRIgmUCSZAESZAESZAEyx0iQRIkwTKBJEiCv5fgvTd1wDmn7QAP4AeIgA4oW0gJWgEzWCZwbQ7gAA7ggLKFOIADOKBMIAeAEAmSIAmSYLlDJEiCJFgmkARJkARJ8N8S/ADTZUewBvnTOQAAAABJRU5ErkJggg==", Base64.NO_WRAP)
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()))
// 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()
uri = FileProvider.getUriForFile(context, "${BuildConfig.APPLICATION_ID}.provider", tmpFile!!)
uri = FileProvider.getUriForFile(context, "$APPLICATION_ID.provider", tmpFile!!)
return Intent(MediaStore.ACTION_IMAGE_CAPTURE)
.putExtra(MediaStore.EXTRA_OUTPUT, uri)
}
@ -127,7 +128,7 @@ fun ManagedActivityResultLauncher<Void?, Uri?>.launchWithFallback() {
try {
// 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) {
// No camera apps available at all
Log.e(TAG, "Camera launcher2: " + e.stackTraceToString())
@ -136,14 +137,15 @@ fun ManagedActivityResultLauncher<Void?, Uri?>.launchWithFallback() {
}
@Composable
fun GetImageBottomSheet(
imageBitmap: MutableState<Uri?>,
onImageChange: (Bitmap) -> Unit,
actual fun GetImageBottomSheet(
imageBitmap: MutableState<URI?>,
onImageChange: (ImageBitmap) -> Unit,
hideBottomSheet: () -> Unit
) {
val context = LocalContext.current
val processPickedImage = { uri: Uri? ->
if (uri != null) {
val uri = uri.toURI()
val bitmap = getBitmapFromUri(uri)
if (bitmap != null) {
imageBitmap.value = uri
@ -159,7 +161,7 @@ fun GetImageBottomSheet(
cameraLauncher.launchWithFallback()
hideBottomSheet()
} 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 androidx.biometric.BiometricManager
@ -6,15 +6,14 @@ import androidx.biometric.BiometricManager.Authenticators.*
import androidx.biometric.BiometricPrompt
import androidx.core.content.ContextCompat
import androidx.fragment.app.FragmentActivity
import chat.simplex.app.SimplexApp
import chat.simplex.app.platform.mainActivity
import chat.simplex.app.views.usersettings.LAMode
import chat.simplex.common.platform.mainActivity
import chat.simplex.common.views.usersettings.LAMode
fun authenticate(
actual fun authenticate(
promptTitle: String,
promptSubtitle: String,
selfDestruct: Boolean = false,
usingLAMode: LAMode = SimplexApp.context.chatModel.controller.appPrefs.laMode.get(),
selfDestruct: Boolean,
usingLAMode: LAMode,
completed: (LAResult) -> Unit
) {
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.LocaleManager
import android.content.res.Resources
import android.graphics.*
import android.graphics.Typeface
import android.graphics.drawable.Drawable
import android.media.MediaMetadataRetriever
import android.net.Uri
import android.os.*
import android.provider.OpenableColumns
import android.text.Spanned
import android.text.SpannedString
import android.text.style.*
import android.util.Log
import androidx.compose.runtime.*
import android.util.Base64
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.platform.*
import androidx.compose.ui.text.*
@ -24,12 +24,13 @@ import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.*
import androidx.core.content.FileProvider
import androidx.core.text.HtmlCompat
import chat.simplex.app.*
import chat.simplex.app.model.*
import chat.simplex.app.platform.getLoadedFilePath
import chat.simplex.common.helpers.*
import chat.simplex.common.model.*
import chat.simplex.common.platform.*
import chat.simplex.res.MR
import dev.icerock.moko.resources.StringResource
import java.io.*
import java.net.URI
fun Spanned.toHtmlWithoutParagraphs(): String {
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)
}
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)
}
@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(
text: CharSequence,
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
fun getLoadedImage(file: CIFile?): Bitmap? {
actual fun getLoadedImage(file: CIFile?): ImageBitmap? {
val filePath = getLoadedFilePath(file)
return if (filePath != null) {
try {
val uri = FileProvider.getUriForFile(SimplexApp.context, "${BuildConfig.APPLICATION_ID}.provider", File(filePath))
val parcelFileDescriptor = SimplexApp.context.contentResolver.openFileDescriptor(uri, "r")
val uri = getAppFileUri(filePath.substringAfterLast(File.separator))
val parcelFileDescriptor = androidAppContext.contentResolver.openFileDescriptor(uri.toUri(), "r")
val fileDescriptor = parcelFileDescriptor?.fileDescriptor
val image = decodeSampledBitmapFromFileDescriptor(fileDescriptor, 1000, 1000)
parcelFileDescriptor?.close()
image
image.asImageBitmap()
} catch (e: Exception) {
null
}
@ -215,33 +210,33 @@ private fun calculateInSampleSize(options: BitmapFactory.Options, reqWidth: Int,
return inSampleSize
}
fun getFileName(uri: Uri): String? {
return SimplexApp.context.contentResolver.query(uri, null, null, null, null)?.use { cursor ->
actual fun getFileName(uri: URI): String? {
return androidAppContext.contentResolver.query(uri.toUri(), null, null, null, null)?.use { cursor ->
val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
cursor.moveToFirst()
cursor.getString(nameIndex)
}
}
fun getAppFilePath(uri: Uri): String? {
return SimplexApp.context.contentResolver.query(uri, null, null, null, null)?.use { cursor ->
actual fun getAppFilePath(uri: URI): String? {
return androidAppContext.contentResolver.query(uri.toUri(), null, null, null, null)?.use { cursor ->
val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
cursor.moveToFirst()
chat.simplex.app.platform.getAppFilePath(cursor.getString(nameIndex))
getAppFilePath(cursor.getString(nameIndex))
}
}
fun getFileSize(uri: Uri): Long? {
return SimplexApp.context.contentResolver.query(uri, null, null, null, null)?.use { cursor ->
actual fun getFileSize(uri: URI): Long? {
return androidAppContext.contentResolver.query(uri.toUri(), null, null, null, null)?.use { cursor ->
val sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE)
cursor.moveToFirst()
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) {
val source = ImageDecoder.createSource(SimplexApp.context.contentResolver, uri)
val source = ImageDecoder.createSource(androidAppContext.contentResolver, uri.toUri())
try {
ImageDecoder.decodeBitmap(source)
} catch (e: android.graphics.ImageDecoder.DecodeException) {
@ -256,12 +251,12 @@ fun getBitmapFromUri(uri: Uri, withAlertOnException: Boolean = true): Bitmap? {
}
} else {
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) {
val source = ImageDecoder.createSource(SimplexApp.context.contentResolver, uri)
val source = ImageDecoder.createSource(androidAppContext.contentResolver, uri.toUri())
try {
ImageDecoder.decodeDrawable(source)
} 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 {
val ext = if (asPng) "png" else "jpg"
val tmpDir = SimplexApp.context.getDir("temp", Application.MODE_PRIVATE)
return File(tmpDir.absolutePath + File.separator + generateNewFileName("IMG", ext)).apply {
return File(getTempFilesDirectory() + File.separator + generateNewFileName("IMG", ext)).apply {
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()
}
deleteOnExit()
SimplexApp.context.chatModel.filesToDelete.add(this)
ChatModel.filesToDelete.add(this)
}
} catch (e: Exception) {
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()
mmr.setDataSource(SimplexApp.context, uri)
mmr.setDataSource(androidAppContext, uri.toUri())
val durationMs = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toLong()
val image = when {
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)
}
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.util.Log
@ -7,6 +7,8 @@ import androidx.camera.core.*
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.view.PreviewView
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.LocalLifecycleOwner
import androidx.compose.ui.viewinterop.AndroidView
@ -16,14 +18,14 @@ import boofcv.alg.color.ColorFormat
import boofcv.android.ConvertCameraImage
import boofcv.factory.fiducial.FactoryFiducial
import boofcv.struct.image.GrayU8
import chat.simplex.app.TAG
import chat.simplex.common.platform.TAG
import com.google.common.util.concurrent.ListenableFuture
import java.util.concurrent.*
// Adapted from learntodroid - https://gist.github.com/learntodroid/8f839be0b29d0378f843af70607bd7f5
@Composable
fun QRCodeScanner(onBarcode: (String) -> Unit) {
actual fun QRCodeScanner(onBarcode: (String) -> Unit) {
val context = LocalContext.current
val lifecycleOwner = LocalLifecycleOwner.current
var preview by remember { mutableStateOf<Preview?>(null) }
@ -50,7 +52,8 @@ fun QRCodeScanner(onBarcode: (String) -> Unit) {
)
implementationMode = PreviewView.ImplementationMode.COMPATIBLE
}
}
},
modifier = Modifier.clipToBounds()
) { previewView ->
val cameraSelector: CameraSelector = CameraSelector.Builder()
.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 androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import chat.simplex.app.model.ChatModel
import chat.simplex.common.model.ChatModel
import com.google.accompanist.permissions.rememberPermissionState
@Composable
fun ScanToConnectView(chatModel: ChatModel, close: () -> Unit) {
actual fun ScanToConnectView(chatModel: ChatModel, close: () -> Unit) {
val cameraPermissionState = rememberPermissionState(permission = Manifest.permission.CAMERA)
LaunchedEffect(Unit) {
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.os.Build
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import chat.simplex.app.SimplexApp
import chat.simplex.common.platform.ntfManager
import com.google.accompanist.permissions.rememberPermissionState
@Composable
fun SetNotificationsModeAdditions() {
actual fun SetNotificationsModeAdditions() {
if (Build.VERSION.SDK_INT >= 33) {
val notificationsPermissionState = rememberPermissionState(Manifest.permission.POST_NOTIFICATIONS)
LaunchedEffect(notificationsPermissionState.hasPermission) {
if (notificationsPermissionState.hasPermission) {
SimplexApp.context.chatModel.controller.ntfManager.createNtfChannelsMaybeShowAlert()
ntfManager.createNtfChannelsMaybeShowAlert()
} else {
notificationsPermissionState.launchPermissionRequest()
}
}
} else {
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 SectionDividerSpaced
import SectionItemView
import SectionItemViewSpaceBetween
import SectionSpacer
import SectionView
import android.app.Activity
import android.content.ComponentName
import android.content.Context
import android.content.pm.PackageManager
import android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DEFAULT
import android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_ENABLED
import android.net.Uri
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.desktop.ui.tooling.preview.Preview
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.material.*
import androidx.compose.material.MaterialTheme.colors
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.*
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.toBitmap
import chat.simplex.app.*
import chat.simplex.app.R
import chat.simplex.app.helpers.saveAppLocale
import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.usersettings.AppearanceScope.ColorEditor
import chat.simplex.app.views.usersettings.AppearanceScope.LangSelector
import chat.simplex.app.views.usersettings.AppearanceScope.ThemesSection
import com.godaddy.android.colorpicker.*
import chat.simplex.common.R
import chat.simplex.common.model.*
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.*
import chat.simplex.common.model.ChatModel
import chat.simplex.common.platform.*
import chat.simplex.common.helpers.APPLICATION_ID
import chat.simplex.common.helpers.saveAppLocale
import chat.simplex.common.views.usersettings.AppearanceScope.ColorEditor
import chat.simplex.res.MR
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) {
DEFAULT(R.mipmap.icon),
DARK_BLUE(R.mipmap.icon_dark_blue),
DEFAULT(R.drawable.icon_round_common),
DARK_BLUE(R.drawable.icon_dark_blue_round_common),
}
@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()) }
fun setAppIcon(newIcon: AppIcon) {
if (appIcon.value == newIcon) return
val newComponent = ComponentName(BuildConfig.APPLICATION_ID, "chat.simplex.app.MainActivity_${newIcon.name.lowercase()}")
val oldComponent = ComponentName(BuildConfig.APPLICATION_ID, "chat.simplex.app.MainActivity_${appIcon.value.name.lowercase()}")
SimplexApp.context.packageManager.setComponentEnabledSetting(
val newComponent = ComponentName(APPLICATION_ID, "chat.simplex.app.MainActivity_${newIcon.name.lowercase()}")
val oldComponent = ComponentName(APPLICATION_ID, "chat.simplex.app.MainActivity_${appIcon.value.name.lowercase()}")
androidAppContext.packageManager.setComponentEnabledSetting(
newComponent,
COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP
)
SimplexApp.context.packageManager.setComponentEnabledSetting(
androidAppContext.packageManager.setComponentEnabledSetting(
oldComponent,
PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP
)
@ -80,7 +62,7 @@ fun AppearanceView(m: ChatModel, showSettingsModal: (@Composable (ChatModel) ->
appIcon.value = newIcon
}
AppearanceLayout(
AppearanceScope.AppearanceLayout(
appIcon,
m.controller.appPrefs.appLanguage,
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>,
languagePref: SharedPreference<String?>,
systemDarkTheme: SharedPreference<String?>,
@ -108,14 +91,14 @@ fun AppearanceView(m: ChatModel, showSettingsModal: (@Composable (ChatModel) ->
AppBarTitle(stringResource(MR.strings.appearance_settings))
SectionView(stringResource(MR.strings.settings_section_title_language), padding = PaddingValues()) {
val context = LocalContext.current
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
// SectionItemWithValue(
// generalGetString(MR.strings.settings_section_title_language).lowercase().replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.US) else it.toString() },
// remember { mutableStateOf("system") },
// listOf(ValueTitleDesc("system", generalGetString(MR.strings.change_verb), "")),
// onSelected = { openSystemLangPicker(context as? Activity ?: return@SectionItemWithValue) }
// )
// } else {
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
// SectionItemWithValue(
// generalGetString(MR.strings.settings_section_title_language).lowercase().replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.US) else it.toString() },
// remember { mutableStateOf("system") },
// listOf(ValueTitleDesc("system", generalGetString(MR.strings.change_verb), "")),
// onSelected = { openSystemLangPicker(context as? Activity ?: return@SectionItemWithValue) }
// )
// } else {
val state = rememberSaveable { mutableStateOf(languagePref.get() ?: "system") }
LangSelector(state) {
state.value = it
@ -124,14 +107,14 @@ fun AppearanceView(m: ChatModel, showSettingsModal: (@Composable (ChatModel) ->
val activity = context as? Activity
if (activity != null) {
if (it == "system") {
saveAppLocale(languagePref, activity)
activity.saveAppLocale(languagePref)
} else {
saveAppLocale(languagePref, activity, it)
activity.saveAppLocale(languagePref, it)
}
}
}
}
// }
// }
}
SectionDividerSpaced()
@ -165,16 +148,16 @@ fun AppearanceView(m: ChatModel, showSettingsModal: (@Composable (ChatModel) ->
}
private fun findEnabledIcon(): AppIcon = AppIcon.values().first { icon ->
SimplexApp.context.packageManager.getComponentEnabledSetting(
ComponentName(BuildConfig.APPLICATION_ID, "chat.simplex.app.MainActivity_${icon.name.lowercase()}")
androidAppContext.packageManager.getComponentEnabledSetting(
ComponentName(APPLICATION_ID, "chat.simplex.app.MainActivity_${icon.name.lowercase()}")
).let { it == COMPONENT_ENABLED_STATE_DEFAULT || it == COMPONENT_ENABLED_STATE_ENABLED }
}
@Preview(showBackground = true)
@Preview
@Composable
fun PreviewAppearanceSettings() {
SimpleXTheme {
AppearanceLayout(
AppearanceScope.AppearanceLayout(
icon = remember { mutableStateOf(AppIcon.DARK_BLUE) },
languagePref = 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 android.view.WindowManager
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import androidx.fragment.app.FragmentActivity
import chat.simplex.app.model.ChatController
import chat.simplex.app.model.ChatModel
import chat.simplex.common.model.ChatModel
import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
@Composable
fun PrivacyDeviceSection(
actual fun PrivacyDeviceSection(
showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
setPerformLA: (Boolean) -> Unit,
) {
SectionView(stringResource(MR.strings.settings_section_title_device)) {
ChatLockItem(ChatModel, showSettingsModal, setPerformLA)
ChatLockItem(showSettingsModal, setPerformLA)
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) {
(context as? FragmentActivity)?.window?.setFlags(
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 androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import chat.simplex.app.model.ServerCfg
import chat.simplex.common.model.ServerCfg
import com.google.accompanist.permissions.rememberPermissionState
@Composable
fun ScanProtocolServer(onNext: (ServerCfg) -> Unit) {
actual fun ScanProtocolServer(onNext: (ServerCfg) -> Unit) {
val cameraPermissionState = rememberPermissionState(permission = Manifest.permission.CAMERA)
LaunchedEffect(Unit) {
cameraPermissionState.launchPermissionRequest()

View File

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

View File

@ -19,7 +19,7 @@ extern void __rel_iplt_start(void){};
extern void reallocarray(void){};
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);
int ret = pipe_std_to_socket(name);
(*env)->ReleaseStringUTFChars(env, socket_name, name);
@ -27,7 +27,7 @@ Java_chat_simplex_app_platform_Backend_1commonKt_pipeStdOutToSocket(JNIEnv *env,
}
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);
setLineBuffering();
}
@ -44,7 +44,7 @@ extern char *chat_parse_server(const char *str);
extern char *chat_password_hash(const char *pwd, const char *salt);
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 *_dbKey = (*env)->GetStringUTFChars(env, dbKey, 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
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);
jstring res = (*env)->NewStringUTF(env, chat_send_cmd((void*)controller, _msg));
(*env)->ReleaseStringUTFChars(env, msg, _msg);
@ -75,17 +75,17 @@ Java_chat_simplex_app_platform_Backend_1commonKt_chatSendCmd(JNIEnv *env, __unus
}
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));
}
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));
}
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);
jstring res = (*env)->NewStringUTF(env, chat_parse_markdown(_str));
(*env)->ReleaseStringUTFChars(env, str, _str);
@ -93,7 +93,7 @@ Java_chat_simplex_app_platform_Backend_1commonKt_chatParseMarkdown(JNIEnv *env,
}
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);
jstring res = (*env)->NewStringUTF(env, chat_parse_server(_str));
(*env)->ReleaseStringUTFChars(env, str, _str);
@ -101,7 +101,7 @@ Java_chat_simplex_app_platform_Backend_1commonKt_chatParseServer(JNIEnv *env, __
}
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 *_salt = (*env)->GetStringUTFChars(env, salt, JNI_FALSE);
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){};
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);
}
@ -31,7 +31,7 @@ extern char *chat_parse_server(const char *str);
extern char *chat_password_hash(const char *pwd, const char *salt);
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 *_dbKey = (*env)->GetStringUTFChars(env, dbKey, 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
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);
jstring res = (*env)->NewStringUTF(env, chat_send_cmd((void*)controller, _msg));
(*env)->ReleaseStringUTFChars(env, msg, _msg);
@ -62,17 +62,17 @@ Java_chat_simplex_common_platform_BackendKt_chatSendCmd(JNIEnv *env, jclass claz
}
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));
}
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));
}
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);
jstring res = (*env)->NewStringUTF(env, chat_parse_markdown(_str));
(*env)->ReleaseStringUTFChars(env, str, _str);
@ -80,7 +80,7 @@ Java_chat_simplex_common_platform_BackendKt_chatParseMarkdown(JNIEnv *env, jclas
}
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);
jstring res = (*env)->NewStringUTF(env, chat_parse_server(_str));
(*env)->ReleaseStringUTFChars(env, str, _str);
@ -88,7 +88,7 @@ Java_chat_simplex_common_platform_BackendKt_chatParseServer(JNIEnv *env, jclass
}
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 *_salt = (*env)->GetStringUTFChars(env, salt, JNI_FALSE);
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.foundation.layout.*
import androidx.compose.material.*
@ -10,44 +9,44 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.unit.dp
import chat.simplex.app.model.ChatModel
import chat.simplex.app.ui.theme.DEFAULT_PADDING
import chat.simplex.app.ui.theme.SimpleButton
import chat.simplex.app.views.SplashView
import chat.simplex.app.views.call.ActiveCallView
import chat.simplex.app.views.call.IncomingCallAlertView
import chat.simplex.app.views.chat.ChatView
import chat.simplex.app.views.chatlist.ChatListView
import chat.simplex.app.views.chatlist.ShareListView
import chat.simplex.app.views.database.DatabaseErrorView
import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.onboarding.*
import chat.simplex.app.views.usersettings.LAMode
import chat.simplex.app.views.usersettings.laUnavailableInstructionAlert
import chat.simplex.common.model.*
import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.DEFAULT_PADDING
import chat.simplex.common.views.helpers.SimpleButton
import chat.simplex.common.views.SplashView
import chat.simplex.common.views.call.ActiveCallView
import chat.simplex.common.views.call.IncomingCallAlertView
import chat.simplex.common.views.chat.ChatView
import chat.simplex.common.views.chatlist.ChatListView
import chat.simplex.common.views.chatlist.ShareListView
import chat.simplex.common.views.database.DatabaseErrorView
import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.onboarding.*
import chat.simplex.common.views.usersettings.*
import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import kotlinx.coroutines.delay
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.launch
@Composable
fun MainPage(
chatModel: ChatModel,
userAuthorized: MutableState<Boolean?>,
laFailed: MutableState<Boolean>,
destroyedAfterBackPress: MutableState<Boolean>,
runAuthenticate: () -> Unit,
setPerformLA: (Boolean) -> Unit,
showLANotice: () -> Unit
) {
fun AppScreen() {
ProvideWindowInsets(windowInsetsAnimationsEnabled = true) {
Surface(color = MaterialTheme.colors.background) {
MainScreen()
}
}
}
@Composable
fun MainScreen() {
val chatModel = ChatModel
var showChatDatabaseError by rememberSaveable {
mutableStateOf(chatModel.chatDbStatus.value != DBMigrationResult.OK && chatModel.chatDbStatus.value != null)
}
LaunchedEffect(chatModel.chatDbStatus.value) {
showChatDatabaseError = chatModel.chatDbStatus.value != DBMigrationResult.OK && chatModel.chatDbStatus.value != null
}
var showAdvertiseLAAlert by remember { mutableStateOf(false) }
LaunchedEffect(showAdvertiseLAAlert) {
if (
@ -57,8 +56,7 @@ fun MainPage(
&& chatModel.chats.isNotEmpty()
&& chatModel.activeCallInvitation.value == null
) {
showLANotice()
}
AppLock.showLANotice(ChatModel.controller.appPrefs.laNoticeShown) }
}
LaunchedEffect(chatModel.showAdvertiseLAUnavailableAlert.value) {
if (chatModel.showAdvertiseLAUnavailableAlert.value) {
@ -83,8 +81,8 @@ fun MainPage(
stringResource(MR.strings.auth_unlock),
icon = painterResource(MR.images.ic_lock),
click = {
laFailed.value = false
runAuthenticate()
AppLock.laFailed.value = false
AppLock.runAuthenticate()
}
)
}
@ -117,7 +115,7 @@ fun MainPage(
) {
val stopped = chatModel.chatRunning.value == false
if (chatModel.sharedContent.value == null)
ChatListView(chatModel, setPerformLA, stopped)
ChatListView(chatModel, AppLock::setPerformLA, stopped)
else
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 {
ChatView(it, chatModel, onComposed)
}
@ -157,22 +155,23 @@ fun MainPage(
onboarding == OnboardingStage.Step4_SetNotificationsMode -> SetNotificationsMode(chatModel)
}
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)) {
LaunchedEffect(Unit) {
// 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
if (!(destroyedAfterBackPress.value && chatModel.controller.appPrefs.laMode.get() == LAMode.SYSTEM)) {
runAuthenticate()
if (!(AppLock.destroyedAfterBackPress.value && chatModel.controller.appPrefs.laMode.get() == LAMode.SYSTEM)) {
AppLock.runAuthenticate()
}
}
if (chatModel.controller.appPrefs.performLA.get() && laFailed.value) {
if (chatModel.controller.appPrefs.performLA.get() && AppLock.laFailed.value) {
AuthView()
} else {
SplashView()
}
} else if (chatModel.showCallView.value) {
ActiveCallView(chatModel)
ActiveCallView()
}
ModalManager.shared.showPasscodeInView()
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.
// Let's prolong the unlocked period to 3 sec for screen rotation to take place
if (chatModel.controller.appPrefs.laLockDelay.get() == 0) {
AppLock.enteredBackground.value = SystemClock.elapsedRealtime() + 3000
AppLock.enteredBackground.value = AppLock.elapsedRealtime() + 3000
}
}
}
@Composable
private fun InitializationView() {
fun InitializationView() {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
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.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Modifier
import chat.simplex.app.model.ChatModel
import chat.simplex.app.model.SharedPreference
import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.localauth.SetAppPasscodeView
import chat.simplex.app.views.usersettings.*
import chat.simplex.common.model.*
import chat.simplex.common.platform.Log
import chat.simplex.common.platform.TAG
import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.localauth.SetAppPasscodeView
import chat.simplex.common.views.usersettings.*
import chat.simplex.res.MR
import kotlinx.coroutines.*
@ -70,8 +70,8 @@ object AppLock {
private fun initialEnableLA() {
val m = ChatModel
val appPrefs = m.controller.appPrefs
m.controller.appPrefs.laMode.set(LAMode.SYSTEM)
val appPrefs = ChatController.appPrefs
appPrefs.laMode.set(LAMode.SYSTEM)
authenticate(
generalGetString(MR.strings.auth_enable_simplex_lock),
generalGetString(MR.strings.auth_confirm_credential),
@ -99,19 +99,18 @@ object AppLock {
}
private fun setPasscode() {
val chatModel = ChatModel
val appPrefs = chatModel.controller.appPrefs
val appPrefs = ChatController.appPrefs
ModalManager.shared.showCustomModal { close ->
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
SetAppPasscodeView(
submit = {
chatModel.performLA.value = true
ChatModel.performLA.value = true
appPrefs.performLA.set(true)
appPrefs.laMode.set(LAMode.PASSCODE)
laTurnedOnAlert()
},
cancel = {
chatModel.performLA.value = false
ChatModel.performLA.value = false
appPrefs.performLA.set(false)
laPasscodeNotSetAlert()
},
@ -122,7 +121,7 @@ object AppLock {
}
fun setAuthState() {
userAuthorized.value = !ChatModel.controller.appPrefs.performLA.get()
userAuthorized.value = !ChatController.appPrefs.performLA.get()
}
fun runAuthenticate() {
@ -169,7 +168,7 @@ object AppLock {
}
fun setPerformLA(on: Boolean) {
ChatModel.controller.appPrefs.laNoticeShown.set(true)
ChatController.appPrefs.laNoticeShown.set(true)
if (on) {
enableLA()
} 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.runtime.*
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.font.*
import androidx.compose.ui.text.style.TextDecoration
import chat.simplex.app.R
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.call.*
import chat.simplex.app.views.chat.ComposeState
import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.onboarding.OnboardingStage
import chat.simplex.app.views.usersettings.NotificationPreviewMode
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.call.*
import chat.simplex.common.views.chat.ComposeState
import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.onboarding.OnboardingStage
import chat.simplex.common.platform.AudioPlayer
import chat.simplex.res.MR
import dev.icerock.moko.resources.ImageResource
import dev.icerock.moko.resources.StringResource
@ -26,6 +24,7 @@ import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.*
import java.io.File
import java.net.URI
import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle
import java.util.*
@ -65,14 +64,21 @@ object ChatModel {
val clearOverlays = mutableStateOf<Boolean>(false)
// set when app is opened via contact or invitation URI
val appOpenUrl = mutableStateOf<Uri?>(null)
val appOpenUrl = mutableStateOf<URI?>(null)
// preferences
val notificationsMode by lazy { mutableStateOf(NotificationsMode.values().firstOrNull { it.name == controller.appPrefs.notificationsMode.get() } ?: NotificationsMode.default) }
val notificationPreviewMode by lazy { mutableStateOf(NotificationPreviewMode.values().firstOrNull { it.name == controller.appPrefs.notificationPreviewMode.get() } ?: NotificationPreviewMode.default) }
val performLA by lazy { mutableStateOf(controller.appPrefs.performLA.get()) }
val notificationPreviewMode by lazy {
mutableStateOf(
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 incognito by lazy { mutableStateOf(controller.appPrefs.incognito.get()) }
val incognito by lazy { mutableStateOf(ChatController.appPrefs.incognito.get()) }
// current WebRTC call
val callManager = CallManager(this)
@ -94,7 +100,7 @@ object ChatModel {
val sharedContent = mutableStateOf(null as SharedContent?)
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) {
currentUser.value
@ -2409,3 +2415,11 @@ data class ChatItemVersion(
val itemVersionTs: 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 android.os.Build
import android.util.Log
import chat.simplex.app.views.helpers.*
import chat.simplex.common.views.helpers.*
import androidx.compose.runtime.*
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.Painter
import dev.icerock.moko.resources.compose.painterResource
import chat.simplex.app.*
import chat.simplex.app.platform.*
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.call.*
import chat.simplex.app.views.newchat.ConnectViaLinkTab
import chat.simplex.app.views.onboarding.OnboardingStage
import chat.simplex.app.views.usersettings.*
import chat.simplex.common.*
import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.call.*
import chat.simplex.common.views.newchat.ConnectViaLinkTab
import chat.simplex.common.views.onboarding.OnboardingStage
import chat.simplex.common.views.usersettings.*
import com.charleskorn.kaml.Yaml
import com.charleskorn.kaml.YamlConfiguration
import chat.simplex.res.MR
import com.russhwolf.settings.Settings
import kotlinx.coroutines.*
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
@ -45,19 +43,17 @@ enum class SimplexLinkMode {
BROWSER;
companion object {
val default = SimplexLinkMode.DESCRIPTION
val default = DESCRIPTION
}
}
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
private val runServiceInBackground = mkBoolPreference(SHARED_PREFS_RUN_SERVICE_IN_BACKGROUND, true)
val notificationsMode = mkStrPreference(SHARED_PREFS_NOTIFICATIONS_MODE,
if (!runServiceInBackground.get()) NotificationsMode.OFF.name else NotificationsMode.default.name
)
val notificationsMode = mkEnumPreference(
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 backgroundServiceNoticeShown = mkBoolPreference(SHARED_PREFS_SERVICE_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 encryptedSelfDestructPassphrase = mkStrPreference(SHARED_PREFS_ENCRYPTED_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 selfDestruct = mkBoolPreference(SHARED_PREFS_SELF_DESTRUCT, false)
val selfDestructDisplayName = mkStrPreference(SHARED_PREFS_SELF_DESTRUCT_DISPLAY_NAME, null)
@ -154,7 +150,7 @@ class AppPreferences {
json.encodeToString(MapSerializer(String.serializer(), ThemeOverrides.serializer()), it)
}, decode = {
json.decodeFromString(MapSerializer(String.serializer(), ThemeOverrides.serializer()), it)
}, sharedPreferencesThemes)
}, settingsThemes)
val whatsNewVersion = mkStrPreference(SHARED_PREFS_WHATS_NEW_VERSION, null)
val lastMigratedVersionCode = mkIntPreference(SHARED_PREFS_LAST_MIGRATED_VERSION_CODE, 0)
@ -162,65 +158,73 @@ class AppPreferences {
private fun mkIntPreference(prefName: String, default: Int) =
SharedPreference(
get = fun() = sharedPreferences.getInt(prefName, default),
set = fun(value) = sharedPreferences.edit().putInt(prefName, value).apply()
get = fun() = settings.getInt(prefName, default),
set = fun(value) = settings.putInt(prefName, value)
)
private fun mkLongPreference(prefName: String, default: Long) =
SharedPreference(
get = fun() = sharedPreferences.getLong(prefName, default),
set = fun(value) = sharedPreferences.edit().putLong(prefName, value).apply()
get = fun() = settings.getLong(prefName, default),
set = fun(value) = settings.putLong(prefName, value)
)
private fun mkTimeoutPreference(prefName: String, default: Long, proxyDefault: Long): SharedPreference<Long> {
val d = if (networkUseSocksProxy.get()) proxyDefault else default
return SharedPreference(
get = fun() = sharedPreferences.getLong(prefName, d),
set = fun(value) = sharedPreferences.edit().putLong(prefName, value).apply()
get = fun() = settings.getLong(prefName, d),
set = fun(value) = settings.putLong(prefName, value)
)
}
private fun mkBoolPreference(prefName: String, default: Boolean) =
SharedPreference(
get = fun() = sharedPreferences.getBoolean(prefName, default),
set = fun(value) = sharedPreferences.edit().putBoolean(prefName, value).apply()
get = fun() = settings.getBoolean(prefName, default),
set = fun(value) = settings.putBoolean(prefName, value)
)
private fun mkStrPreference(prefName: String, default: String?): SharedPreference<String?> =
SharedPreference(
get = fun() = sharedPreferences.getString(prefName, default),
set = fun(value) = sharedPreferences.edit().putString(prefName, value).apply()
get = {
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> =
SharedPreference(
get = fun() = sharedPreferences.getString(prefName, default.toString())?.construct() ?: default,
set = fun(value) = sharedPreferences.edit().putString(prefName, value.toString()).apply()
get = fun() = settings.getString(prefName, default.toString()).construct() ?: default,
set = fun(value) = settings.putString(prefName, value.toString())
)
/**
* Provide `[commit] = true` to save preferences right now, not after some unknown period of time.
* So in case of a crash this value will be saved 100%
* */
private fun mkDatePreference(prefName: String, default: Instant?, commit: Boolean = false): SharedPreference<Instant?> =
// LALAL
private fun mkDatePreference(prefName: String, default: Instant?): SharedPreference<Instant?> =
SharedPreference(
get = {
val pref = sharedPreferences.getString(prefName, default?.toEpochMilliseconds()?.toString())
pref?.let { Instant.fromEpochMilliseconds(pref.toLong()) }
val nullValue = "----------------------"
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 {
if (commit) it.commit() else it.apply()
}
set = fun(value) = if (value?.toEpochMilliseconds() != null) settings.putString(prefName, value.toEpochMilliseconds().toString()) else settings.remove(prefName)
)
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(
get = fun() = decode(prefs.getString(prefName, encode(default))!!),
set = fun(value) = prefs.edit().putString(prefName, encode(value)).apply()
get = fun() = decode(prefs.getString(prefName, encode(default))),
set = fun(value) = prefs.putString(prefName, encode(value))
)
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"
private const val SHARED_PREFS_AUTO_RESTART_WORKER_VERSION = "AutoRestartWorkerVersion"
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_LINK_PREVIEWS = "PrivacyLinkPreviews"
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_SHOW_UNREAD_AND_FAVORITES = "ShowUnreadAndFavorites"
private const val SHARED_PREFS_CHAT_ARCHIVE_NAME = "ChatArchiveName"
@ -294,7 +298,6 @@ private const val MESSAGE_TIMEOUT: Int = 15_000_000
object ChatController {
var ctrl: ChatCtrl? = -1
val appPrefs: AppPreferences by lazy { AppPreferences() }
val ntfManager by lazy { NtfManager }
val chatModel = ChatModel
private var receiverStarted = false
@ -328,7 +331,7 @@ object ChatController {
chatModel.userCreated.value = true
apiSetIncognito(chatModel.incognito.value)
getUserChatData()
chatModel.controller.appPrefs.chatLastStart.set(Clock.System.now())
appPrefs.chatLastStart.set(Clock.System.now())
chatModel.chatRunning.value = true
startReceiver()
Log.d(TAG, "startChat: started")
@ -940,7 +943,8 @@ object ChatController {
val r = sendCmd(CC.ApiShowMyAddress(userId))
if (r is CR.UserContactLink) return r.contactLink
if (r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorStore
&& r.chatError.storeError is StoreError.UserContactLinkNotFound) {
&& r.chatError.storeError is StoreError.UserContactLinkNotFound
) {
return null
}
Log.e(TAG, "apiGetUserAddress bad response: ${r.responseType} ${r.details}")
@ -952,7 +956,8 @@ object ChatController {
val r = sendCmd(CC.ApiAddressAutoAccept(userId, autoAccept))
if (r is CR.UserContactLinkUpdated) return r.contactLink
if (r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorStore
&& r.chatError.storeError is StoreError.UserContactLinkNotFound) {
&& r.chatError.storeError is StoreError.UserContactLinkNotFound
) {
return null
}
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))) {
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)
}
}
@ -1534,7 +1539,7 @@ object ChatController {
// TODO check encryption is compatible
withCall(r, r.contact) { call ->
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()
Log.d(TAG, ".callOffer iceServers $iceServers")
chatModel.callCommand.value = WCallCommand.Offer(
@ -3836,11 +3841,9 @@ sealed class 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
get() = requiresIgnoringBatterySinceSdk <= Build.VERSION.SDK_INT
enum class NotificationsMode() {
OFF, PERIODIC, SERVICE, /*INSTANT - for Firebase notifications */;
companion object {
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.app.SimplexService
import chat.simplex.app.TAG
import chat.simplex.app.model.*
import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.onboarding.OnboardingStage
import chat.simplex.common.model.*
import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.onboarding.OnboardingStage
import kotlinx.serialization.decodeFromString
// ghc's rts
@ -31,7 +28,6 @@ val appPreferences: AppPreferences
val chatController: ChatController = ChatController
suspend fun initChatController(useKey: String? = null, confirmMigrations: MigrationConfirmation? = null, startChat: Boolean = true) {
val dbKey = useKey ?: DatabaseUtils.useDatabaseKey()
val dbAbsolutePathPrefix = getFilesDirectory()
@ -65,12 +61,7 @@ suspend fun initChatController(useKey: String? = null, confirmMigrations: Migrat
savedOnboardingStage
}
chatController.startChat(user)
// Prevents from showing "Enable notifications" alert when onboarding wasn't complete yet
if (chatModel.onboardingStage.value == OnboardingStage.OnboardingComplete) {
SimplexService.showBackgroundServiceNoticeIfNeeded()
if (appPreferences.notificationsMode.get() == NotificationsMode.SERVICE.name)
SimplexService.start()
}
platform.androidChatInitializedAndStarted()
}
}
}

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

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.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.isSystemInDarkTheme
@ -8,13 +8,13 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.*
import androidx.compose.ui.unit.dp
import chat.simplex.app.SimplexApp
import chat.simplex.app.platform.isInNightMode
import chat.simplex.app.views.helpers.*
import chat.simplex.res.MR
import chat.simplex.common.model.ChatController
import chat.simplex.common.platform.isInNightMode
import chat.simplex.common.views.helpers.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import chat.simplex.res.MR
enum class DefaultTheme {
SYSTEM, LIGHT, DARK, SIMPLEX;
@ -264,7 +264,7 @@ fun SimpleXTheme(darkTheme: Boolean? = null, content: @Composable () -> Unit) {
}
val systemDark = isSystemInDarkTheme()
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
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.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import chat.simplex.app.R
import chat.simplex.app.SimplexApp
import chat.simplex.app.model.AppPreferences
import chat.simplex.app.views.helpers.generalGetString
import androidx.compose.ui.text.font.FontFamily
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 {
private val appPrefs: AppPreferences by lazy {
SimplexApp.context.chatModel.controller.appPrefs
}
private val appPrefs: AppPreferences = ChatController.appPrefs
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.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.font.*
import androidx.compose.ui.unit.sp
// 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.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.activity.compose.BackHandler
import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.*
@ -9,20 +8,18 @@ import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material.*
import androidx.compose.runtime.*
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.font.FontFamily
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.sp
import chat.simplex.app.model.*
import chat.simplex.app.platform.shareText
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.chat.*
import chat.simplex.app.views.helpers.*
import com.google.accompanist.insets.ProvideWindowInsets
import com.google.accompanist.insets.navigationBarsWithImePadding
import chat.simplex.common.model.*
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.chat.*
import chat.simplex.common.views.helpers.*
import chat.simplex.common.model.ChatModel
import chat.simplex.common.platform.*
@Composable
fun TerminalView(chatModel: ChatModel, close: () -> Unit) {
@ -30,12 +27,12 @@ fun TerminalView(chatModel: ChatModel, close: () -> Unit) {
BackHandler(onBack = {
close()
})
TerminalLayout(
remember { chatModel.terminalItems },
composeState,
sendCommand = { sendCommand(chatModel, composeState) },
close
)
TerminalLayout(
remember { chatModel.terminalItems },
composeState,
sendCommand = { sendCommand(chatModel, composeState) },
close
)
}
private fun sendCommand(chatModel: ChatModel, composeState: MutableState<ComposeState>) {
@ -118,7 +115,7 @@ fun TerminalLog(terminalItems: List<TerminalItem>) {
onDispose { lazyListState = listState.firstVisibleItemIndex to listState.firstVisibleItemScrollOffset }
}
val reversedTerminalItems by remember { derivedStateOf { terminalItems.reversed().toList() } }
val context = LocalContext.current
val clipboard = LocalClipboardManager.current
LazyColumn(state = listState, reverseLayout = true) {
items(reversedTerminalItems) { item ->
Text(
@ -129,7 +126,7 @@ fun TerminalLog(terminalItems: List<TerminalItem>) {
modifier = Modifier
.fillMaxWidth()
.clickable {
ModalManager.shared.showModal(endButtons = { ShareButton { shareText(item.details) } }) {
ModalManager.shared.showModal(endButtons = { ShareButton { clipboard.shareText(item.details) } }) {
SelectionContainer(modifier = Modifier.verticalScroll(rememberScrollState())) {
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,
showBackground = true,
name = "Dark Mode"
)
)*/
@Composable
fun PreviewTerminalLayout() {
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.layout.*
@ -22,17 +22,14 @@ import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.style.*
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.app.*
import chat.simplex.app.R
import chat.simplex.app.model.ChatModel
import chat.simplex.app.model.Profile
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.onboarding.OnboardingStage
import chat.simplex.app.views.onboarding.ReadableText
import com.google.accompanist.insets.navigationBarsWithImePadding
import chat.simplex.common.model.ChatModel
import chat.simplex.common.model.Profile
import chat.simplex.common.platform.navigationBarsWithImePadding
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.onboarding.OnboardingStage
import chat.simplex.common.views.onboarding.ReadableText
import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.stringResource
import kotlinx.coroutines.delay
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.app.TAG
import chat.simplex.app.model.ChatModel
import chat.simplex.app.views.helpers.withApi
import chat.simplex.common.model.ChatModel
import chat.simplex.common.platform.*
import chat.simplex.common.views.helpers.withApi
import kotlinx.datetime.Clock
import kotlin.time.Duration.Companion.minutes
@ -15,10 +14,10 @@ class CallManager(val chatModel: ChatModel) {
if (invitation.user.showNotifications) {
if (Clock.System.now() - invitation.callTs <= 3.minutes) {
activeCallInvitation.value = invitation
controller.ntfManager.notifyCallInvitation(invitation)
ntfManager.notifyCallInvitation(invitation)
} else {
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)
if (invitation.contact.id == activeCallInvitation.value?.contact?.id) {
activeCallInvitation.value = null
controller.ntfManager.cancelCallNotification()
ntfManager.cancelCallNotification()
}
}
}
@ -88,7 +87,7 @@ class CallManager(val chatModel: ChatModel) {
callInvitations.remove(invitation.contact.id)
if (invitation.contact.id == activeCallInvitation.value?.contact?.id) {
activeCallInvitation.value = null
controller.ntfManager.cancelCallNotification()
ntfManager.cancelCallNotification()
}
withApi {
if (!controller.apiRejectCall(invitation.contact)) {
@ -101,7 +100,7 @@ class CallManager(val chatModel: ChatModel) {
fun reportCallRemoteEnded(invitation: RcvCallInvitation) {
if (chatModel.activeCallInvitation.value?.contact?.id == invitation.contact.id) {
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.layout.*
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.graphics.Color
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.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import chat.simplex.app.R
import chat.simplex.app.SimplexApp
import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.ProfileImage
import chat.simplex.app.views.usersettings.ProfilePreview
import chat.simplex.common.model.*
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.ProfileImage
import chat.simplex.common.views.usersettings.ProfilePreview
import chat.simplex.common.platform.ntfManager
import chat.simplex.common.platform.SoundPlayer
import chat.simplex.res.MR
import kotlinx.datetime.Clock
@Composable
fun IncomingCallAlertView(invitation: RcvCallInvitation, chatModel: ChatModel) {
val cm = chatModel.callManager
val cxt = LocalContext.current
val scope = rememberCoroutineScope()
LaunchedEffect(true) { SoundPlayer.shared.start(scope, sound = !chatModel.showCallView.value) }
DisposableEffect(true) { onDispose { SoundPlayer.shared.stop() } }
LaunchedEffect(true) { SoundPlayer.start(scope, sound = !chatModel.showCallView.value) }
DisposableEffect(true) { onDispose { SoundPlayer.stop() } }
IncomingCallAlertLayout(
invitation,
chatModel,
rejectCall = { cm.endCall(invitation = invitation) },
ignoreCall = {
chatModel.activeCallInvitation.value = null
chatModel.controller.ntfManager.cancelCallNotification()
ntfManager.cancelCallNotification()
},
acceptCall = { cm.acceptIncomingCall(invitation = invitation) }
)
@ -114,7 +112,7 @@ fun PreviewIncomingCallAlertLayout() {
sharedKey = null,
callTs = Clock.System.now()
),
chatModel = SimplexApp.context.chatModel,
chatModel = ChatModel,
rejectCall = {},
ignoreCall = {},
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 dev.icerock.moko.resources.compose.stringResource
import chat.simplex.app.model.*
import chat.simplex.app.views.helpers.generalGetString
import chat.simplex.common.views.helpers.generalGetString
import chat.simplex.common.model.*
import chat.simplex.res.MR
import kotlinx.datetime.Instant
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 InfoRowEllipsis
@ -8,8 +8,7 @@ import SectionItemView
import SectionSpacer
import SectionTextFooter
import SectionView
import android.widget.Toast
import androidx.activity.compose.BackHandler
import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
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.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import chat.simplex.app.SimplexApp
import chat.simplex.app.model.*
import chat.simplex.app.platform.shareText
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.newchat.QRCode
import chat.simplex.app.views.usersettings.*
import chat.simplex.common.model.*
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.newchat.QRCode
import chat.simplex.common.views.usersettings.*
import chat.simplex.common.platform.*
import chat.simplex.res.MR
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.*
@ -135,7 +132,7 @@ fun deleteContactDialog(chatInfo: ChatInfo, chatModel: ChatModel, close: (() ->
if (r) {
chatModel.removeChat(chatInfo.id)
chatModel.chatId.value = null
chatModel.controller.ntfManager.cancelNotificationsForChat(chatInfo.id)
ntfManager.cancelNotificationsForChat(chatInfo.id)
close?.invoke()
}
}
@ -154,7 +151,7 @@ fun clearChatDialog(chatInfo: ChatInfo, chatModel: ChatModel, close: (() -> Unit
val updatedChatInfo = chatModel.controller.apiClearChat(chatInfo.chatType, chatInfo.apiId)
if (updatedChatInfo != null) {
chatModel.clearChat(updatedChatInfo)
chatModel.controller.ntfManager.cancelNotificationsForChat(chatInfo.id)
ntfManager.cancelNotificationsForChat(chatInfo.id)
close?.invoke()
}
}
@ -214,7 +211,8 @@ fun ChatInfoLayout(
if (contact.contactLink != null) {
SectionView(stringResource(MR.strings.address_section_title).uppercase()) {
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))
}
SectionDividerSpaced()
@ -397,7 +395,7 @@ fun SimplexServers(text: String, servers: List<String>) {
val clipboardManager: ClipboardManager = LocalClipboardManager.current
InfoRowEllipsis(text, info) {
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 SectionBottomSpacer
@ -12,20 +12,21 @@ import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalClipboardManager
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.stringResource
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.app.model.*
import chat.simplex.app.platform.copyText
import chat.simplex.app.platform.shareText
import chat.simplex.app.ui.theme.CurrentColors
import chat.simplex.app.ui.theme.DEFAULT_PADDING
import chat.simplex.app.views.chat.item.ItemAction
import chat.simplex.app.views.chat.item.MarkdownText
import chat.simplex.app.views.helpers.*
import chat.simplex.common.model.*
import chat.simplex.common.ui.theme.CurrentColors
import chat.simplex.common.ui.theme.DEFAULT_PADDING
import chat.simplex.common.views.chat.item.ItemAction
import chat.simplex.common.views.chat.item.MarkdownText
import chat.simplex.common.views.helpers.*
import chat.simplex.common.platform.shareText
import chat.simplex.res.MR
@Composable
@ -81,13 +82,15 @@ fun ChatItemInfoView(ci: ChatItem, ciInfo: ChatItemInfo, devTools: Boolean) {
}
}
if (text != "") {
val clipboard = LocalClipboardManager.current
DefaultDropdownMenu(showMenu) {
ItemAction(stringResource(MR.strings.share_verb), painterResource(MR.images.ic_share), onClick = {
shareText(text)
clipboard.shareText(text)
showMenu.value = false
})
val clipboard = LocalClipboardManager.current
ItemAction(stringResource(MR.strings.copy_verb), painterResource(MR.images.ic_content_copy), onClick = {
copyText(text)
clipboard.setText(AnnotatedString(text))
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 android.graphics.Bitmap
import android.net.Uri
import android.util.Log
import androidx.activity.compose.BackHandler
import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.foundation.*
import androidx.compose.foundation.gestures.*
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.intl.Locale
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.core.content.FileProvider
import chat.simplex.app.*
import chat.simplex.app.model.*
import chat.simplex.app.platform.*
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.call.*
import chat.simplex.app.views.chat.group.*
import chat.simplex.app.views.chat.item.*
import chat.simplex.app.views.chatlist.*
import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.helpers.AppBarHeight
import com.google.accompanist.insets.ProvideWindowInsets
import com.google.accompanist.insets.navigationBarsWithImePadding
import chat.simplex.common.model.*
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.call.*
import chat.simplex.common.views.chat.group.*
import chat.simplex.common.views.chat.item.*
import chat.simplex.common.views.chatlist.*
import chat.simplex.common.views.helpers.*
import chat.simplex.common.model.GroupInfo
import chat.simplex.common.platform.*
import chat.simplex.common.platform.AudioPlayer
import chat.simplex.res.MR
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import kotlinx.datetime.Clock
import java.io.File
import java.net.URI
import kotlin.math.sign
@Composable
@ -101,8 +95,7 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: () -> Unit) {
.collect { activeChat.value = it }
}
}
val view = LocalView.current
val context = LocalContext.current
val view = LocalMultiplatformView()
if (activeChat.value == null || user == null) {
chatModel.chatId.value = null
} else {
@ -114,6 +107,7 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: () -> Unit) {
chatModel.chats.firstOrNull { chat -> chat.chatInfo.id == chatId }?.chatStats?.unreadCount ?: 0
}
}
val clipboard = LocalClipboardManager.current
ChatLayout(
chat,
@ -279,7 +273,7 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: () -> Unit) {
val ciInfo = chatModel.controller.apiGetChatItemInfo(cInfo.chatType, cInfo.apiId, cItem.id)
if (ciInfo != null) {
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())
}
@ -297,7 +291,7 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: () -> Unit) {
},
markRead = { range, unreadCountAfter ->
chatModel.markChatItemsRead(chat.chatInfo, range, unreadCountAfter)
chatModel.controller.ntfManager.cancelNotificationsForChat(chat.id)
ntfManager.cancelNotificationsForChat(chat.id)
withBGApi {
chatModel.controller.apiChatRead(
chat.chatInfo.chatType,
@ -422,7 +416,7 @@ fun ChatInfoToolbar(
val barButtons = arrayListOf<@Composable RowScope.() -> Unit>()
val menuItems = arrayListOf<@Composable () -> Unit>()
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
showSearch = true
})
@ -602,8 +596,8 @@ fun BoxWithConstraintsScope.ChatItemsList(
.distinctUntilChanged()
.filter { !stopListening }
.collect {
onComposed()
stopListening = true
onComposed()
stopListening = true
}
}
DisposableEffectOnGone(
@ -966,8 +960,8 @@ private fun markUnreadChatAsRead(activeChat: MutableState<Chat?>, chatModel: Cha
}
sealed class ProviderMedia {
data class Image(val uri: Uri, val image: Bitmap): ProviderMedia()
data class Video(val uri: Uri, val preview: String): ProviderMedia()
data class Image(val uri: URI, val image: ImageBitmap): ProviderMedia()
data class Video(val uri: URI, val preview: String): ProviderMedia()
}
private fun providerForGallery(
@ -1004,17 +998,17 @@ private fun providerForGallery(
val item = item(internalIndex, initialChatId)?.second ?: return null
return when (item.content.msgContent) {
is MsgContent.MCImage -> {
val imageBitmap: Bitmap? = getLoadedImage(item.file)
val imageBitmap: ImageBitmap? = getLoadedImage(item.file)
val filePath = getLoadedFilePath(item.file)
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)
} else null
}
is MsgContent.MCVideo -> {
val filePath = getLoadedFilePath(item.file)
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)
} else null
}
@ -1058,12 +1052,11 @@ private fun ViewConfiguration.bigTouchSlop(slop: Float = 50f) = object: ViewConf
override val touchSlop: Float get() = slop
}
@Preview(showBackground = true)
@Preview(
@Preview/*(
uiMode = Configuration.UI_MODE_NIGHT_YES,
showBackground = true,
name = "Dark Mode"
)
)*/
@Composable
fun PreviewChatLayout() {
SimpleXTheme {
@ -1125,7 +1118,7 @@ fun PreviewChatLayout() {
}
}
@Preview(showBackground = true)
@Preview
@Composable
fun PreviewGroupChatLayout() {
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.layout.*
import androidx.compose.material.*
@ -7,10 +10,8 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import chat.simplex.app.R
import chat.simplex.app.ui.theme.*
import chat.simplex.common.ui.theme.*
import chat.simplex.res.MR
@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.background
@ -10,15 +10,13 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
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.stringResource
import androidx.compose.ui.unit.dp
import chat.simplex.app.R
import chat.simplex.app.platform.base64ToBitmap
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.UploadContent
import chat.simplex.common.platform.base64ToBitmap
import chat.simplex.res.MR
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.UploadContent
@Composable
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]
if (content is UploadContent.Video) {
Box(contentAlignment = Alignment.Center) {
val imageBitmap = base64ToBitmap(item).asImageBitmap()
val imageBitmap = base64ToBitmap(item)
Image(
imageBitmap,
"preview video",
@ -53,7 +51,7 @@ fun ComposeImageView(media: ComposePreview.MediaPreview, cancelImages: () -> Uni
)
}
} else {
val imageBitmap = base64ToBitmap(item).asImageBitmap()
val imageBitmap = base64ToBitmap(item)
Image(
imageBitmap,
"preview image",

View File

@ -1,11 +1,6 @@
@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.shape.CircleShape
import androidx.compose.material.*
@ -15,19 +10,20 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
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.stringResource
import androidx.compose.ui.unit.dp
import chat.simplex.app.model.*
import chat.simplex.app.platform.*
import chat.simplex.app.views.chat.item.*
import chat.simplex.app.views.helpers.*
import chat.simplex.common.model.*
import chat.simplex.common.platform.*
import chat.simplex.common.views.chat.item.*
import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.serialization.*
import java.io.File
import java.net.URI
import java.nio.file.Files
@Serializable
@ -36,7 +32,7 @@ sealed class ComposePreview {
@Serializable class CLinkPreview(val linkPreview: LinkPreview?): 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 class FilePreview(val fileName: String, val uri: Uri): ComposePreview()
@Serializable class FilePreview(val fileName: String, val uri: URI): ComposePreview()
}
@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
fun ComposeView(
chatModel: ChatModel,
@ -159,7 +163,6 @@ fun ComposeView(
attachmentOption: MutableState<AttachmentOption?>,
showChooseAttachment: () -> Unit
) {
val context = LocalContext.current
val linkUrl = rememberSaveable { mutableStateOf<String?>(null) }
val prevLinkUrl = rememberSaveable { mutableStateOf<String?>(null) }
val pendingLinkUrl = rememberSaveable { mutableStateOf<String?>(null) }
@ -168,11 +171,11 @@ fun ComposeView(
val maxFileSize = getMaxFileSize(FileProtocol.XFTP)
val smallFont = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onBackground)
val textStyle = remember { mutableStateOf(smallFont) }
val processPickedMedia = { uris: List<Uri>, text: String? ->
val processPickedMedia = { uris: List<URI>, text: String? ->
val content = ArrayList<UploadContent>()
val imagesPreview = ArrayList<String>()
uris.forEach { uri ->
var bitmap: Bitmap? = null
var bitmap: ImageBitmap? = null
when {
isImage(uri) -> {
// Image
@ -210,7 +213,7 @@ fun ComposeView(
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) {
val fileSize = getFileSize(uri)
if (fileSize != null && fileSize <= maxFileSize) {
@ -312,6 +315,8 @@ fun ComposeView(
return null
}
suspend fun sendMessageAsync(text: String?, live: Boolean, ttl: Int?): ChatItem? {
val cInfo = chat.chatInfo
val cs = composeState.value
@ -402,7 +407,7 @@ fun ComposeView(
is ComposePreview.VoicePreview -> {
val tmpFile = File(preview.voice)
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) {
Files.move(tmpFile.toPath(), actualFile.toPath())
}
@ -434,7 +439,7 @@ fun ComposeView(
(cs.preview is ComposePreview.MediaPreview ||
cs.preview is ComposePreview.FilePreview ||
cs.preview is ComposePreview.VoicePreview)
) {
) {
sent = send(cInfo, MsgContent.MCText(msgText), quotedItemId, null, live, ttl)
}
}
@ -492,7 +497,7 @@ fun ComposeView(
recState.value = RecordingState.NotStarted
composeState.value = composeState.value.copy(preview = ComposePreview.NoPreview)
withBGApi {
RecorderNative.stopRecording?.invoke()
RecorderInterface.stopRecording?.invoke()
AudioPlayer.stop(filePath)
filePath?.let { File(it).delete() }
}
@ -699,32 +704,26 @@ fun ComposeView(
}
}
val activity = LocalContext.current as Activity
DisposableEffect(Unit) {
val orientation = activity.resources.configuration.orientation
onDispose {
if (orientation == activity.resources.configuration.orientation) {
val cs = composeState.value
if (cs.liveMessage != null && (cs.message.isNotEmpty() || cs.liveMessage.sent)) {
sendMessage(null)
resetLinkPreview()
clearCurrentDraft()
deleteUnusedFiles()
} else if (composeState.value.inProgress) {
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()
DisposableEffectOnGone {
val cs = composeState.value
if (cs.liveMessage != null && (cs.message.isNotEmpty() || cs.liveMessage.sent)) {
sendMessage(null)
resetLinkPreview()
clearCurrentDraft()
deleteUnusedFiles()
} else if (composeState.value.inProgress) {
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()
}
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.desktop.ui.tooling.preview.Preview
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
@ -12,13 +15,12 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity
import dev.icerock.moko.resources.compose.painterResource
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.sp
import chat.simplex.app.R
import chat.simplex.app.model.durationText
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.*
import chat.simplex.common.model.durationText
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.*
import chat.simplex.common.platform.AudioPlayer
import chat.simplex.res.MR
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 SectionBottomSpacer
@ -15,11 +15,10 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import dev.icerock.moko.resources.compose.stringResource
import chat.simplex.app.R
import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.usersettings.PreferenceToggle
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.usersettings.PreferenceToggle
import chat.simplex.common.model.*
import chat.simplex.res.MR
@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.layout.*
@ -10,12 +10,11 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import dev.icerock.moko.resources.compose.painterResource
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 chat.simplex.app.R
import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.chat.item.*
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.chat.item.*
import chat.simplex.common.model.*
import chat.simplex.res.MR
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.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import chat.simplex.app.ui.theme.DEFAULT_PADDING
import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.newchat.QRCodeScanner
import chat.simplex.common.ui.theme.DEFAULT_PADDING
import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.newchat.QRCodeScanner
import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.stringResource
@Composable
expect fun ScanCodeView(verifyCode: (String?, cb: (Boolean) -> Unit) -> Unit, close: () -> Unit)
@Composable
fun ScanCodeLayout(verifyCode: (String?, cb: (Boolean) -> Unit) -> Unit, close: () -> Unit) {
Column(

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