kopia lustrzana https://github.com/vitorpamplona/amethyst
321 wiersze
12 KiB
Kotlin
321 wiersze
12 KiB
Kotlin
/**
|
|
* Copyright (c) 2024 Vitor Pamplona
|
|
*
|
|
* 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.
|
|
*/
|
|
package com.vitorpamplona.amethyst.ui
|
|
|
|
import android.annotation.SuppressLint
|
|
import android.content.Context
|
|
import android.content.Intent
|
|
import android.net.ConnectivityManager
|
|
import android.net.Network
|
|
import android.net.NetworkCapabilities
|
|
import android.os.Build
|
|
import android.os.Bundle
|
|
import android.util.Log
|
|
import androidx.activity.compose.setContent
|
|
import androidx.activity.result.contract.ActivityResultContracts
|
|
import androidx.annotation.RequiresApi
|
|
import androidx.appcompat.app.AppCompatActivity
|
|
import androidx.compose.runtime.mutableStateOf
|
|
import com.vitorpamplona.amethyst.LocalPreferences
|
|
import com.vitorpamplona.amethyst.ServiceManager
|
|
import com.vitorpamplona.amethyst.model.LocalCache
|
|
import com.vitorpamplona.amethyst.service.HttpClientManager
|
|
import com.vitorpamplona.amethyst.service.lang.LanguageTranslatorService
|
|
import com.vitorpamplona.amethyst.service.notifications.PushNotificationUtils
|
|
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
|
|
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
|
|
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
|
|
|
|
class MainActivity : AppCompatActivity() {
|
|
val isOnMobileDataState = mutableStateOf(false)
|
|
private val isOnWifiDataState = mutableStateOf(false)
|
|
|
|
// 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")
|
|
|
|
setContent {
|
|
val sharedPreferencesViewModel = prepareSharedViewModel(act = this)
|
|
AppScreen(sharedPreferencesViewModel = sharedPreferencesViewModel, serviceManager = serviceManager)
|
|
}
|
|
}
|
|
|
|
fun prepareToLaunchSigner() {
|
|
shouldPauseService = false
|
|
}
|
|
|
|
@OptIn(DelicateCoroutinesApi::class)
|
|
override fun onResume() {
|
|
super.onResume()
|
|
|
|
Log.d("Lifetime Event", "MainActivity.onResume")
|
|
|
|
// starts muted every time
|
|
DEFAULT_MUTED_SETTING.value = true
|
|
|
|
// Keep connection alive if it's calling the signer app
|
|
Log.d("shouldPauseService", "shouldPauseService onResume: $shouldPauseService")
|
|
if (shouldPauseService) {
|
|
GlobalScope.launch(Dispatchers.IO) { serviceManager.justStart() }
|
|
}
|
|
|
|
GlobalScope.launch(Dispatchers.IO) {
|
|
PushNotificationUtils.init(LocalPreferences.allSavedAccounts())
|
|
}
|
|
|
|
val connectivityManager =
|
|
(getSystemService(ConnectivityManager::class.java) as ConnectivityManager)
|
|
connectivityManager.registerDefaultNetworkCallback(networkCallback)
|
|
connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork)?.let {
|
|
updateNetworkCapabilities(it)
|
|
}
|
|
|
|
// resets state until next External Signer Call
|
|
Timer().schedule(350) { shouldPauseService = true }
|
|
}
|
|
|
|
override fun onPause() {
|
|
Log.d("Lifetime Event", "MainActivity.onPause")
|
|
|
|
GlobalScope.launch(Dispatchers.IO) {
|
|
LanguageTranslatorService.clear()
|
|
}
|
|
serviceManager.cleanObservers()
|
|
|
|
// if (BuildConfig.DEBUG) {
|
|
GlobalScope.launch(Dispatchers.IO) { debugState(this@MainActivity) }
|
|
// }
|
|
|
|
Log.d("shouldPauseService", "shouldPauseService onPause: $shouldPauseService")
|
|
if (shouldPauseService) {
|
|
GlobalScope.launch(Dispatchers.IO) { serviceManager.pauseForGood() }
|
|
}
|
|
|
|
(getSystemService(ConnectivityManager::class.java) as ConnectivityManager)
|
|
.unregisterNetworkCallback(networkCallback)
|
|
|
|
super.onPause()
|
|
}
|
|
|
|
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")
|
|
}
|
|
|
|
override fun onDestroy() {
|
|
Log.d("Lifetime Event", "MainActivity.onDestroy")
|
|
|
|
GlobalScope.launch(Dispatchers.Main) {
|
|
keepPlayingMutex?.stop()
|
|
keepPlayingMutex?.release()
|
|
keepPlayingMutex = null
|
|
}
|
|
|
|
super.onDestroy()
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
|
|
changedNetwork = true
|
|
}
|
|
|
|
if (changedNetwork) {
|
|
if (isOnMobileData) {
|
|
HttpClientManager.setDefaultTimeout(HttpClientManager.DEFAULT_TIMEOUT_ON_MOBILE)
|
|
} else {
|
|
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()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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/*"))
|
|
}
|
|
}
|
|
}
|
|
|
|
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 {
|
|
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
|
|
}
|
|
}
|
|
}
|