Skip to content

Commit

Permalink
#1274 feat: show a notification presenting the new assistant trigger …
Browse files Browse the repository at this point in the history
…feature and fix notification trampolines
  • Loading branch information
sds100 committed Dec 4, 2024
1 parent 947c641 commit cff5897
Show file tree
Hide file tree
Showing 19 changed files with 302 additions and 413 deletions.
23 changes: 21 additions & 2 deletions app/src/main/java/io/github/sds100/keymapper/BaseMainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import androidx.navigation.findNavController
import io.github.sds100.keymapper.Constants.PACKAGE_NAME
import io.github.sds100.keymapper.databinding.ActivityMainBinding
import io.github.sds100.keymapper.system.permissions.RequestPermissionDelegate
import io.github.sds100.keymapper.util.launchRepeatOnLifecycle
import io.github.sds100.keymapper.util.ui.showPopups
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
Expand All @@ -26,6 +27,9 @@ abstract class BaseMainActivity : AppCompatActivity() {
companion object {
const val ACTION_SHOW_ACCESSIBILITY_SETTINGS_NOT_FOUND_DIALOG =
"$PACKAGE_NAME.ACTION_SHOW_ACCESSIBILITY_SETTINGS_NOT_FOUND_DIALOG"

const val ACTION_USE_ASSISTANT_TRIGGER =
"$PACKAGE_NAME.ACTION_USE_ASSISTANT_TRIGGER"
}

private val viewModel by viewModels<ActivityViewModel> {
Expand Down Expand Up @@ -61,8 +65,23 @@ abstract class BaseMainActivity : AppCompatActivity() {
}
.launchIn(lifecycleScope)

if (intent.action == ACTION_SHOW_ACCESSIBILITY_SETTINGS_NOT_FOUND_DIALOG) {
viewModel.onCantFindAccessibilitySettings()
// Must launch when the activity is resumed
// so the nav controller can be found
launchRepeatOnLifecycle(Lifecycle.State.RESUMED) {
when (intent.action) {
ACTION_SHOW_ACCESSIBILITY_SETTINGS_NOT_FOUND_DIALOG -> {
viewModel.onCantFindAccessibilitySettings()
}

ACTION_USE_ASSISTANT_TRIGGER -> {
findNavController(R.id.container).navigate(
NavAppDirections.actionToConfigKeymap(
keymapUid = null,
showAdvancedTriggers = true,
),
)
}
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -217,13 +217,13 @@ class KeyMapperApp : MultiDexApplication() {
ServiceLocator.settingsRepository(this),
notificationAdapter,
suAdapter,
permissionAdapter,
),
UseCases.pauseMappings(this),
UseCases.showImePicker(this),
UseCases.controlAccessibilityService(this),
UseCases.toggleCompatibleIme(this),
ShowHideInputMethodUseCaseImpl(ServiceLocator.accessibilityServiceAdapter(this)),
UseCases.fingerprintGesturesSupported(this),
UseCases.onboarding(this),
ServiceLocator.resourceProvider(this),
)
Expand Down
1 change: 1 addition & 0 deletions app/src/main/java/io/github/sds100/keymapper/UseCases.kt
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ object UseCases {
fun controlAccessibilityService(ctx: Context): ControlAccessibilityServiceUseCase =
ControlAccessibilityServiceUseCaseImpl(
ServiceLocator.accessibilityServiceAdapter(ctx),
ServiceLocator.permissionAdapter(ctx),
)

fun toggleCompatibleIme(ctx: Context) =
Expand Down
8 changes: 3 additions & 5 deletions app/src/main/java/io/github/sds100/keymapper/data/Keys.kt
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,9 @@ object Keys {
val hideHomeScreenAlerts = booleanPreferencesKey("pref_hide_home_screen_alerts")
val acknowledgedGuiKeyboard = booleanPreferencesKey("pref_acknowledged_gui_keyboard")
val showDeviceDescriptors = booleanPreferencesKey("pref_show_device_descriptors")
val approvedFingerprintFeaturePrompt =
booleanPreferencesKey("pref_approved_fingerprint_feature_prompt")

val approvedAssistantTriggerFeaturePrompt =
booleanPreferencesKey("pref_approved_assistant_trigger_feature_prompt")
val shownParallelTriggerOrderExplanation =
booleanPreferencesKey("key_shown_parallel_trigger_order_warning")
val shownSequenceTriggerExplanation =
Expand All @@ -58,9 +59,6 @@ object Keys {
val fingerprintGesturesAvailable =
booleanPreferencesKey("fingerprint_gestures_available")

val approvedSetupChosenDevicesAgain =
booleanPreferencesKey("pref_approved_new_choose_devices_settings")

val rerouteKeyEvents = booleanPreferencesKey("key_reroute_key_events_from_specified_devices")
val devicesToRerouteKeyEvents =
stringSetPreferencesKey("key_devices_to_reroute_key_events")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ class ConfigKeyMapFragment : ConfigMappingFragment() {
viewModel.loadKeymap(it)
}
}

if (args.showAdvancedTriggers) {
viewModel.configTriggerViewModel.showAdvancedTriggersBottomSheet = true
}
}

viewModel.configTriggerViewModel.setupNavigation(this)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package io.github.sds100.keymapper.onboarding

import androidx.datastore.preferences.core.stringSetPreferencesKey
import io.github.sds100.keymapper.Constants
import io.github.sds100.keymapper.actions.ActionData
import io.github.sds100.keymapper.actions.canUseImeToPerform
Expand Down Expand Up @@ -59,11 +58,6 @@ class OnboardingUseCaseImpl(
preferenceRepository.set(Keys.acknowledgedGuiKeyboard, true)
}

override var approvedFingerprintFeaturePrompt by PrefDelegate(
Keys.approvedFingerprintFeaturePrompt,
false,
)

override var shownParallelTriggerOrderExplanation by PrefDelegate(
Keys.shownParallelTriggerOrderExplanation,
false,
Expand All @@ -74,7 +68,7 @@ class OnboardingUseCaseImpl(
)

override val showWhatsNew = get(Keys.lastInstalledVersionCodeHomeScreen)
.map { it ?: -1 < Constants.VERSION_CODE }
.map { (it ?: -1) < Constants.VERSION_CODE }

override fun showedWhatsNew() {
set(Keys.lastInstalledVersionCodeHomeScreen, Constants.VERSION_CODE)
Expand All @@ -85,67 +79,30 @@ class OnboardingUseCaseImpl(
readText()
}

override val showFingerprintFeatureNotificationIfAvailable: Flow<Boolean> by lazy {
override var approvedAssistantTriggerFeaturePrompt by PrefDelegate(
Keys.approvedAssistantTriggerFeaturePrompt,
false,
)

/**
* Show the assistant trigger only when they *upgrade* to the new version and after they've
* completed the app intro, which asks them whether they want to receive notifications.
*/
override val showAssistantTriggerFeatureNotification: Flow<Boolean> =
combine(
get(Keys.lastInstalledVersionCodeBackground).map { it ?: -1 },
showWhatsNew,
get(Keys.approvedFingerprintFeaturePrompt).map { it ?: false },
get(Keys.shownAppIntro).map { it ?: false },
) { oldVersionCode, showWhatsNew, approvedPrompt, shownAppIntro ->
// has the user opened the app and will have already seen that they can remap fingerprint gestures
val handledUpdateInHomeScreen = !showWhatsNew

oldVersionCode < VersionHelper.FINGERPRINT_GESTURES_MIN_VERSION &&
!handledUpdateInHomeScreen &&
!approvedPrompt &&
shownAppIntro
get(Keys.approvedAssistantTriggerFeaturePrompt).map { it ?: false },
) { oldVersionCode, shownAppIntro, approvedPrompt ->
oldVersionCode < VersionHelper.ASSISTANT_TRIGGER_MIN_VERSION &&
shownAppIntro &&
!approvedPrompt
}
}

override fun showedFingerprintFeatureNotificationIfAvailable() {
override fun showedAssistantTriggerFeatureNotification() {
set(Keys.lastInstalledVersionCodeBackground, Constants.VERSION_CODE)
}

override val showSetupChosenDevicesAgainNotification: Flow<Boolean> =
get(Keys.approvedSetupChosenDevicesAgain).map { it ?: false }.map { approvedPreviously ->
val bluetoothDevicesThatShowImePicker =
get(stringSetPreferencesKey("pref_bluetooth_devices_show_ime_picker")).first()
?: emptySet()

val bluetoothDevicesThatChangeIme =
get(stringSetPreferencesKey("pref_bluetooth_devices")).first() ?: emptySet()

val previouslyChoseBluetoothDevices =
bluetoothDevicesThatShowImePicker.isNotEmpty() || bluetoothDevicesThatChangeIme.isNotEmpty()

return@map !approvedPreviously && previouslyChoseBluetoothDevices
}

override fun approvedSetupChosenDevicesAgainNotification() {
set(Keys.approvedSetupChosenDevicesAgain, true)
}

override val showSetupChosenDevicesAgainAppIntro: Flow<Boolean> =
get(Keys.approvedSetupChosenDevicesAgain).map { it ?: false }.map { approvedPreviously ->

val bluetoothDevicesThatShowImePicker =
get(stringSetPreferencesKey("pref_bluetooth_devices_show_ime_picker")).first()
?: emptySet()

val bluetoothDevicesThatChangeIme =
get(stringSetPreferencesKey("pref_bluetooth_devices")).first()
?: emptySet()

val previouslyChoseBluetoothDevices =
bluetoothDevicesThatShowImePicker.isNotEmpty() || bluetoothDevicesThatChangeIme.isNotEmpty()

return@map !approvedPreviously && previouslyChoseBluetoothDevices
}

override fun approvedSetupChosenDevicesAgainAppIntro() {
set(Keys.approvedSetupChosenDevicesAgain, true)
}

override val showQuickStartGuideHint: Flow<Boolean> = get(Keys.shownQuickStartGuideHint).map {
if (it == null) {
true
Expand Down Expand Up @@ -194,18 +151,12 @@ interface OnboardingUseCase {
fun isTvDevice(): Boolean
fun neverShowGuiKeyboardPromptsAgain()

var approvedFingerprintFeaturePrompt: Boolean
var shownParallelTriggerOrderExplanation: Boolean
var shownSequenceTriggerExplanation: Boolean

val showFingerprintFeatureNotificationIfAvailable: Flow<Boolean>
fun showedFingerprintFeatureNotificationIfAvailable()

val showSetupChosenDevicesAgainNotification: Flow<Boolean>
fun approvedSetupChosenDevicesAgainNotification()

val showSetupChosenDevicesAgainAppIntro: Flow<Boolean>
fun approvedSetupChosenDevicesAgainAppIntro()
val showAssistantTriggerFeatureNotification: Flow<Boolean>
fun showedAssistantTriggerFeatureNotification()
var approvedAssistantTriggerFeaturePrompt: Boolean

val showWhatsNew: Flow<Boolean>
fun showedWhatsNew()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package io.github.sds100.keymapper.system.accessibility

import io.github.sds100.keymapper.system.permissions.Permission
import io.github.sds100.keymapper.system.permissions.PermissionAdapter
import kotlinx.coroutines.flow.Flow

/**
Expand All @@ -8,6 +10,7 @@ import kotlinx.coroutines.flow.Flow

class ControlAccessibilityServiceUseCaseImpl(
private val adapter: ServiceAdapter,
private val permissionAdapter: PermissionAdapter,
) : ControlAccessibilityServiceUseCase {
override val serviceState: Flow<ServiceState> = adapter.state

Expand All @@ -26,11 +29,19 @@ class ControlAccessibilityServiceUseCaseImpl(
override fun stopService() {
adapter.stop()
}

/**
* @return whether the user must manually start/stop the service.
*/
override fun isUserInteractionRequired(): Boolean {
return !permissionAdapter.isGranted(Permission.WRITE_SECURE_SETTINGS)
}
}

interface ControlAccessibilityServiceUseCase {
val serviceState: Flow<ServiceState>
fun startService(): Boolean
fun restartService(): Boolean
fun stopService()
fun isUserInteractionRequired(): Boolean
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import android.os.Build
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import com.google.android.material.color.DynamicColors
import io.github.sds100.keymapper.MainActivity
import io.github.sds100.keymapper.R
import io.github.sds100.keymapper.util.color
import kotlinx.coroutines.CoroutineScope
Expand Down Expand Up @@ -36,8 +37,8 @@ class AndroidNotificationAdapter(
setContentTitle(notification.title)
setContentText(notification.text)

if (notification.onClickActionId != null) {
val pendingIntent = createActionPendingIntent(notification.onClickActionId)
if (notification.onClickAction != null) {
val pendingIntent = createActionIntent(notification.onClickAction)
setContentIntent(pendingIntent)
}

Expand All @@ -58,12 +59,12 @@ class AndroidNotificationAdapter(
setVisibility(NotificationCompat.VISIBILITY_SECRET)
}

notification.actions.forEach { action ->
for (action in notification.actions) {
addAction(
NotificationCompat.Action(
0,
action.text,
createActionPendingIntent(action.id),
createActionIntent(action.intentType),
),
)
}
Expand Down Expand Up @@ -100,11 +101,29 @@ class AndroidNotificationAdapter(
}
}

private fun createActionPendingIntent(actionId: String): PendingIntent {
val intent = Intent(ctx, NotificationClickReceiver::class.java).apply {
action = actionId
}
private fun createActionIntent(intentType: NotificationIntentType): PendingIntent {
when (intentType) {
is NotificationIntentType.Broadcast -> {
val intent = Intent(ctx, NotificationClickReceiver::class.java).apply {
action = intentType.action
}

return PendingIntent.getBroadcast(ctx, 0, intent, PendingIntent.FLAG_IMMUTABLE)
}

return PendingIntent.getBroadcast(ctx, 0, intent, PendingIntent.FLAG_IMMUTABLE)
is NotificationIntentType.MainActivity -> {
val intent = Intent(ctx, MainActivity::class.java).apply {
action = intentType.customIntentAction ?: Intent.ACTION_MAIN
}

return PendingIntent.getActivity(ctx, 0, intent, PendingIntent.FLAG_IMMUTABLE)
}

is NotificationIntentType.Activity -> {
val intent = Intent(intentType.action)

return PendingIntent.getActivity(ctx, 0, intent, PendingIntent.FLAG_IMMUTABLE)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package io.github.sds100.keymapper.system.notifications
import android.os.Build
import io.github.sds100.keymapper.data.Keys
import io.github.sds100.keymapper.data.repositories.PreferenceRepository
import io.github.sds100.keymapper.system.permissions.Permission
import io.github.sds100.keymapper.system.permissions.PermissionAdapter
import io.github.sds100.keymapper.system.root.SuAdapter
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
Expand All @@ -16,6 +18,7 @@ class ManageNotificationsUseCaseImpl(
private val preferences: PreferenceRepository,
private val notificationAdapter: NotificationAdapter,
private val suAdapter: SuAdapter,
private val permissionAdapter: PermissionAdapter,
) : ManageNotificationsUseCase {

override val showImePickerNotification: Flow<Boolean> =
Expand Down Expand Up @@ -82,6 +85,10 @@ class ManageNotificationsUseCaseImpl(
override fun deleteChannel(channelId: String) {
notificationAdapter.deleteChannel(channelId)
}

override fun isPermissionGranted(): Boolean {
return permissionAdapter.isGranted(Permission.POST_NOTIFICATIONS)
}
}

interface ManageNotificationsUseCase {
Expand All @@ -94,6 +101,7 @@ interface ManageNotificationsUseCase {
*/
val onActionClick: Flow<String>

fun isPermissionGranted(): Boolean
fun show(notification: NotificationModel)
fun dismiss(notificationId: Int)
fun createChannel(channel: NotificationChannelModel)
Expand Down
Loading

0 comments on commit cff5897

Please sign in to comment.