-
-
Notifications
You must be signed in to change notification settings - Fork 2.8k
Expand file tree
/
Copy pathAnkiDroidWidgetSmall.kt
More file actions
242 lines (226 loc) · 11.1 KB
/
AnkiDroidWidgetSmall.kt
File metadata and controls
242 lines (226 loc) · 11.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
/*
* 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.content.res.Configuration
import android.os.Build
import android.os.IBinder
import android.util.TypedValue
import android.view.View
import android.widget.RemoteViews
import androidx.annotation.LayoutRes
import androidx.core.app.PendingIntentCompat
import androidx.core.content.ContextCompat
import androidx.core.content.edit
import com.ichi2.anki.R
import com.ichi2.widget.bridge.WidgetAnalytics
import com.ichi2.widget.bridge.WidgetDependencies
import timber.log.Timber
import kotlin.math.sqrt
/**
* 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: AppWidgetIds,
usageAnalytics: WidgetAnalytics,
) {
WidgetStatus.updateInBackground(context)
}
override fun onEnabled(context: Context) {
super.onEnabled(context)
val preferences = WidgetDependencies.preferences.sharedPrefs(context)
preferences.edit(commit = true) { putBoolean("widgetSmallEnabled", true) }
}
override fun onDisabled(context: Context) {
super.onDisabled(context)
val preferences = WidgetDependencies.preferences.sharedPrefs(context)
preferences.edit(commit = true) { putBoolean("widgetSmallEnabled", false) }
}
override fun onReceive(
context: Context,
intent: Intent,
) {
if (intent.action.contentEquals("com.sec.android.widgetapp.APPWIDGET_RESIZE")) {
updateWidgetDimensions(context, RemoteViews(context.packageName, R.layout.widget_small), AnkiDroidWidgetSmall::class.java)
}
super.onReceive(context, intent)
}
class UpdateService : Service() {
/**
* Updates the UI of [AnkiDroidWidgetSmall]
*
* Externals callers should use [WidgetStatus.updateInBackground]
*/
fun doUpdate(context: Context) {
val appWidgetManager = getAppWidgetManager(context) ?: return
val remoteViews = buildUpdate(context)
appWidgetManager.updateAppWidget(ComponentName(context, AnkiDroidWidgetSmall::class.java), remoteViews)
}
@Deprecated("Implement onStartCommand(Intent, int, int) instead.") // TODO
override fun onStart(intent: Intent, startId: Int) {
Timber.i("SmallWidget: OnStart")
val manager = getAppWidgetManager(this) ?: return
val updateViews = buildUpdate(this)
val thisWidget = ComponentName(this, AnkiDroidWidgetSmall::class.java)
manager.updateAppWidget(thisWidget, updateViews)
}
@get:LayoutRes
private val widgetSmallLayout: Int
get() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) return R.layout.widget_small
return if (disableMaterialYouDynamicColor) R.layout.widget_small_unthemed else R.layout.widget_small
}
private fun buildUpdate(context: Context): RemoteViews {
Timber.d("updating small widget UI")
val updateViews = RemoteViews(context.packageName, widgetSmallLayout)
val mounted = WidgetDependencies.appState.isSdCardMounted
if (!mounted) {
updateViews.setViewVisibility(R.id.widget_due, View.INVISIBLE)
updateViews.setViewVisibility(R.id.widget_eta, View.INVISIBLE)
updateViews.setViewVisibility(R.id.ankidroid_widget_small_finish_layout, View.GONE)
if (mountReceiver == null) {
mountReceiver =
object : BroadcastReceiver() {
override fun onReceive(
@Suppress("LocalVariableName") context_doNotUse: Context,
intent: Intent,
) {
// baseContext() is null, applicationContext() throws a NPE,
// context may not have the locale override from AnkiDroidApp
val action = intent.action
if (action != null && action == Intent.ACTION_MEDIA_MOUNTED) {
Timber.d("mountReceiver - Action = Media Mounted")
if (remounted) {
WidgetStatus.updateInBackground(WidgetDependencies.appState.applicationInstance)
remounted = false
if (mountReceiver != null) {
WidgetDependencies.appState.applicationInstance.unregisterReceiver(mountReceiver)
}
} else {
remounted = true
}
}
}
}
val iFilter = IntentFilter()
iFilter.addAction(Intent.ACTION_MEDIA_MOUNTED)
iFilter.addDataScheme("file")
ContextCompat.registerReceiver(
WidgetDependencies.appState.applicationInstance,
mountReceiver,
iFilter,
ContextCompat.RECEIVER_EXPORTED,
)
}
} else {
// Compute the total number of cards due.
val (dueCardsCount, eta) = WidgetStatus.fetchSmall(context)
if (dueCardsCount == 0) {
updateViews.setViewVisibility(R.id.ankidroid_widget_small_finish_layout, View.VISIBLE)
updateViews.setViewVisibility(R.id.widget_eta, View.INVISIBLE)
updateViews.setViewVisibility(R.id.widget_due, View.INVISIBLE)
} else {
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, dueCardsCount.toString())
updateViews.setContentDescription(
R.id.widget_due,
context.resources.getQuantityString(R.plurals.widget_cards_due, dueCardsCount, dueCardsCount),
)
if (eta <= 0) {
updateViews.setViewVisibility(R.id.widget_eta, View.INVISIBLE)
} else {
updateViews.setViewVisibility(R.id.widget_eta, View.VISIBLE)
val etaText = if (Build.VERSION.SDK_INT >= 31) "⏱$eta" else "$eta"
updateViews.setTextViewText(R.id.widget_eta, etaText)
updateViews.setContentDescription(
R.id.widget_eta,
context.resources.getQuantityString(R.plurals.widget_eta, eta, eta),
)
}
}
}
// Add a click listener to open Anki from the icon.
// This should be always there, whether there are due cards or not.
val ankiDroidIntent = WidgetDependencies.intentFactory.intentToMainActivity(context)
val pendingAnkiDroidIntent =
PendingIntentCompat.getActivity(
context,
0,
ankiDroidIntent,
PendingIntent.FLAG_UPDATE_CURRENT,
false,
)
updateViews.setOnClickPendingIntent(R.id.ankidroid_widget_small_button, pendingAnkiDroidIntent)
updateWidgetDimensions(context, updateViews, AnkiDroidWidgetSmall::class.java)
return updateViews
}
override fun onBind(arg0: Intent): IBinder? {
Timber.d("onBind")
return null
}
}
companion object {
private var mountReceiver: BroadcastReceiver? = null
private var remounted = false
private fun updateWidgetDimensions(
context: Context,
updateViews: RemoteViews,
cls: Class<*>,
) {
val manager = getAppWidgetManager(context) ?: return
val ids = manager.getAppWidgetIdsEx(ComponentName(context, cls))
for (id in ids) {
val scale = context.resources.displayMetrics.density
val options = manager.getAppWidgetOptions(id)
var width: Float
var height: Float
if (context.resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) {
width = options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH).toFloat()
height = options.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT).toFloat()
} else {
width = options.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH).toFloat()
height = options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT).toFloat()
}
var horizontal: Int
var vertical: Int
var text: Float
if (width / height > 0.8) {
horizontal = (((width - height * 0.8) / 2 + 4) * scale + 0.5f).toInt()
vertical = (4 * scale + 0.5f).toInt()
text = (sqrt(height * 0.8 / width) * 18).toFloat()
} else {
vertical = (((height - width * 1.25) / 2 + 4) * scale + 0.5f).toInt()
horizontal = (4 * scale + 0.5f).toInt()
text = (sqrt(width * 1.25 / height) * 18).toFloat()
}
updateViews.setTextViewTextSize(R.id.widget_due, TypedValue.COMPLEX_UNIT_SP, text)
updateViews.setTextViewTextSize(R.id.widget_eta, TypedValue.COMPLEX_UNIT_SP, text)
updateViews.setViewPadding(R.id.ankidroid_widget_text_layout, horizontal, vertical, horizontal, vertical)
}
}
}
}