Skip to content

Commit

Permalink
Introducing AnalyticsWidgetProvider as ancestor of all widget classes.
Browse files Browse the repository at this point in the history
This will ensure any widgets Timber and send analytics on enabling, disabling and updating.
  • Loading branch information
xenonnn4w authored and Arthur-Milchior committed Jul 26, 2024
1 parent d76e695 commit f7d1b97
Show file tree
Hide file tree
Showing 4 changed files with 182 additions and 34 deletions.
27 changes: 7 additions & 20 deletions AnkiDroid/src/main/java/com/ichi2/widget/AddNoteWidget.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,34 +15,21 @@
package com.ichi2.widget

import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProvider
import android.content.Context
import android.widget.RemoteViews
import androidx.core.app.PendingIntentCompat
import com.ichi2.anki.IntentHandler
import com.ichi2.anki.R
import com.ichi2.anki.analytics.UsageAnalytics
import com.ichi2.anki.noteeditor.NoteEditorLauncher
import timber.log.Timber

class AddNoteWidget : AppWidgetProvider() {
override fun onEnabled(context: Context) {
super.onEnabled(context)
UsageAnalytics.sendAnalyticsEvent(this.javaClass.simpleName, "enabled")
}

override fun onDisabled(context: Context) {
super.onDisabled(context)
UsageAnalytics.sendAnalyticsEvent(this.javaClass.simpleName, "disabled")
}
class AddNoteWidget : AnalyticsWidgetProvider() {

override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
super.onUpdate(context, appWidgetManager, appWidgetIds)
if (!IntentHandler.grantedStoragePermissions(context, showToast = false)) {
Timber.w("Opening AddNote widget without storage access")
return
}
Timber.d("onUpdate")
override fun performUpdate(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray,
usageAnalytics: UsageAnalytics
) {
updateWidgets(context, appWidgetManager, appWidgetIds)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/*
* Copyright (c) 2024 Anoop <xenonnn4w@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.widget

import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProvider
import android.content.Context
import androidx.annotation.CallSuper
import com.ichi2.anki.IntentHandler
import com.ichi2.anki.analytics.UsageAnalytics
import timber.log.Timber

/**
* AnalyticsWidgetProvider is an abstract base class for App Widgets that integrates
* with UsageAnalytics to send analytics events when the widget is enabled, disabled,
* or updated.
*
* This class should always be used as the base class for App Widgets in this application.
* Direct usage of AppWidgetProvider should be avoided.
* TODO: Add a lint rule to forbid the direct use of AppWidgetProvider.
*
* Subclasses must override [performUpdate] to define the widget update logic.
*
* - To use this class, extend it and implement the [performUpdate] method.
* - Override [onUpdate] if additional logic is required beyond [performUpdate].
*/
abstract class AnalyticsWidgetProvider : AppWidgetProvider() {

/**
* Called when the widget is enabled. Sends an analytics event.
*
* @param context The context in which the receiver is running.
*/
@CallSuper
override fun onEnabled(context: Context) {
super.onEnabled(context)
Timber.d("${this.javaClass.name}: Widget enabled")
UsageAnalytics.sendAnalyticsEvent(this.javaClass.simpleName, "enabled")
}

/**
* Called when the widget is disabled. Sends an analytics event.
*
* @param context The context in which the receiver is running.
*/
@CallSuper
override fun onDisabled(context: Context) {
super.onDisabled(context)
Timber.d("${this.javaClass.name}: Widget disabled")
UsageAnalytics.sendAnalyticsEvent(this.javaClass.simpleName, "disabled")
}

/**
* Called to update the widget. Checks storage permissions and delegates to [performUpdate].
*
* @param context The context in which the receiver is running.
* @param appWidgetManager The AppWidgetManager instance to use for updating widgets.
* @param appWidgetIds The app widget IDs to update.
*/
final override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
super.onUpdate(context, appWidgetManager, appWidgetIds)
if (!IntentHandler.grantedStoragePermissions(context, showToast = false)) {
Timber.w("Opening widget ${this.javaClass.name} without storage access")
return
}
// Pass usageAnalytics to performUpdate
Timber.d("${this.javaClass.name}: performUpdate")
performUpdate(context, appWidgetManager, appWidgetIds, UsageAnalytics)
}

/**
* Override this method to implement Widget functionality
*
* Called when the [AnalyticsWidgetProvider] is asked to provide [RemoteViews] for a set of Widgets AND the Anki collection is accessible.
*
* @see AppWidgetProvider.onUpdate
*
* @param context The context in which the receiver is running.
* @param appWidgetManager The AppWidgetManager instance to use for updating widgets.
* @param appWidgetIds The app widget IDs to update.
* @param usageAnalytics The UsageAnalytics instance for logging analytics events.
*/

abstract fun performUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray, usageAnalytics: UsageAnalytics)
}
23 changes: 9 additions & 14 deletions AnkiDroid/src/main/java/com/ichi2/widget/AnkiDroidWidgetSmall.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ package com.ichi2.widget
import android.app.PendingIntent
import android.app.Service
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProvider
import android.content.BroadcastReceiver
import android.content.ComponentName
import android.content.Context
Expand All @@ -33,7 +32,6 @@ import androidx.core.content.ContextCompat
import androidx.core.content.edit
import com.ichi2.anki.AnkiDroidApp
import com.ichi2.anki.IntentHandler
import com.ichi2.anki.IntentHandler.Companion.grantedStoragePermissions
import com.ichi2.anki.R
import com.ichi2.anki.analytics.UsageAnalytics
import com.ichi2.anki.preferences.sharedPrefs
Expand All @@ -42,30 +40,27 @@ import com.ichi2.utils.KotlinCleanup
import timber.log.Timber
import kotlin.math.sqrt

class AnkiDroidWidgetSmall : AppWidgetProvider() {
override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
Timber.d("SmallWidget: onUpdate")
if (!grantedStoragePermissions(context, showToast = false)) {
Timber.w("Opening AnkiDroid Small widget without storage access")
return
}
/**
* AnkiDroidWidgetSmall is a small-sized home screen widget for the AnkiDroid application.
* This widget displays the number of due cards and an estimated review time.
* It updates periodically and can respond to certain actions like resizing.
*/

class AnkiDroidWidgetSmall : AnalyticsWidgetProvider() {

override fun performUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray, usageAnalytics: UsageAnalytics) {
WidgetStatus.updateInBackground(context)
}

override fun onEnabled(context: Context) {
super.onEnabled(context)
Timber.d("SmallWidget: Widget enabled")
val preferences = context.sharedPrefs()
preferences.edit(commit = true) { putBoolean("widgetSmallEnabled", true) }
UsageAnalytics.sendAnalyticsEvent(this.javaClass.simpleName, "enabled")
}

override fun onDisabled(context: Context) {
super.onDisabled(context)
Timber.d("SmallWidget: Widget disabled")
val preferences = context.sharedPrefs()
preferences.edit(commit = true) { putBoolean("widgetSmallEnabled", false) }
UsageAnalytics.sendAnalyticsEvent(this.javaClass.simpleName, "disabled")
}

override fun onReceive(context: Context, intent: Intent) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* Copyright (c) 2024 David Allison <davidallisongithub@gmail.com>
* Copyright (c) 2024 Anoop <xenonnn4w@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/>.
*/

import android.appwidget.AppWidgetManager
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.ichi2.anki.RobolectricTest
import com.ichi2.anki.analytics.UsageAnalytics
import com.ichi2.widget.AnalyticsWidgetProvider
import io.mockk.every
import io.mockk.mockkObject
import io.mockk.unmockkObject
import io.mockk.verify
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class AnalyticalWidgetProviderTest : RobolectricTest() {

@Before
override fun setUp() {
super.setUp()
mockkObject(UsageAnalytics)
every { UsageAnalytics.sendAnalyticsEvent(any(), any()) } answers { }
}

@After
override fun tearDown() {
super.tearDown()
unmockkObject(UsageAnalytics)
}

@Test
fun testAnalyticsEventLogging() {
val widgetProvider = TestWidgetProvider()

widgetProvider.onEnabled(targetContext)

verify { UsageAnalytics.sendAnalyticsEvent("TestWidgetProvider", "enabled") }
}

private class TestWidgetProvider : AnalyticsWidgetProvider() {
override fun performUpdate(
context: android.content.Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray,
usageAnalytics: UsageAnalytics
) {
// Do nothing
}
}
}

0 comments on commit f7d1b97

Please sign in to comment.