multiplatform: make ChatModel and ChatController as object (#2639)

* multiplatform: make ChatModel and ChatController as object

* fix off uninitialized

* more contexts without Context

* smaller diff

* name

* refactor

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
This commit is contained in:
Stanislav Dmitrenko
2023-07-03 17:55:37 +03:00
committed by GitHub
parent 3bd5fc7463
commit 9238ac3445
44 changed files with 276 additions and 309 deletions
@@ -24,7 +24,7 @@ import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.*
import chat.simplex.app.MainActivity.Companion.enteredBackground
import chat.simplex.app.model.*
import chat.simplex.app.model.NtfManager.Companion.getUserIdFromIntent
import chat.simplex.app.model.NtfManager.getUserIdFromIntent
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.SplashView
import chat.simplex.app.views.call.ActiveCallView
@@ -42,6 +42,7 @@ import chat.simplex.app.views.onboarding.*
import chat.simplex.app.views.usersettings.LAMode
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.distinctUntilChanged
import java.lang.ref.WeakReference
class MainActivity: FragmentActivity() {
companion object {
@@ -65,6 +66,7 @@ class MainActivity: FragmentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
SimplexApp.context.mainActivity = WeakReference(this)
// testJson()
val m = vm.chatModel
applyAppLocale(m.controller.appPrefs.appLanguage)
@@ -181,7 +183,6 @@ class MainActivity: FragmentActivity() {
else
generalGetString(R.string.auth_unlock),
selfDestruct = true,
this@MainActivity,
completed = { laResult ->
when (laResult) {
LAResult.Success ->
@@ -250,7 +251,6 @@ class MainActivity: FragmentActivity() {
authenticate(
generalGetString(R.string.auth_enable_simplex_lock),
generalGetString(R.string.auth_confirm_credential),
activity = activity,
completed = { laResult ->
when (laResult) {
LAResult.Success -> {
@@ -317,7 +317,6 @@ class MainActivity: FragmentActivity() {
generalGetString(R.string.auth_confirm_credential)
else
"",
activity = activity,
completed = { laResult ->
val prefPerformLA = m.controller.appPrefs.performLA
when (laResult) {
@@ -353,7 +352,6 @@ class MainActivity: FragmentActivity() {
generalGetString(R.string.auth_confirm_credential)
else
generalGetString(R.string.auth_disable_simplex_lock),
activity = activity,
completed = { laResult ->
val prefPerformLA = m.controller.appPrefs.performLA
val selfDestructPref = m.controller.appPrefs.selfDestruct
@@ -14,6 +14,7 @@ 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
@@ -37,13 +38,20 @@ external fun chatParseServer(str: String): String
external fun chatPasswordHash(pwd: String, salt: String): String
class SimplexApp: Application(), LifecycleEventObserver {
var mainActivity: WeakReference<MainActivity> = WeakReference(null)
val chatModel: ChatModel
get() = chatController.chatModel
val appPreferences: AppPreferences
get() = chatController.appPrefs
val chatController: ChatController = ChatController
var isAppOnForeground: Boolean = false
val defaultLocale: Locale = Locale.getDefault()
suspend fun initChatController(useKey: String? = null, confirmMigrations: MigrationConfirmation? = null, startChat: Boolean = true) {
val dbKey = useKey ?: DatabaseUtils.useDatabaseKey()
val dbAbsolutePathPrefix = getFilesDirectory(SimplexApp.context)
val dbAbsolutePathPrefix = getFilesDirectory()
val confirm = confirmMigrations ?: if (appPreferences.confirmDBUpgrades.get()) MigrationConfirmation.Error else MigrationConfirmation.YesUp
val migrated: Array<Any> = chatMigrateInit(dbAbsolutePathPrefix, dbKey, confirm.value)
val res: DBMigrationResult = kotlin.runCatching {
@@ -84,21 +92,6 @@ class SimplexApp: Application(), LifecycleEventObserver {
}
}
val chatModel: ChatModel
get() = chatController.chatModel
private val ntfManager: NtfManager by lazy {
NtfManager(applicationContext, appPreferences)
}
private val appPreferences: AppPreferences by lazy {
AppPreferences(applicationContext)
}
val chatController: ChatController by lazy {
ChatController(0L, ntfManager, applicationContext, appPreferences)
}
override fun onCreate() {
super.onCreate()
@@ -162,12 +155,12 @@ class SimplexApp: Application(), LifecycleEventObserver {
fun allowToStartServiceAfterAppExit() = with(chatModel.controller) {
appPrefs.notificationsMode.get() == NotificationsMode.SERVICE.name &&
(!NotificationsMode.SERVICE.requiresIgnoringBattery || isIgnoringBatteryOptimizations(chatModel.controller.appContext))
(!NotificationsMode.SERVICE.requiresIgnoringBattery || isIgnoringBatteryOptimizations())
}
private fun allowToStartPeriodically() = with(chatModel.controller) {
appPrefs.notificationsMode.get() == NotificationsMode.PERIODIC.name &&
(!NotificationsMode.PERIODIC.requiresIgnoringBattery || isIgnoringBatteryOptimizations(chatModel.controller.appContext))
(!NotificationsMode.PERIODIC.requiresIgnoringBattery || isIgnoringBatteryOptimizations())
}
/*
@@ -34,7 +34,8 @@ import kotlin.time.*
* Without this annotation an animation from ChatList to ChatView has 1 frame per the whole animation. Don't delete it
* */
@Stable
class ChatModel(val controller: ChatController) {
object ChatModel {
val controller: ChatController = ChatController
val onboardingStage = mutableStateOf<OnboardingStage?>(null)
val currentUser = mutableStateOf<User?>(null)
val users = mutableStateListOf<UserInfo>()
@@ -65,11 +66,11 @@ class ChatModel(val controller: ChatController) {
val appOpenUrl = mutableStateOf<Uri?>(null)
// preferences
val notificationsMode = mutableStateOf(NotificationsMode.default)
var notificationPreviewMode = mutableStateOf(NotificationPreviewMode.default)
val performLA = mutableStateOf(false)
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 showAdvertiseLAUnavailableAlert = mutableStateOf(false)
var incognito = mutableStateOf(false)
val incognito by lazy { mutableStateOf(controller.appPrefs.incognito.get()) }
// current WebRTC call
val callManager = CallManager(this)
@@ -91,7 +92,7 @@ class ChatModel(val controller: ChatController) {
val sharedContent = mutableStateOf(null as SharedContent?)
val filesToDelete = mutableSetOf<File>()
val simplexLinkMode = mutableStateOf(controller.appPrefs.simplexLinkMode.get())
val simplexLinkMode by lazy { mutableStateOf(controller.appPrefs.simplexLinkMode.get()) }
fun getUser(userId: Long): User? = if (currentUser.value?.userId == userId) {
currentUser.value
@@ -1,15 +1,17 @@
package chat.simplex.app.model
import android.Manifest
import android.app.*
import android.app.TaskStackBuilder
import android.content.*
import android.content.pm.PackageManager
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.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.app.*
import chat.simplex.app.*
import chat.simplex.app.views.call.*
import chat.simplex.app.views.chatlist.acceptContactRequest
@@ -17,29 +19,29 @@ import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.usersettings.NotificationPreviewMode
import kotlinx.datetime.Clock
class NtfManager(val context: Context, private val appPreferences: AppPreferences) {
companion object {
const val MessageChannel: String = "chat.simplex.app.MESSAGE_NOTIFICATION"
const val MessageGroup: String = "chat.simplex.app.MESSAGE_NOTIFICATION"
const val OpenChatAction: String = "chat.simplex.app.OPEN_CHAT"
const val ShowChatsAction: String = "chat.simplex.app.SHOW_CHATS"
object NtfManager {
const val MessageChannel: String = "chat.simplex.app.MESSAGE_NOTIFICATION"
const val MessageGroup: String = "chat.simplex.app.MESSAGE_NOTIFICATION"
const val OpenChatAction: String = "chat.simplex.app.OPEN_CHAT"
const val ShowChatsAction: String = "chat.simplex.app.SHOW_CHATS"
// DO NOT change notification channel settings / names
const val CallChannel: String = "chat.simplex.app.CALL_NOTIFICATION_1"
const val AcceptCallAction: String = "chat.simplex.app.ACCEPT_CALL"
const val RejectCallAction: String = "chat.simplex.app.REJECT_CALL"
const val CallNotificationId: Int = -1
// DO NOT change notification channel settings / names
const val CallChannel: String = "chat.simplex.app.CALL_NOTIFICATION_1"
const val AcceptCallAction: String = "chat.simplex.app.ACCEPT_CALL"
const val RejectCallAction: String = "chat.simplex.app.REJECT_CALL"
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 context: Context
get() = SimplexApp.context
private const val UserIdKey: String = "userId"
private const val ChatIdKey: String = "chatId"
fun getUserIdFromIntent(intent: Intent?): Long? {
val userId = intent?.getLongExtra(UserIdKey, -1L)
return if (userId == -1L || userId == null) null else userId
}
fun getUserIdFromIntent(intent: Intent?): Long? {
val userId = intent?.getLongExtra(UserIdKey, -1L)
return if (userId == -1L || userId == null) null else userId
}
private val manager: NotificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
private val manager: NotificationManager = SimplexApp.context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
private var prevNtfTime = mutableMapOf<String, Long>()
private val msgNtfTimeoutMs = 30000L
@@ -58,7 +60,7 @@ class NtfManager(val context: Context, private val appPreferences: AppPreference
.setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
.build()
val soundUri = Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE + "://" + context.packageName + "/" + R.raw.ring_once)
Log.d(TAG,"callNotificationChannel sound: $soundUri")
Log.d(TAG, "callNotificationChannel sound: $soundUri")
callChannel.setSound(soundUri, attrs)
callChannel.enableVibration(true)
// the numbers below are explained here: https://developer.android.com/reference/android/os/Vibrator
@@ -70,8 +72,8 @@ class NtfManager(val context: Context, private val appPreferences: AppPreference
fun cancelNotificationsForChat(chatId: String) {
prevNtfTime.remove(chatId)
manager.cancel(chatId.hashCode())
val msgNtfs = manager.activeNotifications.filter {
ntf -> ntf.notification.channelId == MessageChannel
val msgNtfs = manager.activeNotifications.filter { ntf ->
ntf.notification.channelId == MessageChannel
}
if (msgNtfs.count() == 1) {
// Have a group notification with no children so cancel it
@@ -110,7 +112,6 @@ class NtfManager(val context: Context, private val appPreferences: AppPreference
val now = Clock.System.now().toEpochMilliseconds()
val recentNotification = (now - prevNtfTime.getOrDefault(chatId, 0) < msgNtfTimeoutMs)
prevNtfTime[chatId] = now
val previewMode = appPreferences.notificationPreviewMode.get()
val title = if (previewMode == NotificationPreviewMode.HIDDEN.name) generalGetString(R.string.notification_preview_somebody) else displayName
val content = if (previewMode != NotificationPreviewMode.MESSAGE.name) generalGetString(R.string.notification_preview_new_message) else msgText
@@ -145,7 +146,6 @@ class NtfManager(val context: Context, private val appPreferences: AppPreference
}
builder.addAction(0, actionButton, actionPendingIntent)
}
val summary = NotificationCompat.Builder(context, MessageChannel)
.setSmallIcon(R.drawable.ntf_icon)
.setColor(0x88FFFF)
@@ -157,14 +157,17 @@ class NtfManager(val context: Context, private val appPreferences: AppPreference
with(NotificationManagerCompat.from(context)) {
// using cInfo.id only shows one notification per chat and updates it when the message arrives
notify(chatId.hashCode(), builder.build())
notify(0, summary)
if (ActivityCompat.checkSelfPermission(SimplexApp.context, Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED) {
notify(chatId.hashCode(), builder.build())
notify(0, summary)
}
}
}
fun notifyCallInvitation(invitation: RcvCallInvitation) {
val keyguardManager = getKeyguardManager(context)
Log.d(TAG,
Log.d(
TAG,
"notifyCallInvitation pre-requests: " +
"keyguard locked ${keyguardManager.isKeyguardLocked}, " +
"callOnLockScreen ${appPreferences.callOnLockScreen.get()}, " +
@@ -223,7 +226,9 @@ class NtfManager(val context: Context, private val appPreferences: AppPreference
// This makes notification sound and vibration repeat endlessly
notification.flags = notification.flags or NotificationCompat.FLAG_INSISTENT
with(NotificationManagerCompat.from(context)) {
notify(CallNotificationId, notification)
if (ActivityCompat.checkSelfPermission(SimplexApp.context, Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED) {
notify(CallNotificationId, notification)
}
}
}
@@ -237,7 +242,7 @@ class NtfManager(val context: Context, private val appPreferences: AppPreference
fun hasNotificationsForChat(chatId: String): Boolean = manager.activeNotifications.any { it.id == chatId.hashCode() }
private fun hideSecrets(cItem: ChatItem) : String {
private fun hideSecrets(cItem: ChatItem): String {
val md = cItem.formattedText
return if (md != null) {
var res = ""
@@ -302,14 +307,16 @@ class NtfManager(val context: Context, private val appPreferences: AppPreference
}
val apiId = chatId.replace("<@", "").toLongOrNull() ?: return
acceptContactRequest(apiId, cInfo, isCurrentUser, m)
m.controller.ntfManager.cancelNotificationsForChat(chatId)
cancelNotificationsForChat(chatId)
}
RejectCallAction -> {
val invitation = m.callInvitations[chatId]
if (invitation != null) {
m.callManager.endCall(invitation = invitation)
}
}
else -> {
Log.e(TAG, "Unknown action. Make sure you provide action from NotificationAction enum")
}
@@ -58,9 +58,9 @@ enum class SimplexLinkMode {
}
}
class AppPreferences(val context: Context) {
private val sharedPreferences: SharedPreferences = context.getSharedPreferences(SHARED_PREFS_ID, Context.MODE_PRIVATE)
private val sharedPreferencesThemes: SharedPreferences = context.getSharedPreferences(SHARED_PREFS_THEMES_ID, Context.MODE_PRIVATE)
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)
@@ -298,21 +298,16 @@ class AppPreferences(val context: Context) {
private const val MESSAGE_TIMEOUT: Int = 15_000_000
open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val appContext: Context, val appPrefs: AppPreferences) {
val chatModel = ChatModel(this)
object ChatController {
var ctrl: ChatCtrl? = -1
val appPrefs: AppPreferences by lazy { AppPreferences() }
val ntfManager by lazy { NtfManager }
val chatModel = ChatModel
private var receiverStarted = false
var lastMsgReceivedTimestamp: Long = System.currentTimeMillis()
private set
init {
chatModel.notificationsMode.value =
kotlin.runCatching { NotificationsMode.valueOf(appPrefs.notificationsMode.get()!!) }.getOrDefault(NotificationsMode.default)
chatModel.notificationPreviewMode.value =
kotlin.runCatching { NotificationPreviewMode.valueOf(appPrefs.notificationPreviewMode.get()!!) }.getOrDefault(NotificationPreviewMode.default)
chatModel.performLA.value = appPrefs.performLA.get()
chatModel.incognito.value = appPrefs.incognito.get()
}
private fun currentUserId(funcName: String): Long {
val userId = chatModel.currentUser.value?.userId
if (userId == null) {
@@ -328,8 +323,8 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
try {
if (chatModel.chatRunning.value == true) return
apiSetNetworkConfig(getNetCfg())
apiSetTempFolder(getTempFilesDirectory(appContext))
apiSetFilesFolder(getAppFilesDirectory(appContext))
apiSetTempFolder(getTempFilesDirectory())
apiSetFilesFolder(getAppFilesDirectory())
apiSetXFTPConfig(getXFTPCfg())
val justStarted = apiStartChat()
val users = listUsers()
@@ -1602,7 +1597,7 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
mc is MsgContent.MCFile
&& fileName != null
) {
removeFile(appContext, fileName)
removeFile(fileName)
}
}
@@ -1671,7 +1666,7 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
if (!appPrefs.backgroundServiceNoticeShown.get()) {
// the branch for the new users who have never seen service notice
if (!mode.requiresIgnoringBattery || isIgnoringBatteryOptimizations(appContext)) {
if (!mode.requiresIgnoringBattery || isIgnoringBatteryOptimizations()) {
showBGServiceNotice(mode)
} else {
showBGServiceNoticeIgnoreOptimization(mode)
@@ -1679,7 +1674,7 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
// set both flags, so that if the user doesn't allow ignoring optimizations, the service will be disabled without additional notice
appPrefs.backgroundServiceNoticeShown.set(true)
appPrefs.backgroundServiceBatteryNoticeShown.set(true)
} else if (mode.requiresIgnoringBattery && !isIgnoringBatteryOptimizations(appContext)) {
} else if (mode.requiresIgnoringBattery && !isIgnoringBatteryOptimizations()) {
// the branch for users who have app installed, and have seen the service notice,
// but the battery optimization for the app is on (Android 12) AND the service is running
if (appPrefs.backgroundServiceBatteryNoticeShown.get()) {
@@ -1738,7 +1733,7 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
private fun showBGServiceNoticeIgnoreOptimization(mode: NotificationsMode) = AlertManager.shared.showAlert {
val ignoreOptimization = {
AlertManager.shared.hideAlert()
askAboutIgnoringBatteryOptimization(appContext)
askAboutIgnoringBatteryOptimization()
}
AlertDialog(
onDismissRequest = ignoreOptimization,
@@ -1800,19 +1795,19 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
)
}
fun isIgnoringBatteryOptimizations(context: Context): Boolean {
val powerManager = context.getSystemService(Application.POWER_SERVICE) as PowerManager
return powerManager.isIgnoringBatteryOptimizations(context.packageName)
fun isIgnoringBatteryOptimizations(): Boolean {
val powerManager = SimplexApp.context.getSystemService(Application.POWER_SERVICE) as PowerManager
return powerManager.isIgnoringBatteryOptimizations(SimplexApp.context.packageName)
}
private fun askAboutIgnoringBatteryOptimization(context: Context) {
private fun askAboutIgnoringBatteryOptimization() {
Intent().apply {
@SuppressLint("BatteryLife")
action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
data = Uri.parse("package:${context.packageName}")
data = Uri.parse("package:${SimplexApp.context.packageName}")
// This flag is needed when you start a new activity from non-Activity context
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
context.startActivity(this)
SimplexApp.context.startActivity(this)
}
}
@@ -128,7 +128,7 @@ fun TerminalLog(terminalItems: List<TerminalItem>) {
modifier = Modifier
.fillMaxWidth()
.clickable {
ModalManager.shared.showModal(endButtons = { ShareButton { shareText(context, item.details) } }) {
ModalManager.shared.showModal(endButtons = { ShareButton { shareText(item.details) } }) {
SelectionContainer(modifier = Modifier.verticalScroll(rememberScrollState())) {
Text(item.details, modifier = Modifier.padding(horizontal = DEFAULT_PADDING).padding(bottom = DEFAULT_PADDING))
}
@@ -33,7 +33,7 @@ import androidx.compose.ui.unit.sp
import chat.simplex.app.*
import chat.simplex.app.R
import chat.simplex.app.model.*
import chat.simplex.app.model.NtfManager.Companion.OpenChatAction
import chat.simplex.app.model.NtfManager.OpenChatAction
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.ProfileImage
import kotlinx.datetime.Clock
@@ -28,7 +28,7 @@ fun IncomingCallAlertView(invitation: RcvCallInvitation, chatModel: ChatModel) {
val cm = chatModel.callManager
val cxt = LocalContext.current
val scope = rememberCoroutineScope()
LaunchedEffect(true) { SoundPlayer.shared.start(cxt, scope, sound = !chatModel.showCallView.value) }
LaunchedEffect(true) { SoundPlayer.shared.start(scope, sound = !chatModel.showCallView.value) }
DisposableEffect(true) { onDispose { SoundPlayer.shared.stop() } }
IncomingCallAlertLayout(
invitation,
@@ -16,7 +16,7 @@ class SoundPlayer {
private var player: MediaPlayer? = null
var playing = false
fun start(cxt: Context, scope: CoroutineScope, sound: Boolean) {
fun start(scope: CoroutineScope, sound: Boolean) {
player?.reset()
player = MediaPlayer().apply {
setAudioAttributes(
@@ -28,7 +28,7 @@ class SoundPlayer {
setDataSource(SimplexApp.context, Uri.parse("android.resource://" + SimplexApp.context.packageName + "/" + R.raw.ring_once))
prepare()
}
val vibrator = ContextCompat.getSystemService(cxt, Vibrator::class.java)
val vibrator = ContextCompat.getSystemService(SimplexApp.context, Vibrator::class.java)
val effect = VibrationEffect.createOneShot(250, VibrationEffect.DEFAULT_AMPLITUDE)
playing = true
withScope(scope) {
@@ -211,10 +211,9 @@ fun ChatInfoLayout(
SectionDividerSpaced()
if (contact.contactLink != null) {
val context = LocalContext.current
SectionView(stringResource(R.string.address_section_title).uppercase()) {
QRCode(contact.contactLink, Modifier.padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF).aspectRatio(1f))
ShareAddressButton { shareText(context, contact.contactLink) }
ShareAddressButton { shareText(contact.contactLink) }
SectionTextFooter(stringResource(R.string.you_can_share_this_address_with_your_contacts).format(contact.displayName))
}
SectionDividerSpaced()
@@ -12,7 +12,6 @@ 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.LocalContext
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
@@ -32,7 +31,6 @@ fun ChatItemInfoView(ci: ChatItem, ciInfo: ChatItemInfo, devTools: Boolean) {
val sent = ci.chatDir.sent
val appColors = CurrentColors.collectAsState().value.appColors
val itemColor = if (sent) appColors.sentMessage else appColors.receivedMessage
val context = LocalContext.current
val uriHandler = LocalUriHandler.current
@Composable
@@ -83,11 +81,11 @@ fun ChatItemInfoView(ci: ChatItem, ciInfo: ChatItemInfo, devTools: Boolean) {
if (text != "") {
DefaultDropdownMenu(showMenu) {
ItemAction(stringResource(R.string.share_verb), painterResource(R.drawable.ic_share), onClick = {
shareText(context, text)
shareText(text)
showMenu.value = false
})
ItemAction(stringResource(R.string.copy_verb), painterResource(R.drawable.ic_content_copy), onClick = {
copyText(context, text)
copyText(text)
showMenu.value = false
})
}
@@ -278,7 +278,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(context, itemInfoShareText(cItem, ciInfo, chatModel.controller.appPrefs.developerTools.get()))
shareText(itemInfoShareText(cItem, ciInfo, chatModel.controller.appPrefs.developerTools.get()))
} }) {
ChatItemInfoView(cItem, ciInfo, devTools = chatModel.controller.appPrefs.developerTools.get())
}
@@ -976,7 +976,7 @@ private fun providerForGallery(
scrollTo: (Int) -> Unit
): ImageGalleryProvider {
fun canShowMedia(item: ChatItem): Boolean =
(item.content.msgContent is MsgContent.MCImage || item.content.msgContent is MsgContent.MCVideo) && (item.file?.loaded == true && getLoadedFilePath(SimplexApp.context, item.file) != null)
(item.content.msgContent is MsgContent.MCImage || item.content.msgContent is MsgContent.MCVideo) && (item.file?.loaded == true && getLoadedFilePath(item.file) != null)
fun item(skipInternalIndex: Int, initialChatId: Long): Pair<Int, ChatItem>? {
var processedInternalIndex = -skipInternalIndex.sign
@@ -1003,15 +1003,15 @@ private fun providerForGallery(
val item = item(internalIndex, initialChatId)?.second ?: return null
return when (item.content.msgContent) {
is MsgContent.MCImage -> {
val imageBitmap: Bitmap? = getLoadedImage(SimplexApp.context, item.file)
val filePath = getLoadedFilePath(SimplexApp.context, item.file)
val imageBitmap: Bitmap? = 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))
ProviderMedia.Image(uri, imageBitmap)
} else null
}
is MsgContent.MCVideo -> {
val filePath = getLoadedFilePath(SimplexApp.context, item.file)
val filePath = getLoadedFilePath(item.file)
if (filePath != null) {
val uri = FileProvider.getUriForFile(SimplexApp.context, "${BuildConfig.APPLICATION_ID}.provider", File(filePath))
ProviderMedia.Video(uri, (item.content.msgContent as MsgContent.MCVideo).image)
@@ -201,7 +201,7 @@ fun ComposeView(
val imagesPreview = ArrayList<String>()
uris.forEach { uri ->
var bitmap: Bitmap? = null
val isImage = MimeTypeMap.getSingleton().getMimeTypeFromExtension(getFileName(SimplexApp.context, uri)?.split(".")?.last())?.contains("image/") == true
val isImage = MimeTypeMap.getSingleton().getMimeTypeFromExtension(getFileName(uri)?.split(".")?.last())?.contains("image/") == true
when {
isImage -> {
// Image
@@ -209,10 +209,10 @@ fun ComposeView(
bitmap = if (drawable != null) getBitmapFromUri(uri) else null
val isAnimNewApi = Build.VERSION.SDK_INT >= 28 && drawable is AnimatedImageDrawable
val isAnimOldApi = Build.VERSION.SDK_INT < 28 &&
(getFileName(SimplexApp.context, uri)?.endsWith(".gif") == true || getFileName(SimplexApp.context, uri)?.endsWith(".webp") == true)
(getFileName(uri)?.endsWith(".gif") == true || getFileName(uri)?.endsWith(".webp") == true)
if (isAnimNewApi || isAnimOldApi) {
// It's a gif or webp
val fileSize = getFileSize(context, uri)
val fileSize = getFileSize(uri)
if (fileSize != null && fileSize <= maxFileSize) {
content.add(UploadContent.AnimatedImage(uri))
} else {
@@ -244,9 +244,9 @@ fun ComposeView(
}
val processPickedFile = { uri: Uri?, text: String? ->
if (uri != null) {
val fileSize = getFileSize(context, uri)
val fileSize = getFileSize(uri)
if (fileSize != null && fileSize <= maxFileSize) {
val fileName = getFileName(SimplexApp.context, uri)
val fileName = getFileName(uri)
if (fileName != null) {
composeState.value = composeState.value.copy(message = text ?: composeState.value.message, preview = ComposePreview.FilePreview(fileName, uri))
}
@@ -381,7 +381,7 @@ fun ComposeView(
chatModel.addChatItem(cInfo, aChatItem.chatItem)
return aChatItem.chatItem
}
if (file != null) removeFile(context, file)
if (file != null) removeFile(file)
return null
}
@@ -460,9 +460,9 @@ fun ComposeView(
is ComposePreview.MediaPreview -> {
preview.content.forEachIndexed { index, it ->
val file = when (it) {
is UploadContent.SimpleImage -> saveImage(context, it.uri)
is UploadContent.AnimatedImage -> saveAnimImage(context, it.uri)
is UploadContent.Video -> saveFileFromUri(context, it.uri)
is UploadContent.SimpleImage -> saveImage(it.uri)
is UploadContent.AnimatedImage -> saveAnimImage(it.uri)
is UploadContent.Video -> saveFileFromUri(it.uri)
}
if (file != null) {
files.add(file)
@@ -477,7 +477,7 @@ fun ComposeView(
is ComposePreview.VoicePreview -> {
val tmpFile = File(preview.voice)
AudioPlayer.stop(tmpFile.absolutePath)
val actualFile = File(getAppFilePath(SimplexApp.context, tmpFile.name.replaceAfter(RecorderNative.extension, "")))
val actualFile = File(getAppFilePath(tmpFile.name.replaceAfter(RecorderNative.extension, "")))
withContext(Dispatchers.IO) {
Files.move(tmpFile.toPath(), actualFile.toPath())
}
@@ -486,7 +486,7 @@ fun ComposeView(
msgs.add(MsgContent.MCVoice(if (msgs.isEmpty()) msgText else "", preview.durationMs / 1000))
}
is ComposePreview.FilePreview -> {
val file = saveFileFromUri(context, preview.uri)
val file = saveFileFromUri(preview.uri)
if (file != null) {
files.add((file))
msgs.add(MsgContent.MCFile(if (msgs.isEmpty()) msgText else ""))
@@ -10,7 +10,6 @@ import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontFamily
@@ -86,9 +85,8 @@ private fun VerifyCodeLayout(
maxLines = 20
)
}
val context = LocalContext.current
Box(Modifier.weight(1f)) {
IconButton({ shareText(context, connectionCode) }, Modifier.size(20.dp).align(Alignment.CenterStart)) {
IconButton({ shareText(connectionCode) }, Modifier.size(20.dp).align(Alignment.CenterStart)) {
Icon(painterResource(R.drawable.ic_share_filled), null, tint = MaterialTheme.colors.primary)
}
}
@@ -10,7 +10,6 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
@@ -26,7 +25,6 @@ fun GroupLinkView(chatModel: ChatModel, groupInfo: GroupInfo, connReqContact: St
var groupLink by rememberSaveable { mutableStateOf(connReqContact) }
val groupLinkMemberRole = rememberSaveable { mutableStateOf(memberRole) }
var creatingLink by rememberSaveable { mutableStateOf(false) }
val cxt = LocalContext.current
fun createLink() {
creatingLink = true
withApi {
@@ -50,7 +48,7 @@ fun GroupLinkView(chatModel: ChatModel, groupInfo: GroupInfo, connReqContact: St
groupLinkMemberRole,
creatingLink,
createLink = ::createLink,
share = { shareText(cxt, groupLink ?: return@GroupLinkLayout) },
share = { shareText(groupLink ?: return@GroupLinkLayout) },
updateLink = {
val role = groupLinkMemberRole.value
if (role != null) {
@@ -220,7 +220,7 @@ fun GroupMemberInfoLayout(
val context = LocalContext.current
SectionView(stringResource(R.string.address_section_title).uppercase()) {
QRCode(member.contactLink, Modifier.padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF).aspectRatio(1f))
ShareAddressButton { shareText(context, member.contactLink) }
ShareAddressButton { shareText(member.contactLink) }
if (contactId != null) {
if (knownDirectChat(contactId) == null && !groupInfo.fullGroupPreferences.directMessages.on) {
ConnectViaAddressButton(onClick = { connectViaAddress(member.contactLink) })
@@ -98,7 +98,7 @@ private fun GroupWelcomeLayout(
},
wt.value.isEmpty()
)
CopyTextButton { copyText(SimplexApp.context, wt.value) }
CopyTextButton { copyText(wt.value) }
SectionDividerSpaced(maxBottomPadding = false)
SaveButton(
save = save,
@@ -106,7 +106,7 @@ private fun GroupWelcomeLayout(
)
} else {
TextPreview(wt.value, linkMode)
CopyTextButton { copyText(SimplexApp.context, wt.value) }
CopyTextButton { copyText(wt.value) }
}
SectionBottomSpacer()
}
@@ -32,7 +32,7 @@ fun CIFileView(
receiveFile: (Long) -> Unit
) {
val context = LocalContext.current
val saveFileLauncher = rememberSaveFileLauncher(cxt = context, ciFile = file)
val saveFileLauncher = rememberSaveFileLauncher(ciFile = file)
@Composable
fun fileIcon(
@@ -95,7 +95,7 @@ fun CIFileView(
)
}
is CIFileStatus.RcvComplete -> {
val filePath = getLoadedFilePath(context, file)
val filePath = getLoadedFilePath(file)
if (filePath != null) {
saveFileLauncher.launch(file.fileName)
} else {
@@ -139,8 +139,8 @@ fun CIImageView(
}
fun imageAndFilePath(file: CIFile?): Pair<Bitmap?, String?> {
val imageBitmap: Bitmap? = getLoadedImage(SimplexApp.context, file)
val filePath = getLoadedFilePath(SimplexApp.context, file)
val imageBitmap: Bitmap? = getLoadedImage(file)
val filePath = getLoadedFilePath(file)
return imageBitmap to filePath
}
@@ -160,7 +160,7 @@ fun CIImageView(
val view = LocalView.current
imageView(imagePainter, onClick = {
hideKeyboard(view)
if (getLoadedFilePath(context, file) != null) {
if (getLoadedFilePath(file) != null) {
ModalManager.shared.showCustomModal(animated = false) { close ->
ImageFullScreenView(imageProvider, close)
}
@@ -1,6 +1,5 @@
package chat.simplex.app.views.chat.item
import SectionSpacer
import SectionView
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
@@ -8,7 +7,6 @@ import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontStyle
@@ -33,9 +31,8 @@ fun InvalidJSONView(json: String) {
Column {
Spacer(Modifier.height(DEFAULT_PADDING))
SectionView {
val context = LocalContext.current
SettingsActionItem(painterResource(R.drawable.ic_share), generalGetString(R.string.share_verb), click = {
shareText(context, json)
shareText(json)
})
}
Column(Modifier.padding(DEFAULT_PADDING).fillMaxWidth().verticalScroll(rememberScrollState())) {
@@ -46,7 +46,7 @@ fun CIVideoView(
contentAlignment = Alignment.TopEnd
) {
val context = LocalContext.current
val filePath = remember(file) { getLoadedFilePath(SimplexApp.context, file) }
val filePath = remember(file) { getLoadedFilePath(file) }
val preview = remember(image) { base64ToBitmap(image) }
if (file != null && filePath != null) {
val uri = remember(filePath) { FileProvider.getUriForFile(context, "${BuildConfig.APPLICATION_ID}.provider", File(filePath)) }
@@ -100,7 +100,7 @@ fun CIVideoView(
@Composable
private fun VideoView(uri: Uri, file: CIFile, defaultPreview: Bitmap, defaultDuration: Long, showMenu: MutableState<Boolean>, onClick: () -> Unit) {
val context = LocalContext.current
val player = remember(uri) { VideoPlayer.getOrCreate(uri, false, defaultPreview, defaultDuration, true, context) }
val player = remember(uri) { VideoPlayer.getOrCreate(uri, false, defaultPreview, defaultDuration, true) }
val videoPlaying = remember(uri.path) { player.videoPlaying }
val progress = remember(uri.path) { player.progress }
val duration = remember(uri.path) { player.duration }
@@ -44,7 +44,7 @@ fun CIVoiceView(
) {
if (file != null) {
val context = LocalContext.current
val filePath = remember(file.filePath, file.fileStatus) { getLoadedFilePath(context, file) }
val filePath = remember(file.filePath, file.fileStatus) { getLoadedFilePath(file) }
var brokenAudio by rememberSaveable(file.filePath) { mutableStateOf(false) }
val audioPlaying = rememberSaveable(file.filePath) { mutableStateOf(false) }
val progress = rememberSaveable(file.filePath) { mutableStateOf(0) }
@@ -3,7 +3,6 @@ package chat.simplex.app.views.chat.item
import android.Manifest
import android.os.Build
import androidx.compose.foundation.*
import androidx.compose.foundation.gestures.scrollable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
@@ -21,13 +20,11 @@ import androidx.compose.ui.text.font.FontWeight
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.app.ui.theme.*
import chat.simplex.app.views.chat.*
import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.usersettings.IncognitoView
import com.google.accompanist.permissions.rememberPermissionState
import kotlinx.datetime.Clock
@@ -59,7 +56,7 @@ fun ChatItemView(
val showMenu = remember { mutableStateOf(false) }
val revealed = remember { mutableStateOf(false) }
val fullDeleteAllowed = remember(cInfo) { cInfo.featureEnabled(ChatFeature.FullDelete) }
val saveFileLauncher = rememberSaveFileLauncher(cxt = context, ciFile = cItem.file)
val saveFileLauncher = rememberSaveFileLauncher(ciFile = cItem.file)
val onLinkLongClick = { _: String -> showMenu.value = true }
val live = composeState.value.liveMessage != null
@@ -176,26 +173,26 @@ fun ChatItemView(
})
}
ItemAction(stringResource(R.string.share_verb), painterResource(R.drawable.ic_share), onClick = {
val filePath = getLoadedFilePath(SimplexApp.context, cItem.file)
val filePath = getLoadedFilePath(cItem.file)
when {
filePath != null -> shareFile(context, cItem.text, filePath)
else -> shareText(context, cItem.content.text)
filePath != null -> shareFile(cItem.text, filePath)
else -> shareText(cItem.content.text)
}
showMenu.value = false
})
ItemAction(stringResource(R.string.copy_verb), painterResource(R.drawable.ic_content_copy), onClick = {
copyText(context, cItem.content.text)
copyText(cItem.content.text)
showMenu.value = false
})
if (cItem.content.msgContent is MsgContent.MCImage || cItem.content.msgContent is MsgContent.MCVideo || cItem.content.msgContent is MsgContent.MCFile || cItem.content.msgContent is MsgContent.MCVoice) {
val filePath = getLoadedFilePath(context, cItem.file)
val filePath = getLoadedFilePath(cItem.file)
if (filePath != null) {
val writePermissionState = rememberPermissionState(permission = Manifest.permission.WRITE_EXTERNAL_STORAGE)
ItemAction(stringResource(R.string.save_verb), painterResource(R.drawable.ic_download), onClick = {
when (cItem.content.msgContent) {
is MsgContent.MCImage -> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R || writePermissionState.hasPermission) {
saveImage(context, cItem.file)
saveImage(cItem.file)
} else {
writePermissionState.launchPermissionRequest()
}
@@ -178,7 +178,7 @@ fun ImageFullScreenView(imageProvider: () -> ImageGalleryProvider, close: () ->
@Composable
private fun VideoView(modifier: Modifier, uri: Uri, defaultPreview: Bitmap, currentPage: Boolean) {
val context = LocalContext.current
val player = remember(uri) { VideoPlayer.getOrCreate(uri, true, defaultPreview, 0L, true, context) }
val player = remember(uri) { VideoPlayer.getOrCreate(uri, true, defaultPreview, 0L, true) }
val isCurrentPage = rememberUpdatedState(currentPage)
val play = {
player.play(true)
@@ -14,15 +14,14 @@ import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import chat.simplex.app.*
import chat.simplex.app.R
import chat.simplex.app.TAG
import chat.simplex.app.model.ChatModel
import chat.simplex.app.ui.theme.SimpleXTheme
import chat.simplex.app.views.helpers.*
@@ -36,8 +35,8 @@ import java.util.*
@Composable
fun ChatArchiveView(m: ChatModel, title: String, archiveName: String, archiveTime: Instant) {
val context = LocalContext.current
val archivePath = "${getFilesDirectory(context)}/$archiveName"
val saveArchiveLauncher = rememberSaveArchiveLauncher(cxt = context, archivePath)
val archivePath = "${getFilesDirectory()}/$archiveName"
val saveArchiveLauncher = rememberSaveArchiveLauncher(archivePath)
ChatArchiveLayout(
title,
archiveTime,
@@ -82,10 +81,11 @@ fun ChatArchiveLayout(
}
@Composable
private fun rememberSaveArchiveLauncher(cxt: Context, chatArchivePath: String): ManagedActivityResultLauncher<String, Uri?> =
private fun rememberSaveArchiveLauncher(chatArchivePath: String): ManagedActivityResultLauncher<String, Uri?> =
rememberLauncherForActivityResult(
contract = ActivityResultContracts.CreateDocument(),
onResult = { destination ->
val cxt = SimplexApp.context
try {
destination?.let {
val contentResolver = cxt.contentResolver
@@ -497,7 +497,7 @@ fun PreviewDatabaseEncryptionLayout() {
SimpleXTheme {
DatabaseEncryptionLayout(
useKeychain = remember { mutableStateOf(true) },
prefs = AppPreferences(SimplexApp.context),
prefs = AppPreferences(),
chatDbEncrypted = true,
currentKey = remember { mutableStateOf("") },
newKey = remember { mutableStateOf("") },
@@ -42,7 +42,7 @@ fun DatabaseErrorView(
var storedDBKey by remember { mutableStateOf(DatabaseUtils.ksDatabasePassword.get()) }
var useKeychain by remember { mutableStateOf(appPreferences.storeDBPassphrase.get()) }
val context = LocalContext.current
val restoreDbFromBackup = remember { mutableStateOf(shouldShowRestoreDbButton(appPreferences, context)) }
val restoreDbFromBackup = remember { mutableStateOf(shouldShowRestoreDbButton(appPreferences)) }
fun callRunChat(confirmMigrations: MigrationConfirmation? = null) {
val useKey = if (useKeychain) null else dbKey.value
@@ -164,7 +164,7 @@ fun DatabaseErrorView(
title = generalGetString(R.string.restore_database_alert_title),
text = generalGetString(R.string.restore_database_alert_desc),
confirmText = generalGetString(R.string.restore_database_alert_confirm),
onConfirm = { restoreDb(restoreDbFromBackup, appPreferences, context) },
onConfirm = { restoreDb(restoreDbFromBackup, appPreferences) },
destructive = true,
)
}
@@ -226,7 +226,8 @@ private fun runChat(
}
}
private fun shouldShowRestoreDbButton(prefs: AppPreferences, context: Context): Boolean {
private fun shouldShowRestoreDbButton(prefs: AppPreferences): Boolean {
val context = SimplexApp.context
val startedAt = prefs.encryptionStartedAt.get() ?: return false
/** Just in case there is any small difference between reported Java's [Clock.System.now] and Linux's time on a file */
val safeDiffInTime = 10_000L
@@ -238,7 +239,8 @@ private fun shouldShowRestoreDbButton(prefs: AppPreferences, context: Context):
startedAt.toEpochMilliseconds() - safeDiffInTime <= filesAgent.lastModified()
}
private fun restoreDb(restoreDbFromBackup: MutableState<Boolean>, prefs: AppPreferences, context: Context) {
private fun restoreDb(restoreDbFromBackup: MutableState<Boolean>, prefs: AppPreferences) {
val context = SimplexApp.context
val filesChatBase = context.dataDir.absolutePath + File.separator + "files_chat.db"
val filesAgentBase = context.dataDir.absolutePath + File.separator + "files_agent.db"
try {
@@ -299,7 +301,7 @@ fun PreviewChatInfoLayout() {
SimpleXTheme {
DatabaseErrorView(
remember { mutableStateOf(DBMigrationResult.ErrorNotADatabase("simplex_v1_chat.db")) },
AppPreferences(SimplexApp.context)
AppPreferences()
)
}
}
@@ -57,11 +57,11 @@ fun DatabaseView(
val chatArchiveTime = remember { mutableStateOf(prefs.chatArchiveTime.get()) }
val chatLastStart = remember { mutableStateOf(prefs.chatLastStart.get()) }
val chatArchiveFile = remember { mutableStateOf<String?>(null) }
val saveArchiveLauncher = rememberSaveArchiveLauncher(cxt = context, chatArchiveFile)
val appFilesCountAndSize = remember { mutableStateOf(directoryFileCountAndSize(getAppFilesDirectory(context))) }
val saveArchiveLauncher = rememberSaveArchiveLauncher(chatArchiveFile)
val appFilesCountAndSize = remember { mutableStateOf(directoryFileCountAndSize(getAppFilesDirectory())) }
val importArchiveLauncher = rememberGetContentLauncher { uri: Uri? ->
if (uri != null) {
importArchiveAlert(m, context, uri, appFilesCountAndSize, progressIndicator)
importArchiveAlert(m, uri, appFilesCountAndSize, progressIndicator)
}
}
LaunchedEffect(m.chatRunning) {
@@ -88,17 +88,17 @@ fun DatabaseView(
m.currentUser.value,
m.users,
startChat = { startChat(m, runChat, chatLastStart, m.chatDbChanged) },
stopChatAlert = { stopChatAlert(m, runChat, context) },
exportArchive = { exportArchive(context, m, progressIndicator, chatArchiveName, chatArchiveTime, chatArchiveFile, saveArchiveLauncher) },
stopChatAlert = { stopChatAlert(m, runChat) },
exportArchive = { exportArchive(m, progressIndicator, chatArchiveName, chatArchiveTime, chatArchiveFile, saveArchiveLauncher) },
deleteChatAlert = { deleteChatAlert(m, progressIndicator) },
deleteAppFilesAndMedia = { deleteFilesAndMediaAlert(context, appFilesCountAndSize) },
deleteAppFilesAndMedia = { deleteFilesAndMediaAlert(appFilesCountAndSize) },
onChatItemTTLSelected = {
val oldValue = chatItemTTL.value
chatItemTTL.value = it
if (it < oldValue) {
setChatItemTTLAlert(m, chatItemTTL, progressIndicator, appFilesCountAndSize, context)
setChatItemTTLAlert(m, chatItemTTL, progressIndicator, appFilesCountAndSize)
} else if (it != oldValue) {
setCiTTL(m, chatItemTTL, progressIndicator, appFilesCountAndSize, context)
setCiTTL(m, chatItemTTL, progressIndicator, appFilesCountAndSize)
}
},
showSettingsModal
@@ -283,13 +283,12 @@ private fun setChatItemTTLAlert(
m: ChatModel, selectedChatItemTTL: MutableState<ChatItemTTL>,
progressIndicator: MutableState<Boolean>,
appFilesCountAndSize: MutableState<Pair<Int, Long>>,
context: Context
) {
AlertManager.shared.showAlertDialog(
title = generalGetString(R.string.enable_automatic_deletion_question),
text = generalGetString(R.string.enable_automatic_deletion_message),
confirmText = generalGetString(R.string.delete_messages),
onConfirm = { setCiTTL(m, selectedChatItemTTL, progressIndicator, appFilesCountAndSize, context) },
onConfirm = { setCiTTL(m, selectedChatItemTTL, progressIndicator, appFilesCountAndSize) },
onDismiss = { selectedChatItemTTL.value = m.chatItemTTL.value },
destructive = true,
)
@@ -387,12 +386,12 @@ private fun startChat(m: ChatModel, runChat: MutableState<Boolean?>, chatLastSta
}
}
private fun stopChatAlert(m: ChatModel, runChat: MutableState<Boolean?>, context: Context) {
private fun stopChatAlert(m: ChatModel, runChat: MutableState<Boolean?>) {
AlertManager.shared.showAlertDialog(
title = generalGetString(R.string.stop_chat_question),
text = generalGetString(R.string.stop_chat_to_export_import_or_delete_chat_database),
confirmText = generalGetString(R.string.stop_chat_confirmation),
onConfirm = { authStopChat(m, runChat, context) },
onConfirm = { authStopChat(m, runChat) },
onDismiss = { runChat.value = true }
)
}
@@ -404,16 +403,15 @@ private fun exportProhibitedAlert() {
)
}
private fun authStopChat(m: ChatModel, runChat: MutableState<Boolean?>, context: Context) {
private fun authStopChat(m: ChatModel, runChat: MutableState<Boolean?>) {
if (m.controller.appPrefs.performLA.get()) {
authenticate(
generalGetString(R.string.auth_stop_chat),
generalGetString(R.string.auth_log_in_using_credential),
activity = context as FragmentActivity,
completed = { laResult ->
when (laResult) {
LAResult.Success, is LAResult.Unavailable -> {
stopChat(m, runChat, context)
stopChat(m, runChat)
}
is LAResult.Error -> {
runChat.value = true
@@ -425,11 +423,11 @@ private fun authStopChat(m: ChatModel, runChat: MutableState<Boolean?>, context:
}
)
} else {
stopChat(m, runChat, context)
stopChat(m, runChat)
}
}
private fun stopChat(m: ChatModel, runChat: MutableState<Boolean?>, context: Context) {
private fun stopChat(m: ChatModel, runChat: MutableState<Boolean?>) {
withApi {
try {
runChat.value = false
@@ -455,7 +453,6 @@ suspend fun deleteChatAsync(m: ChatModel) {
}
private fun exportArchive(
context: Context,
m: ChatModel,
progressIndicator: MutableState<Boolean>,
chatArchiveName: MutableState<String?>,
@@ -466,7 +463,7 @@ private fun exportArchive(
progressIndicator.value = true
withApi {
try {
val archiveFile = exportChatArchive(m, context, chatArchiveName, chatArchiveTime, chatArchiveFile)
val archiveFile = exportChatArchive(m, chatArchiveName, chatArchiveTime, chatArchiveFile)
chatArchiveFile.value = archiveFile
saveArchiveLauncher.launch(archiveFile.substringAfterLast("/"))
progressIndicator.value = false
@@ -479,7 +476,6 @@ private fun exportArchive(
private suspend fun exportChatArchive(
m: ChatModel,
context: Context,
chatArchiveName: MutableState<String?>,
chatArchiveTime: MutableState<Instant?>,
chatArchiveFile: MutableState<String?>
@@ -487,10 +483,10 @@ private suspend fun exportChatArchive(
val archiveTime = Clock.System.now()
val ts = SimpleDateFormat("yyyy-MM-dd'T'HHmmss", Locale.US).format(Date.from(archiveTime.toJavaInstant()))
val archiveName = "simplex-chat.$ts.zip"
val archivePath = "${getFilesDirectory(context)}/$archiveName"
val config = ArchiveConfig(archivePath, parentTempDirectory = context.cacheDir.toString())
val archivePath = "${getFilesDirectory()}/$archiveName"
val config = ArchiveConfig(archivePath, parentTempDirectory = SimplexApp.context.cacheDir.toString())
m.controller.apiExportArchive(config)
deleteOldArchive(m, context)
deleteOldArchive(m)
m.controller.appPrefs.chatArchiveName.set(archiveName)
chatArchiveName.value = archiveName
m.controller.appPrefs.chatArchiveTime.set(archiveTime)
@@ -499,10 +495,10 @@ private suspend fun exportChatArchive(
return archivePath
}
private fun deleteOldArchive(m: ChatModel, context: Context) {
private fun deleteOldArchive(m: ChatModel) {
val chatArchiveName = m.controller.appPrefs.chatArchiveName.get()
if (chatArchiveName != null) {
val file = File("${getFilesDirectory(context)}/$chatArchiveName")
val file = File("${getFilesDirectory()}/$chatArchiveName")
val fileDeleted = file.delete()
if (fileDeleted) {
m.controller.appPrefs.chatArchiveName.set(null)
@@ -514,15 +510,16 @@ private fun deleteOldArchive(m: ChatModel, context: Context) {
}
@Composable
private fun rememberSaveArchiveLauncher(cxt: Context, chatArchiveFile: MutableState<String?>): ManagedActivityResultLauncher<String, Uri?> =
private fun rememberSaveArchiveLauncher(chatArchiveFile: MutableState<String?>): ManagedActivityResultLauncher<String, Uri?> =
rememberLauncherForActivityResult(
contract = ActivityResultContracts.CreateDocument(),
onResult = { destination ->
val cxt = SimplexApp.context
try {
destination?.let {
val filePath = chatArchiveFile.value
if (filePath != null) {
val contentResolver = cxt.contentResolver
val contentResolver = SimplexApp.context.contentResolver
contentResolver.openOutputStream(destination)?.let { stream ->
val outputStream = BufferedOutputStream(stream)
File(filePath).inputStream().use { it.copyTo(outputStream) }
@@ -544,7 +541,6 @@ private fun rememberSaveArchiveLauncher(cxt: Context, chatArchiveFile: MutableSt
private fun importArchiveAlert(
m: ChatModel,
context: Context,
importedArchiveUri: Uri,
appFilesCountAndSize: MutableState<Pair<Int, Long>>,
progressIndicator: MutableState<Boolean>
@@ -553,29 +549,28 @@ private fun importArchiveAlert(
title = generalGetString(R.string.import_database_question),
text = generalGetString(R.string.your_current_chat_database_will_be_deleted_and_replaced_with_the_imported_one),
confirmText = generalGetString(R.string.import_database_confirmation),
onConfirm = { importArchive(m, context, importedArchiveUri, appFilesCountAndSize, progressIndicator) },
onConfirm = { importArchive(m, importedArchiveUri, appFilesCountAndSize, progressIndicator) },
destructive = true,
)
}
private fun importArchive(
m: ChatModel,
context: Context,
importedArchiveUri: Uri,
appFilesCountAndSize: MutableState<Pair<Int, Long>>,
progressIndicator: MutableState<Boolean>
) {
progressIndicator.value = true
val archivePath = saveArchiveFromUri(context, importedArchiveUri)
val archivePath = saveArchiveFromUri(importedArchiveUri)
if (archivePath != null) {
withApi {
try {
m.controller.apiDeleteStorage()
try {
val config = ArchiveConfig(archivePath, parentTempDirectory = context.cacheDir.toString())
val config = ArchiveConfig(archivePath, parentTempDirectory = SimplexApp.context.cacheDir.toString())
val archiveErrors = m.controller.apiImportArchive(config)
DatabaseUtils.ksDatabasePassword.remove()
appFilesCountAndSize.value = directoryFileCountAndSize(getAppFilesDirectory(context))
appFilesCountAndSize.value = directoryFileCountAndSize(getAppFilesDirectory())
if (archiveErrors.isEmpty()) {
operationEnded(m, progressIndicator) {
AlertManager.shared.showAlertMsg(generalGetString(R.string.chat_database_imported), text = generalGetString(R.string.restart_the_app_to_use_imported_chat_database))
@@ -601,12 +596,12 @@ private fun importArchive(
}
}
private fun saveArchiveFromUri(context: Context, importedArchiveUri: Uri): String? {
private fun saveArchiveFromUri(importedArchiveUri: Uri): String? {
return try {
val inputStream = context.contentResolver.openInputStream(importedArchiveUri)
val archiveName = getFileName(context, importedArchiveUri)
val inputStream = SimplexApp.context.contentResolver.openInputStream(importedArchiveUri)
val archiveName = getFileName(importedArchiveUri)
if (inputStream != null && archiveName != null) {
val archivePath = "${context.cacheDir}/$archiveName"
val archivePath = "${SimplexApp.context.cacheDir}/$archiveName"
val destFile = File(archivePath)
IOUtils.copy(inputStream, FileOutputStream(destFile))
archivePath
@@ -651,7 +646,6 @@ private fun setCiTTL(
chatItemTTL: MutableState<ChatItemTTL>,
progressIndicator: MutableState<Boolean>,
appFilesCountAndSize: MutableState<Pair<Int, Long>>,
context: Context
) {
Log.d(TAG, "DatabaseView setChatItemTTL ${chatItemTTL.value.seconds ?: -1}")
progressIndicator.value = true
@@ -660,11 +654,11 @@ private fun setCiTTL(
m.controller.setChatItemTTL(chatItemTTL.value)
// Update model on success
m.chatItemTTL.value = chatItemTTL.value
afterSetCiTTL(m, progressIndicator, appFilesCountAndSize, context)
afterSetCiTTL(m, progressIndicator, appFilesCountAndSize)
} catch (e: Exception) {
// Rollback to model's value
chatItemTTL.value = m.chatItemTTL.value
afterSetCiTTL(m, progressIndicator, appFilesCountAndSize, context)
afterSetCiTTL(m, progressIndicator, appFilesCountAndSize)
AlertManager.shared.showAlertMsg(generalGetString(R.string.error_changing_message_deletion), e.stackTraceToString())
}
}
@@ -673,11 +667,10 @@ private fun setCiTTL(
private fun afterSetCiTTL(
m: ChatModel,
progressIndicator: MutableState<Boolean>,
appFilesCountAndSize: MutableState<Pair<Int, Long>>,
context: Context
appFilesCountAndSize: MutableState<Pair<Int, Long>>
) {
progressIndicator.value = false
appFilesCountAndSize.value = directoryFileCountAndSize(getAppFilesDirectory(context))
appFilesCountAndSize.value = directoryFileCountAndSize(getAppFilesDirectory())
withApi {
try {
val chats = m.controller.apiGetChats()
@@ -688,19 +681,19 @@ private fun afterSetCiTTL(
}
}
private fun deleteFilesAndMediaAlert(context: Context, appFilesCountAndSize: MutableState<Pair<Int, Long>>) {
private fun deleteFilesAndMediaAlert(appFilesCountAndSize: MutableState<Pair<Int, Long>>) {
AlertManager.shared.showAlertDialog(
title = generalGetString(R.string.delete_files_and_media_question),
text = generalGetString(R.string.delete_files_and_media_desc),
confirmText = generalGetString(R.string.delete_verb),
onConfirm = { deleteFiles(appFilesCountAndSize, context) },
onConfirm = { deleteFiles(appFilesCountAndSize) },
destructive = true
)
}
private fun deleteFiles(appFilesCountAndSize: MutableState<Pair<Int, Long>>, context: Context) {
deleteAppFiles(context)
appFilesCountAndSize.value = directoryFileCountAndSize(getAppFilesDirectory(context))
private fun deleteFiles(appFilesCountAndSize: MutableState<Pair<Int, Long>>) {
deleteAppFiles()
appFilesCountAndSize.value = directoryFileCountAndSize(getAppFilesDirectory())
}
private fun operationEnded(m: ChatModel, progressIndicator: MutableState<Boolean>, alert: () -> Unit) {
@@ -2,8 +2,7 @@ package chat.simplex.app.views.helpers
import android.util.Log
import chat.simplex.app.*
import chat.simplex.app.model.AppPreferences
import chat.simplex.app.model.SharedPreference
import chat.simplex.app.model.*
import chat.simplex.app.views.usersettings.Cryptor
import kotlinx.serialization.*
import java.io.File
@@ -13,7 +12,7 @@ object DatabaseUtils {
private val cryptor = Cryptor()
private val appPreferences: AppPreferences by lazy {
AppPreferences(SimplexApp.context)
ChatController.appPrefs
}
private const val DATABASE_PASSWORD_ALIAS: String = "databasePassword"
@@ -111,7 +111,7 @@ fun base64ToBitmap(base64ImageString: String): Bitmap {
class CustomTakePicturePreview(var uri: Uri?, var tmpFile: File?): ActivityResultContract<Void?, Uri?>() {
@CallSuper
override fun createIntent(context: Context, input: Void?): Intent {
tmpFile = File.createTempFile("image", ".bmp", File(getAppFilesDirectory(SimplexApp.context)))
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!!)
@@ -40,10 +40,10 @@ fun authenticate(
promptTitle: String,
promptSubtitle: String,
selfDestruct: Boolean = false,
activity: FragmentActivity,
usingLAMode: LAMode = SimplexApp.context.chatModel.controller.appPrefs.laMode.get(),
completed: (LAResult) -> Unit
) {
val activity = SimplexApp.context.mainActivity.get() ?: return completed(LAResult.Error(""))
when (usingLAMode) {
LAMode.SYSTEM -> when {
SDK_INT in 28..29 ->
@@ -51,7 +51,7 @@ class RecorderNative(): Recorder {
rec.setAudioEncodingBitRate(32000)
rec.setMaxDuration(MAX_VOICE_MILLIS_FOR_SENDING)
val tmpDir = SimplexApp.context.getDir("temp", Application.MODE_PRIVATE)
val fileToSave = File.createTempFile(generateNewFileName(SimplexApp.context, "voice", "${extension}_"), ".tmp", tmpDir)
val fileToSave = File.createTempFile(generateNewFileName("voice", "${extension}_"), ".tmp", tmpDir)
fileToSave.deleteOnExit()
val path = fileToSave.absolutePath
filePath = path
@@ -18,17 +18,19 @@ import chat.simplex.app.model.CIFile
import java.io.BufferedOutputStream
import java.io.File
fun shareText(cxt: Context, text: String) {
fun shareText(text: String) {
val sendIntent: Intent = Intent().apply {
action = Intent.ACTION_SEND
putExtra(Intent.EXTRA_TEXT, text)
type = "text/plain"
}
val shareIntent = Intent.createChooser(sendIntent, null)
cxt.startActivity(shareIntent)
// 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)
}
fun shareFile(cxt: Context, text: String, filePath: String) {
fun shareFile(text: String, filePath: String) {
val uri = FileProvider.getUriForFile(SimplexApp.context, "${BuildConfig.APPLICATION_ID}.provider", File(filePath))
val ext = filePath.substringAfterLast(".")
val mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext) ?: return
@@ -41,32 +43,37 @@ fun shareFile(cxt: Context, text: String, filePath: String) {
type = mimeType
}
val shareIntent = Intent.createChooser(sendIntent, null)
cxt.startActivity(shareIntent)
// 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)
}
fun copyText(cxt: Context, text: String) {
val clipboard = ContextCompat.getSystemService(cxt, ClipboardManager::class.java)
fun copyText(text: String) {
val clipboard = ContextCompat.getSystemService(SimplexApp.context, ClipboardManager::class.java)
clipboard?.setPrimaryClip(ClipData.newPlainText("text", text))
}
fun sendEmail(context: Context, subject: String, body: CharSequence) {
fun 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 {
context.startActivity(emailIntent)
SimplexApp.context.startActivity(emailIntent)
} catch (e: ActivityNotFoundException) {
Log.e(TAG, "No activity was found for handling email intent")
}
}
@Composable
fun rememberSaveFileLauncher(cxt: Context, ciFile: CIFile?): ManagedActivityResultLauncher<String, Uri?> =
fun rememberSaveFileLauncher(ciFile: CIFile?): ManagedActivityResultLauncher<String, Uri?> =
rememberLauncherForActivityResult(
contract = ActivityResultContracts.CreateDocument(),
onResult = { destination ->
destination?.let {
val filePath = getLoadedFilePath(cxt, ciFile)
val cxt = SimplexApp.context
val filePath = getLoadedFilePath(ciFile)
if (filePath != null) {
val contentResolver = cxt.contentResolver
contentResolver.openOutputStream(destination)?.let { stream ->
@@ -95,8 +102,9 @@ fun imageMimeType(fileName: String): String {
}
/** Before calling, make sure the user allows to write to external storage [Manifest.permission.WRITE_EXTERNAL_STORAGE] */
fun saveImage(cxt: Context, ciFile: CIFile?) {
val filePath = getLoadedFilePath(cxt, ciFile)
fun saveImage(ciFile: CIFile?) {
val cxt = SimplexApp.context
val filePath = getLoadedFilePath(ciFile)
val fileName = ciFile?.fileName
if (filePath != null && fileName != null) {
val values = ContentValues()
@@ -247,30 +247,30 @@ const val MAX_FILE_SIZE_SMP: Long = 8000000
const val MAX_FILE_SIZE_XFTP: Long = 1_073_741_824 // 1GB
fun getFilesDirectory(context: Context): String {
return context.filesDir.toString()
fun getFilesDirectory(): String {
return SimplexApp.context.filesDir.toString()
}
fun getTempFilesDirectory(context: Context): String {
return "${getFilesDirectory(context)}/temp_files"
fun getTempFilesDirectory(): String {
return "${getFilesDirectory()}/temp_files"
}
fun getAppFilesDirectory(context: Context): String {
return "${getFilesDirectory(context)}/app_files"
fun getAppFilesDirectory(): String {
return "${getFilesDirectory()}/app_files"
}
fun getAppFilePath(context: Context, fileName: String): String {
return "${getAppFilesDirectory(context)}/$fileName"
fun getAppFilePath(fileName: String): String {
return "${getAppFilesDirectory()}/$fileName"
}
fun getAppFileUri(fileName: String): Uri {
return Uri.parse("${getAppFilesDirectory(SimplexApp.context)}/$fileName")
return Uri.parse("${getAppFilesDirectory()}/$fileName")
}
fun getLoadedFilePath(context: Context, file: CIFile?): String? {
fun getLoadedFilePath(file: CIFile?): String? {
return if (file?.filePath != null && file.loaded) {
val filePath = getAppFilePath(context, file.filePath)
val filePath = getAppFilePath(file.filePath)
if (File(filePath).exists()) filePath else null
} else {
null
@@ -278,12 +278,12 @@ fun getLoadedFilePath(context: Context, file: CIFile?): String? {
}
// https://developer.android.com/training/data-storage/shared/documents-files#bitmap
fun getLoadedImage(context: Context, file: CIFile?): Bitmap? {
val filePath = getLoadedFilePath(context, file)
fun getLoadedImage(file: CIFile?): Bitmap? {
val filePath = getLoadedFilePath(file)
return if (filePath != null) {
try {
val uri = FileProvider.getUriForFile(context, "${BuildConfig.APPLICATION_ID}.provider", File(filePath))
val parcelFileDescriptor = context.contentResolver.openFileDescriptor(uri, "r")
val uri = FileProvider.getUriForFile(SimplexApp.context, "${BuildConfig.APPLICATION_ID}.provider", File(filePath))
val parcelFileDescriptor = SimplexApp.context.contentResolver.openFileDescriptor(uri, "r")
val fileDescriptor = parcelFileDescriptor?.fileDescriptor
val image = decodeSampledBitmapFromFileDescriptor(fileDescriptor, 1000, 1000)
parcelFileDescriptor?.close()
@@ -329,24 +329,24 @@ private fun calculateInSampleSize(options: BitmapFactory.Options, reqWidth: Int,
return inSampleSize
}
fun getFileName(context: Context, uri: Uri): String? {
return context.contentResolver.query(uri, null, null, null, null)?.use { cursor ->
fun getFileName(uri: Uri): String? {
return SimplexApp.context.contentResolver.query(uri, null, null, null, null)?.use { cursor ->
val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
cursor.moveToFirst()
cursor.getString(nameIndex)
}
}
fun getAppFilePath(context: Context, uri: Uri): String? {
return context.contentResolver.query(uri, null, null, null, null)?.use { cursor ->
fun getAppFilePath(uri: Uri): String? {
return SimplexApp.context.contentResolver.query(uri, null, null, null, null)?.use { cursor ->
val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
cursor.moveToFirst()
getAppFilePath(context, cursor.getString(nameIndex))
getAppFilePath(cursor.getString(nameIndex))
}
}
fun getFileSize(context: Context, uri: Uri): Long? {
return context.contentResolver.query(uri, null, null, null, null)?.use { cursor ->
fun getFileSize(uri: Uri): Long? {
return SimplexApp.context.contentResolver.query(uri, null, null, null, null)?.use { cursor ->
val sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE)
cursor.moveToFirst()
cursor.getLong(sizeIndex)
@@ -369,7 +369,7 @@ fun getBitmapFromUri(uri: Uri, withAlertOnException: Boolean = true): Bitmap? {
null
}
} else {
BitmapFactory.decodeFile(getAppFilePath(SimplexApp.context, uri))
BitmapFactory.decodeFile(getAppFilePath(uri))
}
}
@@ -389,7 +389,7 @@ fun getDrawableFromUri(uri: Uri, withAlertOnException: Boolean = true): Drawable
null
}
} else {
Drawable.createFromPath(getAppFilePath(SimplexApp.context, uri))
Drawable.createFromPath(getAppFilePath(uri))
}
}
@@ -409,17 +409,17 @@ fun getThemeFromUri(uri: Uri, withAlertOnException: Boolean = true): ThemeOverri
return null
}
fun saveImage(context: Context, uri: Uri): String? {
fun saveImage(uri: Uri): String? {
val bitmap = getBitmapFromUri(uri) ?: return null
return saveImage(context, bitmap)
return saveImage(bitmap)
}
fun saveImage(context: Context, image: Bitmap): String? {
fun saveImage(image: Bitmap): String? {
return try {
val ext = if (image.hasAlpha()) "png" else "jpg"
val dataResized = resizeImageToDataSize(image, ext == "png", maxDataSize = MAX_IMAGE_SIZE)
val fileToSave = generateNewFileName(context, "IMG", ext)
val file = File(getAppFilePath(context, fileToSave))
val fileToSave = generateNewFileName("IMG", ext)
val file = File(getAppFilePath(fileToSave))
val output = FileOutputStream(file)
dataResized.writeTo(output)
output.flush()
@@ -431,9 +431,9 @@ fun saveImage(context: Context, image: Bitmap): String? {
}
}
fun saveAnimImage(context: Context, uri: Uri): String? {
fun saveAnimImage(uri: Uri): String? {
return try {
val filename = getFileName(context, uri)?.lowercase()
val filename = getFileName(uri)?.lowercase()
var ext = when {
// remove everything but extension
filename?.contains(".") == true -> filename.replaceBeforeLast('.', "").replace(".", "")
@@ -441,10 +441,10 @@ fun saveAnimImage(context: Context, uri: Uri): String? {
}
// Just in case the image has a strange extension
if (ext.length < 3 || ext.length > 4) ext = "gif"
val fileToSave = generateNewFileName(context, "IMG", ext)
val file = File(getAppFilePath(context, fileToSave))
val fileToSave = generateNewFileName("IMG", ext)
val file = File(getAppFilePath(fileToSave))
val output = FileOutputStream(file)
context.contentResolver.openInputStream(uri)!!.use { input ->
SimplexApp.context.contentResolver.openInputStream(uri)!!.use { input ->
output.use { output ->
input.copyTo(output)
}
@@ -460,7 +460,7 @@ fun saveTempImageUncompressed(image: Bitmap, 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(SimplexApp.context, "IMG", ext)).apply {
return File(tmpDir.absolutePath + File.separator + generateNewFileName("IMG", ext)).apply {
outputStream().use { out ->
image.compress(if (asPng) Bitmap.CompressFormat.PNG else Bitmap.CompressFormat.JPEG, 85, out)
out.flush()
@@ -474,13 +474,13 @@ fun saveTempImageUncompressed(image: Bitmap, asPng: Boolean): File? {
}
}
fun saveFileFromUri(context: Context, uri: Uri): String? {
fun saveFileFromUri(uri: Uri): String? {
return try {
val inputStream = context.contentResolver.openInputStream(uri)
val fileToSave = getFileName(context, uri)
val inputStream = SimplexApp.context.contentResolver.openInputStream(uri)
val fileToSave = getFileName(uri)
if (inputStream != null && fileToSave != null) {
val destFileName = uniqueCombine(context, fileToSave)
val destFile = File(getAppFilePath(context, destFileName))
val destFileName = uniqueCombine(fileToSave)
val destFile = File(getAppFilePath(destFileName))
IOUtils.copy(inputStream, FileOutputStream(destFile))
destFileName
} else {
@@ -493,21 +493,21 @@ fun saveFileFromUri(context: Context, uri: Uri): String? {
}
}
fun generateNewFileName(context: Context, prefix: String, ext: String): String {
fun generateNewFileName(prefix: String, ext: String): String {
val sdf = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US)
sdf.timeZone = TimeZone.getTimeZone("GMT")
val timestamp = sdf.format(Date())
return uniqueCombine(context, "${prefix}_$timestamp.$ext")
return uniqueCombine("${prefix}_$timestamp.$ext")
}
fun uniqueCombine(context: Context, fileName: String): String {
fun uniqueCombine(fileName: String): String {
val orig = File(fileName)
val name = orig.nameWithoutExtension
val ext = orig.extension
fun tryCombine(n: Int): String {
val suffix = if (n == 0) "" else "_$n"
val f = "$name$suffix.$ext"
return if (File(getAppFilePath(context, f)).exists()) tryCombine(n + 1) else f
return if (File(getAppFilePath(f)).exists()) tryCombine(n + 1) else f
}
return tryCombine(0)
}
@@ -530,8 +530,8 @@ fun formatBytes(bytes: Long): String {
}
}
fun removeFile(context: Context, fileName: String): Boolean {
val file = File(getAppFilePath(context, fileName))
fun removeFile(fileName: String): Boolean {
val file = File(getAppFilePath(fileName))
val fileDeleted = file.delete()
if (!fileDeleted) {
Log.e(chat.simplex.app.TAG, "Util.kt removeFile error")
@@ -539,11 +539,11 @@ fun removeFile(context: Context, fileName: String): Boolean {
return fileDeleted
}
fun deleteAppFiles(context: Context) {
val dir = File(getAppFilesDirectory(context))
fun deleteAppFiles() {
val dir = File(getAppFilesDirectory())
try {
dir.list()?.forEach {
removeFile(context, it)
removeFile(it)
}
} catch (e: java.lang.Exception) {
Log.e(TAG, "Util deleteAppFiles error: ${e.stackTraceToString()}")
@@ -24,7 +24,6 @@ class VideoPlayer private constructor(
private val defaultPreview: Bitmap,
defaultDuration: Long,
soundEnabled: Boolean,
context: Context
) {
companion object {
private val players: MutableMap<Pair<Uri, Boolean>, VideoPlayer> = mutableMapOf()
@@ -36,9 +35,8 @@ class VideoPlayer private constructor(
defaultPreview: Bitmap,
defaultDuration: Long,
soundEnabled: Boolean,
context: Context
): VideoPlayer =
players.getOrPut(uri to gallery) { VideoPlayer(uri, gallery, defaultPreview, defaultDuration, soundEnabled, context) }
players.getOrPut(uri to gallery) { VideoPlayer(uri, gallery, defaultPreview, defaultDuration, soundEnabled) }
fun enableSound(enable: Boolean, fileName: String?, gallery: Boolean): Boolean =
player(fileName, gallery)?.enableSound(enable) == true
@@ -76,8 +74,8 @@ class VideoPlayer private constructor(
setPreviewAndDuration()
}
val player = ExoPlayer.Builder(context,
DefaultRenderersFactory(context))
val player = ExoPlayer.Builder(SimplexApp.context,
DefaultRenderersFactory(SimplexApp.context))
/*.setLoadControl(DefaultLoadControl.Builder()
.setPrioritizeTimeOverSizeThresholds(false) // Could probably save some megabytes in memory in case it will be needed
.createDefaultLoadControl())*/
@@ -108,7 +106,7 @@ class VideoPlayer private constructor(
}
private fun start(seek: Long? = null, onProgressUpdate: (position: Long?, state: TrackState) -> Unit): Boolean {
val filepath = getAppFilePath(SimplexApp.context, uri)
val filepath = getAppFilePath(uri)
if (filepath == null || !File(filepath).exists()) {
Log.e(TAG, "No such file: $uri")
brokenVideo.value = true
@@ -12,7 +12,6 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
@@ -24,11 +23,10 @@ import chat.simplex.app.views.usersettings.SettingsActionItem
@Composable
fun AddContactView(connReqInvitation: String, connIncognito: Boolean) {
val cxt = LocalContext.current
AddContactLayout(
connReq = connReqInvitation,
connIncognito = connIncognito,
share = { shareText(cxt, connReqInvitation) },
share = { shareText(connReqInvitation) },
learnMore = {
ModalManager.shared.showModal {
Column(
@@ -11,7 +11,6 @@ import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
@@ -45,7 +44,6 @@ fun ContactConnectionInfoView(
}
}
}
val context = LocalContext.current
ContactConnectionInfoLayout(
connReq = connReqInvitation,
contactConnection,
@@ -53,7 +51,7 @@ fun ContactConnectionInfoView(
focusAlias,
deleteConnection = { deleteContactConnectionAlert(contactConnection, chatModel, close) },
onLocalAliasChanged = { setContactAlias(contactConnection, it, chatModel) },
share = { if (connReqInvitation != null) shareText(context, connReqInvitation) },
share = { if (connReqInvitation != null) shareText(connReqInvitation) },
learnMore = {
ModalManager.shared.showModal {
Column(
@@ -27,7 +27,6 @@ fun QRCode(
tintColor: Color = Color(0xff062d56),
withLogo: Boolean = true
) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
BoxWithConstraints {
@@ -47,7 +46,7 @@ fun QRCode(
.let { if (withLogo) it.addLogo() else it }
val file = saveTempImageUncompressed(image, false)
if (file != null) {
shareFile(context, "", file.absolutePath)
shareFile("", file.absolutePath)
}
}
}
@@ -9,7 +9,6 @@ import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
@@ -24,16 +23,14 @@ import chat.simplex.app.views.newchat.QRCode
@Composable
fun CreateSimpleXAddress(m: ChatModel) {
val context = LocalContext.current
var progressIndicator by remember { mutableStateOf(false) }
val userAddress = remember { m.userAddress }
CreateSimpleXAddressLayout(
userAddress.value,
share = { address: String -> shareText(context, address) },
share = { address: String -> shareText(address) },
sendEmail = { address ->
sendEmail(
context,
generalGetString(R.string.email_invite_subject),
generalGetString(R.string.email_invite_body).format(address.connReqContact)
)
@@ -236,9 +236,8 @@ fun CustomizeThemeView(editColor: (ThemeColor, Color) -> Unit) {
}
SectionSpacer()
SectionView {
val context = LocalContext.current
val theme = remember { mutableStateOf(null as String?) }
val exportThemeLauncher = rememberSaveThemeLauncher(context, theme)
val exportThemeLauncher = rememberSaveThemeLauncher(theme)
SectionItemView({
val overrides = ThemeManager.currentThemeOverridesForExport(isInDarkTheme)
theme.value = yaml.encodeToString<ThemeOverrides>(overrides)
@@ -375,10 +374,11 @@ private fun DarkThemeSelector(state: State<String?>, onSelected: (String) -> Uni
//}
@Composable
private fun rememberSaveThemeLauncher(cxt: Context, theme: MutableState<String?>): ManagedActivityResultLauncher<String, Uri?> =
private fun rememberSaveThemeLauncher(theme: MutableState<String?>): ManagedActivityResultLauncher<String, Uri?> =
rememberLauncherForActivityResult(
contract = ActivityResultContracts.CreateDocument(),
onResult = { destination ->
val cxt = SimplexApp.context
try {
destination?.let {
val theme = theme.value
@@ -188,7 +188,7 @@ fun notificationPreviewModes(): List<ValueTitleDesc<NotificationPreviewMode>> {
fun changeNotificationsMode(mode: NotificationsMode, chatModel: ChatModel) {
chatModel.controller.appPrefs.notificationsMode.set(mode.name)
if (mode.requiresIgnoringBattery && !chatModel.controller.isIgnoringBatteryOptimizations(chatModel.controller.appContext)) {
if (mode.requiresIgnoringBattery && !chatModel.controller.isIgnoringBatteryOptimizations()) {
chatModel.controller.appPrefs.backgroundServiceNoticeShown.set(false)
}
chatModel.notificationsMode.value = mode
@@ -143,7 +143,7 @@ fun SimplexLockView(
} else {
generalGetString(R.string.chat_lock)
},
generalGetString(R.string.change_lock_mode), activity = activity
generalGetString(R.string.change_lock_mode)
) { laResult ->
when (laResult) {
is LAResult.Error -> {
@@ -153,7 +153,7 @@ fun SimplexLockView(
LAResult.Success -> {
when (toLAMode) {
LAMode.SYSTEM -> {
authenticate(generalGetString(R.string.auth_enable_simplex_lock), promptSubtitle = "", activity = activity, usingLAMode = toLAMode) { laResult ->
authenticate(generalGetString(R.string.auth_enable_simplex_lock), promptSubtitle = "", usingLAMode = toLAMode) { laResult ->
when (laResult) {
LAResult.Success -> {
currentLAMode.set(toLAMode)
@@ -189,7 +189,7 @@ fun SimplexLockView(
}
fun toggleSelfDestruct(selfDestruct: SharedPreference<Boolean>) {
authenticate(generalGetString(R.string.la_current_app_passcode), generalGetString(R.string.change_self_destruct_mode), activity = activity) { laResult ->
authenticate(generalGetString(R.string.la_current_app_passcode), generalGetString(R.string.change_self_destruct_mode)) { laResult ->
when (laResult) {
is LAResult.Error -> laFailedAlert()
is LAResult.Failed -> { /* Can be called multiple times on every failure */ }
@@ -208,7 +208,7 @@ fun SimplexLockView(
}
fun changeLAPassword() {
authenticate(generalGetString(R.string.la_current_app_passcode), generalGetString(R.string.la_change_app_passcode), activity = activity) { laResult ->
authenticate(generalGetString(R.string.la_current_app_passcode), generalGetString(R.string.la_change_app_passcode)) { laResult ->
when (laResult) {
LAResult.Success -> {
ModalManager.shared.showCustomModal { close ->
@@ -231,7 +231,7 @@ fun SimplexLockView(
}
fun changeSelfDestructPassword() {
authenticate(generalGetString(R.string.la_current_app_passcode), generalGetString(R.string.change_self_destruct_passcode), activity = activity) { laResult ->
authenticate(generalGetString(R.string.la_current_app_passcode), generalGetString(R.string.change_self_destruct_passcode)) { laResult ->
when (laResult) {
LAResult.Success -> {
ModalManager.shared.showCustomModal { close ->
@@ -6,7 +6,6 @@ import SectionItemView
import SectionItemViewWithIcon
import SectionView
import TextIconSpaced
import android.content.*
import android.content.res.Configuration
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
@@ -45,7 +44,6 @@ fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean, FragmentActivity)
if (user != null) {
val requireAuth = remember { chatModel.controller.appPrefs.performLA.state }
val context = LocalContext.current
SettingsLayout(
profile = user.profile,
stopped,
@@ -92,7 +90,7 @@ fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean, FragmentActivity)
LaunchedEffect(Unit) {
if (autoShow) {
autoShow = false
runAuth(title, desc, context, onFinishAuth)
runAuth(title, desc, onFinishAuth)
}
}
Box(
@@ -103,7 +101,7 @@ fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean, FragmentActivity)
stringResource(R.string.auth_unlock),
icon = painterResource(R.drawable.ic_lock),
click = {
runAuth(title, desc, context, onFinishAuth)
runAuth(title, desc, onFinishAuth)
}
)
}
@@ -481,11 +479,10 @@ fun PreferenceToggleWithIcon(
}
}
private fun runAuth(title: String, desc: String, context: Context, onFinish: (success: Boolean) -> Unit) {
private fun runAuth(title: String, desc: String, onFinish: (success: Boolean) -> Unit) {
authenticate(
title,
desc,
activity = context as FragmentActivity,
completed = { laResult ->
onFinish(laResult == LAResult.Success || laResult is LAResult.Unavailable)
}
@@ -16,7 +16,6 @@ import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
@@ -36,7 +35,6 @@ fun UserAddressView(
shareViaProfile: Boolean = false,
close: () -> Unit
) {
val context = LocalContext.current
val shareViaProfile = remember { mutableStateOf(shareViaProfile) }
var progressIndicator by remember { mutableStateOf(false) }
val onCloseHandler: MutableState<(close: () -> Unit) -> Unit> = remember { mutableStateOf({ _ -> }) }
@@ -94,10 +92,9 @@ fun UserAddressView(
}
}
},
share = { userAddress: String -> shareText(context, userAddress) },
share = { userAddress: String -> shareText(userAddress) },
sendEmail = { userAddress ->
sendEmail(
context,
generalGetString(R.string.email_invite_subject),
generalGetString(R.string.email_invite_body).format(userAddress.connReqContact)
)