1616
1717package com.ichi2.anki.reviewreminders
1818
19+ import android.Manifest
1920import android.content.Context
21+ import android.content.Intent
22+ import android.os.Build
2023import android.os.Bundle
24+ import android.provider.Settings
2125import android.view.LayoutInflater
2226import android.view.View
2327import android.view.ViewGroup
28+ import androidx.activity.result.ActivityResultLauncher
29+ import androidx.activity.result.contract.ActivityResultContracts
30+ import androidx.core.net.toUri
2431import androidx.core.view.isVisible
2532import androidx.fragment.app.Fragment
2633import androidx.fragment.app.activityViewModels
@@ -33,12 +40,30 @@ import androidx.recyclerview.widget.RecyclerView
3340import com.ichi2.anki.R
3441import com.ichi2.anki.databinding.FragmentReminderTroubleshootingBinding
3542import com.ichi2.anki.databinding.ItemTroubleshootingCheckBinding
43+ import com.ichi2.anki.settings.Prefs
3644import com.ichi2.anki.utils.ext.launchCollectionInLifecycleScope
3745import com.ichi2.anki.utils.ext.onWindowFocusChanged
3846import com.ichi2.anki.utils.ext.setBackgroundTint
3947import com.ichi2.themes.Themes
48+ import com.ichi2.utils.Permissions.requestPermissionThroughDialogOrSettings
4049import com.ichi2.utils.dp
4150import dev.androidbroadcast.vbpd.viewBinding
51+ import timber.log.Timber
52+
53+ /* *
54+ * An action which a user can take to resolve a [check][TroubleshootingCheck].
55+ *
56+ * Example: Disabling power saving mode.
57+ *
58+ * `null` for checks which are not immediately resolvable: we should not offer to turn
59+ * 'Do not disturb' off, as this is a user preference.
60+ */
61+ private data class ResolveCheckAction (
62+ val label : String ,
63+ /* * A hardcoded (non-translatable) description of the action for logging purposes. */
64+ val logDescription : String ,
65+ val action : () -> Unit ,
66+ )
4267
4368/* * %alpha to use for the circular background of icons */
4469private const val BACKGROUND_ALPHA = 0.12f
@@ -50,6 +75,11 @@ class ReminderTroubleshootingFragment : Fragment(R.layout.fragment_reminder_trou
5075
5176 private val binding by viewBinding(FragmentReminderTroubleshootingBinding ::bind)
5277
78+ internal val notificationPermissionLauncher: ActivityResultLauncher <String > =
79+ registerForActivityResult(ActivityResultContracts .RequestPermission ()) {
80+ viewModel.refreshChecks()
81+ }
82+
5383 override fun onViewCreated (
5484 view : View ,
5585 savedInstanceState : Bundle ? ,
@@ -99,7 +129,10 @@ class ReminderTroubleshootingFragment : Fragment(R.layout.fragment_reminder_trou
99129 }
100130
101131 private fun setupTroubleshootingChecks () {
102- val checksAdapter = TroubleshootingChecksAdapter ()
132+ val checksAdapter =
133+ TroubleshootingChecksAdapter { check ->
134+ with (this ) { check.resolveAction() }
135+ }
103136 binding.checksList.adapter = checksAdapter
104137 viewModel.state.launchCollectionInLifecycleScope { state ->
105138 val visibleChecks = state.checks.filter { it.result !is CheckResult .Unavailable }
@@ -134,7 +167,9 @@ internal fun reminderTroubleshootingViewModelFactory(context: Context): ViewMode
134167 }
135168}
136169
137- private class TroubleshootingChecksAdapter : ListAdapter <TroubleshootingCheck , TroubleshootingChecksAdapter .ViewHolder >(DIFF_CALLBACK ) {
170+ private class TroubleshootingChecksAdapter (
171+ private val getResolveAction : (TroubleshootingCheck ) -> ResolveCheckAction ? ,
172+ ) : ListAdapter<TroubleshootingCheck, TroubleshootingChecksAdapter.ViewHolder>(DIFF_CALLBACK ) {
138173 class ViewHolder (
139174 val binding : ItemTroubleshootingCheckBinding ,
140175 ) : RecyclerView.ViewHolder(binding.root)
@@ -184,6 +219,24 @@ private class TroubleshootingChecksAdapter : ListAdapter<TroubleshootingCheck, T
184219 }
185220 holder.binding.itemContainer.setBackgroundResource(backgroundRes)
186221
222+ // Action link + item click: only actionable for failed/warning checks
223+ val needsFix = check.result.hasIssue
224+ val resolveAction = if (needsFix) getResolveAction(check) else null
225+ holder.binding.actionLink.isVisible = resolveAction != null
226+ if (resolveAction != null ) {
227+ holder.binding.actionLink.text = resolveAction.label
228+ val clickListener =
229+ View .OnClickListener {
230+ Timber .i(" Launching fix for '%s': %s" , check.title(), resolveAction.logDescription)
231+ resolveAction.action()
232+ }
233+ holder.binding.itemContainer.setOnClickListener(clickListener)
234+ holder.binding.actionLink.setOnClickListener(clickListener)
235+ } else {
236+ holder.binding.itemContainer.setOnClickListener(null )
237+ holder.binding.itemContainer.isClickable = false
238+ }
239+
187240 val gap = if (position < lastIndex) 2 .dp.toPx(context) else 0
188241 val lp = holder.itemView.layoutParams as ? ViewGroup .MarginLayoutParams
189242 lp?.bottomMargin = gap
@@ -269,3 +322,60 @@ private fun CheckResult.tintColor(context: Context): Int =
269322 is CheckResult .Unavailable -> context.getColor(android.R .color.darker_gray)
270323 is CheckResult .Error -> context.getColor(android.R .color.holo_red_dark)
271324 }
325+
326+ /* *
327+ * Returns a [ResolveCheckAction] for the check.
328+ */
329+ context(fragment: ReminderTroubleshootingFragment )
330+ private fun TroubleshootingCheck.resolveAction (): ResolveCheckAction ? {
331+ val context = fragment.requireContext()
332+
333+ // TODO: move labels to string resources
334+ fun requestNotificationPermission (): ResolveCheckAction ? {
335+ if (Build .VERSION .SDK_INT < Build .VERSION_CODES .TIRAMISU ) return null
336+ return ResolveCheckAction (
337+ label = " Grant permission" ,
338+ logDescription = " requesting POST_NOTIFICATIONS via system dialog or app settings" ,
339+ ) {
340+ fragment.requestPermissionThroughDialogOrSettings(
341+ activity = fragment.requireActivity(),
342+ permission = Manifest .permission.POST_NOTIFICATIONS ,
343+ permissionRequestedFlag = Prefs ::notificationsPermissionRequested,
344+ permissionRequestLauncher = fragment.notificationPermissionLauncher,
345+ )
346+ }
347+ }
348+
349+ // Opens the full battery optimization list. The user must manually find the app.
350+ // For 'full' (non-Play) builds, ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS could be used
351+ // with the REQUEST_IGNORE_BATTERY_OPTIMIZATIONS manifest permission for a direct dialog,
352+ // but Google Play restricts that permission.
353+ fun requestUnrestrictedBackgroundUsage () =
354+ ResolveCheckAction (label = " Open battery settings" , logDescription = " opening ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS" ) {
355+ context.startActivity(Intent (Settings .ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS ))
356+ }
357+
358+ fun openBatterySaverSettings () =
359+ ResolveCheckAction (label = " Open battery settings" , logDescription = " opening ACTION_BATTERY_SAVER_SETTINGS" ) {
360+ context.startActivity(Intent (Settings .ACTION_BATTERY_SAVER_SETTINGS ))
361+ }
362+
363+ fun openExactAlarmSettings (): ResolveCheckAction ? {
364+ if (Build .VERSION .SDK_INT < Build .VERSION_CODES .S ) return null
365+ return ResolveCheckAction (label = " Grant permission" , logDescription = " opening ACTION_REQUEST_SCHEDULE_EXACT_ALARM" ) {
366+ context.startActivity(
367+ Intent (Settings .ACTION_REQUEST_SCHEDULE_EXACT_ALARM ).apply {
368+ data = " package:${context.packageName} " .toUri()
369+ },
370+ )
371+ }
372+ }
373+
374+ return when (this ) {
375+ is TroubleshootingCheck .NotificationPermission -> requestNotificationPermission()
376+ is TroubleshootingCheck .DoNotDisturbOff -> null
377+ is TroubleshootingCheck .UnrestrictedOptimizationEnabled -> requestUnrestrictedBackgroundUsage()
378+ is TroubleshootingCheck .PowerSavingModeOff -> openBatterySaverSettings()
379+ is TroubleshootingCheck .ExactAlarmPermission -> openExactAlarmSettings()
380+ }
381+ }
0 commit comments