Skip to content

Commit

Permalink
Only notifies for permission loss when restarting
Browse files Browse the repository at this point in the history
  • Loading branch information
Nain57 committed Nov 17, 2024
1 parent 1014eb6 commit 36c116b
Show file tree
Hide file tree
Showing 25 changed files with 200 additions and 261 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -107,9 +107,9 @@ class OverlayManager @Inject internal constructor(
}

/** Destroys all overlays in the backstack except the root one. */
fun navigateUpToRoot(context: Context, completionListener: () -> Unit) {
fun navigateUpToRoot(context: Context, completionListener: (() -> Unit)? = null) {
if (overlayBackStack.size <= 1) {
completionListener()
completionListener?.invoke()
return
}

Expand Down
5 changes: 3 additions & 2 deletions core/common/ui/src/main/res/drawable/ic_badge_error.xml
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
android:shape="oval"
android:tint="?attr/colorError">

<solid
android:color="?attr/colorError"/>
android:color="@color/md_theme_light_error"/>

<size
android:width="6dp"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ class DetectorEngine @Inject constructor(
resultCode: Int,
data: Intent,
androidExecutor: SmartActionExecutor,
onRecordingStopped: () -> Unit,
onRecordingStopped: (() -> Unit)?,
) {
if (_state.value != DetectorState.CREATED) {
Log.w(TAG, "startScreenRecord: Screen record is already started")
Expand All @@ -140,7 +140,7 @@ class DetectorEngine @Inject constructor(
startProjection(context, resultCode, data) {
Log.i(TAG, "projection lost")
this@DetectorEngine.stopScreenRecord()
onRecordingStopped()
onRecordingStopped?.invoke()
}
startScreenRecord(context, displayConfigManager.displayConfig.sizePx)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@ class DetectionRepository @Inject constructor(
private val coroutineScopeIo: CoroutineScope =
CoroutineScope(SupervisorJob() + ioDispatcher)

/** Interacts with the OS to execute the actions */
private var actionExecutor: SmartActionExecutor? = null

private var projectionErrorHandler: (() -> Unit)? = null

/** Stop the detection automatically after selected delay */
private var autoStopJob: Job? = null

Expand Down Expand Up @@ -105,19 +110,23 @@ class DetectionRepository @Inject constructor(
_scenarioId.value = identifier
}

fun setExecutor(androidExecutor: SmartActionExecutor) {
actionExecutor = androidExecutor
}

fun setProjectionErrorHandler(handler: () -> Unit) {
projectionErrorHandler = handler
}

fun getScenarioId(): Identifier? = _scenarioId.value

fun isRunning(): Boolean =
detectorEngine.state.value == DetectorState.DETECTING

fun startScreenRecord(
context: Context,
resultCode: Int,
data: Intent,
androidExecutor: SmartActionExecutor,
onProjectionLost: () -> Unit,
) {
detectorEngine.startScreenRecord(context, resultCode, data, androidExecutor, onProjectionLost)
fun startScreenRecord(context: Context, resultCode: Int, data: Intent) {
actionExecutor?.let { executor ->
detectorEngine.startScreenRecord(context, resultCode, data, executor, projectionErrorHandler)
}
}

suspend fun startDetection(context: Context, progressListener: ScenarioProcessingListener?, autoStopDuration: Duration? = null) {
Expand Down Expand Up @@ -151,11 +160,14 @@ class DetectionRepository @Inject constructor(
}

fun stopScreenRecord() {
projectionErrorHandler = null

detectorEngine.apply {
stopScreenRecord()
clear()
}

actionExecutor = null
_scenarioId.value = null
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import com.buzbuz.smartautoclicker.core.ui.utils.getDynamicColorsContext
import com.buzbuz.smartautoclicker.feature.smart.config.R
import com.buzbuz.smartautoclicker.feature.smart.config.databinding.OverlayMenuBinding
import com.buzbuz.smartautoclicker.feature.smart.config.di.ScenarioConfigViewModelsEntryPoint
import com.buzbuz.smartautoclicker.feature.smart.config.ui.common.starters.newRestartMediaProjectionStarterOverlay
import com.buzbuz.smartautoclicker.feature.smart.config.ui.scenario.ScenarioDialog
import com.buzbuz.smartautoclicker.feature.smart.debugging.di.DebuggingViewModelsEntryPoint
import com.buzbuz.smartautoclicker.feature.smart.debugging.ui.overlay.DebugModel
Expand Down Expand Up @@ -117,7 +118,8 @@ class MainMenu(private val onStopClicked: () -> Unit) : OverlayMenu() {

lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
launch { viewModel.canStartScenario.collect(::updatePlayPauseButtonEnabledState) }
launch { viewModel.isStartButtonEnabled.collect(::updatePlayPauseButtonEnabledState) }
launch { viewModel.isMediaProjectionStarted.collect(::updateProjectionErrorBadge) }
launch { viewModel.detectionState.collect(::updateDetectionState) }
launch { viewModel.nativeLibError.collect(::showNativeLibErrorDialogIfNeeded) }
launch { debuggingViewModel.isDebugging.collect(::updateDebugOverlayViewVisibility) }
Expand Down Expand Up @@ -168,11 +170,7 @@ class MainMenu(private val onStopClicked: () -> Unit) : OverlayMenu() {
override fun onMenuItemClicked(viewId: Int) {
when (viewId) {
R.id.btn_play -> onPlayPauseClicked()
R.id.btn_click_list -> {
viewModel.startScenarioEdition {
showScenarioConfigDialog()
}
}
R.id.btn_click_list -> onConfigureClicked()
R.id.btn_stop -> onStopClicked()
}
}
Expand All @@ -185,7 +183,28 @@ class MainMenu(private val onStopClicked: () -> Unit) : OverlayMenu() {
)
}

fun onMediaProjectionLost() {
overlayManager.navigateUpToRoot(context)
viewModel.cancelScenarioChanges()
}

private fun onConfigureClicked() {
if (viewModel.shouldRestartMediaProjection()) {
showRestartMediaProjectionScreen()
return
}

viewModel.startScenarioEdition {
showScenarioConfigDialog()
}
}

private fun onPlayPauseClicked() {
if (viewModel.shouldRestartMediaProjection()) {
showRestartMediaProjectionScreen()
return
}

if (viewModel.shouldShowStopVolumeDownTutorialDialog()) {
showStopVolumeDownTutorialDialog()
return
Expand Down Expand Up @@ -261,6 +280,10 @@ class MainMenu(private val onStopClicked: () -> Unit) : OverlayMenu() {
}
}

private fun updateProjectionErrorBadge(isProjectionStarted: Boolean) {
viewBinding.errorBadge.visibility = if (isProjectionStarted) View.GONE else View.VISIBLE
}

/**
* Observe the values for the debug and update the debug views.
* @return the coroutine job for the observable. Can be cancelled to stop the observation.
Expand Down Expand Up @@ -323,4 +346,12 @@ class MainMenu(private val onStopClicked: () -> Unit) : OverlayMenu() {
.create()
.showAsOverlay()
}

private fun showRestartMediaProjectionScreen() {
overlayManager.navigateTo(
context = context,
newOverlay = newRestartMediaProjectionStarterOverlay(context),
hideCurrent = true,
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -83,11 +83,18 @@ class MainMenuModel @Inject constructor(
UiState.Idle,
)

val isMediaProjectionStarted: StateFlow<Boolean> = detectionRepository.detectionState
.map { it == DetectionState.RECORDING || it == DetectionState.DETECTING }
.stateIn(viewModelScope, SharingStarted.Eagerly, true)

/** Tells if the scenario can be started. Edited scenario must be synchronized and engine should allow it. */
val canStartScenario: Flow<Boolean> = detectionRepository.canStartDetection
.combine(editionRepository.isEditionSynchronized) { canStartDetection, isSynchronized ->
canStartDetection && isSynchronized
}
val isStartButtonEnabled: Flow<Boolean> = combine(
detectionRepository.canStartDetection,
editionRepository.isEditionSynchronized,
isMediaProjectionStarted
) { canStartDetection, isSynchronized, isProjectionStarted ->
(canStartDetection || !isProjectionStarted) && isSynchronized
}

/** Tells if the detector can't work due to a native library load error. */
val nativeLibError: Flow<Boolean> = detectionRepository.detectionState
Expand Down Expand Up @@ -184,6 +191,9 @@ class MainMenuModel @Inject constructor(
}
}

fun shouldRestartMediaProjection(): Boolean =
!isMediaProjectionStarted.value

fun shouldShowStopVolumeDownTutorialDialog(): Boolean =
detectionState.value == UiState.Idle && tutorialRepository.shouldShowStopWithVolumeDownTutorialDialog()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
package com.buzbuz.smartautoclicker.feature.smart.config.ui.common.starters

import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.os.Bundle
import android.util.Log
Expand All @@ -29,66 +28,70 @@ import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity

import com.buzbuz.smartautoclicker.core.base.extensions.showAsOverlay
import com.buzbuz.smartautoclicker.core.common.overlays.manager.OverlayManager
import com.buzbuz.smartautoclicker.core.display.recorder.showMediaProjectionWarning
import com.buzbuz.smartautoclicker.feature.smart.config.R

import com.google.android.material.dialog.MaterialAlertDialogBuilder

import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject

@AndroidEntryPoint
class RequestMediaProjectionActivity : AppCompatActivity() {
class RestartMediaProjectionActivity : AppCompatActivity() {

companion object {

fun getStartIntent(context: Context): Intent =
Intent(context, RequestMediaProjectionActivity::class.java)
Intent(context, RestartMediaProjectionActivity::class.java)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
}

private val viewModel: MediaProjectionLostViewModel by viewModels()
private val viewModel: RestartMediaProjectionViewModel by viewModels()

@Inject lateinit var overlayManager: OverlayManager

/** The result launcher for the projection permission dialog. */
private lateinit var projectionActivityResult: ActivityResultLauncher<Intent>
private var dialog: AlertDialog? = null

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_media_projection_lost)
setContentView(R.layout.activity_transparent)

overlayManager.hideAll()
projectionActivityResult =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode != RESULT_OK) {
finish()
val data = result.data
if (data == null || result.resultCode != RESULT_OK) {
finishActivity()
return@registerForActivityResult
}

Log.i(TAG, "Media projection us running, start scenario")
viewModel.restartScreenRecord(this, result.resultCode, data)

viewModel.startSmartScenario(result.resultCode, result.data!!)
dialog?.dismiss()
finish()
finishActivity()
}

dialog = showProjectionLostDialog()
}

override fun onStop() {
super.onStop()
dialog?.dismiss()
}

private fun showProjectionLostDialog(): AlertDialog {
return MaterialAlertDialogBuilder(this)
private fun showProjectionLostDialog() =
MaterialAlertDialogBuilder(this)
.setTitle(R.string.dialog_overlay_title_warning)
.setMessage(R.string.message_error_media_projection_lost)
.setPositiveButton(R.string.yes) { _: DialogInterface, _: Int ->
projectionActivityResult.showMediaProjectionWarning(this) { finish() }
}
.setNegativeButton(R.string.no) { _: DialogInterface, _: Int ->
viewModel.stopApp()
.setPositiveButton(R.string.yes) { _, _ ->
projectionActivityResult.showMediaProjectionWarning(this) { finishActivity() }
}
.setNegativeButton(R.string.no) { _, _ -> finishActivity() }
.create().also { it.showAsOverlay() }

private fun finishActivity() {
dialog?.dismiss()
overlayManager.restoreVisibility()
overlayManager.navigateUp(this)
finish()
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,2 +1,46 @@
package com.buzbuz.smartautoclicker.feature.smart.config.ui.common.starters
/*
* Copyright (C) 2024 Kevin Buzeau
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.buzbuz.smartautoclicker.feature.smart.config.ui.common.starters

import android.content.Context
import android.content.Intent
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope

import com.buzbuz.smartautoclicker.core.base.di.Dispatcher
import com.buzbuz.smartautoclicker.core.base.di.HiltCoroutineDispatchers
import com.buzbuz.smartautoclicker.core.processing.domain.DetectionRepository

import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.launch
import javax.inject.Inject


@HiltViewModel
class RestartMediaProjectionViewModel @Inject constructor(
@Dispatcher(HiltCoroutineDispatchers.IO) private val ioDispatcher: CoroutineDispatcher,
private val detectionRepository: DetectionRepository,
) : ViewModel() {

fun restartScreenRecord(context: Context, resultCode: Int, data: Intent) {
viewModelScope.launch(ioDispatcher) {
detectionRepository.startScreenRecord(context, resultCode, data)
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ internal fun newWebBrowserStarterOverlay(uri: Uri) = ActivityStarterOverlayMenu(
fallbackIntent = getOpenWebBrowserPickerIntent(uri),
)

internal fun newRestartMediaProjectionStarterOverlay(context: Context) = ActivityStarterOverlayMenu(
intent = RestartMediaProjectionActivity.getStartIntent(context)
)

internal fun newNotificationPermissionStarterOverlay(context: Context) = ActivityStarterOverlayMenu(
intent = RequestNotificationPermissionActivity.getStartIntent(context)
)
Expand Down
Loading

0 comments on commit 36c116b

Please sign in to comment.