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> { throw IllegalStateException("not allowed here!") } val LocalDevicePosture = staticCompositionLocalOf> { throw IllegalStateException("not allowed here!") } @AndroidEntryPoint class MainActivityV2 : BaseComposeActivity() { private val handler = Handler(Looper.getMainLooper()) private val newMessageReceiver: BroadcastReceiver = NewMessageReceiver() private val notificationCountFlow: MutableSharedFlow = MutableSharedFlow(replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST) private val devicePostureFlow: StateFlow by lazy { WindowInfoTracker.getOrCreate(this) .windowLayoutInfo(this) .flowWithLifecycle(lifecycle) .map { layoutInfo -> val foldingFeature = layoutInfo.displayFeatures .filterIsInstance() .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) } } } } } }