TiebaLite/app/src/main/java/com/huanchengfly/tieba/post/MainActivityV2.kt

305 lines
13 KiB
Kotlin

package com.huanchengfly.tieba.post
import android.annotation.SuppressLint
import android.app.job.JobInfo
import android.app.job.JobScheduler
import android.content.BroadcastReceiver
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import androidx.compose.animation.AnimatedContentScope
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.VisibilityThreshold
import androidx.compose.animation.core.spring
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.Alignment
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.flowWithLifecycle
import androidx.lifecycle.lifecycleScope
import androidx.navigation.plusAssign
import androidx.window.layout.FoldingFeature
import androidx.window.layout.WindowInfoTracker
import com.google.accompanist.navigation.animation.rememberAnimatedNavController
import com.google.accompanist.navigation.material.ExperimentalMaterialNavigationApi
import com.google.accompanist.navigation.material.ModalBottomSheetLayout
import com.google.accompanist.navigation.material.rememberBottomSheetNavigator
import com.google.accompanist.systemuicontroller.SystemUiController
import com.huanchengfly.tieba.post.api.retrofit.exception.getErrorMessage
import com.huanchengfly.tieba.post.arch.BaseComposeActivity
import com.huanchengfly.tieba.post.services.NotifyJobService
import com.huanchengfly.tieba.post.ui.common.theme.compose.ExtendedTheme
import com.huanchengfly.tieba.post.ui.page.NavGraphs
import com.huanchengfly.tieba.post.ui.page.destinations.MainPageDestination
import com.huanchengfly.tieba.post.ui.utils.DevicePosture
import com.huanchengfly.tieba.post.ui.utils.isBookPosture
import com.huanchengfly.tieba.post.ui.utils.isSeparating
import com.huanchengfly.tieba.post.ui.widgets.compose.AlertDialog
import com.huanchengfly.tieba.post.ui.widgets.compose.DialogNegativeButton
import com.huanchengfly.tieba.post.ui.widgets.compose.DialogPositiveButton
import com.huanchengfly.tieba.post.ui.widgets.compose.rememberDialogState
import com.huanchengfly.tieba.post.utils.AccountUtil
import com.huanchengfly.tieba.post.utils.ClientUtils
import com.huanchengfly.tieba.post.utils.JobServiceUtil
import com.huanchengfly.tieba.post.utils.PermissionUtils
import com.huanchengfly.tieba.post.utils.TiebaUtil
import com.huanchengfly.tieba.post.utils.isIgnoringBatteryOptimizations
import com.huanchengfly.tieba.post.utils.newIntentFilter
import com.huanchengfly.tieba.post.utils.requestIgnoreBatteryOptimizations
import com.huanchengfly.tieba.post.utils.requestPermission
import com.ramcosta.composedestinations.DestinationsNavHost
import com.ramcosta.composedestinations.animations.defaults.RootNavGraphDefaultAnimations
import com.ramcosta.composedestinations.animations.rememberAnimatedNavHostEngine
import com.ramcosta.composedestinations.navigation.dependency
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
val LocalNotificationCountFlow =
staticCompositionLocalOf<Flow<Int>> { throw IllegalStateException("not allowed here!") }
val LocalDevicePosture =
staticCompositionLocalOf<State<DevicePosture>> { throw IllegalStateException("not allowed here!") }
@AndroidEntryPoint
class MainActivityV2 : BaseComposeActivity() {
private val handler = Handler(Looper.getMainLooper())
private val newMessageReceiver: BroadcastReceiver = NewMessageReceiver()
private val notificationCountFlow: MutableSharedFlow<Int> =
MutableSharedFlow(replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
private val devicePostureFlow: StateFlow<DevicePosture> by lazy {
WindowInfoTracker.getOrCreate(this)
.windowLayoutInfo(this)
.flowWithLifecycle(lifecycle)
.map { layoutInfo ->
val foldingFeature =
layoutInfo.displayFeatures
.filterIsInstance<FoldingFeature>()
.firstOrNull()
when {
isBookPosture(foldingFeature) ->
DevicePosture.BookPosture(foldingFeature.bounds)
isSeparating(foldingFeature) ->
DevicePosture.Separating(foldingFeature.bounds, foldingFeature.orientation)
else -> DevicePosture.NormalPosture
}
}
.stateIn(
scope = lifecycleScope,
started = SharingStarted.Eagerly,
initialValue = DevicePosture.NormalPosture
)
}
private fun fetchAccount() {
lifecycleScope.launch(Dispatchers.Main) {
if (AccountUtil.isLoggedIn()) {
AccountUtil.fetchAccountFlow()
.flowOn(Dispatchers.IO)
.catch { e ->
toastShort(e.getErrorMessage())
e.printStackTrace()
}
.collect()
}
}
}
private fun requestNotificationPermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && AccountUtil.isLoggedIn()) {
requestPermission {
permissions = listOf(PermissionUtils.POST_NOTIFICATIONS)
description = getString(R.string.desc_permission_post_notifications)
}
}
}
private fun initAutoSign() {
runCatching {
TiebaUtil.initAutoSign(this)
}
}
override fun onStart() {
super.onStart()
runCatching {
ContextCompat.registerReceiver(
this,
newMessageReceiver,
newIntentFilter(NotifyJobService.ACTION_NEW_MESSAGE),
ContextCompat.RECEIVER_NOT_EXPORTED
)
startService(Intent(this, NotifyJobService::class.java))
val builder = JobInfo.Builder(
JobServiceUtil.getJobId(this),
ComponentName(this, NotifyJobService::class.java)
)
.setPersisted(true)
.setPeriodic(30 * 60 * 1000L)
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
val jobScheduler = getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler
jobScheduler.schedule(builder.build())
}
handler.postDelayed({
requestNotificationPermission()
}, 100)
}
override fun onCreate(savedInstanceState: Bundle?) {
installSplashScreen()
super.onCreate(savedInstanceState)
window.decorView.setBackgroundColor(Color.TRANSPARENT)
window.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
launch {
ClientUtils.setActiveTimestamp()
}
}
override fun onCreateContent(systemUiController: SystemUiController) {
super.onCreateContent(systemUiController)
fetchAccount()
initAutoSign()
}
@OptIn(ExperimentalAnimationApi::class, ExperimentalMaterialNavigationApi::class)
@Composable
override fun createContent() {
val okSignAlertDialogState = rememberDialogState()
AlertDialog(
dialogState = okSignAlertDialogState,
title = { Text(text = stringResource(id = R.string.title_dialog_oksign_battery_optimization)) },
content = { Text(text = stringResource(id = R.string.message_dialog_oksign_battery_optimization)) },
buttons = {
DialogPositiveButton(
text = stringResource(id = R.string.button_go_to_ignore_battery_optimization),
onClick = {
requestIgnoreBatteryOptimizations()
}
)
DialogNegativeButton(
text = stringResource(id = R.string.button_cancel)
)
DialogNegativeButton(
text = stringResource(id = R.string.button_dont_remind_again),
onClick = {
appPreferences.ignoreBatteryOptimizationsDialog = true
}
)
},
)
LaunchedEffect(Unit) {
if (appPreferences.autoSign && !isIgnoringBatteryOptimizations() && !appPreferences.ignoreBatteryOptimizationsDialog) {
okSignAlertDialogState.show()
}
}
CompositionLocalProvider(
LocalNotificationCountFlow provides notificationCountFlow,
LocalDevicePosture provides devicePostureFlow.collectAsState(),
) {
Surface(
color = ExtendedTheme.colors.background
) {
val animationSpec = spring(
stiffness = Spring.StiffnessMediumLow,
visibilityThreshold = IntOffset.VisibilityThreshold
)
val engine = rememberAnimatedNavHostEngine(
navHostContentAlignment = Alignment.TopStart,
rootDefaultAnimations = RootNavGraphDefaultAnimations(
enterTransition = {
slideIntoContainer(
AnimatedContentScope.SlideDirection.Start,
animationSpec = animationSpec,
initialOffset = { it }
)
},
exitTransition = {
slideOutOfContainer(
AnimatedContentScope.SlideDirection.End,
animationSpec = animationSpec,
targetOffset = { -it }
)
},
popEnterTransition = {
slideIntoContainer(
AnimatedContentScope.SlideDirection.Start,
animationSpec = animationSpec,
initialOffset = { -it }
)
},
popExitTransition = {
slideOutOfContainer(
AnimatedContentScope.SlideDirection.End,
animationSpec = animationSpec,
targetOffset = { it }
)
},
),
)
val navController = rememberAnimatedNavController()
val bottomSheetNavigator =
rememberBottomSheetNavigator(animationSpec = spring(stiffness = Spring.StiffnessMediumLow))
navController.navigatorProvider += bottomSheetNavigator
ModalBottomSheetLayout(
bottomSheetNavigator = bottomSheetNavigator,
sheetShape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)
) {
DestinationsNavHost(
navController = navController,
navGraph = NavGraphs.root,
engine = engine,
dependenciesContainerBuilder = {
dependency(MainPageDestination) { this@MainActivityV2 }
}
)
}
}
}
}
private inner class NewMessageReceiver : BroadcastReceiver() {
@SuppressLint("RestrictedApi")
override fun onReceive(context: Context, intent: Intent) {
if (intent.action == NotifyJobService.ACTION_NEW_MESSAGE) {
val channel = intent.getStringExtra("channel")
val count = intent.getIntExtra("count", 0)
if (channel != null && channel == NotifyJobService.CHANNEL_TOTAL) {
lifecycleScope.launch {
notificationCountFlow.emit(count)
}
}
}
}
}
}