WIP Store/testing

This commit is contained in:
Kelvin 2023-11-17 22:17:49 +01:00
parent 10e3d2122f
commit 99c06c516f
15 changed files with 439 additions and 29 deletions

View file

@ -39,7 +39,7 @@ protobuf {
android {
namespace 'com.futo.platformplayer'
compileSdk 33
compileSdk 34
flavorDimensions "buildType"
productFlavors {
stable {

View file

@ -1,24 +1,18 @@
package com.futo.platformplayer.states
import android.Manifest
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageManager
import android.media.AudioManager
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import android.net.Uri
import android.os.Environment
import android.provider.DocumentsContract
import android.util.DisplayMetrics
import androidx.activity.ComponentActivity
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.net.toUri
import androidx.documentfile.provider.DocumentFile
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
@ -28,10 +22,9 @@ import com.futo.platformplayer.R
import com.futo.platformplayer.activities.CaptchaActivity
import com.futo.platformplayer.activities.IWithResultLauncher
import com.futo.platformplayer.activities.MainActivity
import com.futo.platformplayer.api.media.Serializer
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
import com.futo.platformplayer.api.media.platforms.js.DevJSClient
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.internal.JSHttpClient
import com.futo.platformplayer.background.BackgroundWorker
import com.futo.platformplayer.cache.ChannelContentCache
import com.futo.platformplayer.casting.StateCasting
@ -43,20 +36,20 @@ import com.futo.platformplayer.logging.AndroidLogConsumer
import com.futo.platformplayer.logging.FileLogConsumer
import com.futo.platformplayer.logging.LogLevel
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.HistoryVideo
import com.futo.platformplayer.receivers.AudioNoisyReceiver
import com.futo.platformplayer.services.DownloadService
import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.stores.db.types.DBHistory
import com.futo.platformplayer.stores.v2.ManagedStore
import com.stripe.android.core.utils.encodeToJson
import kotlinx.coroutines.*
import kotlinx.serialization.encodeToString
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import java.io.File
import java.time.OffsetDateTime
import java.util.*
import java.util.concurrent.TimeUnit
import kotlin.system.measureTimeMillis
import kotlin.time.measureTime
/***
* This class contains global context for unconventional cases where obtaining context is hard.
@ -545,7 +538,73 @@ class StateApp {
StateAnnouncement.instance.registerDidYouKnow();
Logger.i(TAG, "MainApp Started: Finished");
if(true) {
Logger.i(TAG, "TEST:--------(200)---------");
testHistoryDB(200);
Logger.i(TAG, "TEST:--------(1000)---------");
testHistoryDB(1000);
Logger.i(TAG, "TEST:--------(2000)---------");
testHistoryDB(2000);
Logger.i(TAG, "TEST:--------(4000)---------");
testHistoryDB(4000);
Logger.i(TAG, "TEST:--------(6000)---------");
testHistoryDB(6000);
}
}
fun testHistoryDB(count: Int) {
Logger.i(TAG, "TEST: Starting tests");
StatePlaylists.instance._historyDBStore.deleteAll();
val testHistoryItem = StatePlaylists.instance.getHistory().first();
val testItemJson = StatePlaylists.instance.getHistory().first().video.toJson();
val now = OffsetDateTime.now();
val testSet = (0..count).map { HistoryVideo(Json.decodeFromString<SerializedPlatformVideo>(testItemJson.replace(testHistoryItem.video.url, UUID.randomUUID().toString())), it.toLong(), now.minusHours(it.toLong())) }
Logger.i(TAG, "TEST: Inserting (${testSet.size})");
val insertMS = measureTimeMillis {
for(item in testSet)
StatePlaylists.instance._historyDBStore.insert(item);
};
Logger.i(TAG, "TEST: Inserting in ${insertMS}ms");
var fetched: List<DBHistory.Index>? = null;
val fetchMS = measureTimeMillis {
fetched = StatePlaylists.instance._historyDBStore.getAll();
Logger.i(TAG, "TEST: Fetched: ${fetched?.size}");
};
Logger.i(TAG, "TEST: Fetch speed ${fetchMS}MS");
val deserializeMS = measureTimeMillis {
val deserialized = StatePlaylists.instance._historyDBStore.convertObjects(fetched!!);
Logger.i(TAG, "TEST: Deserialized: ${deserialized.size}");
};
Logger.i(TAG, "TEST: Deserialize speed ${deserializeMS}MS");
var fetchedIndex: List<DBHistory.Index>? = null;
val fetchIndexMS = measureTimeMillis {
fetchedIndex = StatePlaylists.instance._historyDBStore.getAllIndexes();
Logger.i(TAG, "TEST: Fetched Index: ${fetchedIndex!!.size}");
};
Logger.i(TAG, "TEST: Fetched Index speed ${fetchIndexMS}ms");
val fetchFromIndex = measureTimeMillis {
for(preItem in testSet) {
val item = StatePlaylists.instance.historyIndex[preItem.video.url];
if(item == null)
throw IllegalStateException("Missing item [${preItem.video.url}]");
if(item.url != preItem.video.url)
throw IllegalStateException("Mismatch item [${preItem.video.url}]");
}
};
Logger.i(TAG, "TEST: Index Lookup speed ${fetchFromIndex}ms");
val page1 = StatePlaylists.instance._historyDBStore.getPage(0, 20);
val page2 = StatePlaylists.instance._historyDBStore.getPage(1, 20);
val page3 = StatePlaylists.instance._historyDBStore.getPage(2, 20);
}
fun mainAppStartedWithExternalFiles(context: Context) {
if(!Settings.instance.didFirstStart) {
if(StateBackup.hasAutomaticBackup()) {

View file

@ -3,6 +3,7 @@ package com.futo.platformplayer.states
import android.content.Context
import android.net.Uri
import androidx.core.content.FileProvider
import androidx.sqlite.db.SimpleSQLiteQuery
import com.futo.platformplayer.R
import com.futo.platformplayer.api.media.PlatformID
import com.futo.platformplayer.api.media.exceptions.NoPlatformClientException
@ -19,6 +20,8 @@ import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.HistoryVideo
import com.futo.platformplayer.models.Playlist
import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.stores.db.ManagedDBStore
import com.futo.platformplayer.stores.db.types.DBHistory
import com.futo.platformplayer.stores.v2.ManagedStore
import com.futo.platformplayer.stores.v2.ReconstructStore
import kotlinx.serialization.encodeToString
@ -26,6 +29,8 @@ import kotlinx.serialization.json.Json
import java.io.File
import java.time.OffsetDateTime
import java.time.temporal.ChronoUnit
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ConcurrentMap
/***
* Used to maintain playlists
@ -50,6 +55,11 @@ class StatePlaylists {
.withRestore(PlaylistBackup())
.load();
val historyIndex: ConcurrentMap<Any, DBHistory.Index> = ConcurrentHashMap();
val _historyDBStore = ManagedDBStore.create("history", DBHistory.Descriptor())
.withIndex({ it.url }, historyIndex)
.load();
val playlistShareDir = FragmentedStorage.getOrCreateDirectory("shares");
var onHistoricVideoChanged = Event2<IPlatformVideo, Long>();

View file

@ -2,6 +2,8 @@ package com.futo.platformplayer.stores
import com.futo.platformplayer.Settings
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.stores.db.ManagedDBIndex
import com.futo.platformplayer.stores.db.ManagedDBStore
import com.futo.platformplayer.stores.v2.JsonStoreSerializer
import com.futo.platformplayer.stores.v2.ManagedStore
import com.futo.platformplayer.stores.v2.StoreSerializer

View file

@ -0,0 +1,27 @@
package com.futo.platformplayer.stores.db
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.Query
import androidx.room.Update
/*
@Dao
class ManagedDBContext<T, I: ManagedDBIndex<T>> {
fun get(id: Int): I;
fun gets(vararg id: Int): List<I>;
fun getAll(): List<I>;
@Insert
fun insert(index: I);
@Insert
fun insertAll(vararg indexes: I)
@Update
fun update(index: I);
@Delete
fun delete(index: I);
}*/

View file

@ -0,0 +1,11 @@
package com.futo.platformplayer.stores.db
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.Update
@Dao
interface ManagedDBContextPaged<T, I: ManagedDBIndex<T>> {
fun getPaged(page: Int, pageSize: Int): List<I>;
}

View file

@ -0,0 +1,33 @@
package com.futo.platformplayer.stores.db
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.Query
import androidx.room.RawQuery
import androidx.room.Update
import androidx.sqlite.db.SupportSQLiteQuery
@Dao
interface ManagedDBDAOBase<T, I: ManagedDBIndex<T>> {
@RawQuery
fun get(query: SupportSQLiteQuery): I;
@RawQuery
fun getMultiple(query: SupportSQLiteQuery): List<I>;
@RawQuery
fun action(query: SupportSQLiteQuery): Int
@Insert
fun insert(index: I): Long;
@Insert
fun insertAll(vararg indexes: I)
@Update
fun update(index: I);
@Delete
fun delete(index: I);
}

View file

@ -0,0 +1,7 @@
package com.futo.platformplayer.stores.db
import androidx.room.RoomDatabase
abstract class ManagedDBDatabase<T, I: ManagedDBIndex<T>, D: ManagedDBDAOBase<T, I>>: RoomDatabase() {
abstract fun base(): D;
}

View file

@ -0,0 +1,16 @@
package com.futo.platformplayer.stores.db
import androidx.sqlite.db.SimpleSQLiteQuery
import com.futo.platformplayer.models.HistoryVideo
import com.futo.platformplayer.stores.db.types.DBHistory
abstract class ManagedDBDescriptor<T, I: ManagedDBIndex<T>, D: ManagedDBDatabase<T, I, DA>, DA: ManagedDBDAOBase<T, I>> {
abstract fun dbClass(): Class<D>;
abstract fun create(obj: T): I;
open val ordered: String? = null;
open fun sqlIndexOnly(tableName: String): SimpleSQLiteQuery? = null;
open fun sqlPage(tableName: String, page: Int, length: Int): SimpleSQLiteQuery? = null;
}

View file

@ -1,8 +1,14 @@
package com.futo.platformplayer.stores.db
import androidx.room.ColumnInfo
import androidx.room.Ignore
import androidx.room.PrimaryKey
import com.futo.platformplayer.api.media.Serializer
open class ManagedDBIndex(
@PrimaryKey(true)
val id: Int? = null
)
interface ManagedDBIndex<T> {
var id: Long?
var serialized: ByteArray?
@get:Ignore
var obj: T?;
}

View file

@ -0,0 +1,11 @@
package com.futo.platformplayer.stores.db
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.Update
@Dao
interface ManagedDBIndexOnly<T, I: ManagedDBIndex<T>> {
fun getIndex(): List<I>;
}

View file

@ -1,28 +1,40 @@
package com.futo.platformplayer.stores.db
import androidx.room.Room
import androidx.sqlite.db.SimpleSQLiteQuery
import com.futo.platformplayer.assume
import com.futo.platformplayer.stores.v2.ManagedStore
import com.futo.platformplayer.stores.v2.ReconstructStore
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.stores.v2.JsonStoreSerializer
import com.futo.platformplayer.stores.v2.StoreSerializer
import java.io.File
import kotlinx.serialization.KSerializer
import java.util.concurrent.ConcurrentMap
import kotlin.reflect.KClass
import kotlin.reflect.KType
class ManagedDBStore<I, T> {
class ManagedDBStore<I: ManagedDBIndex<T>, T, D: ManagedDBDatabase<T, I, DA>, DA: ManagedDBDAOBase<T, I>> {
private val _class: KType;
private val _name: String;
private val _serializer: StoreSerializer<T>;
private var _db: ManagedDBDatabase<T, I, *>? = null;
private var _dbDaoBase: ManagedDBDAOBase<T, I>? = null;
val dbDaoBase: ManagedDBDAOBase<T, I> get() = _dbDaoBase ?: throw IllegalStateException("Not initialized db [${name}]");
private var _isLoaded = false;
private var _dbDescriptor: ManagedDBDescriptor<T, I, D, DA>;
private var _withUnique: ((I) -> Any)? = null;
private val _sqlAll: SimpleSQLiteQuery;
private val _sqlDeleteAll: SimpleSQLiteQuery;
private var _sqlIndexed: SimpleSQLiteQuery? = null;
val className: String? get() = _class.classifier?.assume<KClass<*>>()?.simpleName;
val name: String;
constructor(name: String, clazz: KType, serializer: StoreSerializer<T>, niceName: String? = null) {
private val _indexes: ArrayList<Pair<(I)->Any, ConcurrentMap<Any, I>>> = arrayListOf();
constructor(name: String, descriptor: ManagedDBDescriptor<T, I, D, DA>, clazz: KType, serializer: StoreSerializer<T>, niceName: String? = null) {
_dbDescriptor = descriptor;
_name = name;
this.name = niceName ?: name.let {
if(it.isNotEmpty())
@ -31,11 +43,119 @@ class ManagedDBStore<I, T> {
};
_serializer = serializer;
_class = clazz;
_sqlAll = SimpleSQLiteQuery("SELECT * FROM $_name" + if(descriptor.ordered.isNullOrEmpty()) "" else " ${descriptor.ordered}");
_sqlDeleteAll = SimpleSQLiteQuery("DELETE FROM ${_name}");
_sqlIndexed = descriptor.sqlIndexOnly(_name);
}
fun load() {
throw NotImplementedError();
_isLoaded = true;
fun withIndex(keySelector: (I)->Any, indexContainer: ConcurrentMap<Any, I>): ManagedDBStore<I, T, D, DA> {
if(_sqlIndexed == null)
throw IllegalStateException("Can only create indexes if sqlIndexOnly is implemented");
_indexes.add(Pair(keySelector, indexContainer));
return this;
}
fun load(): ManagedDBStore<I, T, D, DA> {
_db = Room.databaseBuilder(StateApp.instance.context, _dbDescriptor.dbClass(), _name)
.fallbackToDestructiveMigration()
.allowMainThreadQueries()
.build()
_dbDaoBase = _db!!.base() as ManagedDBDAOBase<T, I>;
if(_indexes.any()) {
val allItems = _dbDaoBase!!.getMultiple(_sqlIndexed!!);
for(index in _indexes)
index.second.putAll(allItems.associateBy(index.first));
}
return this;
}
fun insert(obj: T) {
val newIndex = _dbDescriptor.create(obj);
newIndex.serialized = serialize(obj);
newIndex.id = dbDaoBase.insert(newIndex);
newIndex.serialized = null;
if(!_indexes.isEmpty()) {
for (index in _indexes) {
val key = index.first(newIndex);
index.second.put(key, newIndex);
}
}
}
fun update(id: Long, obj: T) {
val newIndex = _dbDescriptor.create(obj);
newIndex.id = id;
newIndex.serialized = serialize(obj);
dbDaoBase.update(newIndex);
newIndex.serialized = null;
if(!_indexes.isEmpty()) {
for (index in _indexes) {
val key = index.first(newIndex);
index.second.put(key, newIndex);
}
}
}
fun getAllIndexes(): List<I> {
if(_sqlIndexed == null)
throw IllegalStateException("Can only create indexes if sqlIndexOnly is implemented");
return dbDaoBase.getMultiple(_sqlIndexed!!);
}
fun getAllObjects(): List<T> = convertObjects(getAll());
fun getAll(): List<I> {
return dbDaoBase.getMultiple(_sqlAll);
}
fun getObject(id: Long) = convertObject(get(id));
fun get(id: Long): I {
return dbDaoBase.get(SimpleSQLiteQuery("SELECT * FROM $_name WHERE id = ?", arrayOf(id)));
}
fun getAllObjects(vararg id: Long): List<T> = convertObjects(getAll(*id));
fun getAll(vararg id: Long): List<I> {
return dbDaoBase.getMultiple(SimpleSQLiteQuery("SELECT * FROM $_name WHERE id IN (?)", arrayOf(id)));
}
fun getPageObjects(page: Int, length: Int): List<T> = convertObjects(getPage(page, length));
fun getPage(page: Int, length: Int): List<I> {
val query = _dbDescriptor.sqlPage(_name, page, length) ?: throw IllegalStateException("Paged db not setup for ${_name}");
return dbDaoBase.getMultiple(query);
}
fun delete(item: I) {
dbDaoBase.delete(item);
for(index in _indexes)
index.second.remove(index.first(item));
}
fun deleteAll() {
dbDaoBase.action(_sqlDeleteAll);
for(index in _indexes)
index.second.clear();
}
fun convertObject(index: ManagedDBIndex<T>): T? {
return index.serialized?.let {
_serializer.deserialize(_class, it);
};
}
fun convertObjects(indexes: List<ManagedDBIndex<T>>): List<T> {
return indexes.mapNotNull { convertObject(it) };
}
fun serialize(obj: T): ByteArray {
return _serializer.serialize(_class, obj);
}
companion object {
inline fun <reified T, I: ManagedDBIndex<T>, D: ManagedDBDatabase<T, I, DA>, DA: ManagedDBDAOBase<T, I>> create(name: String, descriptor: ManagedDBDescriptor<T, I, D, DA>, serializer: KSerializer<T>? = null)
= ManagedDBStore(name, descriptor, kotlin.reflect.typeOf<T>(), JsonStoreSerializer.create(serializer));
}
}

View file

@ -0,0 +1,36 @@
package com.futo.platformplayer.stores.db.types
import androidx.room.ColumnInfo
import androidx.room.Ignore
import androidx.room.PrimaryKey
import com.futo.platformplayer.api.media.models.video.SerializedPlatformContent
import com.futo.platformplayer.models.HistoryVideo
import com.futo.platformplayer.stores.db.ManagedDBIndex
class DBChannelCache {
companion object {
const val TABLE_NAME = "channelCache";
}
class Index: ManagedDBIndex<SerializedPlatformContent> {
@PrimaryKey(true)
override var id: Long? = null;
@ColumnInfo(typeAffinity = ColumnInfo.BLOB)
override var serialized: ByteArray? = null;
@Ignore
override var obj: SerializedPlatformContent? = null;
var feedType: String? = null;
var channelUrl: String? = null;
constructor() {}
constructor(sCache: SerializedPlatformContent) {
id = null;
serialized = null;
obj = sCache;
channelUrl = sCache.author.url;
}
}
}

View file

@ -0,0 +1,69 @@
package com.futo.platformplayer.stores.db.types
import androidx.room.ColumnInfo
import androidx.room.Dao
import androidx.room.Database
import androidx.room.Entity
import androidx.room.Ignore
import androidx.room.PrimaryKey
import androidx.room.Query
import androidx.sqlite.db.SimpleSQLiteQuery
import com.futo.platformplayer.api.media.models.video.SerializedPlatformContent
import com.futo.platformplayer.models.HistoryVideo
import com.futo.platformplayer.stores.db.ManagedDBDAOBase
import com.futo.platformplayer.stores.db.ManagedDBDatabase
import com.futo.platformplayer.stores.db.ManagedDBDescriptor
import com.futo.platformplayer.stores.db.ManagedDBIndex
import com.futo.platformplayer.stores.db.ManagedDBStore
import kotlin.reflect.KType
class DBHistory {
companion object {
const val TABLE_NAME = "history";
}
@Dao
interface DBDAO: ManagedDBDAOBase<HistoryVideo, Index> {}
@Database(entities = [Index::class], version = 2)
abstract class DB: ManagedDBDatabase<HistoryVideo, Index, DBDAO>() {
abstract override fun base(): DBDAO;
}
class Descriptor: ManagedDBDescriptor<HistoryVideo, Index, DB, DBDAO>() {
override fun create(obj: HistoryVideo): Index = Index(obj);
override fun dbClass(): Class<DB> = DB::class.java;
//Optional
override fun sqlIndexOnly(tableName: String): SimpleSQLiteQuery = SimpleSQLiteQuery("SELECT id, url, position, date FROM $TABLE_NAME");
override fun sqlPage(tableName: String, page: Int, length: Int): SimpleSQLiteQuery = SimpleSQLiteQuery("SELECT * FROM $TABLE_NAME ORDER BY date DESC, id DESC LIMIT ? OFFSET ?", arrayOf(length, page * length));
}
@Entity(TABLE_NAME)
class Index: ManagedDBIndex<HistoryVideo> {
@PrimaryKey(true)
override var id: Long? = null;
@ColumnInfo(typeAffinity = ColumnInfo.BLOB)
override var serialized: ByteArray? = null;
@Ignore
override var obj: HistoryVideo? = null;
var url: String;
var position: Long;
var date: Long;
constructor() {
url = "";
position = 0;
date = 0;
}
constructor(historyVideo: HistoryVideo) {
id = null;
serialized = null;
url = historyVideo.video.url;
position = historyVideo.position;
date = historyVideo.date.toEpochSecond();
obj = historyVideo;
}
}
}

View file

@ -92,7 +92,11 @@ class GestureControlView : LinearLayout {
override fun onDown(p0: MotionEvent): Boolean { return false; }
override fun onShowPress(p0: MotionEvent) = Unit;
override fun onSingleTapUp(p0: MotionEvent): Boolean { return false; }
override fun onScroll(p0: MotionEvent, p1: MotionEvent, distanceX: Float, distanceY: Float): Boolean {
override fun onFling(p0: MotionEvent?, p1: MotionEvent, p2: Float, p3: Float): Boolean { return false; }
override fun onScroll(p0: MotionEvent?, p1: MotionEvent, distanceX: Float, distanceY: Float): Boolean {
if(p0 == null)
return false;
if (_isFullScreen && _adjustingBrightness) {
val adjustAmount = (distanceY * 2) / height;
_brightnessFactor = (_brightnessFactor + adjustAmount).coerceAtLeast(0.0f).coerceAtMost(1.0f);
@ -132,8 +136,7 @@ class GestureControlView : LinearLayout {
return true;
}
override fun onLongPress(p0: MotionEvent) = Unit;
override fun onFling(p0: MotionEvent, p1: MotionEvent, p2: Float, p3: Float): Boolean { return false; }
override fun onLongPress(p0: MotionEvent) = Unit
});
gestureController.setOnDoubleTapListener(object : GestureDetector.OnDoubleTapListener {