amethyst/app/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt

321 wiersze
12 KiB
Kotlin
Czysty Zwykły widok Historia

2024-01-06 15:44:32 +00:00
/**
2024-02-15 23:31:26 +00:00
* Copyright (c) 2024 Vitor Pamplona
2024-01-06 15:44:32 +00:00
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
2023-01-11 18:31:20 +00:00
package com.vitorpamplona.amethyst.ui
import android.annotation.SuppressLint
2023-03-22 20:50:18 +00:00
import android.content.Context
import android.content.Intent
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
import android.os.Build
2023-01-11 18:31:20 +00:00
import android.os.Bundle
import android.util.Log
2023-01-11 18:31:20 +00:00
import androidx.activity.compose.setContent
2023-03-22 20:50:18 +00:00
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresApi
2023-07-05 18:12:24 +00:00
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.runtime.mutableStateOf
2023-03-12 05:24:51 +00:00
import com.vitorpamplona.amethyst.LocalPreferences
2023-01-23 16:58:06 +00:00
import com.vitorpamplona.amethyst.ServiceManager
import com.vitorpamplona.amethyst.model.LocalCache
2024-01-27 16:58:39 +00:00
import com.vitorpamplona.amethyst.service.HttpClientManager
import com.vitorpamplona.amethyst.service.lang.LanguageTranslatorService
import com.vitorpamplona.amethyst.service.notifications.PushNotificationUtils
2024-01-06 15:44:32 +00:00
import com.vitorpamplona.amethyst.ui.components.DEFAULT_MUTED_SETTING
import com.vitorpamplona.amethyst.ui.components.keepPlayingMutex
import com.vitorpamplona.amethyst.ui.navigation.Route
import com.vitorpamplona.amethyst.ui.navigation.debugState
import com.vitorpamplona.quartz.encoders.Nip19Bech32
2024-02-22 20:40:07 +00:00
import com.vitorpamplona.quartz.encoders.Nip47WalletConnect
import com.vitorpamplona.quartz.events.ChannelCreateEvent
import com.vitorpamplona.quartz.events.ChannelMessageEvent
import com.vitorpamplona.quartz.events.ChannelMetadataEvent
import com.vitorpamplona.quartz.events.CommunityDefinitionEvent
import com.vitorpamplona.quartz.events.LiveActivitiesEvent
import com.vitorpamplona.quartz.events.PrivateDmEvent
import kotlinx.coroutines.CancellationException
2024-01-06 15:44:32 +00:00
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import java.net.URLEncoder
import java.nio.charset.StandardCharsets
import java.util.Timer
import kotlin.concurrent.schedule
2023-01-11 18:31:20 +00:00
2023-07-05 18:12:24 +00:00
class MainActivity : AppCompatActivity() {
2024-04-17 20:25:13 +00:00
val isOnMobileDataState = mutableStateOf(false)
private val isOnWifiDataState = mutableStateOf(false)
2023-01-11 18:31:20 +00:00
// Service Manager is only active when the activity is active.
val serviceManager = ServiceManager()
private var shouldPauseService = true
@RequiresApi(Build.VERSION_CODES.R)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Log.d("Lifetime Event", "MainActivity.onCreate")
2023-11-22 16:00:52 +00:00
setContent {
2024-04-17 20:25:13 +00:00
val sharedPreferencesViewModel = prepareSharedViewModel(act = this)
AppScreen(sharedPreferencesViewModel = sharedPreferencesViewModel, serviceManager = serviceManager)
}
2023-03-07 18:46:44 +00:00
}
2023-11-22 16:00:52 +00:00
fun prepareToLaunchSigner() {
shouldPauseService = false
2023-03-07 18:46:44 +00:00
}
@OptIn(DelicateCoroutinesApi::class)
override fun onResume() {
super.onResume()
2023-11-22 16:00:52 +00:00
Log.d("Lifetime Event", "MainActivity.onResume")
2023-11-22 16:00:52 +00:00
// starts muted every time
DEFAULT_MUTED_SETTING.value = true
2024-01-06 15:44:32 +00:00
// Keep connection alive if it's calling the signer app
Log.d("shouldPauseService", "shouldPauseService onResume: $shouldPauseService")
if (shouldPauseService) {
GlobalScope.launch(Dispatchers.IO) { serviceManager.justStart() }
}
2023-11-22 16:00:52 +00:00
GlobalScope.launch(Dispatchers.IO) {
PushNotificationUtils.init(LocalPreferences.allSavedAccounts())
}
2023-11-22 21:10:55 +00:00
val connectivityManager =
(getSystemService(ConnectivityManager::class.java) as ConnectivityManager)
connectivityManager.registerDefaultNetworkCallback(networkCallback)
connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork)?.let {
updateNetworkCapabilities(it)
}
2024-01-06 15:44:32 +00:00
// resets state until next External Signer Call
Timer().schedule(350) { shouldPauseService = true }
2023-11-22 16:00:52 +00:00
}
override fun onPause() {
Log.d("Lifetime Event", "MainActivity.onPause")
2024-01-06 15:44:32 +00:00
GlobalScope.launch(Dispatchers.IO) {
LanguageTranslatorService.clear()
}
serviceManager.cleanObservers()
2024-01-06 15:44:32 +00:00
// if (BuildConfig.DEBUG) {
GlobalScope.launch(Dispatchers.IO) { debugState(this@MainActivity) }
// }
2024-01-06 15:44:32 +00:00
Log.d("shouldPauseService", "shouldPauseService onPause: $shouldPauseService")
if (shouldPauseService) {
GlobalScope.launch(Dispatchers.IO) { serviceManager.pauseForGood() }
}
2024-01-06 15:44:32 +00:00
(getSystemService(ConnectivityManager::class.java) as ConnectivityManager)
.unregisterNetworkCallback(networkCallback)
2024-01-06 15:44:32 +00:00
super.onPause()
2023-03-07 18:46:44 +00:00
}
override fun onStart() {
super.onStart()
Log.d("Lifetime Event", "MainActivity.onStart")
}
override fun onStop() {
super.onStop()
// Graph doesn't completely clear.
// GlobalScope.launch(Dispatchers.Default) {
// serviceManager.trimMemory()
// }
Log.d("Lifetime Event", "MainActivity.onStop")
2024-01-06 15:44:32 +00:00
}
override fun onDestroy() {
Log.d("Lifetime Event", "MainActivity.onDestroy")
GlobalScope.launch(Dispatchers.Main) {
keepPlayingMutex?.stop()
keepPlayingMutex?.release()
keepPlayingMutex = null
}
2024-01-06 15:44:32 +00:00
super.onDestroy()
2024-01-06 15:44:32 +00:00
}
/**
* Release memory when the UI becomes hidden or when system resources become low.
*
* @param level the memory-related event that was raised.
*/
@OptIn(DelicateCoroutinesApi::class)
override fun onTrimMemory(level: Int) {
super.onTrimMemory(level)
println("Trim Memory $level")
GlobalScope.launch(Dispatchers.Default) { serviceManager.trimMemory() }
}
fun updateNetworkCapabilities(networkCapabilities: NetworkCapabilities): Boolean {
val isOnMobileData = networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)
val isOnWifi = networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
var changedNetwork = false
if (isOnMobileDataState.value != isOnMobileData) {
isOnMobileDataState.value = isOnMobileData
changedNetwork = true
}
if (isOnWifiDataState.value != isOnWifi) {
isOnWifiDataState.value = isOnWifi
2024-01-06 15:44:32 +00:00
changedNetwork = true
}
2024-01-06 15:44:32 +00:00
if (changedNetwork) {
if (isOnMobileData) {
2024-01-27 16:58:39 +00:00
HttpClientManager.setDefaultTimeout(HttpClientManager.DEFAULT_TIMEOUT_ON_MOBILE)
} else {
2024-01-27 16:58:39 +00:00
HttpClientManager.setDefaultTimeout(HttpClientManager.DEFAULT_TIMEOUT_ON_WIFI)
}
}
return changedNetwork
}
@OptIn(DelicateCoroutinesApi::class)
private val networkCallback =
object : ConnectivityManager.NetworkCallback() {
var lastNetwork: Network? = null
override fun onAvailable(network: Network) {
super.onAvailable(network)
Log.d("ServiceManager NetworkCallback", "onAvailable: $shouldPauseService")
if (shouldPauseService && lastNetwork != null && lastNetwork != network) {
GlobalScope.launch(Dispatchers.IO) { serviceManager.forceRestart() }
}
lastNetwork = network
}
// Network capabilities have changed for the network
override fun onCapabilitiesChanged(
network: Network,
networkCapabilities: NetworkCapabilities,
) {
super.onCapabilitiesChanged(network, networkCapabilities)
GlobalScope.launch(Dispatchers.IO) {
Log.d(
"ServiceManager NetworkCallback",
"onCapabilitiesChanged: ${network.networkHandle} hasMobileData ${isOnMobileDataState.value} hasWifi ${isOnWifiDataState.value}",
)
if (updateNetworkCapabilities(networkCapabilities) && shouldPauseService) {
serviceManager.forceRestart()
}
}
}
}
2023-01-25 00:53:22 +00:00
}
2023-03-22 20:50:18 +00:00
class GetMediaActivityResultContract : ActivityResultContracts.GetContent() {
@SuppressLint("MissingSuperCall")
override fun createIntent(
context: Context,
input: String,
): Intent {
// Force only images and videos to be selectable
// Force OPEN Document because of the resulting URI must be passed to the
// Playback service and the picker's permissions only allow the activity to read the URI
return Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
// Force only images and videos to be selectable
type = "*/*"
putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("image/*", "video/*"))
}
2023-03-22 20:50:18 +00:00
}
}
fun uriToRoute(uri: String?): String? {
return if (uri.equals("nostr:Notifications", true)) {
Route.Notification.route.replace("{scrollToTop}", "true")
} else {
if (uri?.startsWith("nostr:Hashtag?id=") == true) {
Route.Hashtag.route.replace("{id}", uri.removePrefix("nostr:Hashtag?id="))
} else {
val nip19 = Nip19Bech32.uriToRoute(uri)?.entity
when (nip19) {
is Nip19Bech32.NPub -> "User/${nip19.hex}"
is Nip19Bech32.NProfile -> "User/${nip19.hex}"
is Nip19Bech32.Note -> "Note/${nip19.hex}"
is Nip19Bech32.NEvent -> {
if (nip19.kind == PrivateDmEvent.KIND) {
nip19.author?.let { "RoomByAuthor/$it" }
} else if (
nip19.kind == ChannelMessageEvent.KIND ||
nip19.kind == ChannelCreateEvent.KIND ||
nip19.kind == ChannelMetadataEvent.KIND
) {
"Channel/${nip19.hex}"
} else {
"Event/${nip19.hex}"
}
}
is Nip19Bech32.NAddress -> {
if (nip19.kind == CommunityDefinitionEvent.KIND) {
"Community/${nip19.atag}"
} else if (nip19.kind == LiveActivitiesEvent.KIND) {
"Channel/${nip19.atag}"
} else {
"Event/${nip19.atag}"
}
}
is Nip19Bech32.NEmbed -> {
if (LocalCache.getNoteIfExists(nip19.event.id) == null) {
LocalCache.verifyAndConsume(nip19.event, null)
}
"Event/${nip19.event.id}"
}
else -> null
}
}
?: try {
uri?.let {
2024-02-22 20:40:07 +00:00
Nip47WalletConnect.parse(it)
val encodedUri = URLEncoder.encode(it, StandardCharsets.UTF_8.toString())
Route.Home.base + "?nip47=" + encodedUri
}
} catch (e: Exception) {
if (e is CancellationException) throw e
null
}
}
}