-
Notifications
You must be signed in to change notification settings - Fork 632
Fix part of #5663: Use local thumbnails in dev builds #6136
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from 2 commits
41ae9c3
e3d8a89
7d67bee
b0a43e8
0cce034
e9a0fc8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,111 @@ | ||
| package org.oppia.android.app.application.dev | ||
|
||
|
|
||
| import android.content.ContentProvider | ||
| import android.content.ContentValues | ||
| import android.content.UriMatcher | ||
| import android.database.Cursor | ||
| import android.graphics.Bitmap | ||
| import android.graphics.Canvas | ||
| import android.net.Uri | ||
| import android.os.ParcelFileDescriptor | ||
| import androidx.appcompat.content.res.AppCompatResources | ||
| import org.oppia.android.app.views.R | ||
| import java.io.File | ||
| import java.io.FileOutputStream | ||
|
|
||
| /** | ||
| * A developer-only [ContentProvider] that serves local drawable resources as thumbnail images. | ||
| * | ||
| * In developer builds, thumbnail image loading is redirected from Google Cloud Storage to this | ||
| * content provider via the `content://org.oppia.android.provider` URI scheme. When a thumbnail | ||
| * filename (e.g., `baker.img`) is requested, this provider maps it to the corresponding local | ||
| * drawable resource (e.g., `lesson_thumbnail_graphic_baker.xml`) and returns the rendered image. | ||
| * | ||
| * This allows proto lessons with `thumbnail_filename` placeholders to successfully load thumbnails | ||
| * without needing access to GCS, which is unavailable in dev builds. | ||
| */ | ||
| class ThumbnailContentProvider : ContentProvider() { | ||
|
|
||
| companion object { | ||
| private const val AUTHORITY = "org.oppia.android.provider" | ||
| private const val THUMBNAIL_MATCH = 1 | ||
| private val uriMatcher = UriMatcher(UriMatcher.NO_MATCH).apply { | ||
| // Match any path under the authority — the thumbnail filename is extracted from the | ||
| // last path segment of the URI. | ||
| addURI(AUTHORITY, "*/*/*/*/#", THUMBNAIL_MATCH) | ||
| addURI(AUTHORITY, "*/*/*/*", THUMBNAIL_MATCH) | ||
| addURI(AUTHORITY, "thumbnail/*", THUMBNAIL_MATCH) | ||
| addURI(AUTHORITY, "#", THUMBNAIL_MATCH) | ||
|
||
| } | ||
|
|
||
| /** | ||
| * Maps thumbnail filenames (e.g., "baker.img") to their corresponding drawable resource IDs. | ||
| * | ||
| * This mapping must be kept in sync with the `thumbnail_filename` values defined in the | ||
| * textproto asset files. | ||
| */ | ||
| private val THUMBNAIL_FILENAME_TO_DRAWABLE_MAP = mapOf( | ||
| "baker.img" to R.drawable.lesson_thumbnail_graphic_baker, | ||
| "child_with_book.img" to R.drawable.lesson_thumbnail_graphic_child_with_book, | ||
| "child_with_cupcakes.img" to R.drawable.lesson_thumbnail_graphic_child_with_cupcakes, | ||
| "child_with_fractions_homework.img" to | ||
| R.drawable.lesson_thumbnail_graphic_child_with_fractions_homework, | ||
| "duck_and_chicken.img" to R.drawable.lesson_thumbnail_graphic_duck_and_chicken, | ||
| "person_with_pie_chart.img" to R.drawable.lesson_thumbnail_graphic_person_with_pie_chart | ||
| ) | ||
|
|
||
| /** Default drawable used when a filename doesn't match any known thumbnail. */ | ||
| private val DEFAULT_THUMBNAIL_DRAWABLE = R.drawable.lesson_thumbnail_graphic_baker | ||
| } | ||
|
|
||
| override fun onCreate(): Boolean = true | ||
|
|
||
| override fun query( | ||
| uri: Uri, | ||
| projection: Array<String>?, | ||
| selection: String?, | ||
| selectionArgs: Array<String>?, | ||
| sortOrder: String? | ||
| ): Cursor? = null | ||
|
|
||
| override fun getType(uri: Uri): String = "image/png" | ||
|
||
|
|
||
| override fun insert(uri: Uri, values: ContentValues?): Uri? = null | ||
|
|
||
| override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int = 0 | ||
|
|
||
| override fun update( | ||
| uri: Uri, | ||
| values: ContentValues?, | ||
| selection: String?, | ||
| selectionArgs: Array<String>? | ||
| ): Int = 0 | ||
|
|
||
| override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor? { | ||
| val context = context ?: return null | ||
| val thumbnailFilename = uri.lastPathSegment ?: return null | ||
|
|
||
| val drawableResId = THUMBNAIL_FILENAME_TO_DRAWABLE_MAP[thumbnailFilename] | ||
| ?: DEFAULT_THUMBNAIL_DRAWABLE | ||
|
|
||
| val drawable = AppCompatResources.getDrawable(context, drawableResId) | ||
| ?: return null | ||
|
|
||
| // Render the drawable (which may be a vector/XML drawable) to a bitmap. | ||
| val width = if (drawable.intrinsicWidth > 0) drawable.intrinsicWidth else 192 | ||
| val height = if (drawable.intrinsicHeight > 0) drawable.intrinsicHeight else 192 | ||
| val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) | ||
| val canvas = Canvas(bitmap) | ||
| drawable.setBounds(0, 0, canvas.width, canvas.height) | ||
| drawable.draw(canvas) | ||
|
|
||
| // Write the bitmap to a temporary file and return a file descriptor. | ||
| val cacheFile = File(context.cacheDir, "thumbnail_${thumbnailFilename.hashCode()}.png") | ||
| FileOutputStream(cacheFile).use { outputStream -> | ||
| bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream) | ||
| } | ||
| bitmap.recycle() | ||
|
|
||
| return ParcelFileDescriptor.open(cacheFile, ParcelFileDescriptor.MODE_READ_ONLY) | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,6 +7,7 @@ subtopic_ids: 4 | |
| is_published: true | ||
| has_practice_questions: true | ||
| topic_thumbnail { | ||
| thumbnail_filename: "child_with_fractions_homework.img" | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here & elsewhere: how did you decide on which thumbnails to use? We ought to match exactly the current JSON behavior to make it an easier before-and-after check. |
||
| } | ||
| written_translations { | ||
| key: "title" | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -117,6 +117,21 @@ kt_android_library( | |
| ], | ||
| ) | ||
|
|
||
| kt_android_library( | ||
| name = "dev_image_parsing_module", | ||
| srcs = [ | ||
| "DevImageParsingModule.kt", | ||
|
||
| ], | ||
| visibility = [ | ||
| "//:oppia_testing_visibility", | ||
| ], | ||
| deps = [ | ||
| ":image_parsing_annonations", | ||
| "//:dagger", | ||
| "//third_party:javax_inject_javax_inject", | ||
| ], | ||
| ) | ||
|
|
||
| kt_android_library( | ||
| name = "image_targets", | ||
| srcs = [ | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| package org.oppia.android.util.parser.image | ||
|
|
||
| import dagger.Module | ||
| import dagger.Provides | ||
| import javax.inject.Singleton | ||
|
|
||
| /** | ||
| * Provides developer-only image-extraction URL dependencies. | ||
| * | ||
| * This module replaces [ImageParsingModule] in developer builds. It overrides the default GCS | ||
| * prefix with a `content://` URI scheme that routes image requests to a local | ||
| * [ContentProvider][android.content.ContentProvider] instead of Google Cloud Storage. This allows | ||
| * thumbnail images to be served from local drawable resources in dev builds where GCS assets are | ||
| * inaccessible. | ||
| */ | ||
| @Module | ||
| class DevImageParsingModule { | ||
| @Provides | ||
| @DefaultGcsPrefix | ||
| @Singleton | ||
| fun provideDefaultGcsPrefix(): String { | ||
| return "content://org.oppia.android.provider" | ||
|
||
| } | ||
|
|
||
| @Provides | ||
| @ImageDownloadUrlTemplate | ||
| @Singleton | ||
| fun provideImageDownloadUrlTemplate(): String { | ||
| return "%s/%s/assets/image/%s" | ||
| } | ||
|
|
||
| @Provides | ||
| @ThumbnailDownloadUrlTemplate | ||
| @Singleton | ||
| fun provideThumbnailDownloadUrlTemplate(): String { | ||
| return "%s/%s/assets/thumbnail/%s" | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This needs to go into its own
AndroidManifest.xmlin the same package as the content provider. We will rely on the manifest merge to include this at the end. Adding it here means we need to ship the provider in production which we absolutely don't want to do.