Skip to content

Commit

Permalink
multiplatform: make ChatModel and ChatController as object
Browse files Browse the repository at this point in the history
  • Loading branch information
avently committed Jul 1, 2023
1 parent 8e849bb commit 25e4144
Show file tree
Hide file tree
Showing 24 changed files with 180 additions and 191 deletions.
Expand Up @@ -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
Expand Down
Expand Up @@ -37,13 +37,19 @@ external fun chatParseServer(str: String): String
external fun chatPasswordHash(pwd: String, salt: String): String

class SimplexApp: Application(), LifecycleEventObserver {
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 {
Expand Down Expand Up @@ -84,21 +90,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()
Expand Down Expand Up @@ -162,12 +153,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())
}

/*
Expand Down
Expand Up @@ -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>()
Expand Down Expand Up @@ -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 = mutableStateOf(try { NotificationsMode.valueOf(ChatController.appPrefs.notificationsMode.get()!!) } catch(e: Exception) { NotificationsMode.default })
var notificationPreviewMode = mutableStateOf(try { NotificationPreviewMode.valueOf(ChatController.appPrefs.notificationPreviewMode.get()!!) } catch(e: Exception) { NotificationPreviewMode.default })
val performLA by lazy { mutableStateOf(controller.appPrefs.performLA.get()) }
val showAdvertiseLAUnavailableAlert = mutableStateOf(false)
var incognito = mutableStateOf(false)
val incognito by lazy { mutableStateOf(ChatController.appPrefs.incognito.get()) }

// current WebRTC call
val callManager = CallManager(this)
Expand All @@ -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
Expand Down
@@ -1,45 +1,47 @@
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
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

Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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()}, " +
Expand Down Expand Up @@ -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)
}
}
}

Expand All @@ -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 = ""
Expand Down Expand Up @@ -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")
}
Expand Down
Expand Up @@ -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)
Expand Down Expand Up @@ -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) {
Expand All @@ -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()
Expand Down Expand Up @@ -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)
}
}

Expand Down Expand Up @@ -1671,15 +1666,15 @@ 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)
}
// 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()) {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)
}
}

Expand Down
Expand Up @@ -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
Expand Down

0 comments on commit 25e4144

Please sign in to comment.