From 845609362edaa85beee759e65c38ee8d9e5ea055 Mon Sep 17 00:00:00 2001 From: Prateek Singh Date: Tue, 24 Aug 2021 18:06:36 +0530 Subject: [PATCH] Refactored Small widget --- AnkiDroid/src/main/AndroidManifest.xml | 5 + .../main/java/com/ichi2/anki/CardBrowser.java | 2 - .../main/java/com/ichi2/anki/DeckPicker.java | 3 - .../java/com/ichi2/anki/ModelBrowser.java | 2 - .../java/com/ichi2/anki/ModelFieldEditor.java | 2 - .../main/java/com/ichi2/anki/NoteEditor.java | 2 - .../main/java/com/ichi2/anki/Reviewer.java | 4 - .../com/ichi2/anki/StudyOptionsActivity.java | 2 - .../com/ichi2/anki/services/BootService.java | 7 +- .../anki/services/NotificationService.java | 97 +++++++++++++------ .../ichi2/widget/AnkiDroidWidgetSmall.java | 24 ++++- .../main/java/com/ichi2/widget/WidgetAlarm.kt | 75 ++++++++++++++ .../java/com/ichi2/widget/WidgetStatus.java | 7 -- .../main/res/xml/widget_provider_small.xml | 2 +- 14 files changed, 173 insertions(+), 61 deletions(-) create mode 100644 AnkiDroid/src/main/java/com/ichi2/widget/WidgetAlarm.kt diff --git a/AnkiDroid/src/main/AndroidManifest.xml b/AnkiDroid/src/main/AndroidManifest.xml index db03e8739329..5dc51b53ab23 100644 --- a/AnkiDroid/src/main/AndroidManifest.xml +++ b/AnkiDroid/src/main/AndroidManifest.xml @@ -408,6 +408,11 @@ /> + + + = minCardsDue) { - // Build basic notification - String cardsDueText = context.getResources() - .getQuantityString(R.plurals.widget_minimum_cards_due_notification_ticker_text, dueCardsCount, dueCardsCount); - - // This generates a log warning "Use of stream types is deprecated..." - // The NotificationCompat code uses setSound() no matter what we do and triggers it. - NotificationCompat.Builder builder = - new NotificationCompat.Builder(context, - NotificationChannels.getId(NotificationChannels.Channel.GENERAL)) - .setCategory(NotificationCompat.CATEGORY_REMINDER) - .setSmallIcon(R.drawable.ic_stat_notify) - .setColor(ContextCompat.getColor(context, R.color.material_light_blue_700)) - .setContentTitle(cardsDueText) - .setTicker(cardsDueText); - // Enable vibrate and blink if set in preferences - if (preferences.getBoolean("widgetVibrate", false)) { - builder.setVibrate(new long[] { 1000, 1000, 1000}); + + switch (intent.getIntExtra(CARD_NOTIFICATION_TYPE, CARD_NOTIFICATION_ALARM)) { + case CARD_NOTIFICATION_WIDGET: { + // We have to show the notification always. Because we are checking the condition before sending. + Timber.d("Cards Notification request received from Widget Update Notification."); + buildNotification(context, dueCardsCount, preferences, manager); } - if (preferences.getBoolean("widgetBlink", false)) { - builder.setLights(Color.BLUE, 1000, 1000); + break; + case CARD_NOTIFICATION_ALARM: { + Timber.d("Cards Notification request received from Alarm Notification."); + if (dueCardsCount >= minCardsDue) { + buildNotification(context, dueCardsCount, preferences, manager); + } else { + // Cancel the existing notification, if any. + cancelNotification(manager); + } + } + break; + default: { + Timber.d("Cards Notification request received but not found Notification Type in Intent Extra"); + // Cancel all the pending notification. + cancelNotification(manager); } - // Creates an explicit intent for an Activity in your app - Intent resultIntent = new Intent(context, DeckPicker.class); - resultIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); - PendingIntent resultPendingIntent = CompatHelper.getCompat().getImmutableActivityIntent(context, 0, resultIntent, - PendingIntent.FLAG_UPDATE_CURRENT); - builder.setContentIntent(resultPendingIntent); - // mId allows you to update the notification later on. - manager.notify(WIDGET_NOTIFY_ID, builder.build()); - } else { - // Cancel the existing notification, if any. - manager.cancel(WIDGET_NOTIFY_ID); } } + + private void buildNotification(Context context, int dueCardsCount, SharedPreferences preferences, NotificationManager manager) { + Timber.d("Building Cards Notification"); + // Build basic notification + String cardsDueText = context.getResources() + .getQuantityString(R.plurals.widget_minimum_cards_due_notification_ticker_text, dueCardsCount, dueCardsCount); + + // This generates a log warning "Use of stream types is deprecated..." + // The NotificationCompat code uses setSound() no matter what we do and triggers it. + NotificationCompat.Builder builder = + new NotificationCompat.Builder(context, + NotificationChannels.getId(NotificationChannels.Channel.GENERAL)) + .setCategory(NotificationCompat.CATEGORY_REMINDER) + .setSmallIcon(R.drawable.ic_stat_notify) + .setColor(ContextCompat.getColor(context, R.color.material_light_blue_700)) + .setContentTitle(cardsDueText) + .setTicker(cardsDueText); + // Enable vibrate and blink if set in preferences + if (preferences.getBoolean("widgetVibrate", false)) { + builder.setVibrate(new long[] { 1000, 1000, 1000}); + } + if (preferences.getBoolean("widgetBlink", false)) { + builder.setLights(Color.BLUE, 1000, 1000); + } + // Creates an explicit intent for an Activity in your app + Intent resultIntent = new Intent(context, DeckPicker.class); + resultIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + PendingIntent resultPendingIntent = CompatHelper.getCompat().getImmutableActivityIntent(context, 0, resultIntent, + PendingIntent.FLAG_UPDATE_CURRENT); + builder.setContentIntent(resultPendingIntent); + // mId allows you to update the notification later on. + manager.notify(WIDGET_NOTIFY_ID, builder.build()); + } + + private void cancelNotification(NotificationManager manager) { + Timber.d("Canceling Cards Notification."); + manager.cancel(WIDGET_NOTIFY_ID); + } } \ No newline at end of file diff --git a/AnkiDroid/src/main/java/com/ichi2/widget/AnkiDroidWidgetSmall.java b/AnkiDroid/src/main/java/com/ichi2/widget/AnkiDroidWidgetSmall.java index b1d685a66de6..304e34c384f0 100644 --- a/AnkiDroid/src/main/java/com/ichi2/widget/AnkiDroidWidgetSmall.java +++ b/AnkiDroid/src/main/java/com/ichi2/widget/AnkiDroidWidgetSmall.java @@ -27,6 +27,7 @@ import android.content.res.Configuration; import android.os.Bundle; import android.os.IBinder; +import android.util.Log; import android.util.TypedValue; import android.view.View; import android.widget.RemoteViews; @@ -35,8 +36,10 @@ import com.ichi2.anki.IntentHandler; import com.ichi2.anki.R; import com.ichi2.anki.analytics.UsageAnalytics; +import com.ichi2.anki.services.NotificationService; import com.ichi2.compat.CompatHelper; +import androidx.localbroadcastmanager.content.LocalBroadcastManager; import timber.log.Timber; public class AnkiDroidWidgetSmall extends AppWidgetProvider { @@ -59,6 +62,7 @@ public void onEnabled(Context context) { Timber.d("SmallWidget: Widget enabled"); SharedPreferences preferences = AnkiDroidApp.getSharedPrefs(context); preferences.edit().putBoolean("widgetSmallEnabled", true).commit(); + new WidgetAlarm().setAlarm(context.getApplicationContext()); UsageAnalytics.sendAnalyticsEvent(this.getClass().getSimpleName(), "enabled"); } @@ -70,6 +74,7 @@ public void onDisabled(Context context) { SharedPreferences preferences = AnkiDroidApp.getSharedPrefs(context); preferences.edit().putBoolean("widgetSmallEnabled", false).commit(); UsageAnalytics.sendAnalyticsEvent(this.getClass().getSimpleName(), "disabled"); + new WidgetAlarm().stopAlarm(context.getApplicationContext()); } @Override @@ -120,8 +125,9 @@ public static class UpdateService extends Service { public void doUpdate(Context context) { + Timber.d("UPDATING WIDGET"); AppWidgetManager.getInstance(context) - .updateAppWidget(new ComponentName(context, AnkiDroidWidgetSmall.class), buildUpdate(context, true)); + .updateAppWidget(new ComponentName(context, AnkiDroidWidgetSmall.class), buildUpdate(context, true, "DO UPDATE")); } @Override @@ -129,7 +135,7 @@ public void doUpdate(Context context) { public void onStart(Intent intent, int startId) { Timber.i("SmallWidget: OnStart"); - RemoteViews updateViews = buildUpdate(this, true); + RemoteViews updateViews = buildUpdate(this, true, "ON START"); ComponentName thisWidget = new ComponentName(this, AnkiDroidWidgetSmall.class); AppWidgetManager manager = AppWidgetManager.getInstance(this); @@ -137,7 +143,7 @@ public void onStart(Intent intent, int startId) { } - private RemoteViews buildUpdate(Context context, boolean updateDueDecksNow) { + private RemoteViews buildUpdate(Context context, boolean updateDueDecksNow, String update) { Timber.d("buildUpdate"); RemoteViews updateViews = new RemoteViews(context.getPackageName(), R.layout.widget_small); @@ -188,6 +194,18 @@ public void onReceive(Context context, Intent intent) { } updateViews.setViewVisibility(R.id.widget_due, View.INVISIBLE); } else { + // NOTIFICATION + SharedPreferences preferences = AnkiDroidApp.getSharedPrefs(context); + int lastUpdatedCard = preferences.getInt("widgetCard", 0); + // If their is any change in card the show the notification. + if (lastUpdatedCard != mDueCardsCount) { + Intent intent = new Intent(NotificationService.INTENT_ACTION); + Context appContext = context.getApplicationContext(); + intent.putExtra(NotificationService.CARD_NOTIFICATION_TYPE, NotificationService.CARD_NOTIFICATION_WIDGET); + LocalBroadcastManager.getInstance(appContext).sendBroadcast(intent); + preferences.edit().putInt("widgetCard", mDueCardsCount).apply(); + } + updateViews.setViewVisibility(R.id.ankidroid_widget_small_finish_layout, View.INVISIBLE); updateViews.setViewVisibility(R.id.widget_due, View.VISIBLE); updateViews.setTextViewText(R.id.widget_due, Integer.toString(mDueCardsCount)); diff --git a/AnkiDroid/src/main/java/com/ichi2/widget/WidgetAlarm.kt b/AnkiDroid/src/main/java/com/ichi2/widget/WidgetAlarm.kt new file mode 100644 index 000000000000..602f65a636e2 --- /dev/null +++ b/AnkiDroid/src/main/java/com/ichi2/widget/WidgetAlarm.kt @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2021 Prateek Singh + * 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 . + */ + +package com.ichi2.widget + +import android.annotation.SuppressLint +import android.app.AlarmManager +import android.app.PendingIntent +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import com.ichi2.anki.CollectionHelper +import timber.log.Timber +import java.util.* + +class WidgetAlarm() : BroadcastReceiver() { + + val INTERVAL_HOUR: Long = AlarmManager.INTERVAL_HOUR + val INTERVAL_HALF_HOUR: Long = AlarmManager.INTERVAL_HALF_HOUR + val INTERVAL_FIFTEEN_MINUTES: Long = AlarmManager.INTERVAL_FIFTEEN_MINUTES + val INTERVAL_DAY: Long = AlarmManager.INTERVAL_DAY + private val TIME_INTERVAL: Long = 15000 + private val REQUEST_CODE: Int = 5 + + override fun onReceive(context: Context?, intent: Intent?) { + Timber.d("Widget Alarm BRODCAST RECIVED") + // Update the widget. + AnkiDroidWidgetSmall.UpdateService().doUpdate(context) + } + + /** + * Starts the Widget Alarm. Used to update Widget in fixed interval of time. + * @param context Application Context. + * + * FIXME: Remove Suppress Lint(Finding better way). + * */ + @SuppressLint("UnspecifiedImmutableFlag") + fun setAlarm(context: Context) { + val calendar: Calendar = CollectionHelper.getInstance().getCol(context).time.calendar() + calendar.add(Calendar.MILLISECOND, TIME_INTERVAL.toInt()) + + val alarmIntent = Intent(context, WidgetAlarm::class.java).let { intent -> + PendingIntent.getBroadcast(context, REQUEST_CODE, intent, 0) + } + with(context.getSystemService(Context.ALARM_SERVICE) as AlarmManager) { + setRepeating(AlarmManager.RTC, calendar.timeInMillis, TIME_INTERVAL, alarmIntent) + Timber.d("Widget Alarm Set At: ${calendar.timeInMillis} interval -> $TIME_INTERVAL") + } + } + + /** + * Stops the Widget Alarm. + * @param context Application Context. + * + * FIXME: Remove Suppress Lint(Finding better way). + * */ + @SuppressLint("UnspecifiedImmutableFlag") + fun stopAlarm(context: Context) { + val alarmIntent = Intent(context, WidgetAlarm::class.java) + val pendingIntent = PendingIntent.getBroadcast(context, REQUEST_CODE, alarmIntent, 0) + val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager + alarmManager.cancel(pendingIntent) + Timber.d("Small Widget Alarm Stopped.") + } +} diff --git a/AnkiDroid/src/main/java/com/ichi2/widget/WidgetStatus.java b/AnkiDroid/src/main/java/com/ichi2/widget/WidgetStatus.java index e21bf6f2c7d5..4b057a204cb0 100644 --- a/AnkiDroid/src/main/java/com/ichi2/widget/WidgetStatus.java +++ b/AnkiDroid/src/main/java/com/ichi2/widget/WidgetStatus.java @@ -44,9 +44,6 @@ private WidgetStatus() { /** * Request the widget to update its status. - * TODO Mike - we can reduce battery usage by widget users by removing updatePeriodMillis from metadata - * and replacing it with an alarm we set so device doesn't wake to update the widget, see: - * https://developer.android.com/guide/topics/appwidgets/#MetaData */ @SuppressWarnings("deprecation") // #7108: AsyncTask public static void update(Context context) { @@ -82,10 +79,6 @@ protected Context doInBackground(Context... params) { } new AnkiDroidWidgetSmall.UpdateService().doUpdate(context); - // Shows the notification when widget is not set on Home Screen - Intent intent = new Intent(NotificationService.INTENT_ACTION); - Context appContext = context.getApplicationContext(); - LocalBroadcastManager.getInstance(appContext).sendBroadcast(intent); return context; } } diff --git a/AnkiDroid/src/main/res/xml/widget_provider_small.xml b/AnkiDroid/src/main/res/xml/widget_provider_small.xml index a3f6badcca14..c6ee210bc44c 100644 --- a/AnkiDroid/src/main/res/xml/widget_provider_small.xml +++ b/AnkiDroid/src/main/res/xml/widget_provider_small.xml @@ -3,4 +3,4 @@ android:initialLayout="@layout/widget_small" android:minHeight="40dp" android:minWidth="40dp" - android:updatePeriodMillis="3600000" /> + android:updatePeriodMillis="0" />