forked from ankidroid/Anki-Android
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implementation of CardsDueWidget to display the number of due cards a…
…long with additional card analysis data. Features: - Displays the number of due cards and estimated time of arrival (ETA) based on current deck data. - Updates widget views periodically or upon receiving specific broadcast intents. Note: -The reviewer need to switch enabled to true in order to test the widget, also which can be accomplished by removing the android:enabled="false".
- Loading branch information
Showing
7 changed files
with
396 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
209 changes: 209 additions & 0 deletions
209
AnkiDroid/src/main/java/com/ichi2/widget/CardsDueWidget.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,209 @@ | ||
/* | ||
* Copyright (c) 2024 Anoop <[email protected]> | ||
* | ||
* 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.app.PendingIntent | ||
import android.app.Service | ||
import android.appwidget.AppWidgetManager | ||
import android.content.BroadcastReceiver | ||
import android.content.ComponentName | ||
import android.content.Context | ||
import android.content.Intent | ||
import android.content.IntentFilter | ||
import android.os.IBinder | ||
import android.view.View | ||
import android.widget.RemoteViews | ||
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.preferences.sharedPrefs | ||
import com.ichi2.compat.CompatHelper.Companion.registerReceiverCompat | ||
import timber.log.Timber | ||
|
||
/** | ||
* CardsDueWidget is a widget for displaying the number of due cards with additional card analysis data. | ||
*/ | ||
class CardsDueWidget : AnalyticsWidgetProvider() { | ||
|
||
/** | ||
* Updates the widget when called. This is the main method for refreshing the widget's view. | ||
* | ||
* @param context The context from which the method is called. | ||
* @param appWidgetManager The AppWidgetManager instance used to update the widget. | ||
* @param appWidgetIds The IDs of the widgets to be updated. | ||
* @param usageAnalytics The usage analytics instance for tracking widget interactions. | ||
*/ | ||
override fun performUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray, usageAnalytics: UsageAnalytics) { | ||
updateWidget(context) | ||
} | ||
|
||
override fun onEnabled(context: Context) { | ||
super.onEnabled(context) | ||
val preferences = context.sharedPrefs() | ||
preferences.edit(commit = true) { putBoolean("cardsDueWidgetEnabled", true) } | ||
} | ||
|
||
override fun onDisabled(context: Context) { | ||
super.onDisabled(context) | ||
val preferences = context.sharedPrefs() | ||
preferences.edit(commit = true) { putBoolean("cardsDueWidgetEnabled", false) } | ||
} | ||
|
||
/** | ||
* Handles broadcasted intents of the widget. | ||
* Triggers a widget update upon receiving any supported action. | ||
* | ||
* @param context The context from which the method is called. | ||
* @param intent The intent that was received. | ||
*/ | ||
override fun onReceive(context: Context, intent: Intent) { | ||
super.onReceive(context, intent) | ||
if (intent.action.contentEquals("com.sec.android.widgetapp.APPWIDGET_RESIZE")) { | ||
updateWidget(context) | ||
} | ||
updateWidget(context) // Ensures the widget is updated after receiving any intent | ||
} | ||
|
||
/** | ||
* UpdateService is a background service responsible for updating the widget periodically or on demand. | ||
*/ | ||
class UpdateService : Service() { | ||
/** The cached number of total due cards. */ | ||
private var dueCardsCount = 0 | ||
|
||
/** | ||
* Called when the service is started. Updates the widget immediately. | ||
* | ||
* @param intent The intent that started the service. | ||
* @param flags Additional data about the start request. | ||
* @param startId A unique integer representing this specific request to start. | ||
* @return The mode in which the system should handle the service if it is killed. | ||
*/ | ||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { | ||
Timber.i("CardsDueWidget: onStartCommand") | ||
val manager = getAppWidgetManager(this) ?: return super.onStartCommand(intent, flags, startId) | ||
val updateViews = buildUpdate(this) | ||
val thisWidget = ComponentName(this, CardsDueWidget::class.java) | ||
manager.updateAppWidget(thisWidget, updateViews) | ||
updateWidget(this) // Trigger an instant update | ||
return super.onStartCommand(intent, flags, startId) | ||
} | ||
|
||
/** | ||
* Constructs the RemoteViews object that defines the widget's layout and updates it with | ||
* the latest data. | ||
* | ||
* @param context The context from which the method is called. | ||
* @return The RemoteViews object that will be displayed in the widget. | ||
*/ | ||
private fun buildUpdate(context: Context): RemoteViews { | ||
Timber.d("buildUpdate") | ||
val updateViews = RemoteViews(context.packageName, R.layout.widget_cards_due) | ||
val mounted = AnkiDroidApp.isSdCardMounted | ||
if (!mounted) { | ||
updateViews.setViewVisibility(R.id.dueNumberTextCardsDue, View.INVISIBLE) | ||
updateViews.setViewVisibility(R.id.etaNumberTextCardsDue, View.INVISIBLE) | ||
if (mountReceiver == null) { | ||
mountReceiver = object : BroadcastReceiver() { | ||
override fun onReceive(context: Context, intent: Intent) { | ||
val action = intent.action | ||
if (action != null && action == Intent.ACTION_MEDIA_MOUNTED) { | ||
Timber.d("mountReceiver - Action = Media Mounted") | ||
if (remounted) { | ||
WidgetStatus.updateInBackground(AnkiDroidApp.instance) | ||
remounted = false | ||
if (mountReceiver != null) { | ||
AnkiDroidApp.instance.unregisterReceiver(mountReceiver) | ||
} | ||
} else { | ||
remounted = true | ||
} | ||
} | ||
} | ||
} | ||
val iFilter = IntentFilter() | ||
iFilter.addAction(Intent.ACTION_MEDIA_MOUNTED) | ||
iFilter.addDataScheme("file") | ||
AnkiDroidApp.instance.registerReceiverCompat(mountReceiver, iFilter, ContextCompat.RECEIVER_EXPORTED) | ||
} | ||
} else { | ||
val counts = WidgetStatus.fetchSmall(context) | ||
dueCardsCount = counts[0] | ||
val eta = counts[1] | ||
updateViews.setViewVisibility(R.id.dueNumberTextCardsDue, if (dueCardsCount > 0) View.VISIBLE else View.INVISIBLE) | ||
updateViews.setViewVisibility(R.id.etaNumberTextCardsDue, if (eta > 0 && dueCardsCount > 0) View.VISIBLE else View.INVISIBLE) | ||
updateViews.setTextViewText(R.id.dueNumberTextCardsDue, dueCardsCount.toString()) | ||
updateViews.setTextViewText(R.id.etaNumberTextCardsDue, eta.toString()) | ||
} | ||
|
||
val ankiDroidIntent = Intent(context, IntentHandler::class.java).apply { | ||
action = Intent.ACTION_MAIN | ||
addCategory(Intent.CATEGORY_LAUNCHER) | ||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK // Ensures the correct task is opened | ||
} | ||
val pendingAnkiDroidIntent = PendingIntent.getActivity( | ||
context, | ||
0, | ||
ankiDroidIntent, | ||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE // Ensures the intent is immutable if needed | ||
) | ||
updateViews.setOnClickPendingIntent(R.id.mainLayoutCardsDue, pendingAnkiDroidIntent) | ||
return updateViews | ||
} | ||
|
||
/** | ||
* This service does not support binding, so this method returns null. | ||
* | ||
* @param intent The intent that was used to bind to the service. | ||
* @return Always returns null as binding is not supported. | ||
*/ | ||
override fun onBind(intent: Intent?): IBinder? { | ||
Timber.d("onBind") | ||
return null | ||
} | ||
} | ||
|
||
companion object { | ||
private var mountReceiver: BroadcastReceiver? = null | ||
private var remounted = false | ||
|
||
/** | ||
* Updates the widget by fetching the latest data and applying it to the widget's views. | ||
* | ||
* @param context The context from which the method is called. | ||
*/ | ||
private fun updateWidget(context: Context) { | ||
val appWidgetManager = AppWidgetManager.getInstance(context) | ||
val ids = appWidgetManager.getAppWidgetIds(ComponentName(context, CardsDueWidget::class.java)) | ||
for (id in ids) { | ||
val updateViews = RemoteViews(context.packageName, R.layout.widget_cards_due) | ||
val counts = WidgetStatus.fetchSmall(context) | ||
val dueCardsCount = counts[0] | ||
val eta = counts[1] | ||
updateViews.setViewVisibility(R.id.dueNumberTextCardsDue, if (dueCardsCount > 0) View.VISIBLE else View.INVISIBLE) | ||
updateViews.setViewVisibility(R.id.etaNumberTextCardsDue, if (eta > 0 && dueCardsCount > 0) View.VISIBLE else View.INVISIBLE) | ||
updateViews.setTextViewText(R.id.dueNumberTextCardsDue, dueCardsCount.toString()) | ||
updateViews.setTextViewText(R.id.etaNumberTextCardsDue, eta.toString()) | ||
|
||
appWidgetManager.updateAppWidget(id, updateViews) | ||
} | ||
} | ||
} | ||
} |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
<?xml version="1.0" encoding="utf-8"?> | ||
<LinearLayout android:id="@+id/mainLayoutCardsDue" | ||
xmlns:android="http://schemas.android.com/apk/res/android" | ||
xmlns:tools="http://schemas.android.com/tools" | ||
android:layout_width="match_parent" | ||
android:layout_height="wrap_content" | ||
android:background="?attr/colorPrimaryInverse" | ||
android:clickable="true" | ||
android:gravity="center" | ||
android:orientation="vertical" | ||
android:theme="@style/Theme.Material3.DynamicColors.DayNight"> | ||
|
||
<LinearLayout | ||
android:id="@+id/cardsDueDataHolder" | ||
android:layout_width="match_parent" | ||
android:layout_height="wrap_content" | ||
android:background="?attr/colorPrimaryContainer" | ||
android:gravity="center" | ||
android:orientation="horizontal" | ||
android:padding="15dp"> | ||
|
||
<RelativeLayout | ||
android:layout_width="wrap_content" | ||
android:layout_height="wrap_content" | ||
tools:ignore="UselessParent"> | ||
|
||
<TextView | ||
android:id="@+id/dueNumberTextCardsDue" | ||
android:layout_width="wrap_content" | ||
android:layout_height="wrap_content" | ||
android:gravity="center|center_horizontal|center_vertical" | ||
android:textColor="@color/flag_reviewer_red" | ||
android:textSize="26sp" | ||
android:textStyle="bold" | ||
tools:text="102" /> | ||
|
||
<TextView | ||
android:id="@+id/plusTextCardsDue" | ||
android:layout_width="wrap_content" | ||
android:layout_height="wrap_content" | ||
android:layout_alignTop="@id/dueNumberTextCardsDue" | ||
android:layout_marginStart="2dp" | ||
android:layout_toEndOf="@id/dueNumberTextCardsDue" | ||
android:text="+" | ||
android:textColor="?android:attr/textColorPrimary" | ||
android:textSize="12sp" | ||
android:textStyle="bold" | ||
tools:ignore="HardcodedText" /> | ||
|
||
<TextView | ||
android:id="@+id/etaNumberTextCardsDue" | ||
android:layout_width="wrap_content" | ||
android:layout_height="wrap_content" | ||
android:layout_alignTop="@id/plusTextCardsDue" | ||
android:layout_marginStart="2dp" | ||
android:layout_toEndOf="@id/plusTextCardsDue" | ||
android:textColor="?android:attr/textColorPrimary" | ||
android:textSize="12sp" | ||
android:textStyle="bold" | ||
tools:text="30" /> | ||
|
||
<TextView | ||
android:id="@+id/minuteText" | ||
android:layout_width="wrap_content" | ||
android:layout_height="wrap_content" | ||
android:layout_alignTop="@id/etaNumberTextCardsDue" | ||
android:layout_marginStart="2dp" | ||
android:layout_toEndOf="@id/etaNumberTextCardsDue" | ||
android:text="min" | ||
android:textColor="?android:attr/textColorPrimary" | ||
android:textSize="12sp" | ||
android:textStyle="bold" | ||
tools:ignore="HardcodedText" /> | ||
|
||
</RelativeLayout> | ||
</LinearLayout> | ||
|
||
<TextView | ||
android:id="@+id/widgetDescription" | ||
android:layout_width="match_parent" | ||
android:layout_height="wrap_content" | ||
android:gravity="center" | ||
android:padding="15dp" | ||
android:paddingBottom="5dp" | ||
android:text="@string/cards_due_widget" | ||
android:textColor="?android:attr/textColorPrimary" | ||
android:textSize="24sp" | ||
android:textStyle="bold" /> | ||
|
||
</LinearLayout> |
63 changes: 63 additions & 0 deletions
63
AnkiDroid/src/main/res/layout/widget_cards_due_drawable_v31.xml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
<?xml version="1.0" encoding="utf-8" ?> | ||
<LinearLayout xmlns:tools="http://schemas.android.com/tools" | ||
xmlns:android="http://schemas.android.com/apk/res/android" | ||
android:id="@+id/widgetPickerLayoutCardsDueWidget" | ||
android:layout_width="match_parent" | ||
android:layout_height="wrap_content" | ||
android:background="?attr/colorPrimaryInverse" | ||
android:orientation="vertical" | ||
android:gravity="center" | ||
android:theme="@style/Theme.Material3.DynamicColors.DayNight"> | ||
|
||
<LinearLayout | ||
android:id="@+id/cardsDueDataHolder" | ||
android:layout_width="match_parent" | ||
android:layout_height="wrap_content" | ||
android:background="?attr/colorPrimaryContainer" | ||
android:orientation="horizontal" | ||
android:padding="20dp" | ||
android:gravity="center"> | ||
|
||
<RelativeLayout | ||
android:layout_width="wrap_content" | ||
android:layout_height="wrap_content" | ||
tools:ignore="UselessParent"> | ||
|
||
<TextView | ||
android:id="@+id/due_cards_due_widget" | ||
android:layout_width="wrap_content" | ||
android:layout_height="wrap_content" | ||
android:text="102" | ||
android:textColor="@color/flag_reviewer_red" | ||
android:textSize="26sp" | ||
android:textStyle="bold" | ||
tools:ignore="HardcodedText" /> | ||
|
||
<TextView | ||
android:id="@+id/eta_cards_due_widget" | ||
android:layout_width="wrap_content" | ||
android:layout_height="wrap_content" | ||
android:layout_alignTop="@id/due_cards_due_widget" | ||
android:layout_toEndOf="@id/due_cards_due_widget" | ||
android:layout_marginStart="2dp" | ||
android:text="+ 30 min" | ||
android:textColor="?android:attr/textColorPrimary" | ||
android:textSize="12sp" | ||
android:textStyle="bold" | ||
tools:ignore="HardcodedText" /> | ||
</RelativeLayout> | ||
</LinearLayout> | ||
|
||
<TextView | ||
android:id="@+id/deckNameCardsDueWidget" | ||
android:layout_width="match_parent" | ||
android:layout_height="wrap_content" | ||
android:gravity="center" | ||
android:padding="20dp" | ||
android:paddingBottom="5dp" | ||
android:text="@string/cards_due_widget" | ||
android:textColor="?android:attr/textColorPrimary" | ||
android:textSize="24sp" | ||
android:textStyle="bold"/> | ||
|
||
</LinearLayout> |
Oops, something went wrong.