Skip to content

Commit 0224f23

Browse files
david-allisonmikehardy
authored andcommitted
feat(reminders): troubleshooting issue fixes
Allow a user to tap a troubleshooting issue and open an appropriate screen to fix the issue Assisted-by: Claude Opus 4.6
1 parent cb2f818 commit 0224f23

File tree

2 files changed

+129
-2
lines changed

2 files changed

+129
-2
lines changed

AnkiDroid/src/main/java/com/ichi2/anki/reviewreminders/ReminderTroubleshootingFragment.kt

Lines changed: 112 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,18 @@
1616

1717
package com.ichi2.anki.reviewreminders
1818

19+
import android.Manifest
1920
import android.content.Context
21+
import android.content.Intent
22+
import android.os.Build
2023
import android.os.Bundle
24+
import android.provider.Settings
2125
import android.view.LayoutInflater
2226
import android.view.View
2327
import android.view.ViewGroup
28+
import androidx.activity.result.ActivityResultLauncher
29+
import androidx.activity.result.contract.ActivityResultContracts
30+
import androidx.core.net.toUri
2431
import androidx.core.view.isVisible
2532
import androidx.fragment.app.Fragment
2633
import androidx.fragment.app.activityViewModels
@@ -33,12 +40,30 @@ import androidx.recyclerview.widget.RecyclerView
3340
import com.ichi2.anki.R
3441
import com.ichi2.anki.databinding.FragmentReminderTroubleshootingBinding
3542
import com.ichi2.anki.databinding.ItemTroubleshootingCheckBinding
43+
import com.ichi2.anki.settings.Prefs
3644
import com.ichi2.anki.utils.ext.launchCollectionInLifecycleScope
3745
import com.ichi2.anki.utils.ext.onWindowFocusChanged
3846
import com.ichi2.anki.utils.ext.setBackgroundTint
3947
import com.ichi2.themes.Themes
48+
import com.ichi2.utils.Permissions.requestPermissionThroughDialogOrSettings
4049
import com.ichi2.utils.dp
4150
import 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 */
4469
private 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+
}

AnkiDroid/src/main/res/layout/item_troubleshooting_check.xml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44
android:id="@+id/item_container"
55
android:layout_width="match_parent"
66
android:layout_height="wrap_content"
7+
android:foreground="?attr/selectableItemBackground"
8+
android:clipToOutline="true"
9+
android:clickable="true"
10+
android:focusable="true"
711
android:gravity="center_vertical"
812
android:minHeight="72dp"
913
android:orientation="horizontal"
@@ -70,6 +74,19 @@
7074
tools:text="Background usage is disabled. Reminders may not be delivered"
7175
tools:visibility="visible" />
7276

77+
<!-- Widget.Material3.Button.TextButton, but without the vertical padding -->
78+
<TextView
79+
android:id="@+id/action_link"
80+
android:layout_width="wrap_content"
81+
android:layout_height="wrap_content"
82+
android:layout_marginTop="4dp"
83+
android:textAppearance="?attr/textAppearanceLabelLarge"
84+
android:textColor="?attr/colorPrimary"
85+
android:textFontWeight="500"
86+
android:visibility="gone"
87+
tools:text="Open battery settings"
88+
tools:visibility="visible" />
89+
7390
</LinearLayout>
7491

7592
</LinearLayout>

0 commit comments

Comments
 (0)