diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 5e9866ba8..56261b311 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -387,6 +387,13 @@
android:resource="@xml/file_provider_paths"/>
+
+
diff --git a/app/src/main/java/org/tasks/widget/WidgetChipProvider.kt b/app/src/main/java/org/tasks/widget/WidgetChipProvider.kt
index 37a54832a..2fb10afd7 100644
--- a/app/src/main/java/org/tasks/widget/WidgetChipProvider.kt
+++ b/app/src/main/java/org/tasks/widget/WidgetChipProvider.kt
@@ -3,7 +3,6 @@ package org.tasks.widget
import android.content.Context
import android.widget.RemoteViews
import androidx.annotation.ColorInt
-import com.mikepenz.iconics.IconicsDrawable
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.runBlocking
import org.tasks.BuildConfig
@@ -19,7 +18,6 @@ import org.tasks.filters.Filter
import org.tasks.filters.PlaceFilter
import org.tasks.filters.TagFilter
import org.tasks.filters.getIcon
-import org.tasks.icons.OutlinedGoogleMaterial
import org.tasks.kmp.org.tasks.time.getRelativeDateTime
import org.tasks.kmp.org.tasks.time.getTimeString
import org.tasks.time.startOfDay
@@ -115,14 +113,16 @@ class WidgetChipProvider @Inject constructor(
setTextViewText(R.id.chip_text, filter.title)
filter
.getIcon(inventory)
- ?.let {
+ ?.let { iconName ->
try {
- OutlinedGoogleMaterial.getIcon("gmo_$it")
- } catch (_: IllegalArgumentException) {
- null
+ val iconUri = WidgetIconProvider.getIconUri(
+ iconName = iconName,
+ )
+ setImageViewUri(R.id.chip_icon, iconUri)
+ } catch (_: Exception) {
+ setImageViewResource(R.id.chip_icon, defaultIcon)
}
}
- ?.let { setImageViewBitmap(R.id.chip_icon, IconicsDrawable(context, it).toBitmap()) }
?: setImageViewResource(R.id.chip_icon, defaultIcon)
}
@@ -138,4 +138,4 @@ class WidgetChipProvider @Inject constructor(
setColorFilter(R.id.chip_background, tint)
setTextColor(R.id.chip_text, tint)
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/org/tasks/widget/WidgetIconProvider.kt b/app/src/main/java/org/tasks/widget/WidgetIconProvider.kt
new file mode 100644
index 000000000..86f0335b2
--- /dev/null
+++ b/app/src/main/java/org/tasks/widget/WidgetIconProvider.kt
@@ -0,0 +1,118 @@
+package org.tasks.widget
+
+import android.content.ContentProvider
+import android.content.ContentValues
+import android.database.Cursor
+import android.graphics.Bitmap
+import android.graphics.Canvas
+import android.net.Uri
+import android.os.ParcelFileDescriptor
+import androidx.core.graphics.createBitmap
+import androidx.core.net.toUri
+import com.mikepenz.iconics.IconicsDrawable
+import com.mikepenz.iconics.utils.sizeDp
+import org.tasks.BuildConfig
+import org.tasks.icons.OutlinedGoogleMaterial
+import timber.log.Timber
+import java.io.File
+import java.io.FileOutputStream
+
+class WidgetIconProvider : ContentProvider() {
+
+ override fun onCreate() = true
+
+ override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor? {
+ if (mode != "r") {
+ throw SecurityException("Only read access allowed")
+ }
+
+ return try {
+ val segments = uri.pathSegments
+ if (segments.size != 2) return null
+
+ val iconName = segments[1]
+
+ if (!iconName.matches(Regex("^[a-zA-Z0-9_]+$"))) return null
+
+ val cacheFile = getCacheFile(iconName)
+
+ if (!cacheFile.exists()) {
+ generateIcon(cacheFile, iconName)
+ }
+
+ if (cacheFile.exists()) {
+ ParcelFileDescriptor.open(cacheFile, ParcelFileDescriptor.MODE_READ_ONLY)
+ } else {
+ null
+ }
+ } catch (e: Exception) {
+ Timber.e(e, "Failed to open icon file for URI: $uri")
+ null
+ }
+ }
+
+ private fun generateIcon(file: File, iconName: String) {
+ try {
+ val icon = OutlinedGoogleMaterial.getIcon("gmo_$iconName")
+ val context = context ?: return
+
+ val drawable = IconicsDrawable(context, icon).apply {
+ this.sizeDp = 24
+ }
+
+ val bitmap = createBitmap(
+ drawable.intrinsicWidth.coerceAtLeast(1),
+ drawable.intrinsicHeight.coerceAtLeast(1)
+ )
+
+ val canvas = Canvas(bitmap)
+ drawable.setBounds(0, 0, canvas.width, canvas.height)
+ drawable.draw(canvas)
+
+ file.parentFile?.mkdirs()
+ FileOutputStream(file).use { out ->
+ bitmap.compress(Bitmap.CompressFormat.PNG, 100, out)
+ }
+ bitmap.recycle()
+ } catch (e: Exception) {
+ Timber.e(e, "Failed to generate icon: $iconName")
+ file.delete()
+ }
+ }
+
+ private fun getCacheFile(iconName: String): File {
+ val context = context ?: throw IllegalStateException("Context is null")
+ val cacheDir = File(context.cacheDir, "widget_icons")
+ cacheDir.mkdirs()
+ return File(cacheDir, "${iconName}.png")
+ }
+
+ override fun query(
+ uri: Uri,
+ projection: Array?,
+ selection: String?,
+ selectionArgs: Array?,
+ sortOrder: String?,
+ ): Cursor? = null
+
+ override fun insert(uri: Uri, values: ContentValues?): Uri? = null
+
+ override fun update(
+ uri: Uri,
+ values: ContentValues?,
+ selection: String?,
+ selectionArgs: Array?,
+ ): Int = 0
+
+ override fun delete(uri: Uri, selection: String?, selectionArgs: Array?): Int = 0
+
+ override fun getType(uri: Uri): String = "image/png"
+
+ companion object {
+ const val AUTHORITY = "${BuildConfig.APPLICATION_ID}.widgeticons"
+
+ fun getIconUri(iconName: String): Uri {
+ return "content://$AUTHORITY/icon/$iconName".toUri()
+ }
+ }
+}