Simple-Flashlight/app/src/main/kotlin/com/simplemobiletools/flashlight/activities/MainActivity.kt

575 lines
22 KiB
Kotlin

package com.simplemobiletools.flashlight.activities
import android.Manifest
import android.annotation.SuppressLint
import android.app.AlarmManager
import android.app.Application
import android.content.Context
import android.content.Intent
import android.content.pm.ActivityInfo
import android.content.pm.PackageManager
import android.content.pm.ShortcutInfo
import android.graphics.drawable.Icon
import android.graphics.drawable.LayerDrawable
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.ManagedActivityResultLauncher
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.compose.runtime.*
import androidx.compose.ui.res.stringResource
import androidx.core.content.ContextCompat
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewModelScope
import com.google.android.material.math.MathUtils
import com.simplemobiletools.commons.compose.alert_dialog.AlertDialogState
import com.simplemobiletools.commons.compose.alert_dialog.rememberAlertDialogState
import com.simplemobiletools.commons.compose.extensions.*
import com.simplemobiletools.commons.compose.theme.AppThemeSurface
import com.simplemobiletools.commons.dialogs.*
import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.commons.helpers.*
import com.simplemobiletools.commons.models.FAQItem
import com.simplemobiletools.commons.models.RadioItem
import com.simplemobiletools.flashlight.BuildConfig
import com.simplemobiletools.flashlight.R
import com.simplemobiletools.flashlight.dialogs.SleepTimerCustomAlertDialog
import com.simplemobiletools.flashlight.extensions.config
import com.simplemobiletools.flashlight.extensions.startAboutActivity
import com.simplemobiletools.flashlight.helpers.*
import com.simplemobiletools.flashlight.screens.*
import com.simplemobiletools.flashlight.views.AnimatedSleepTimer
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.*
import java.util.*
import kotlin.system.exitProcess
class MainActivity : ComponentActivity() {
companion object {
private const val MAX_STROBO_DELAY = 2000L
private const val MIN_STROBO_DELAY = 10L
}
private val preferences by lazy { config }
private val viewModel by viewModels<MainViewModel>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdgeSimple()
setContent {
AppThemeSurface {
val showMoreApps = onEventValue { !resources.getBoolean(com.simplemobiletools.commons.R.bool.hide_google_relations) }
val sosPermissionLauncher = getCameraPermissionLauncher(onResult = getPermissionResultHandler(true))
val stroboscopePermissionLauncher = getCameraPermissionLauncher(onResult = getPermissionResultHandler(false))
val sleepTimerCustomDialogState = getSleepTimerCustomDialogState()
val sleepTimerDialogState = getSleepTimerDialogState(showCustomSleepTimerDialog = sleepTimerCustomDialogState::show)
val sleepTimerPermissionDialogState = getSleepTimerPermissionDialogState(showSleepTimerDialog = sleepTimerDialogState::show)
MainScreen(
flashlightButton = {
val flashlightActive by viewModel.flashlightOn.collectAsStateWithLifecycle()
FlashlightButton(
onFlashlightPress = { viewModel.toggleFlashlight() },
flashlightActive = flashlightActive,
)
},
brightDisplayButton = {
val showBrightDisplayButton by preferences.brightDisplayFlow.collectAsStateWithLifecycle(
config.brightDisplay,
minActiveState = Lifecycle.State.CREATED
)
if (showBrightDisplayButton) {
BrightDisplayButton(
onBrightDisplayPress = {
startActivity(Intent(applicationContext, BrightDisplayActivity::class.java))
}
)
}
},
sosButton = {
val showSosButton by preferences.sosFlow.collectAsStateWithLifecycle(config.sos, minActiveState = Lifecycle.State.CREATED)
val sosActive by viewModel.sosActive.collectAsStateWithLifecycle()
if (showSosButton) {
SosButton(
sosActive = sosActive,
onSosButtonPress = {
toggleStroboscope(true, sosPermissionLauncher)
},
)
}
},
stroboscopeButton = {
val showStroboscopeButton by preferences.stroboscopeFlow.collectAsStateWithLifecycle(
config.stroboscope,
minActiveState = Lifecycle.State.CREATED
)
val stroboscopeActive by viewModel.stroboscopeActive.collectAsStateWithLifecycle()
if (showStroboscopeButton) {
StroboscopeButton(
stroboscopeActive = stroboscopeActive,
onStroboscopeButtonPress = {
toggleStroboscope(false, stroboscopePermissionLauncher)
},
)
}
},
slidersSection = {
val brightnessBarVisible by viewModel.brightnessBarVisible.collectAsStateWithLifecycle()
val brightnessBarValue by viewModel.brightnessBarValue.collectAsStateWithLifecycle()
val stroboscopeBarVisible by viewModel.stroboscopeBarVisible.collectAsStateWithLifecycle()
val stroboscopeBarValue by viewModel.stroboscopeBarValue.collectAsStateWithLifecycle()
MainScreenSlidersSection(
showBrightnessBar = brightnessBarVisible,
brightnessBarValue = brightnessBarValue,
onBrightnessBarValueChange = viewModel::updateBrightnessBarValue,
showStroboscopeBar = stroboscopeBarVisible,
stroboscopeBarValue = stroboscopeBarValue,
onStroboscopeBarValueChange = viewModel::updateStroboscopeBarValue,
)
},
sleepTimer = {
val timerVisible by viewModel.timerVisible.collectAsStateWithLifecycle()
val timerText by viewModel.timerText.collectAsStateWithLifecycle()
AnimatedSleepTimer(
timerText = timerText,
timerVisible = timerVisible,
onTimerClosePress = { stopSleepTimer() },
)
},
showMoreApps = showMoreApps,
openSettings = ::launchSettings,
openAbout = ::launchAbout,
openSleepTimer = {
showSleepTimerPermission(sleepTimerPermissionDialogState) {
sleepTimerDialogState.show()
}
},
moreAppsFromUs = ::launchMoreAppsFromUsIntent
)
AppLaunched()
CheckAppOnSdCard()
}
}
}
@Composable
private fun SleepTimerRadioDialog(
alertDialogState: AlertDialogState,
onCustomValueSelected: () -> Unit
) {
val lastSleepTimerSeconds by preferences.lastSleepTimerSecondsFlow.collectAsStateWithLifecycle(preferences.lastSleepTimerSeconds)
val items by remember {
derivedStateOf {
val finalItems = ArrayList(listOf(10, 30, 60, 5 * 60, 10 * 60, 30 * 60).map {
RadioItem(it, secondsToString(it))
})
if (finalItems.none { it.id == lastSleepTimerSeconds }) {
finalItems.add(RadioItem(lastSleepTimerSeconds, secondsToString(lastSleepTimerSeconds)))
}
finalItems.sortBy { it.id }
finalItems.add(RadioItem(-1, getString(com.simplemobiletools.commons.R.string.custom)))
finalItems.toImmutableList()
}
}
RadioGroupAlertDialog(
alertDialogState = alertDialogState,
items = items,
selectedItemId = preferences.lastSleepTimerSeconds,
callback = {
if (it as Int == -1) {
onCustomValueSelected()
} else if (it > 0) {
pickedSleepTimer(it)
}
}
)
}
@Composable
private fun AppLaunched(
donateAlertDialogState: AlertDialogState = getDonateAlertDialogState(),
rateStarsAlertDialogState: AlertDialogState = getRateStarsAlertDialogState(),
) {
LaunchedEffect(Unit) {
appLaunchedCompose(
appId = BuildConfig.APPLICATION_ID,
showDonateDialog = donateAlertDialogState::show,
showRateUsDialog = rateStarsAlertDialogState::show,
showUpgradeDialog = {}
)
}
}
@Composable
private fun getDonateAlertDialogState() =
rememberAlertDialogState().apply {
DialogMember {
DonateAlertDialog(alertDialogState = this)
}
}
@Composable
private fun getRateStarsAlertDialogState() = rememberAlertDialogState().apply {
DialogMember {
RateStarsAlertDialog(alertDialogState = this, onRating = ::rateStarsRedirectAndThankYou)
}
}
@Composable
private fun getCameraPermissionLauncher(
onResult: (Boolean) -> Unit
) = rememberLauncherForActivityResult(
contract = ActivityResultContracts.RequestPermission(),
onResult = onResult
)
@Composable
private fun getSleepTimerCustomDialogState() = rememberAlertDialogState().apply {
DialogMember {
SleepTimerCustomAlertDialog(
alertDialogState = this,
onConfirmClick = {
if (it > 0) {
pickedSleepTimer(it)
}
},
)
}
}
@Composable
private fun getSleepTimerDialogState(
showCustomSleepTimerDialog: () -> Unit
) = rememberAlertDialogState().apply {
DialogMember {
SleepTimerRadioDialog(
alertDialogState = this,
onCustomValueSelected = showCustomSleepTimerDialog
)
}
}
@Composable
private fun getSleepTimerPermissionDialogState(
showSleepTimerDialog: () -> Unit
) = rememberAlertDialogState().apply {
DialogMember {
PermissionRequiredAlertDialog(
alertDialogState = this,
text = stringResource(id = com.simplemobiletools.commons.R.string.allow_alarm_sleep_timer),
positiveActionCallback = {
openRequestExactAlarmSettings(baseConfig.appId)
},
negativeActionCallback = showSleepTimerDialog
)
}
}
override fun onResume() {
super.onResume()
viewModel.onResume()
requestedOrientation = if (preferences.forcePortraitMode) ActivityInfo.SCREEN_ORIENTATION_PORTRAIT else ActivityInfo.SCREEN_ORIENTATION_SENSOR
invalidateOptionsMenu()
checkShortcuts()
}
override fun onStart() {
super.onStart()
if (preferences.sleepInTS == 0L) {
viewModel.hideTimer()
(getSystemService(Context.ALARM_SERVICE) as AlarmManager).cancel(getShutDownPendingIntent())
}
}
private fun launchSettings() {
hideKeyboard()
startActivity(Intent(applicationContext, SettingsActivity::class.java))
}
private fun launchAbout() {
val faqItems = arrayListOf(
FAQItem(com.simplemobiletools.commons.R.string.faq_1_title_commons, com.simplemobiletools.commons.R.string.faq_1_text_commons),
FAQItem(com.simplemobiletools.commons.R.string.faq_4_title_commons, com.simplemobiletools.commons.R.string.faq_4_text_commons)
)
if (!resources.getBoolean(com.simplemobiletools.commons.R.bool.hide_google_relations)) {
faqItems.add(FAQItem(com.simplemobiletools.commons.R.string.faq_2_title_commons, com.simplemobiletools.commons.R.string.faq_2_text_commons))
faqItems.add(FAQItem(com.simplemobiletools.commons.R.string.faq_6_title_commons, com.simplemobiletools.commons.R.string.faq_6_text_commons))
}
startAboutActivity(R.string.app_name, 0, BuildConfig.VERSION_NAME, faqItems, true)
}
private fun toggleStroboscope(isSOS: Boolean, launcher: ManagedActivityResultLauncher<String, Boolean>) {
// use the old Camera API for stroboscope, the new Camera Manager is way too slow
if (isNougatPlus()) {
cameraPermissionGranted(isSOS)
} else {
val permission = Manifest.permission.CAMERA
if (ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED) {
cameraPermissionGranted(isSOS)
} else {
launcher.launch(permission)
}
}
}
private fun getPermissionResultHandler(isSos: Boolean): (Boolean) -> Unit = {
handlePermissionResult(isSos, it)
}
private fun handlePermissionResult(isSos: Boolean, granted: Boolean) {
if (granted) {
cameraPermissionGranted(isSos)
} else {
toast(R.string.camera_permission)
}
}
private fun cameraPermissionGranted(isSOS: Boolean) {
if (isSOS) {
viewModel.enableSos()
} else {
viewModel.enableStroboscope()
}
}
private fun showSleepTimerPermission(
showSleepTimerDialogState: AlertDialogState,
callback: () -> Unit
) {
val alarmManager = getSystemService(Context.ALARM_SERVICE) as AlarmManager
if (isSPlus() && !alarmManager.canScheduleExactAlarms()) {
showSleepTimerDialogState.show()
return
}
callback()
}
private fun secondsToString(seconds: Int): String {
val finalHours = seconds / 3600
val finalMinutes = (seconds / 60) % 60
val finalSeconds = seconds % 60
val parts = mutableListOf<String>()
if (finalHours != 0) {
parts.add(resources.getQuantityString(com.simplemobiletools.commons.R.plurals.hours, finalHours, finalHours))
}
if (finalMinutes != 0) {
parts.add(resources.getQuantityString(com.simplemobiletools.commons.R.plurals.minutes, finalMinutes, finalMinutes))
}
if (finalSeconds != 0) {
parts.add(resources.getQuantityString(com.simplemobiletools.commons.R.plurals.seconds, finalSeconds, finalSeconds))
}
return parts.joinToString(separator = " ")
}
private fun pickedSleepTimer(seconds: Int) {
preferences.lastSleepTimerSeconds = seconds
preferences.sleepInTS = System.currentTimeMillis() + seconds * 1000
startSleepTimer()
}
private fun startSleepTimer() {
viewModel.showTimer()
startSleepTimerCountDown()
}
private fun stopSleepTimer() {
viewModel.hideTimer()
stopSleepTimerCountDown()
}
@SuppressLint("NewApi")
private fun checkShortcuts() {
val appIconColor = preferences.appIconColor
if (isNougatMR1Plus() && preferences.lastHandledShortcutColor != appIconColor) {
val createNewContact = getBrightDisplayShortcut(appIconColor)
try {
shortcutManager.dynamicShortcuts = Arrays.asList(createNewContact)
preferences.lastHandledShortcutColor = appIconColor
} catch (ignored: Exception) {
}
}
}
@SuppressLint("NewApi")
private fun getBrightDisplayShortcut(appIconColor: Int): ShortcutInfo {
val brightDisplay = getString(R.string.bright_display)
val drawable = resources.getDrawable(R.drawable.shortcut_bright_display)
(drawable as LayerDrawable).findDrawableByLayerId(R.id.shortcut_bright_display_background).applyColorFilter(appIconColor)
val bmp = drawable.convertToBitmap()
val intent = Intent(this, BrightDisplayActivity::class.java)
intent.action = Intent.ACTION_VIEW
return ShortcutInfo.Builder(this, "bright_display")
.setShortLabel(brightDisplay)
.setLongLabel(brightDisplay)
.setIcon(Icon.createWithBitmap(bmp))
.setIntent(intent)
.build()
}
internal class MainViewModel(
application: Application
) : AndroidViewModel(application) {
private val preferences = application.config
private lateinit var camera: MyCameraImpl
init {
camera = MyCameraImpl.newInstance(application, object : CameraTorchListener {
override fun onTorchEnabled(isEnabled: Boolean) {
camera.onTorchEnabled(isEnabled)
if (isEnabled && camera.supportsBrightnessControl()) {
_brightnessBarValue.value = camera.getCurrentBrightnessLevel().toFloat() / camera.getMaximumBrightnessLevel()
}
}
override fun onTorchUnavailable() {
camera.onCameraNotAvailable()
}
})
if (preferences.turnFlashlightOn) {
camera.enableFlashlight()
}
}
private val _timerText: MutableStateFlow<String> = MutableStateFlow("00:00")
val timerText = _timerText.asStateFlow()
private val _timerVisible: MutableStateFlow<Boolean> = MutableStateFlow(false)
val timerVisible = _timerVisible.asStateFlow()
val flashlightOn = camera.flashlightOnFlow
val brightnessBarVisible = flashlightOn.map {
it && camera.supportsBrightnessControl()
}.stateIn(viewModelScope, SharingStarted.Lazily, false)
private val _sosActive: MutableStateFlow<Boolean> = MutableStateFlow(false)
val sosActive = _sosActive.asStateFlow()
private val _brightnessBarValue: MutableStateFlow<Float> = MutableStateFlow(1f)
val brightnessBarValue = _brightnessBarValue.asStateFlow()
private val _stroboscopeActive: MutableStateFlow<Boolean> = MutableStateFlow(false)
val stroboscopeActive = _stroboscopeActive.asStateFlow()
val stroboscopeBarVisible = stroboscopeActive
private val _stroboscopeBarValue: MutableStateFlow<Float> = MutableStateFlow(0.5f)
val stroboscopeBarValue = _stroboscopeBarValue.asStateFlow()
init {
_stroboscopeBarValue.value = preferences.stroboscopeProgress.toFloat() / MAX_STROBO_DELAY
SleepTimer.timeLeft
.onEach { seconds ->
_timerText.value = seconds.getFormattedDuration()
_timerVisible.value = true
if (seconds == 0) {
exitProcess(0)
}
}
.launchIn(viewModelScope)
MyCameraImpl.cameraError
.onEach { getApplication<Application>().toast(R.string.camera_error) }
.launchIn(viewModelScope)
camera.stroboscopeDisabled
.onEach { _stroboscopeActive.value = false }
.launchIn(viewModelScope)
camera.sosDisabled
.onEach { _sosActive.value = false }
.launchIn(viewModelScope)
}
fun hideTimer() {
_timerVisible.value = false
}
fun showTimer() {
_timerVisible.value = true
}
fun updateBrightnessBarValue(newValue: Float) {
_brightnessBarValue.value = newValue
val max = camera.getMaximumBrightnessLevel()
val min = MIN_BRIGHTNESS_LEVEL
val newLevel = MathUtils.lerp(min.toFloat(), max.toFloat(), newValue)
camera.updateBrightnessLevel(newLevel.toInt())
preferences.brightnessLevel = newLevel.toInt()
}
fun updateStroboscopeBarValue(newValue: Float) {
_stroboscopeBarValue.value = newValue
val max = MAX_STROBO_DELAY
val min = MIN_STROBO_DELAY
val newLevel = MathUtils.lerp(min.toFloat(), max.toFloat(), 1 - newValue)
camera.stroboFrequency = newLevel.toLong()
preferences.stroboscopeFrequency = newLevel.toLong()
preferences.stroboscopeProgress = ((1 - newLevel) * MAX_STROBO_DELAY).toInt()
}
fun onResume() {
camera.handleCameraSetup()
if (preferences.turnFlashlightOn) {
camera.enableFlashlight()
}
if (!preferences.stroboscope && _stroboscopeActive.value) {
camera.stopStroboscope()
}
if (!preferences.sos && _sosActive.value) {
camera.stopSOS()
}
}
override fun onCleared() {
super.onCleared()
releaseCamera()
}
fun toggleFlashlight() {
camera.toggleFlashlight()
}
private fun releaseCamera() {
camera.releaseCamera()
}
fun enableSos() {
_sosActive.value = camera.toggleSOS()
}
fun enableStroboscope() {
_stroboscopeActive.value = camera.toggleStroboscope()
}
}
}