Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AnkiDroid/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,7 @@ dependencies {
implementation project(":compat")
implementation project(":libanki")
implementation project(":vbpd")
implementation project(":widgets")

implementation libs.androidx.activity
implementation libs.androidx.annotation
Expand Down
4 changes: 4 additions & 0 deletions AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidApp.kt
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ import com.ichi2.anki.services.AlarmManagerService
import com.ichi2.anki.services.NotificationService
import com.ichi2.anki.settings.Prefs
import com.ichi2.anki.ui.dialogs.ActivityAgnosticDialogs
import com.ichi2.anki.widget.initWidgetDependencies
import com.ichi2.utils.AdaptionUtil
import com.ichi2.utils.ExceptionUtil
import com.ichi2.utils.LanguageUtil
Expand Down Expand Up @@ -128,6 +129,9 @@ open class AnkiDroidApp :
}
instance = this

// Initialize widget bridge dependencies
initWidgetDependencies()

// Get preferences
val preferences = this.sharedPrefs()

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
/*
* Copyright (c) 2026 Ashish Yadav <mailtoashish693@gmail.com>
*
* 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.ichi2.anki.widget

import android.app.Application
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import com.ichi2.anki.AnkiDroidApp
import com.ichi2.anki.CollectionManager
import com.ichi2.anki.CrashReportService
import com.ichi2.anki.IntentHandler
import com.ichi2.anki.IntentHandler.Companion.intentToReviewDeckFromShortcuts
import com.ichi2.anki.MetaDB
import com.ichi2.anki.R
import com.ichi2.anki.libanki.Collection
import com.ichi2.anki.libanki.DeckId
import com.ichi2.anki.noteeditor.NoteEditorLauncher
import com.ichi2.anki.pages.DeckOptionsDestination
import com.ichi2.anki.preferences.sharedPrefs
import com.ichi2.anki.settings.Prefs
import com.ichi2.widget.AddNoteWidget
import com.ichi2.widget.AnkiDroidWidgetSmall
import com.ichi2.widget.SmallWidgetStatus
import com.ichi2.widget.bridge.WidgetAnalytics
import com.ichi2.widget.bridge.WidgetAppState
import com.ichi2.widget.bridge.WidgetCollectionAccess
import com.ichi2.widget.bridge.WidgetCrashReporter
import com.ichi2.widget.bridge.WidgetDependencies
import com.ichi2.widget.bridge.WidgetIntentFactory
import com.ichi2.widget.bridge.WidgetMetaStorage
import com.ichi2.widget.bridge.WidgetPreferences
import com.ichi2.widget.getAppWidgetIdsEx
import com.ichi2.widget.getAppWidgetManager
import kotlinx.coroutines.CoroutineScope

class WidgetAnalyticsImpl : WidgetAnalytics {
override fun sendAnalyticsEvent(
category: String,
action: String,
value: Int?,
label: String?,
) {
com.ichi2.anki.analytics.UsageAnalytics
.sendAnalyticsEvent(category, action, value, label)
}
}

class WidgetIntentFactoryImpl : WidgetIntentFactory {
override fun grantedStoragePermissions(
context: Context,
showToast: Boolean,
): Boolean = IntentHandler.grantedStoragePermissions(context, showToast)

override fun intentToReviewDeck(
context: Context,
deckId: DeckId,
): Intent = intentToReviewDeckFromShortcuts(context, deckId)

override fun intentToOpenNoteEditor(context: Context): Intent = NoteEditorLauncher.AddNote().toIntent(context)

override fun intentToMainActivity(context: Context): Intent =
Intent(context, IntentHandler::class.java).apply {
action = Intent.ACTION_MAIN
addCategory(Intent.CATEGORY_LAUNCHER)
}

override suspend fun intentToDeckOptions(
context: Context,
deckId: DeckId,
): Intent = DeckOptionsDestination.fromDeckId(deckId).toIntent(context)
}

class WidgetCollectionAccessImpl : WidgetCollectionAccess {
override suspend fun <T> withCol(block: Collection.() -> T): T = CollectionManager.withCol(block)

override suspend fun isCollectionEmpty(): Boolean = com.ichi2.anki.isCollectionEmpty()
}

class WidgetAppStateImpl : WidgetAppState {
override val isSdCardMounted: Boolean
get() = AnkiDroidApp.isSdCardMounted

override val applicationScope: CoroutineScope
get() = AnkiDroidApp.applicationScope

override val applicationInstance: Application
get() = AnkiDroidApp.instance

override fun scheduleNotification(context: Context) {
(context.applicationContext as AnkiDroidApp).scheduleNotification()
}

override fun updateSmallWidgetUi(context: Context) {
AnkiDroidWidgetSmall
.UpdateService()
.doUpdate(context)
}

override fun updateAddNoteWidgets(context: Context) {
val appWidgetManager = getAppWidgetManager(context) ?: return
val widgetIds =
appWidgetManager.getAppWidgetIdsEx(
android.content.ComponentName(context, AddNoteWidget::class.java),
)
AddNoteWidget
.updateWidgets(context, appWidgetManager, widgetIds)
}
}

class WidgetCrashReporterImpl : WidgetCrashReporter {
override fun sendExceptionReport(
throwable: Throwable,
origin: String,
onlyIfSilent: Boolean,
) {
CrashReportService.sendExceptionReport(throwable, origin, onlyIfSilent = onlyIfSilent)
}
}

class WidgetMetaStorageImpl : WidgetMetaStorage {
override fun storeSmallWidgetStatus(
context: Context,
status: SmallWidgetStatus,
) {
MetaDB.storeSmallWidgetStatus(context, status)
}

override fun getWidgetSmallStatus(context: Context): SmallWidgetStatus = MetaDB.getWidgetSmallStatus(context)

override fun getNotificationStatus(context: Context): Int = MetaDB.getNotificationStatus(context)
}

class WidgetPreferencesImpl : WidgetPreferences {
override fun sharedPrefs(context: Context): SharedPreferences = context.sharedPrefs()

override val newReviewRemindersEnabled: Boolean
get() = Prefs.newReviewRemindersEnabled

override fun isLegacyNotificationEnabled(context: Context): Boolean {
val preferences = context.sharedPrefs()
return preferences
.getString(context.getString(R.string.pref_notifications_minimum_cards_due_key), "1000001")!!
.toInt() < 1000000
}
}

/**
* Initializes [WidgetDependencies] with real app-module implementations.
* Should be called from [AnkiDroidApp.onCreate].
*/
fun initWidgetDependencies() {
WidgetDependencies.init(
analytics = WidgetAnalyticsImpl(),
intentFactory = WidgetIntentFactoryImpl(),
collectionAccess = WidgetCollectionAccessImpl(),
appState = WidgetAppStateImpl(),
crashReporter = WidgetCrashReporterImpl(),
metaStorage = WidgetMetaStorageImpl(),
preferences = WidgetPreferencesImpl(),
)
}
8 changes: 4 additions & 4 deletions AnkiDroid/src/main/java/com/ichi2/widget/AddNoteWidget.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,15 @@ import android.content.Context
import android.widget.RemoteViews
import androidx.core.app.PendingIntentCompat
import com.ichi2.anki.R
import com.ichi2.anki.analytics.UsageAnalytics
import com.ichi2.anki.noteeditor.NoteEditorLauncher
import com.ichi2.widget.bridge.WidgetAnalytics
import com.ichi2.widget.bridge.WidgetDependencies

class AddNoteWidget : AnalyticsWidgetProvider() {
override fun performUpdate(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: AppWidgetIds,
usageAnalytics: UsageAnalytics,
usageAnalytics: WidgetAnalytics,
) {
updateWidgets(context, appWidgetManager, appWidgetIds)
}
Expand All @@ -47,7 +47,7 @@ class AddNoteWidget : AnalyticsWidgetProvider() {
appWidgetIds: AppWidgetIds,
) {
val remoteViews = RemoteViews(context.packageName, R.layout.widget_add_note)
val intent = NoteEditorLauncher.AddNote().toIntent(context)
val intent = WidgetDependencies.intentFactory.intentToOpenNoteEditor(context)
val pendingIntent = PendingIntentCompat.getActivity(context, 0, intent, 0, false)
remoteViews.setOnClickPendingIntent(R.id.widget_add_note_button, pendingIntent)
appWidgetManager.updateAppWidget(appWidgetIds, remoteViews)
Expand Down
30 changes: 15 additions & 15 deletions AnkiDroid/src/main/java/com/ichi2/widget/AnkiDroidWidgetSmall.kt
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,9 @@ import androidx.annotation.LayoutRes
import androidx.core.app.PendingIntentCompat
import androidx.core.content.ContextCompat
import androidx.core.content.edit
import com.ichi2.anki.AnkiDroidApp
import com.ichi2.anki.IntentHandler
import com.ichi2.anki.R
import com.ichi2.anki.analytics.UsageAnalytics
import com.ichi2.anki.compat.CompatHelper.Companion.registerReceiverCompat
import com.ichi2.anki.preferences.sharedPrefs
import com.ichi2.widget.bridge.WidgetAnalytics
import com.ichi2.widget.bridge.WidgetDependencies
import timber.log.Timber
import kotlin.math.sqrt

Expand All @@ -52,20 +49,20 @@ class AnkiDroidWidgetSmall : AnalyticsWidgetProvider() {
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: AppWidgetIds,
usageAnalytics: UsageAnalytics,
usageAnalytics: WidgetAnalytics,
) {
WidgetStatus.updateInBackground(context)
}

override fun onEnabled(context: Context) {
super.onEnabled(context)
val preferences = context.sharedPrefs()
val preferences = WidgetDependencies.preferences.sharedPrefs(context)
preferences.edit(commit = true) { putBoolean("widgetSmallEnabled", true) }
}

override fun onDisabled(context: Context) {
super.onDisabled(context)
val preferences = context.sharedPrefs()
val preferences = WidgetDependencies.preferences.sharedPrefs(context)
preferences.edit(commit = true) { putBoolean("widgetSmallEnabled", false) }
}

Expand Down Expand Up @@ -110,7 +107,7 @@ class AnkiDroidWidgetSmall : AnalyticsWidgetProvider() {
private fun buildUpdate(context: Context): RemoteViews {
Timber.d("updating small widget UI")
val updateViews = RemoteViews(context.packageName, widgetSmallLayout)
val mounted = AnkiDroidApp.isSdCardMounted
val mounted = WidgetDependencies.appState.isSdCardMounted
if (!mounted) {
updateViews.setViewVisibility(R.id.widget_due, View.INVISIBLE)
updateViews.setViewVisibility(R.id.widget_eta, View.INVISIBLE)
Expand All @@ -128,10 +125,10 @@ class AnkiDroidWidgetSmall : AnalyticsWidgetProvider() {
if (action != null && action == Intent.ACTION_MEDIA_MOUNTED) {
Timber.d("mountReceiver - Action = Media Mounted")
if (remounted) {
WidgetStatus.updateInBackground(AnkiDroidApp.instance)
WidgetStatus.updateInBackground(WidgetDependencies.appState.applicationInstance)
remounted = false
if (mountReceiver != null) {
AnkiDroidApp.instance.unregisterReceiver(mountReceiver)
WidgetDependencies.appState.applicationInstance.unregisterReceiver(mountReceiver)
}
} else {
remounted = true
Expand All @@ -142,7 +139,12 @@ class AnkiDroidWidgetSmall : AnalyticsWidgetProvider() {
val iFilter = IntentFilter()
iFilter.addAction(Intent.ACTION_MEDIA_MOUNTED)
iFilter.addDataScheme("file")
AnkiDroidApp.instance.registerReceiverCompat(mountReceiver, iFilter, ContextCompat.RECEIVER_EXPORTED)
ContextCompat.registerReceiver(
WidgetDependencies.appState.applicationInstance,
mountReceiver,
iFilter,
ContextCompat.RECEIVER_EXPORTED,
)
}
} else {
// Compute the total number of cards due.
Expand Down Expand Up @@ -176,9 +178,7 @@ class AnkiDroidWidgetSmall : AnalyticsWidgetProvider() {

// Add a click listener to open Anki from the icon.
// This should be always there, whether there are due cards or not.
val ankiDroidIntent = Intent(context, IntentHandler::class.java)
ankiDroidIntent.action = Intent.ACTION_MAIN
ankiDroidIntent.addCategory(Intent.CATEGORY_LAUNCHER)
val ankiDroidIntent = WidgetDependencies.intentFactory.intentToMainActivity(context)
val pendingAnkiDroidIntent =
PendingIntentCompat.getActivity(
context,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@ package com.ichi2.widget
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.ichi2.anki.CollectionManager.withCol
import com.ichi2.anki.common.utils.ext.indexOfOrNull
import com.ichi2.anki.databinding.ItemWidgetDeckConfigBinding
import com.ichi2.anki.libanki.DeckId
import com.ichi2.anki.model.SelectableDeck
import com.ichi2.widget.bridge.WidgetDependencies
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
Expand Down Expand Up @@ -69,7 +69,7 @@ class WidgetConfigScreenAdapter(
coroutineScope.launch {
val deckName =
withContext(Dispatchers.IO) {
withCol { decks.getLegacy(deck.deckId)!!.name }
WidgetDependencies.collectionAccess.withCol { decks.getLegacy(deck.deckId)!!.name }
}
holder.binding.deckNameTextView.text = deckName
}
Expand Down
Loading
Loading