Selaa lähdekoodia

performance improvements

Milan Jurkulak 3 vuotta sitten
vanhempi
sitoutus
362a08b421

+ 26 - 4
app/src/main/kotlin/org/mjdev/tvapp/base/extensions/CursorExt.kt

@@ -13,6 +13,7 @@ import androidx.core.database.getBlobOrNull
 import androidx.core.database.getFloatOrNull
 import androidx.core.database.getIntOrNull
 import androidx.core.database.getStringOrNull
+import org.mjdev.tvapp.base.helpers.cursor.CachingCursor
 import timber.log.Timber
 
 object CursorExt {
@@ -20,21 +21,28 @@ object CursorExt {
     val Cursor.isNotEmpty get() = count > 0
 
     fun Cursor.asMap(
-        projection: Array<String>
+        projection: Array<String> = columnNames
     ): Map<String, Any?> = mutableMapOf<String, Any?>().apply {
         projection.forEach { cname ->
             val idx = getColumnIndex(cname)
             val value = try {
-                this@asMap.get(idx)
+                getColumn(idx)
             } catch (e: Exception) {
                 Timber.e(e)
-                null
+                moveToFirst()
+                try {
+                    getColumn(idx)
+                } catch (e: Exception) {
+                    Timber.e(e)
+                    null
+                }
             }
             this[cname] = value
         }
     }
 
-    fun Cursor.get(idx: Int): Any? = when (getType(idx)) {
+    @Suppress("MemberVisibilityCanBePrivate")
+    fun Cursor.getColumn(idx: Int): Any? = when (getType(idx)) {
         Cursor.FIELD_TYPE_FLOAT -> getFloatOrNull(idx)
         Cursor.FIELD_TYPE_BLOB -> getBlobOrNull(idx)
         Cursor.FIELD_TYPE_INTEGER -> getIntOrNull(idx)
@@ -42,4 +50,18 @@ object CursorExt {
         else -> null
     }
 
+    fun Cursor.getData(idx: Int): Any? {
+        return if (this is CachingCursor)
+            getData(idx)
+        else {
+            try {
+                moveToPosition(idx)
+                asMap()
+            } catch (e: Exception) {
+                Timber.e(e)
+                null
+            }
+        }
+    }
+
 }

+ 16 - 10
app/src/main/kotlin/org/mjdev/tvapp/base/helpers/apps/AppsManager.kt

@@ -25,22 +25,28 @@ import java.util.Collections
 fun rememberAppsManager(): List<App> {
     val context: Context = LocalContext.current
     return remember {
+        AppsManager(context)
+    }
+}
+
+class AppsManager(
+    context: Context
+) : ArrayList<App>() {
+    init {
         val packageManager = context.packageManager
         val mainIntent = Intent(Intent.ACTION_MAIN, null).apply {
             addCategory(Intent.CATEGORY_LAUNCHER)
         }
         val resolveInfo = packageManager.queryIntentActivities(mainIntent, 0)
         Collections.sort(resolveInfo, ResolveInfo.DisplayNameComparator(packageManager))
-        mutableListOf<App>().apply {
-            resolveInfo.forEach { ri ->
-                val title = ri.activityInfo.loadLabel(packageManager).toString()
-                val image = ri.activityInfo
-                    .loadBanner(packageManager) ?: ri.activityInfo
-                    .loadIcon(packageManager)
-                val intent: Intent? =
-                    packageManager.getLaunchIntentForPackage(ri.activityInfo.packageName)
-                add(App(title, image, intent))
-            }
+        resolveInfo.forEach { ri ->
+            val title = ri.activityInfo.loadLabel(packageManager).toString()
+            val image = ri.activityInfo
+                .loadBanner(packageManager) ?: ri.activityInfo
+                .loadIcon(packageManager)
+            val intent: Intent? =
+                packageManager.getLaunchIntentForPackage(ri.activityInfo.packageName)
+            add(App(title, image, intent))
         }
     }
 }

+ 22 - 0
app/src/main/kotlin/org/mjdev/tvapp/base/helpers/cursor/AudioCursor.kt

@@ -0,0 +1,22 @@
+/*
+ * Copyright (c) Milan Jurkulák 2023.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package org.mjdev.tvapp.base.helpers.cursor
+
+import android.content.ContentResolver
+import android.content.Context
+
+class AudioCursor(
+    context: Context,
+    val contentResolver: ContentResolver = context.contentResolver
+) : CachingCursor(
+    context,
+    AudioItem.URI,
+    AudioItem.MEDIA_PROJECTION,
+    transform = { AudioItem(contentResolver, it) }
+)

+ 334 - 0
app/src/main/kotlin/org/mjdev/tvapp/base/helpers/cursor/CachingCursor.kt

@@ -0,0 +1,334 @@
+/*
+ * Copyright (c) Milan Jurkulák 2023.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+@file:Suppress("unused")
+
+package org.mjdev.tvapp.base.helpers.cursor
+
+import android.annotation.SuppressLint
+import android.content.ContentResolver
+import android.content.Context
+import android.database.CharArrayBuffer
+import android.database.ContentObserver
+import android.database.Cursor
+import android.database.DataSetObserver
+import android.net.Uri
+import android.os.Build
+import android.os.Bundle
+import androidx.annotation.RequiresApi
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.platform.LocalContext
+import org.mjdev.tvapp.base.extensions.CursorExt.isNotEmpty
+import timber.log.Timber
+
+@Suppress("FunctionName")
+open class CachingCursor(
+    context: Context,
+    uri: Uri?,
+    private val projection: Array<String>,
+    selection: String? = null,
+    selectionArgs: Array<String>? = null,
+    sortOrder: String? = null,
+    private val prefetchItems: Int = 8,
+    private val cacheItems: Int = 256,
+    private val transform: (Cursor) -> Any? = { it },
+) : Cursor {
+
+    private val resolver: ContentResolver by lazy {
+        context.contentResolver
+    }
+    private val observer = object : ContentObserver(null) {
+        @Suppress("DEPRECATION")
+        override fun onChange(selfChange: Boolean) {
+            cursor?.requery()
+        }
+    }
+    private val cache: MutableList<CachedCursorItem> = mutableListOf()
+    private val cursor: Cursor? by lazy {
+        if (uri != null) {
+            try {
+                resolver.query(
+                    uri, projection, selection, selectionArgs, sortOrder
+                )?.apply {
+                    registerContentObserver(observer)
+                    if (this.isNotEmpty) {
+                        moveToFirst()
+                    }
+                }
+            } catch (e: Exception) {
+                Timber.e(e)
+                null
+            }
+        } else null
+    }
+
+    val isNotEmpty: Boolean get() = count > 0
+
+    init {
+        (0..prefetchItems).forEach { idx -> getData(idx) }
+    }
+
+    private fun _get(idx: Int): Any? {
+        var result: Any? = null
+        if (isCached(idx)) {
+            result = getFromCache(idx)
+        } else {
+            try {
+                if (count >= idx) {
+                    cursor?.moveToPosition(idx)
+                    result = cursor?.let { c -> transform(c) }
+                }
+            } catch (e: Exception) {
+                Timber.e(e)
+            }
+            if (result != null) {
+                insertToCache(idx, result)
+            }
+        }
+        return result
+    }
+
+    fun getData(idx: Int): Any? {
+        (idx - prefetchItems..idx + prefetchItems).forEach { i -> _get(i) }
+        return _get(idx)
+    }
+
+    private fun insertToCache(idx: Int, value: Any) {
+        if (cache.size > cacheItems) {
+            cache.minByOrNull { item ->
+                item.cached
+            }?.let { item ->
+                cache.remove(item)
+            }
+        }
+        cache.add(CachedCursorItem(idx, System.currentTimeMillis(), value))
+    }
+
+    private fun getFromCache(idx: Int): Any? {
+        return cache.firstOrNull { item ->
+            item.idx == idx
+        }?.data
+    }
+
+    private fun isCached(idx: Int): Boolean {
+        return cache.count { item ->
+            item.idx == idx
+        } > 0
+    }
+
+    override fun close() {
+        cache.clear()
+        cursor?.unregisterContentObserver(observer)
+        cursor?.close()
+    }
+
+    override fun getCount(): Int =
+        cursor?.count ?: 0
+
+    override fun getPosition(): Int =
+        cursor?.position ?: -1
+
+    override fun move(idx: Int): Boolean =
+        cursor?.move(idx) ?: false
+
+    override fun moveToPosition(idx: Int): Boolean =
+        cursor?.moveToPosition(idx) ?: false
+
+    override fun moveToFirst(): Boolean =
+        cursor?.moveToFirst() ?: false
+
+    override fun moveToLast(): Boolean =
+        cursor?.moveToLast() ?: false
+
+    override fun moveToNext(): Boolean =
+        cursor?.moveToNext() ?: false
+
+    override fun moveToPrevious(): Boolean =
+        cursor?.moveToPrevious() ?: false
+
+    override fun isFirst(): Boolean =
+        cursor?.isFirst ?: false
+
+    override fun isLast(): Boolean =
+        cursor?.isLast ?: true
+
+    override fun isBeforeFirst(): Boolean =
+        cursor?.isBeforeFirst ?: false
+
+    override fun isAfterLast(): Boolean =
+        cursor?.isAfterLast ?: true
+
+    override fun getColumnIndex(name: String?): Int =
+        cursor?.getColumnIndex(name) ?: -1
+
+    override fun getColumnIndexOrThrow(name: String?): Int =
+        cursor?.getColumnIndexOrThrow(name) ?: -1
+
+    override fun getColumnName(idx: Int): String? =
+        cursor?.getColumnName(idx)
+
+    override fun getColumnNames(): Array<String>? =
+        cursor?.columnNames
+
+    override fun getColumnCount(): Int =
+        cursor?.columnCount ?: 0
+
+    override fun getBlob(idx: Int): ByteArray =
+        getData(idx) as ByteArray
+
+    override fun getString(idx: Int): String =
+        getData(idx).toString()
+
+    override fun copyStringToBuffer(idx: Int, text: CharArrayBuffer?) {
+        cursor?.copyStringToBuffer(idx, text)
+    }
+
+    override fun getShort(idx: Int): Short =
+        getData(idx) as Short
+
+    override fun getInt(idx: Int): Int =
+        getData(idx) as Int
+
+    override fun getLong(idx: Int): Long =
+        getData(idx) as Long
+
+    override fun getFloat(idx: Int): Float =
+        getData(idx) as Float
+
+    override fun getDouble(idx: Int): Double =
+        getData(idx) as Double
+
+    override fun getType(idx: Int): Int =
+        try {
+            cursor?.getType(idx)
+        } catch (e: Exception) {
+            Timber.e(e)
+            moveToFirst()
+            try {
+                cursor?.getType(idx)
+            } catch (e: Exception) {
+                Timber.e(e)
+                null
+            }
+        } ?: -1
+
+    override fun isNull(idx: Int): Boolean =
+        cursor?.isNull(idx) ?: true
+
+    @Suppress("DEPRECATION")
+    @Deprecated("Deprecated in Java")
+    override fun deactivate() {
+        cursor?.deactivate()
+    }
+
+    @Suppress("DEPRECATION")
+    @Deprecated("Deprecated in Java")
+    override fun requery(): Boolean {
+        cache.clear()
+        val result = cursor?.requery() ?: false
+        if (result) {
+            (0..prefetchItems).forEach { idx -> getData(idx) }
+        }
+        return result
+    }
+
+    override fun isClosed(): Boolean =
+        cursor?.isClosed ?: true
+
+    override fun registerContentObserver(observer: ContentObserver?) {
+        cursor?.registerContentObserver(observer)
+    }
+
+    override fun unregisterContentObserver(observer: ContentObserver?) {
+        cursor?.unregisterContentObserver(observer)
+    }
+
+    override fun registerDataSetObserver(observer: DataSetObserver?) {
+        cursor?.registerDataSetObserver(observer)
+    }
+
+    override fun unregisterDataSetObserver(observer: DataSetObserver?) {
+        cursor?.unregisterDataSetObserver(observer)
+    }
+
+    override fun setNotificationUri(cr: ContentResolver?, uri: Uri?) {
+        cursor?.setNotificationUri(cr, uri)
+    }
+
+    override fun getNotificationUri(): Uri? =
+        cursor?.notificationUri
+
+    override fun getWantsAllOnMoveCalls(): Boolean =
+        cursor?.wantsAllOnMoveCalls ?: false
+
+    @RequiresApi(Build.VERSION_CODES.M)
+    override fun setExtras(extras: Bundle?) {
+        cursor?.extras = extras
+    }
+
+    override fun getExtras(): Bundle? =
+        cursor?.extras
+
+    override fun respond(b: Bundle?): Bundle? =
+        cursor?.respond(b)
+
+    data class CachedCursorItem(
+        val idx: Int,
+        val cached: Long,
+        val data: Any?
+    )
+
+    companion object {
+
+        @Deprecated("Please for better performance use viewmodel with cursors.")
+        @SuppressLint("Recycle")
+        @Composable
+        fun rememberCursor(
+            uri: Uri?,
+            projection: Array<String>,
+            selection: String? = null,
+            selectionArgs: Array<String>? = null,
+            sortOrder: String? = null,
+            prefetchItems: Int = 8,
+            cacheItems: Int = 256,
+            transform: (Cursor) -> Any?,
+        ): CachingCursor? {
+            if (uri == null) return null
+            else {
+                val context = LocalContext.current
+                var cursor by remember {
+                    mutableStateOf<CachingCursor?>(null)
+                }
+                DisposableEffect(uri) {
+                    cursor = CachingCursor(
+                        context,
+                        uri,
+                        projection,
+                        selection,
+                        selectionArgs,
+                        sortOrder,
+                        prefetchItems,
+                        cacheItems,
+                        transform,
+                    )
+                    onDispose {
+                        cursor?.close()
+                    }
+                }
+                return cursor
+            }
+        }
+
+    }
+
+}

+ 0 - 170
app/src/main/kotlin/org/mjdev/tvapp/base/helpers/cursor/Cursor.kt

@@ -1,170 +0,0 @@
-/*
- * Copyright (c) Milan Jurkulák 2023.
- *  Contact:
- *  e: mimoccc@gmail.com
- *  e: mj@mjdev.org
- *  w: https://mjdev.org
- */
-
-@file:Suppress("unused")
-
-package org.mjdev.tvapp.base.helpers.cursor
-
-import android.annotation.SuppressLint
-import android.content.ContentResolver
-import android.content.Context
-import android.database.ContentObserver
-import android.database.Cursor
-import android.net.Uri
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.DisposableEffect
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.platform.LocalContext
-import org.mjdev.tvapp.base.extensions.CursorExt.isNotEmpty
-import timber.log.Timber
-
-@SuppressLint("Recycle")
-@Composable
-fun rememberCursor(
-    uri: Uri?,
-    projection: Array<String>,
-    selection: String? = null,
-    selectionArgs: Array<String>? = null,
-    sortOrder: String? = null,
-    prefetchItems: Int = 8,
-    cacheItems: Int = 256,
-    transform: (Cursor) -> Any?,
-): PrefetchCursor? {
-    if (uri == null) return null
-    else {
-        val context = LocalContext.current
-        var cursor by remember {
-            mutableStateOf<PrefetchCursor?>(null)
-        }
-        DisposableEffect(uri) {
-            cursor = PrefetchCursor(
-                context,
-                uri,
-                projection,
-                selection,
-                selectionArgs,
-                sortOrder,
-                prefetchItems,
-                cacheItems,
-                transform,
-            )
-            onDispose {
-                cursor?.close()
-            }
-        }
-        return cursor
-    }
-}
-
-@Suppress("FunctionName")
-class PrefetchCursor(
-    context: Context,
-    uri: Uri?,
-    private val projection: Array<String>,
-    selection: String? = null,
-    selectionArgs: Array<String>? = null,
-    sortOrder: String? = null,
-    private val prefetchItems: Int = 8,
-    private val cacheItems: Int = 256,
-    private val transform: (Cursor) -> Any? = { it },
-) {
-
-    private val resolver: ContentResolver by lazy {
-        context.contentResolver
-    }
-    private val observer = object : ContentObserver(null) {
-        @Suppress("DEPRECATION")
-        override fun onChange(selfChange: Boolean) {
-            cursor?.requery()
-        }
-    }
-    private val cache: MutableList<Pair<Index, Any?>> = mutableListOf()
-    private val cursor: Cursor? by lazy {
-        if (uri != null) {
-            try {
-                resolver.query(
-                    uri, projection, selection, selectionArgs, sortOrder
-                )?.apply {
-                    registerContentObserver(observer)
-                    if (this.isNotEmpty) {
-                        moveToFirst()
-                    }
-                }
-            } catch (e: Exception) {
-                Timber.e(e)
-                null
-            }
-        } else null
-    }
-
-    val count: Int get() = cursor?.count ?: 0
-    val isNotEmpty: Boolean get() = count > 0
-
-    init {
-        (0..prefetchItems).forEach { idx -> get(idx) }
-    }
-
-    private fun _get(idx: Int): Any? {
-        var result: Any? = null
-        if (isCached(idx)) {
-            result = getFromCache(idx)
-        } else {
-            try {
-                if (count >= idx) {
-                    cursor?.moveToPosition(idx)
-                    result = cursor?.let { c -> transform(c) }
-                }
-            } catch (e: Exception) {
-                Timber.e(e)
-            }
-            if (result != null) {
-                insertToCache(idx, result)
-            }
-        }
-        return result
-    }
-
-    fun get(idx: Int): Any? {
-        (idx - prefetchItems..idx + prefetchItems).forEach { i -> _get(i) }
-        return _get(idx)
-    }
-
-    private fun insertToCache(idx: Int, value: Any) {
-        if (cache.size > cacheItems) {
-            cache.minByOrNull { item ->
-                item.first.cached
-            }?.let { item ->
-                cache.remove(item)
-            }
-        }
-        cache.add(Pair(Index(idx, System.currentTimeMillis()), value))
-    }
-
-    private fun getFromCache(idx: Int): Any? {
-        return cache.firstOrNull { item ->
-            item.first.idx == idx
-        }?.second
-    }
-
-    private fun isCached(idx: Int): Boolean {
-        return cache.count { item ->
-            item.first.idx == idx
-        } > 0
-    }
-
-    fun close() {
-        cursor?.unregisterContentObserver(observer)
-        cursor?.close()
-    }
-
-    data class Index(val idx: Int, val cached: Long)
-
-}

+ 22 - 0
app/src/main/kotlin/org/mjdev/tvapp/base/helpers/cursor/PhotoCursor.kt

@@ -0,0 +1,22 @@
+/*
+ * Copyright (c) Milan Jurkulák 2023.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package org.mjdev.tvapp.base.helpers.cursor
+
+import android.content.ContentResolver
+import android.content.Context
+
+class PhotoCursor(
+    context: Context,
+    val contentResolver: ContentResolver = context.contentResolver
+) : CachingCursor(
+    context,
+    PhotoItem.URI,
+    PhotoItem.MEDIA_PROJECTION,
+    transform = { PhotoItem(contentResolver, it) }
+)

+ 3 - 0
app/src/main/kotlin/org/mjdev/tvapp/base/helpers/cursor/PhotoItem.kt

@@ -8,6 +8,7 @@
 
 package org.mjdev.tvapp.base.helpers.cursor
 
+import android.content.ContentResolver
 import android.database.Cursor
 import android.net.Uri
 import android.provider.MediaStore
@@ -15,7 +16,9 @@ import org.mjdev.tvapp.base.extensions.CursorExt.asMap
 import org.mjdev.tvapp.base.interfaces.ItemWithImage
 import org.mjdev.tvapp.base.interfaces.ItemWithTitle
 
+@Suppress("UNUSED_PARAMETER")
 class PhotoItem(
+    contentResolver: ContentResolver,
     c: Cursor
 ) : HashMap<String, Any?>(), ItemWithTitle<String>, ItemWithImage<Any> {
 

+ 22 - 0
app/src/main/kotlin/org/mjdev/tvapp/base/helpers/cursor/VideoCursor.kt

@@ -0,0 +1,22 @@
+/*
+ * Copyright (c) Milan Jurkulák 2023.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package org.mjdev.tvapp.base.helpers.cursor
+
+import android.content.ContentResolver
+import android.content.Context
+
+class VideoCursor(
+    context: Context,
+    val contentResolver: ContentResolver = context.contentResolver
+) : CachingCursor(
+    context,
+    VideoItem.URI,
+    VideoItem.MEDIA_PROJECTION,
+    transform = { VideoItem(contentResolver, it) }
+)

+ 1 - 1
app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/tv/AppsRow.kt

@@ -36,9 +36,9 @@ fun AppsRow(
     startApp: Context.(app: App?) -> Unit = { app ->
         startActivity(app?.intent)
     },
+    apps : List<App> = rememberAppsManager()
 ) {
     val context: Context = LocalContext.current
-    val apps = rememberAppsManager()
     val cardWidth = computeCardWidth(3f)
     if (apps.isNotEmpty()) {
         CategoryRow(

+ 2 - 22
app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/tv/BrowseView.kt

@@ -38,10 +38,6 @@ fun BrowseView(
     modifier: Modifier = Modifier,
     showHeader: Boolean = true,
     showNetworkState: Boolean = true,
-    showApps: Boolean = true,
-    showLocalAudio: Boolean = true,
-    showLocalVideo: Boolean = true,
-    showLocalPhotos: Boolean = true,
     title: Any? = "test",
     messages: List<Any?> = listOf(Unit, Unit, Unit),
     categories: List<Any?> = listOf(Unit, Unit, Unit),
@@ -58,6 +54,7 @@ fun BrowseView(
     backgroundShape: Shape = RoundedCornerShape(roundCornerSize),
     verticalArrangement: Arrangement.Vertical = Arrangement.spacedBy(32.dp),
     contentPadding: PaddingValues = PaddingValues(horizontal = 16.dp, vertical = 8.dp),
+    customRows: List<@Composable () -> Unit> = emptyList(),
     onTitleClicked: () -> Unit = {},
     onClockClicked: () -> Unit = {},
     onMessageBadgeClicked: () -> Unit = {},
@@ -113,24 +110,7 @@ fun BrowseView(
                 onItemClicked = onItemClicked
             )
         }
-        if (showApps) item {
-            AppsRow()
-        }
-        if (showLocalAudio) item {
-            LocalAudioRow(
-                openItem = { item -> onItemClicked(item) }
-            )
-        }
-        if (showLocalVideo) item {
-            LocalVideoRow(
-                openItem = { item -> onItemClicked(item) }
-            )
-        }
-        if (showLocalPhotos) item {
-            LocalPhotosRow(
-                openItem = { item -> onItemClicked(item) }
-            )
-        }
+        items(customRows) { row -> row.invoke() }
         items(categoriesAndItemsMap.map { entry ->
             Pair(entry.key, entry.value)
         }) { entry ->

+ 10 - 7
app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/tv/CursorRow.kt

@@ -37,11 +37,14 @@ import androidx.tv.foundation.lazy.list.rememberTvLazyListState
 import androidx.tv.material3.ExperimentalTvMaterial3Api
 import org.mjdev.tvapp.base.annotations.TvPreview
 import org.mjdev.tvapp.base.extensions.ComposeExt.computeCardWidth
+import org.mjdev.tvapp.base.extensions.CursorExt.getData
+import org.mjdev.tvapp.base.extensions.CursorExt.isNotEmpty
 import org.mjdev.tvapp.base.extensions.ModifierExt.recomposeHighlighter
-import org.mjdev.tvapp.base.helpers.cursor.rememberCursor
+import org.mjdev.tvapp.base.helpers.cursor.CachingCursor.Companion.rememberCursor
 import org.mjdev.tvapp.base.ui.components.card.PhotoCard
 import org.mjdev.tvapp.base.ui.components.text.TextAny
 
+@Suppress("DEPRECATION")
 @SuppressLint("AutoboxingStateValueProperty")
 @OptIn(ExperimentalTvMaterial3Api::class)
 @TvPreview
@@ -59,17 +62,17 @@ fun CursorRow(
     selectionArgs: Array<String>? = null,
     sortOrder: String? = null,
     transform: (Cursor) -> Any? = { it },
-    openItem: Context.(item: Any?) -> Unit = {},
-) {
-    val context: Context = LocalContext.current
-    val cursor = rememberCursor(
+    cursor: Cursor? = rememberCursor(
         uri = uri,
         projection = projection,
         selection = selection,
         selectionArgs = selectionArgs,
         sortOrder = sortOrder,
         transform = transform
-    )
+    ),
+    openItem: Context.(item: Any?) -> Unit = {},
+) {
+    val context: Context = LocalContext.current
     val scrollDelta = remember { mutableFloatStateOf(0f) }
     if (cursor?.isNotEmpty == true) {
         Column(
@@ -105,7 +108,7 @@ fun CursorRow(
                     key = { idx -> idx }
                 ) { idx ->
                     PhotoCard(
-                        item = cursor.get(idx),
+                        item = cursor.getData(idx),
                         contentScale = contentScale,
                         cardWidth = cardWidth,
                         onClick = { i ->

+ 25 - 14
app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/tv/LocalAudioRow.kt

@@ -8,7 +8,9 @@
 
 package org.mjdev.tvapp.base.ui.components.tv
 
+import android.content.ContentResolver
 import android.content.Context
+import android.database.Cursor
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.layout.ContentScale
@@ -21,7 +23,9 @@ import org.mjdev.tvapp.base.annotations.TvPreview
 import org.mjdev.tvapp.base.extensions.ComposeExt.computeCardWidth
 import org.mjdev.tvapp.base.extensions.ComposeExt.rememberContentResolver
 import org.mjdev.tvapp.base.helpers.cursor.AudioItem
+import org.mjdev.tvapp.base.helpers.cursor.CachingCursor.Companion.rememberCursor
 
+@Suppress("DEPRECATION")
 @TvPreview
 @Composable
 fun LocalAudioRow(
@@ -34,22 +38,29 @@ fun LocalAudioRow(
     sortOrder: String? = null,
     cardWidth: Dp = computeCardWidth(),
     contentScale: ContentScale = ContentScale.Crop,
-    openItem: Context.(item: Any?) -> Unit = {},
-) {
-    val contentResolver = rememberContentResolver()
-    CursorRow(
-        title = title,
-        rowState = rowState,
-        padding = padding,
-        backgroundColor = backgroundColor,
-        cardWidth = cardWidth,
-        contentScale = contentScale,
+    contentResolver: ContentResolver = rememberContentResolver(),
+    transform: (Cursor) -> Any? = { c -> AudioItem(contentResolver, c) },
+    cursor: Cursor? = rememberCursor(
         uri = AudioItem.URI,
         projection = AudioItem.MEDIA_PROJECTION,
         selection = selection,
         selectionArgs = selectionArgs,
         sortOrder = sortOrder,
-        transform = { c -> AudioItem(contentResolver, c) },
-        openItem = openItem,
-    )
-}
+        transform = transform
+    ),
+    openItem: Context.(item: Any?) -> Unit = {},
+) = CursorRow(
+    title = title,
+    rowState = rowState,
+    padding = padding,
+    backgroundColor = backgroundColor,
+    cardWidth = cardWidth,
+    contentScale = contentScale,
+    uri = AudioItem.URI,
+    projection = AudioItem.MEDIA_PROJECTION,
+    selection = selection,
+    selectionArgs = selectionArgs,
+    sortOrder = sortOrder,
+    transform = transform,
+    openItem = openItem,
+)

+ 16 - 1
app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/tv/LocalPhotosRow.kt

@@ -8,7 +8,9 @@
 
 package org.mjdev.tvapp.base.ui.components.tv
 
+import android.content.ContentResolver
 import android.content.Context
+import android.database.Cursor
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.layout.ContentScale
@@ -19,8 +21,11 @@ import androidx.tv.foundation.lazy.list.rememberTvLazyListState
 import org.mjdev.tvapp.R
 import org.mjdev.tvapp.base.annotations.TvPreview
 import org.mjdev.tvapp.base.extensions.ComposeExt.computeCardWidth
+import org.mjdev.tvapp.base.extensions.ComposeExt.rememberContentResolver
+import org.mjdev.tvapp.base.helpers.cursor.CachingCursor.Companion.rememberCursor
 import org.mjdev.tvapp.base.helpers.cursor.PhotoItem
 
+@Suppress("DEPRECATION")
 @TvPreview
 @Composable
 fun LocalPhotosRow(
@@ -33,6 +38,16 @@ fun LocalPhotosRow(
     sortOrder: String? = null,
     cardWidth: Dp = computeCardWidth(),
     contentScale: ContentScale = ContentScale.Crop,
+    contentResolver: ContentResolver = rememberContentResolver(),
+    transform: (Cursor) -> Any? = { c -> PhotoItem(contentResolver, c) },
+    cursor: Cursor? = rememberCursor(
+        uri = PhotoItem.URI,
+        projection = PhotoItem.MEDIA_PROJECTION,
+        selection = selection,
+        selectionArgs = selectionArgs,
+        sortOrder = sortOrder,
+        transform = transform
+    ),
     openItem: Context.(item: Any?) -> Unit = {},
 ) = CursorRow(
     title = title,
@@ -46,6 +61,6 @@ fun LocalPhotosRow(
     selection = selection,
     selectionArgs = selectionArgs,
     sortOrder = sortOrder,
-    transform = { c -> PhotoItem(c) },
+    transform = transform,
     openItem = openItem,
 )

+ 14 - 1
app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/tv/LocalVideoRow.kt

@@ -8,7 +8,9 @@
 
 package org.mjdev.tvapp.base.ui.components.tv
 
+import android.content.ContentResolver
 import android.content.Context
+import android.database.Cursor
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.layout.ContentScale
@@ -20,8 +22,10 @@ import org.mjdev.tvapp.R
 import org.mjdev.tvapp.base.annotations.TvPreview
 import org.mjdev.tvapp.base.extensions.ComposeExt.computeCardWidth
 import org.mjdev.tvapp.base.extensions.ComposeExt.rememberContentResolver
+import org.mjdev.tvapp.base.helpers.cursor.CachingCursor.Companion.rememberCursor
 import org.mjdev.tvapp.base.helpers.cursor.VideoItem
 
+@Suppress("DEPRECATION")
 @TvPreview
 @Composable
 fun LocalVideoRow(
@@ -34,9 +38,18 @@ fun LocalVideoRow(
     sortOrder: String? = null,
     cardWidth: Dp = computeCardWidth(),
     contentScale: ContentScale = ContentScale.Crop,
+    contentResolver: ContentResolver = rememberContentResolver(),
+    transform: (Cursor) -> Any? = { c -> VideoItem(contentResolver, c) },
+    cursor: Cursor? = rememberCursor(
+        uri = VideoItem.URI,
+        projection = VideoItem.MEDIA_PROJECTION,
+        selection = selection,
+        selectionArgs = selectionArgs,
+        sortOrder = sortOrder,
+        transform = transform
+    ),
     openItem: Context.(item: Any?) -> Unit = {},
 ) {
-    val contentResolver = rememberContentResolver()
     CursorRow(
         title = title,
         rowState = rowState,

+ 33 - 0
app/src/main/kotlin/org/mjdev/tvapp/module/ProvideModule.kt

@@ -20,6 +20,10 @@ import dagger.hilt.components.SingletonComponent
 import okhttp3.OkHttpClient
 import okhttp3.logging.HttpLoggingInterceptor
 import org.mjdev.tvapp.BuildConfig
+import org.mjdev.tvapp.base.helpers.apps.AppsManager
+import org.mjdev.tvapp.base.helpers.cursor.AudioCursor
+import org.mjdev.tvapp.base.helpers.cursor.PhotoCursor
+import org.mjdev.tvapp.base.helpers.cursor.VideoCursor
 import org.mjdev.tvapp.base.network.CacheInterceptor
 import org.mjdev.tvapp.base.network.NetworkConnectivityService
 import org.mjdev.tvapp.base.network.NetworkConnectivityServiceImpl
@@ -36,6 +40,35 @@ class ProvideModule {
     private val isDebug = BuildConfig.DEBUG
     private val BASE_URL = ""
 
+
+    @Singleton
+    @Provides
+    fun providesAppsManager(
+        @ApplicationContext
+        context: Context
+    ): AppsManager = AppsManager(context)
+
+    @Singleton
+    @Provides
+    fun providesAudioCursor(
+        @ApplicationContext
+        context: Context
+    ): AudioCursor = AudioCursor(context)
+
+    @Singleton
+    @Provides
+    fun providesVideoCursor(
+        @ApplicationContext
+        context: Context
+    ): VideoCursor = VideoCursor(context)
+
+    @Singleton
+    @Provides
+    fun providesPhotoCursor(
+        @ApplicationContext
+        context: Context
+    ): PhotoCursor = PhotoCursor(context)
+
     @Singleton
     @Provides
     fun providesNetworkConnectivityService(

+ 34 - 2
app/src/main/kotlin/org/mjdev/tvapp/ui/pages/MainPage.kt

@@ -27,7 +27,11 @@ import org.mjdev.tvapp.base.extensions.StringExt.asException
 import org.mjdev.tvapp.base.interfaces.ItemWithId
 import org.mjdev.tvapp.base.interfaces.ItemWithUri
 import org.mjdev.tvapp.base.ui.components.page.Page
+import org.mjdev.tvapp.base.ui.components.tv.AppsRow
 import org.mjdev.tvapp.base.ui.components.tv.BrowseView
+import org.mjdev.tvapp.base.ui.components.tv.LocalAudioRow
+import org.mjdev.tvapp.base.ui.components.tv.LocalPhotosRow
+import org.mjdev.tvapp.base.ui.components.tv.LocalVideoRow
 import org.mjdev.tvapp.ui.screens.DetailScreen
 import org.mjdev.tvapp.ui.screens.PlayerScreen
 import org.mjdev.tvapp.viewmodel.MainViewModel
@@ -95,9 +99,37 @@ class MainPage : Page() {
             onTitleClicked = {
                 navController?.openMenu()
             },
-            onItemClicked = onItemClick
+            onItemClicked = onItemClick,
+            customRows = mutableListOf<@Composable () -> Unit>().apply {
+                if (viewModel.appsList.size > 0) {
+                    add { AppsRow(apps = viewModel.appsList) }
+                }
+                if (viewModel.localAudioCursor.count > 0) {
+                    add {
+                        LocalAudioRow(
+                            cursor = viewModel.localAudioCursor,
+                            openItem = { item -> onItemClick(item) }
+                        )
+                    }
+                }
+                if (viewModel.localVideoCursor.count > 0) {
+                    add {
+                        LocalVideoRow(
+                            cursor = viewModel.localVideoCursor,
+                            openItem = { item -> onItemClick(item) }
+                        )
+                    }
+                }
+                if (viewModel.localPhotoCursor.count > 0) {
+                    add {
+                        LocalPhotosRow(
+                            cursor = viewModel.localPhotoCursor,
+                            openItem = { item -> onItemClick(item) }
+                        )
+                    }
+                }
+            }
         )
-
     }
 
 }

+ 16 - 0
app/src/main/kotlin/org/mjdev/tvapp/viewmodel/MainViewModel.kt

@@ -17,6 +17,10 @@ import kotlinx.coroutines.flow.channelFlow
 import kotlinx.coroutines.flow.flow
 import kotlinx.coroutines.flow.stateIn
 import org.mjdev.tvapp.base.extensions.ListExt.asMap
+import org.mjdev.tvapp.base.helpers.apps.AppsManager
+import org.mjdev.tvapp.base.helpers.cursor.AudioCursor
+import org.mjdev.tvapp.base.helpers.cursor.PhotoCursor
+import org.mjdev.tvapp.base.helpers.cursor.VideoCursor
 import org.mjdev.tvapp.base.network.NetworkConnectivityService
 import org.mjdev.tvapp.base.network.NetworkConnectivityServiceImpl
 import org.mjdev.tvapp.base.viewmodel.BaseViewModel
@@ -57,6 +61,18 @@ class MainViewModel @Inject constructor(
         mapOf()
     )
 
+    @Inject
+    lateinit var appsList : AppsManager
+
+    @Inject
+    lateinit var localAudioCursor : AudioCursor
+
+    @Inject
+    lateinit var localVideoCursor : VideoCursor
+
+    @Inject
+    lateinit var localPhotoCursor : PhotoCursor
+
     companion object {
 
         @Suppress("unused")