Skip to content

Implement VideoContentProvider #177

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

Merged
merged 7 commits into from
Mar 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -64,5 +64,16 @@
android:authorities="${applicationId}.provider.storybook_provider"
android:enabled="true"
android:exported="true" />

<provider
android:name=".provider.VideoContentProvider"
android:authorities="${applicationId}.provider.video_provider"
android:enabled="true"
android:exported="true"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/video_file_path"/>
</provider>
</application>
</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
package ai.elimu.content_provider.provider

import ai.elimu.content_provider.BuildConfig
import ai.elimu.content_provider.room.db.RoomDb
import ai.elimu.content_provider.util.FileHelper
import android.content.ContentProvider
import android.content.ContentValues
import android.content.UriMatcher
import android.database.Cursor
import android.net.Uri
import android.os.ParcelFileDescriptor
import android.util.Log
import java.io.FileNotFoundException

class VideoContentProvider : ContentProvider() {

private val TAG = javaClass.name

override fun onCreate(): Boolean {
Log.i(TAG, "onCreate")

return true
}

/**
* Handles query requests from clients.
*/
override fun query(
uri: Uri,
projection: Array<String>?,
selection: String?,
selectionArgs: Array<String>?,
sortOrder: String?
): Cursor? {
Log.i(TAG, "query")

Log.i(TAG, "uri: $uri")
Log.i(TAG, "projection: $projection")
Log.i(TAG, "selection: $selection")
Log.i(TAG, "selectionArgs: $selectionArgs")
Log.i(TAG, "sortOrder: $sortOrder")

val context = context
Log.i(TAG, "context: $context")
if (context == null) {
return null
}

val roomDb = RoomDb.getDatabase(context)
val videoDao = roomDb.videoDao()

val code = MATCHER.match(uri)
Log.i(TAG, "code: $code")
when (code) {
CODE_VIDEOS -> {
// Get the Room Cursor
val cursor = videoDao.loadAllAsCursor()
Log.i(TAG, "cursor: $cursor")

cursor.setNotificationUri(context.contentResolver, uri)

return cursor
}
CODE_VIDEO_ID -> {
// Extract the Video ID from the URI
val pathSegments = uri.pathSegments
Log.i(TAG, "pathSegments: $pathSegments")
val videoIdAsString = pathSegments[1]
val videoId = videoIdAsString.toLong()
Log.i(TAG, "videoId: $videoId")

// Get the Room Cursor
val cursor = videoDao.loadAsCursor(videoId)
Log.i(TAG, "cursor: $cursor")

cursor.setNotificationUri(context.contentResolver, uri)

return cursor
}
CODE_VIDEO_TITLE -> {
// Extract the transcription from the URI
val pathSegments = uri.pathSegments
Log.i(TAG, "pathSegments: $pathSegments")
val title = pathSegments[2]
Log.i(TAG, "title: \"$title\"")

// Get the Room Cursor
val cursor = videoDao.loadByTitleAsCursor(title)
Log.i(TAG, "cursor: $cursor")

cursor.setNotificationUri(context.contentResolver, uri)

return cursor
}
else -> {
throw IllegalArgumentException("Unknown URI: $uri")
}
}
}

/**
* Handles requests for the MIME type of the data at the given URI.
*/
override fun getType(uri: Uri): String? {
Log.i(TAG, "getType")

throw UnsupportedOperationException("Not yet implemented")
}

/**
* Handles requests to insert a new row.
*/
override fun insert(uri: Uri, values: ContentValues?): Uri? {
Log.i(TAG, "insert")

throw UnsupportedOperationException("Not yet implemented")
}

/**
* Handles requests to update one or more rows.
*/
override fun update(
uri: Uri,
values: ContentValues?,
selection: String?,
selectionArgs: Array<String>?
): Int {
Log.i(TAG, "update")

throw UnsupportedOperationException("Not yet implemented")
}

/**
* Handle requests to delete one or more rows.
*/
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int {
Log.i(TAG, "delete")

throw UnsupportedOperationException("Not yet implemented")
}

@Throws(FileNotFoundException::class)
override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor? {
val segments = uri.pathSegments
if (segments.size < 2) {
throw FileNotFoundException("Invalid URI: $uri")
}
val fileId = segments[1]

val roomDb = RoomDb.getDatabase(context)
val videoDao = roomDb.videoDao()
val video = videoDao.load(fileId.toLong())

val videoFile = FileHelper.getVideoFile(video, context)
?: throw FileNotFoundException("File not found!")
if (!videoFile.exists()) {
Log.e("VideoContentProvider", "videoFile doesn't exist: " + videoFile.absolutePath)
throw FileNotFoundException("File not found: " + videoFile.absolutePath)
}
return ParcelFileDescriptor.open(videoFile, ParcelFileDescriptor.MODE_READ_ONLY)
}

companion object {
private const val AUTHORITY: String = BuildConfig.APPLICATION_ID + ".provider.video_provider"

private const val TABLE_VIDEOS = "videos"
private const val CODE_VIDEOS = 1
private const val CODE_VIDEO_ID = 2
private const val CODE_VIDEO_TITLE = 4

private val MATCHER = UriMatcher(UriMatcher.NO_MATCH)

init {
MATCHER.addURI(AUTHORITY, TABLE_VIDEOS, CODE_VIDEOS)
MATCHER.addURI(AUTHORITY, TABLE_VIDEOS + "/#", CODE_VIDEO_ID)
MATCHER.addURI(AUTHORITY, TABLE_VIDEOS + "/by-title/*", CODE_VIDEO_TITLE)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ public interface VideoDao {
// @Query("SELECT * FROM Video i WHERE i.id IN (SELECT Video_id FROM Video_Word WHERE words_id = :wordId)")
// Cursor loadAllByWordLabelAsCursor(Long wordId);

@Query("SELECT * FROM Video WHERE title = :title")
Cursor loadByTitleAsCursor(String title);

@Update
void update(Video video);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import java.io.File;

import ai.elimu.content_provider.room.entity.Image;
import ai.elimu.content_provider.room.entity.Video;
import ai.elimu.model.v2.gson.content.ImageGson;
import ai.elimu.model.v2.gson.content.VideoGson;

Expand Down Expand Up @@ -45,4 +46,15 @@ public static File getVideoFile(VideoGson videoGson, Context context) {
File file = new File(videosDirectory, videoGson.getId() + "_r" + videoGson.getRevisionNumber() + "." + videoGson.getVideoFormat().toString().toLowerCase());
return file;
}

@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
public static File getVideoFile(Video videoGson, Context context) {
if ((videoGson.getId() == null) || (videoGson.getRevisionNumber() == null)) {
return null;
}
File videosDirectory = context.getExternalFilesDir(Environment.DIRECTORY_MOVIES);
return new File(videosDirectory, videoGson.getId()
+ "_r" + videoGson.getRevisionNumber() + "."
+ videoGson.getVideoFormat().toString().toLowerCase());
}
}
4 changes: 4 additions & 0 deletions app/src/main/res/xml/video_file_path.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<paths>
<external-files-path name="movies" path="Movies/" />
</paths>
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import ai.elimu.content_provider.utils.converter.CursorToLetterGsonConverter
import ai.elimu.content_provider.utils.converter.CursorToLetterSoundGsonConverter
import ai.elimu.content_provider.utils.converter.CursorToStoryBookChapterGsonConverter
import ai.elimu.content_provider.utils.converter.CursorToStoryBookGsonConverter
import ai.elimu.content_provider.utils.converter.CursorToVideoGsonConverter
import ai.elimu.content_provider.utils.converter.CursorToWordGsonConverter
import ai.elimu.model.v2.gson.content.EmojiGson
import ai.elimu.model.v2.gson.content.ImageGson
Expand All @@ -16,11 +17,14 @@ import ai.elimu.model.v2.gson.content.LetterSoundGson
import ai.elimu.model.v2.gson.content.StoryBookChapterGson
import ai.elimu.model.v2.gson.content.StoryBookGson
import ai.elimu.model.v2.gson.content.StoryBookParagraphGson
import ai.elimu.model.v2.gson.content.VideoGson
import ai.elimu.model.v2.gson.content.WordGson
import android.content.Context
import android.net.Uri
import android.util.Log
import android.widget.Toast
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext

object ContentProviderUtil {

Expand Down Expand Up @@ -586,4 +590,59 @@ object ContentProviderUtil {

return storyBookChapterGsons
}

suspend fun getAllVideoGSONs(
context: Context,
contentProviderApplicationId: String
): List<VideoGson> {
Log.i(ContentProviderUtil::class.java.name, "getAllVideoGsons")

val videoGsons: MutableList<VideoGson> = ArrayList()

val videosUri =
Uri.parse("content://$contentProviderApplicationId.provider.video_provider/videos")
Log.i(
ContentProviderUtil::class.java.name,
"videosUri: $videosUri"
)
val videosCursor = context.contentResolver.query(videosUri, null, null, null, null)
Log.i(
ContentProviderUtil::class.java.name,
"videosCursor: $videosCursor"
)
if (videosCursor == null) {
Log.e(ContentProviderUtil::class.java.name, "videosCursor == null")
withContext(Dispatchers.Main) {
Toast.makeText(context, "videosCursor == null", Toast.LENGTH_LONG).show()
}
} else {
Log.i(
ContentProviderUtil::class.java.name,
"videosCursor.getCount(): " + videosCursor.count
)
if (videosCursor.count == 0) {
Log.e(ContentProviderUtil::class.java.name, "videosCursor.getCount() == 0")
} else {
var isLast = false
while (!isLast) {
videosCursor.moveToNext()

// Convert from Room to Gson
val videoGson = CursorToVideoGsonConverter.getVideoGson(videosCursor)
videoGsons.add(videoGson)

isLast = videosCursor.isLast
}

videosCursor.close()
Log.i(
ContentProviderUtil::class.java.name,
"videosCursor.isClosed(): " + videosCursor.isClosed
)
}
}
Log.i(ContentProviderUtil::class.java.name, "videoGsons.size(): " + videoGsons.size)

return videoGsons
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package ai.elimu.content_provider.utils.converter

import ai.elimu.model.v2.enums.content.VideoFormat
import ai.elimu.model.v2.gson.content.VideoGson
import android.database.Cursor
import android.util.Log

object CursorToVideoGsonConverter {
private val TAG: String = CursorToVideoGsonConverter::class.java.name

fun getVideoGson(cursor: Cursor): VideoGson {
Log.i(TAG, "getVideoGson")
Log.i(TAG, "Arrays.toString(cursor.getColumnNames()): "
+ cursor.columnNames.contentToString())
if (cursor.isBeforeFirst && !cursor.moveToFirst()) {
throw IllegalArgumentException("Cursor must be positioned on a valid row")
}
val columnId = cursor.getColumnIndex("id")
if (columnId == -1) {
throw IllegalArgumentException("Column 'id' not found in cursor")
}
val id = cursor.getLong(columnId)
Log.i(TAG, "id: $id")

val columnRevisionNumber = cursor.getColumnIndex("revisionNumber")
if (columnRevisionNumber == -1) {
throw IllegalArgumentException("Column 'revisionNumber' not found in cursor")
}
val revisionNumber = cursor.getInt(columnRevisionNumber)
Log.i(TAG, "revisionNumber: $revisionNumber")

val columnTitle = cursor.getColumnIndex("title")
if (columnTitle == -1) {
throw IllegalArgumentException("Column 'title' not found in cursor")
}
val title = cursor.getString(columnTitle)
Log.i(TAG, "title: \"$title\"")

val columnVideoFormat = cursor.getColumnIndex("videoFormat")
if (columnVideoFormat == -1) {
throw IllegalArgumentException("Column 'videoFormat' not found in cursor")
}
val videoFormatAsString = cursor.getString(columnVideoFormat)
Log.i(TAG, "videoFormatAsString: $videoFormatAsString")
val videoFormat = try {
VideoFormat.valueOf(videoFormatAsString)
} catch (e: IllegalArgumentException) {
Log.e(TAG, "Invalid video format: $videoFormatAsString", e)
throw IllegalArgumentException("Invalid video format: $videoFormatAsString", e)
}
Log.i(TAG, "videoFormat: $videoFormat")
val video = VideoGson()
video.id = id
video.revisionNumber = revisionNumber
video.title = title
video.videoFormat = videoFormat
return video
}
}