From c917dc13dabe7781a097383ae89f2d00f32fffcb Mon Sep 17 00:00:00 2001 From: Allan Wang Date: Tue, 5 Mar 2019 18:31:47 -0500 Subject: Create initial room models --- app/build.gradle | 10 ++ .../com/pitchedapps/frost/db/FrostDatabaseTest.kt | 46 ++++++++ app/src/main/AndroidManifest.xml | 1 - .../main/kotlin/com/pitchedapps/frost/FrostApp.kt | 14 ++- .../kotlin/com/pitchedapps/frost/StartActivity.kt | 4 +- .../frost/activities/BaseMainActivity.kt | 6 +- .../pitchedapps/frost/activities/LoginActivity.kt | 6 +- .../frost/activities/TabCustomizerActivity.kt | 6 +- .../kotlin/com/pitchedapps/frost/db/CookiesDb.kt | 126 +++++++++++++++++++++ .../kotlin/com/pitchedapps/frost/db/Database.kt | 62 ++++++++++ .../kotlin/com/pitchedapps/frost/db/DbUtils.kt | 38 +++++++ .../kotlin/com/pitchedapps/frost/db/FbTabsDb.kt | 108 ++++++++++++++++++ .../com/pitchedapps/frost/db/NotificationDb.kt | 72 ++++++++++++ .../com/pitchedapps/frost/dbflow/CookiesDb.kt | 92 --------------- .../kotlin/com/pitchedapps/frost/dbflow/DbUtils.kt | 39 ------- .../com/pitchedapps/frost/dbflow/FbTabsDb.kt | 58 ---------- .../com/pitchedapps/frost/dbflow/NotificationDb.kt | 72 ------------ .../com/pitchedapps/frost/facebook/FbCookie.kt | 8 +- .../frost/facebook/parsers/FrostParser.kt | 2 +- .../frost/facebook/parsers/MessageParser.kt | 2 +- .../frost/facebook/parsers/NotifParser.kt | 2 +- .../frost/services/FrostNotifications.kt | 6 +- .../frost/services/NotificationService.kt | 4 +- .../pitchedapps/frost/settings/Notifications.kt | 4 +- .../com/pitchedapps/frost/utils/Downloader.kt | 2 +- .../kotlin/com/pitchedapps/frost/utils/Utils.kt | 2 +- .../com/pitchedapps/frost/views/AccountItem.kt | 2 +- .../kotlin/com/pitchedapps/frost/web/FrostJSI.kt | 2 +- .../com/pitchedapps/frost/web/LoginWebView.kt | 2 +- .../1.json | 45 ++++++++ .../1.json | 39 +++++++ build.gradle | 2 +- docs/Changelog.md | 4 + gradle.properties | 4 +- 34 files changed, 595 insertions(+), 297 deletions(-) create mode 100644 app/src/androidTest/kotlin/com/pitchedapps/frost/db/FrostDatabaseTest.kt create mode 100644 app/src/main/kotlin/com/pitchedapps/frost/db/CookiesDb.kt create mode 100644 app/src/main/kotlin/com/pitchedapps/frost/db/Database.kt create mode 100644 app/src/main/kotlin/com/pitchedapps/frost/db/DbUtils.kt create mode 100644 app/src/main/kotlin/com/pitchedapps/frost/db/FbTabsDb.kt create mode 100644 app/src/main/kotlin/com/pitchedapps/frost/db/NotificationDb.kt delete mode 100644 app/src/main/kotlin/com/pitchedapps/frost/dbflow/CookiesDb.kt delete mode 100644 app/src/main/kotlin/com/pitchedapps/frost/dbflow/DbUtils.kt delete mode 100644 app/src/main/kotlin/com/pitchedapps/frost/dbflow/FbTabsDb.kt delete mode 100644 app/src/main/kotlin/com/pitchedapps/frost/dbflow/NotificationDb.kt create mode 100644 app/src/schemas/com.pitchedapps.frost.db.FrostPrivateDatabase/1.json create mode 100644 app/src/schemas/com.pitchedapps.frost.db.FrostPublicDatabase/1.json diff --git a/app/build.gradle b/app/build.gradle index 4025568a..4c22ab97 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -26,6 +26,11 @@ android { versionName androidGitVersion.name() multiDexEnabled true testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + javaCompileOptions { + annotationProcessorOptions { + arguments = ["room.schemaLocation": "$projectDir/src/schemas".toString()] + } + } } applicationVariants.all { variant -> @@ -252,6 +257,11 @@ dependencies { implementation "com.sothree.slidinguppanel:library:${SLIDING_PANEL}" + implementation "androidx.room:room-coroutines:${ROOM}" + implementation "android.arch.persistence.room:runtime:${ROOM}" + kapt "android.arch.persistence.room:compiler:${ROOM}" + testImplementation "androidx.room:room-testing:${ROOM}" + } // Validates code and generates apk diff --git a/app/src/androidTest/kotlin/com/pitchedapps/frost/db/FrostDatabaseTest.kt b/app/src/androidTest/kotlin/com/pitchedapps/frost/db/FrostDatabaseTest.kt new file mode 100644 index 00000000..6b68e804 --- /dev/null +++ b/app/src/androidTest/kotlin/com/pitchedapps/frost/db/FrostDatabaseTest.kt @@ -0,0 +1,46 @@ +package com.pitchedapps.frost.db + +import android.content.Context +import androidx.room.Room +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import kotlinx.coroutines.runBlocking +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import kotlin.test.assertEquals + +@RunWith(AndroidJUnit4::class) +class FrostDatabaseTest { + + private lateinit var db: FrostDatabase + + @Before + fun before() { + + val context = ApplicationProvider.getApplicationContext() + val privateDb = Room.inMemoryDatabaseBuilder( + context, FrostPrivateDatabase::class.java + ).build() + val publicDb = Room.inMemoryDatabaseBuilder( + context, FrostPublicDatabase::class.java + ).build() + db = FrostDatabase(privateDb, publicDb) + } + + @After + fun after() { + db.close() + } + + @Test + fun basic() { + val cookie = CookieEntity(id = 1234L, name = "testName", cookie = "testCookie") + runBlocking { + db.cookieDao().insertCookie(cookie) + val cookies = db.cookieDao().selectAll() + assertEquals(listOf(cookie), cookies, "Cookie mismatch") + } + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ad1fcbdc..68e84e4d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -17,7 +17,6 @@ . + */ +package com.pitchedapps.frost.db + +import android.os.Parcelable +import androidx.room.Dao +import androidx.room.Entity +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import com.pitchedapps.frost.utils.L +import com.raizlabs.android.dbflow.annotation.ConflictAction +import com.raizlabs.android.dbflow.annotation.Database +import com.raizlabs.android.dbflow.annotation.PrimaryKey +import com.raizlabs.android.dbflow.annotation.Table +import com.raizlabs.android.dbflow.kotlinextensions.async +import com.raizlabs.android.dbflow.kotlinextensions.delete +import com.raizlabs.android.dbflow.kotlinextensions.eq +import com.raizlabs.android.dbflow.kotlinextensions.from +import com.raizlabs.android.dbflow.kotlinextensions.save +import com.raizlabs.android.dbflow.kotlinextensions.select +import com.raizlabs.android.dbflow.kotlinextensions.where +import com.raizlabs.android.dbflow.structure.BaseModel +import kotlinx.android.parcel.Parcelize +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +/** + * Created by Allan Wang on 2017-05-30. + */ + +@Entity(tableName = "cookies") +@Parcelize +data class CookieEntity( + @androidx.room.PrimaryKey + var id: Long, + var name: String, + var cookie: String +) : Parcelable { + override fun toString(): String = "CookieModel(${hashCode()})" + + fun toSensitiveString(): String = "CookieModel(id=$id, name=$name, cookie=$cookie)" +} + +@Dao +interface CookieDao { + + @Query("SELECT * FROM cookies") + suspend fun selectAll(): List + + @Query("SELECT * FROM cookies WHERE id = :id") + suspend fun selectById(id: Long): CookieEntity? + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertCookie(cookie: CookieEntity) + + @Query("DELETE FROM cookies WHERE id = :id") + suspend fun deleteById(id: Long) +} + +@Database(version = CookiesDb.VERSION) +object CookiesDb { + const val NAME = "Cookies" + const val VERSION = 2 +} + +@Parcelize +@Table(database = CookiesDb::class, allFields = true, primaryKeyConflict = ConflictAction.REPLACE) +data class CookieModel(@PrimaryKey var id: Long = -1L, var name: String? = null, var cookie: String? = null) : + BaseModel(), Parcelable { + + override fun toString(): String = "CookieModel(${hashCode()})" + + fun toSensitiveString(): String = "CookieModel(id=$id, name=$name, cookie=$cookie)" +} + +fun loadFbCookie(id: Long): CookieModel? = + (select from CookieModel::class where (CookieModel_Table.id eq id)).querySingle() + +fun loadFbCookie(name: String): CookieModel? = + (select from CookieModel::class where (CookieModel_Table.name eq name)).querySingle() + +/** + * Loads cookies sorted by name + */ +fun loadFbCookiesAsync(callback: (cookies: List) -> Unit) { + (select from CookieModel::class).orderBy(CookieModel_Table.name, true).async() + .queryListResultCallback { _, tResult -> callback(tResult) }.execute() +} + +fun loadFbCookiesSync(): List = + (select from CookieModel::class).orderBy(CookieModel_Table.name, true).queryList() + +// TODO temp method until dbflow supports coroutines +suspend fun loadFbCookiesSuspend(): List = withContext(Dispatchers.IO) { + loadFbCookiesSync() +} + +inline fun saveFbCookie(cookie: CookieModel, crossinline callback: (() -> Unit) = {}) { + cookie.async save { + L.d { "Fb cookie saved" } + L._d { cookie.toSensitiveString() } + callback() + } +} + +fun removeCookie(id: Long) { + loadFbCookie(id)?.async?.delete { + L.d { "Fb cookie deleted" } + L._d { id } + } +} diff --git a/app/src/main/kotlin/com/pitchedapps/frost/db/Database.kt b/app/src/main/kotlin/com/pitchedapps/frost/db/Database.kt new file mode 100644 index 00000000..34de5e07 --- /dev/null +++ b/app/src/main/kotlin/com/pitchedapps/frost/db/Database.kt @@ -0,0 +1,62 @@ +package com.pitchedapps.frost.db + +import android.content.Context +import androidx.room.Database +import androidx.room.Room +import androidx.room.RoomDatabase +import androidx.room.TypeConverters + +interface FrostPrivateDao { + fun cookieDao(): CookieDao +} + +@Database(entities = [CookieEntity::class], version = 1, exportSchema = true) +abstract class FrostPrivateDatabase : RoomDatabase(), FrostPrivateDao { + companion object { + const val DATABASE_NAME = "frost-priv-db" + } +} + +interface FrostPublicDao { + fun tabDao(): FbTabDao +} + +@Database(entities = [FbTabEntity::class], version = 1, exportSchema = true) +@TypeConverters(FbItemConverter::class) +abstract class FrostPublicDatabase : RoomDatabase(), FrostPublicDao { + companion object { + const val DATABASE_NAME = "frost-db" + } +} + +interface FrostDao : FrostPrivateDao, FrostPublicDao { + fun close() +} + +/** + * Composition of all database interfaces + */ +class FrostDatabase(private val privateDb: FrostPrivateDatabase, private val publicDb: FrostPublicDatabase) : + FrostDao, + FrostPrivateDao by privateDb, + FrostPublicDao by publicDb { + + override fun close() { + privateDb.close() + publicDb.close() + } + + companion object { + fun create(context: Context): FrostDatabase { + val privateDb = Room.databaseBuilder( + context, FrostPrivateDatabase::class.java, + FrostPrivateDatabase.DATABASE_NAME + ).build() + val publicDb = Room.databaseBuilder( + context, FrostPublicDatabase::class.java, + FrostPublicDatabase.DATABASE_NAME + ).build() + return FrostDatabase(privateDb, publicDb) + } + } +} diff --git a/app/src/main/kotlin/com/pitchedapps/frost/db/DbUtils.kt b/app/src/main/kotlin/com/pitchedapps/frost/db/DbUtils.kt new file mode 100644 index 00000000..03aac059 --- /dev/null +++ b/app/src/main/kotlin/com/pitchedapps/frost/db/DbUtils.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2018 Allan Wang + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.pitchedapps.frost.db + +import android.content.Context +import com.pitchedapps.frost.utils.L +import com.raizlabs.android.dbflow.config.FlowManager +import com.raizlabs.android.dbflow.structure.database.transaction.FastStoreModelTransaction + +/** + * Created by Allan Wang on 2017-05-30. + */ + +object DbUtils { + fun db(name: String) = FlowManager.getDatabase(name) + fun dbName(name: String) = "$name.db" + fun deleteDatabase(c: Context, name: String) = c.deleteDatabase(dbName(name)) +} + +inline fun List.replace(dbName: String) { + L.d { "Replacing $dbName.db" } + DbUtils.db(dbName).reset() + FastStoreModelTransaction.saveBuilder(FlowManager.getModelAdapter(T::class.java)).addAll(this).build() +} diff --git a/app/src/main/kotlin/com/pitchedapps/frost/db/FbTabsDb.kt b/app/src/main/kotlin/com/pitchedapps/frost/db/FbTabsDb.kt new file mode 100644 index 00000000..2e2a9d62 --- /dev/null +++ b/app/src/main/kotlin/com/pitchedapps/frost/db/FbTabsDb.kt @@ -0,0 +1,108 @@ +/* + * Copyright 2018 Allan Wang + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.pitchedapps.frost.db + +import androidx.room.Dao +import androidx.room.Entity +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Transaction +import com.pitchedapps.frost.facebook.FbItem +import com.pitchedapps.frost.facebook.defaultTabs +import com.pitchedapps.frost.utils.L +import com.raizlabs.android.dbflow.annotation.Database +import com.raizlabs.android.dbflow.annotation.PrimaryKey +import com.raizlabs.android.dbflow.annotation.Table +import com.raizlabs.android.dbflow.kotlinextensions.database +import com.raizlabs.android.dbflow.kotlinextensions.fastSave +import com.raizlabs.android.dbflow.kotlinextensions.from +import com.raizlabs.android.dbflow.kotlinextensions.select +import com.raizlabs.android.dbflow.structure.BaseModel + +/** + * Created by Allan Wang on 2017-05-30. + */ + +@Entity(tableName = "tabs") +data class FbTabEntity(@androidx.room.PrimaryKey var position: Int, var tab: FbItem) + +@Dao +interface FbTabDao { + + @Query("SELECT * FROM tabs ORDER BY position ASC") + suspend fun _selectAll(): List + + @Query("DELETE FROM tabs") + suspend fun _deleteAll() + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun _insertAll(items: List) + + @Transaction + suspend fun _save(items: List) { + _deleteAll() + _insertAll(items) + } + +// suspend fun save(items: List) { +// _save((items.takeIf { it.isNotEmpty() } ?: defaultTabs()).mapIndexed { index, fbItem -> +// FbTabEntity( +// index, +// fbItem +// ) +// }) +// } +// +// suspend fun selectAll(): List = _selectAll().map { it.tab }.takeIf { it.isNotEmpty() } ?: defaultTabs() +} + +object FbItemConverter { + @androidx.room.TypeConverter + @JvmStatic + fun fromItem(item: FbItem): String = item.name + + @androidx.room.TypeConverter + @JvmStatic + fun toItem(value: String): FbItem = FbItem.valueOf(value) +} + +const val TAB_COUNT = 4 + +@Database(version = FbTabsDb.VERSION) +object FbTabsDb { + const val NAME = "FrostTabs" + const val VERSION = 1 +} + +@Table(database = FbTabsDb::class, allFields = true) +data class FbTabModel(@PrimaryKey var position: Int = -1, var tab: FbItem = FbItem.FEED) : BaseModel() + +/** + * Load tabs synchronously + * Note that tab length should never be a big number anyways + */ +fun loadFbTabs(): List { + val tabs: List? = (select from (FbTabModel::class)).orderBy(FbTabModel_Table.position, true).queryList() + if (tabs?.size == TAB_COUNT) return tabs.map(FbTabModel::tab) + L.d { "No tabs (${tabs?.size}); loading default" } + return defaultTabs() +} + +fun List.save() { + database().beginTransactionAsync(mapIndexed(::FbTabModel).fastSave().build()).execute() +} diff --git a/app/src/main/kotlin/com/pitchedapps/frost/db/NotificationDb.kt b/app/src/main/kotlin/com/pitchedapps/frost/db/NotificationDb.kt new file mode 100644 index 00000000..5b501792 --- /dev/null +++ b/app/src/main/kotlin/com/pitchedapps/frost/db/NotificationDb.kt @@ -0,0 +1,72 @@ +/* + * Copyright 2018 Allan Wang + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.pitchedapps.frost.db + +import com.pitchedapps.frost.utils.L +import com.raizlabs.android.dbflow.annotation.ConflictAction +import com.raizlabs.android.dbflow.annotation.Database +import com.raizlabs.android.dbflow.annotation.Migration +import com.raizlabs.android.dbflow.annotation.PrimaryKey +import com.raizlabs.android.dbflow.annotation.Table +import com.raizlabs.android.dbflow.kotlinextensions.async +import com.raizlabs.android.dbflow.kotlinextensions.eq +import com.raizlabs.android.dbflow.kotlinextensions.from +import com.raizlabs.android.dbflow.kotlinextensions.save +import com.raizlabs.android.dbflow.kotlinextensions.select +import com.raizlabs.android.dbflow.kotlinextensions.where +import com.raizlabs.android.dbflow.sql.SQLiteType +import com.raizlabs.android.dbflow.sql.migration.AlterTableMigration +import com.raizlabs.android.dbflow.structure.BaseModel + +/** + * Created by Allan Wang on 2017-05-30. + */ + +@Database(version = NotificationDb.VERSION) +object NotificationDb { + const val NAME = "Notifications" + const val VERSION = 2 +} + +@Migration(version = 2, database = NotificationDb::class) +class NotificationMigration2(modelClass: Class) : + AlterTableMigration(modelClass) { + override fun onPreMigrate() { + super.onPreMigrate() + addColumn(SQLiteType.INTEGER, "epochIm") + L.d { "Added column" } + } +} + +@Table(database = NotificationDb::class, allFields = true, primaryKeyConflict = ConflictAction.REPLACE) +data class NotificationModel( + @PrimaryKey var id: Long = -1L, + var epoch: Long = -1L, + var epochIm: Long = -1L +) : BaseModel() + +fun lastNotificationTime(id: Long): NotificationModel = + (select from NotificationModel::class where (NotificationModel_Table.id eq id)).querySingle() + ?: NotificationModel(id = id) + +fun saveNotificationTime(notificationModel: NotificationModel, callback: (() -> Unit)? = null) { + notificationModel.async save { + L.d { "Fb notification model saved" } + L._d { notificationModel } + callback?.invoke() + } +} diff --git a/app/src/main/kotlin/com/pitchedapps/frost/dbflow/CookiesDb.kt b/app/src/main/kotlin/com/pitchedapps/frost/dbflow/CookiesDb.kt deleted file mode 100644 index 8411b8d7..00000000 --- a/app/src/main/kotlin/com/pitchedapps/frost/dbflow/CookiesDb.kt +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright 2018 Allan Wang - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package com.pitchedapps.frost.dbflow - -import android.os.Parcelable -import com.pitchedapps.frost.utils.L -import com.raizlabs.android.dbflow.annotation.ConflictAction -import com.raizlabs.android.dbflow.annotation.Database -import com.raizlabs.android.dbflow.annotation.PrimaryKey -import com.raizlabs.android.dbflow.annotation.Table -import com.raizlabs.android.dbflow.kotlinextensions.async -import com.raizlabs.android.dbflow.kotlinextensions.delete -import com.raizlabs.android.dbflow.kotlinextensions.eq -import com.raizlabs.android.dbflow.kotlinextensions.from -import com.raizlabs.android.dbflow.kotlinextensions.save -import com.raizlabs.android.dbflow.kotlinextensions.select -import com.raizlabs.android.dbflow.kotlinextensions.where -import com.raizlabs.android.dbflow.structure.BaseModel -import kotlinx.android.parcel.Parcelize -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext - -/** - * Created by Allan Wang on 2017-05-30. - */ - -@Database(version = CookiesDb.VERSION) -object CookiesDb { - const val NAME = "Cookies" - const val VERSION = 2 -} - -@Parcelize -@Table(database = CookiesDb::class, allFields = true, primaryKeyConflict = ConflictAction.REPLACE) -data class CookieModel(@PrimaryKey var id: Long = -1L, var name: String? = null, var cookie: String? = null) : - BaseModel(), Parcelable { - - override fun toString(): String = "CookieModel(${hashCode()})" - - fun toSensitiveString(): String = "CookieModel(id=$id, name=$name, cookie=$cookie)" -} - -fun loadFbCookie(id: Long): CookieModel? = - (select from CookieModel::class where (CookieModel_Table.id eq id)).querySingle() - -fun loadFbCookie(name: String): CookieModel? = - (select from CookieModel::class where (CookieModel_Table.name eq name)).querySingle() - -/** - * Loads cookies sorted by name - */ -fun loadFbCookiesAsync(callback: (cookies: List) -> Unit) { - (select from CookieModel::class).orderBy(CookieModel_Table.name, true).async() - .queryListResultCallback { _, tResult -> callback(tResult) }.execute() -} - -fun loadFbCookiesSync(): List = - (select from CookieModel::class).orderBy(CookieModel_Table.name, true).queryList() - -// TODO temp method until dbflow supports coroutines -suspend fun loadFbCookiesSuspend(): List = withContext(Dispatchers.IO) { - loadFbCookiesSync() -} - -inline fun saveFbCookie(cookie: CookieModel, crossinline callback: (() -> Unit) = {}) { - cookie.async save { - L.d { "Fb cookie saved" } - L._d { cookie.toSensitiveString() } - callback() - } -} - -fun removeCookie(id: Long) { - loadFbCookie(id)?.async?.delete { - L.d { "Fb cookie deleted" } - L._d { id } - } -} diff --git a/app/src/main/kotlin/com/pitchedapps/frost/dbflow/DbUtils.kt b/app/src/main/kotlin/com/pitchedapps/frost/dbflow/DbUtils.kt deleted file mode 100644 index 740fef62..00000000 --- a/app/src/main/kotlin/com/pitchedapps/frost/dbflow/DbUtils.kt +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2018 Allan Wang - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package com.pitchedapps.frost.dbflow - -import android.content.Context -import com.pitchedapps.frost.utils.L -import com.raizlabs.android.dbflow.config.FlowManager -import com.raizlabs.android.dbflow.structure.database.transaction.FastStoreModelTransaction - -/** - * Created by Allan Wang on 2017-05-30. - */ - -object DbUtils { - - fun db(name: String) = FlowManager.getDatabase(name) - fun dbName(name: String) = "$name.db" - fun deleteDatabase(c: Context, name: String) = c.deleteDatabase(dbName(name)) -} - -inline fun List.replace(dbName: String) { - L.d { "Replacing $dbName.db" } - DbUtils.db(dbName).reset() - FastStoreModelTransaction.saveBuilder(FlowManager.getModelAdapter(T::class.java)).addAll(this).build() -} diff --git a/app/src/main/kotlin/com/pitchedapps/frost/dbflow/FbTabsDb.kt b/app/src/main/kotlin/com/pitchedapps/frost/dbflow/FbTabsDb.kt deleted file mode 100644 index 9d330169..00000000 --- a/app/src/main/kotlin/com/pitchedapps/frost/dbflow/FbTabsDb.kt +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright 2018 Allan Wang - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package com.pitchedapps.frost.dbflow - -import com.pitchedapps.frost.facebook.FbItem -import com.pitchedapps.frost.facebook.defaultTabs -import com.pitchedapps.frost.utils.L -import com.raizlabs.android.dbflow.annotation.Database -import com.raizlabs.android.dbflow.annotation.PrimaryKey -import com.raizlabs.android.dbflow.annotation.Table -import com.raizlabs.android.dbflow.kotlinextensions.database -import com.raizlabs.android.dbflow.kotlinextensions.fastSave -import com.raizlabs.android.dbflow.kotlinextensions.from -import com.raizlabs.android.dbflow.kotlinextensions.select -import com.raizlabs.android.dbflow.structure.BaseModel - -/** - * Created by Allan Wang on 2017-05-30. - */ -const val TAB_COUNT = 4 - -@Database(version = FbTabsDb.VERSION) -object FbTabsDb { - const val NAME = "FrostTabs" - const val VERSION = 1 -} - -@Table(database = FbTabsDb::class, allFields = true) -data class FbTabModel(@PrimaryKey var position: Int = -1, var tab: FbItem = FbItem.FEED) : BaseModel() - -/** - * Load tabs synchronously - * Note that tab length should never be a big number anyways - */ -fun loadFbTabs(): List { - val tabs: List? = (select from (FbTabModel::class)).orderBy(FbTabModel_Table.position, true).queryList() - if (tabs?.size == TAB_COUNT) return tabs.map(FbTabModel::tab) - L.d { "No tabs (${tabs?.size}); loading default" } - return defaultTabs() -} - -fun List.save() { - database().beginTransactionAsync(mapIndexed(::FbTabModel).fastSave().build()).execute() -} diff --git a/app/src/main/kotlin/com/pitchedapps/frost/dbflow/NotificationDb.kt b/app/src/main/kotlin/com/pitchedapps/frost/dbflow/NotificationDb.kt deleted file mode 100644 index a054d95e..00000000 --- a/app/src/main/kotlin/com/pitchedapps/frost/dbflow/NotificationDb.kt +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright 2018 Allan Wang - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package com.pitchedapps.frost.dbflow - -import com.pitchedapps.frost.utils.L -import com.raizlabs.android.dbflow.annotation.ConflictAction -import com.raizlabs.android.dbflow.annotation.Database -import com.raizlabs.android.dbflow.annotation.Migration -import com.raizlabs.android.dbflow.annotation.PrimaryKey -import com.raizlabs.android.dbflow.annotation.Table -import com.raizlabs.android.dbflow.kotlinextensions.async -import com.raizlabs.android.dbflow.kotlinextensions.eq -import com.raizlabs.android.dbflow.kotlinextensions.from -import com.raizlabs.android.dbflow.kotlinextensions.save -import com.raizlabs.android.dbflow.kotlinextensions.select -import com.raizlabs.android.dbflow.kotlinextensions.where -import com.raizlabs.android.dbflow.sql.SQLiteType -import com.raizlabs.android.dbflow.sql.migration.AlterTableMigration -import com.raizlabs.android.dbflow.structure.BaseModel - -/** - * Created by Allan Wang on 2017-05-30. - */ - -@Database(version = NotificationDb.VERSION) -object NotificationDb { - const val NAME = "Notifications" - const val VERSION = 2 -} - -@Migration(version = 2, database = NotificationDb::class) -class NotificationMigration2(modelClass: Class) : - AlterTableMigration(modelClass) { - override fun onPreMigrate() { - super.onPreMigrate() - addColumn(SQLiteType.INTEGER, "epochIm") - L.d { "Added column" } - } -} - -@Table(database = NotificationDb::class, allFields = true, primaryKeyConflict = ConflictAction.REPLACE) -data class NotificationModel( - @PrimaryKey var id: Long = -1L, - var epoch: Long = -1L, - var epochIm: Long = -1L -) : BaseModel() - -fun lastNotificationTime(id: Long): NotificationModel = - (select from NotificationModel::class where (NotificationModel_Table.id eq id)).querySingle() - ?: NotificationModel(id = id) - -fun saveNotificationTime(notificationModel: NotificationModel, callback: (() -> Unit)? = null) { - notificationModel.async save { - L.d { "Fb notification model saved" } - L._d { notificationModel } - callback?.invoke() - } -} diff --git a/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbCookie.kt b/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbCookie.kt index 5683526a..349b415c 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbCookie.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbCookie.kt @@ -19,10 +19,10 @@ package com.pitchedapps.frost.facebook import android.app.Activity import android.content.Context import android.webkit.CookieManager -import com.pitchedapps.frost.dbflow.CookieModel -import com.pitchedapps.frost.dbflow.loadFbCookie -import com.pitchedapps.frost.dbflow.removeCookie -import com.pitchedapps.frost.dbflow.saveFbCookie +import com.pitchedapps.frost.db.CookieModel +import com.pitchedapps.frost.db.loadFbCookie +import com.pitchedapps.frost.db.removeCookie +import com.pitchedapps.frost.db.saveFbCookie import com.pitchedapps.frost.utils.L import com.pitchedapps.frost.utils.Prefs import com.pitchedapps.frost.utils.cookies diff --git a/app/src/main/kotlin/com/pitchedapps/frost/facebook/parsers/FrostParser.kt b/app/src/main/kotlin/com/pitchedapps/frost/facebook/parsers/FrostParser.kt index 5709bb9f..57e7cc94 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/facebook/parsers/FrostParser.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/facebook/parsers/FrostParser.kt @@ -16,7 +16,7 @@ */ package com.pitchedapps.frost.facebook.parsers -import com.pitchedapps.frost.dbflow.CookieModel +import com.pitchedapps.frost.db.CookieModel import com.pitchedapps.frost.facebook.FB_CSS_URL_MATCHER import com.pitchedapps.frost.facebook.formattedFbUrl import com.pitchedapps.frost.facebook.get diff --git a/app/src/main/kotlin/com/pitchedapps/frost/facebook/parsers/MessageParser.kt b/app/src/main/kotlin/com/pitchedapps/frost/facebook/parsers/MessageParser.kt index f05c42e9..b2b59234 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/facebook/parsers/MessageParser.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/facebook/parsers/MessageParser.kt @@ -16,7 +16,7 @@ */ package com.pitchedapps.frost.facebook.parsers -import com.pitchedapps.frost.dbflow.CookieModel +import com.pitchedapps.frost.db.CookieModel import com.pitchedapps.frost.facebook.FB_EPOCH_MATCHER import com.pitchedapps.frost.facebook.FB_MESSAGE_NOTIF_ID_MATCHER import com.pitchedapps.frost.facebook.FbItem diff --git a/app/src/main/kotlin/com/pitchedapps/frost/facebook/parsers/NotifParser.kt b/app/src/main/kotlin/com/pitchedapps/frost/facebook/parsers/NotifParser.kt index b8aa899b..0474e35e 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/facebook/parsers/NotifParser.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/facebook/parsers/NotifParser.kt @@ -16,7 +16,7 @@ */ package com.pitchedapps.frost.facebook.parsers -import com.pitchedapps.frost.dbflow.CookieModel +import com.pitchedapps.frost.db.CookieModel import com.pitchedapps.frost.facebook.FB_EPOCH_MATCHER import com.pitchedapps.frost.facebook.FB_NOTIF_ID_MATCHER import com.pitchedapps.frost.facebook.FbItem diff --git a/app/src/main/kotlin/com/pitchedapps/frost/services/FrostNotifications.kt b/app/src/main/kotlin/com/pitchedapps/frost/services/FrostNotifications.kt index f66f77e2..994f9a18 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/services/FrostNotifications.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/services/FrostNotifications.kt @@ -31,9 +31,9 @@ import ca.allanwang.kau.utils.string import com.pitchedapps.frost.BuildConfig import com.pitchedapps.frost.R import com.pitchedapps.frost.activities.FrostWebActivity -import com.pitchedapps.frost.dbflow.CookieModel -import com.pitchedapps.frost.dbflow.NotificationModel -import com.pitchedapps.frost.dbflow.lastNotificationTime +import com.pitchedapps.frost.db.CookieModel +import com.pitchedapps.frost.db.NotificationModel +import com.pitchedapps.frost.db.lastNotificationTime import com.pitchedapps.frost.enums.OverlayContext import com.pitchedapps.frost.facebook.FbItem import com.pitchedapps.frost.facebook.parsers.FrostParser diff --git a/app/src/main/kotlin/com/pitchedapps/frost/services/NotificationService.kt b/app/src/main/kotlin/com/pitchedapps/frost/services/NotificationService.kt index b1e0ac8c..a895900f 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/services/NotificationService.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/services/NotificationService.kt @@ -21,8 +21,8 @@ import androidx.core.app.NotificationManagerCompat import ca.allanwang.kau.utils.string import com.pitchedapps.frost.BuildConfig import com.pitchedapps.frost.R -import com.pitchedapps.frost.dbflow.CookieModel -import com.pitchedapps.frost.dbflow.loadFbCookiesSync +import com.pitchedapps.frost.db.CookieModel +import com.pitchedapps.frost.db.loadFbCookiesSync import com.pitchedapps.frost.utils.L import com.pitchedapps.frost.utils.Prefs import com.pitchedapps.frost.utils.frostEvent diff --git a/app/src/main/kotlin/com/pitchedapps/frost/settings/Notifications.kt b/app/src/main/kotlin/com/pitchedapps/frost/settings/Notifications.kt index 0200f109..774b0e7f 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/settings/Notifications.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/settings/Notifications.kt @@ -29,8 +29,8 @@ import ca.allanwang.kau.utils.string import com.pitchedapps.frost.BuildConfig import com.pitchedapps.frost.R import com.pitchedapps.frost.activities.SettingsActivity -import com.pitchedapps.frost.dbflow.NotificationModel -import com.pitchedapps.frost.dbflow.loadFbCookiesAsync +import com.pitchedapps.frost.db.NotificationModel +import com.pitchedapps.frost.db.loadFbCookiesAsync import com.pitchedapps.frost.services.fetchNotifications import com.pitchedapps.frost.services.scheduleNotifications import com.pitchedapps.frost.utils.Prefs diff --git a/app/src/main/kotlin/com/pitchedapps/frost/utils/Downloader.kt b/app/src/main/kotlin/com/pitchedapps/frost/utils/Downloader.kt index f4baa242..254297a6 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/utils/Downloader.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/utils/Downloader.kt @@ -29,7 +29,7 @@ import ca.allanwang.kau.utils.showAppInfo import ca.allanwang.kau.utils.string import ca.allanwang.kau.utils.toast import com.pitchedapps.frost.R -import com.pitchedapps.frost.dbflow.loadFbCookie +import com.pitchedapps.frost.db.loadFbCookie import com.pitchedapps.frost.facebook.USER_AGENT_BASIC /** diff --git a/app/src/main/kotlin/com/pitchedapps/frost/utils/Utils.kt b/app/src/main/kotlin/com/pitchedapps/frost/utils/Utils.kt index 4410b26e..1222e93b 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/utils/Utils.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/utils/Utils.kt @@ -62,7 +62,7 @@ import com.pitchedapps.frost.activities.TabCustomizerActivity import com.pitchedapps.frost.activities.WebOverlayActivity import com.pitchedapps.frost.activities.WebOverlayActivityBase import com.pitchedapps.frost.activities.WebOverlayBasicActivity -import com.pitchedapps.frost.dbflow.CookieModel +import com.pitchedapps.frost.db.CookieModel import com.pitchedapps.frost.facebook.FACEBOOK_COM import com.pitchedapps.frost.facebook.FBCDN_NET import com.pitchedapps.frost.facebook.FbCookie diff --git a/app/src/main/kotlin/com/pitchedapps/frost/views/AccountItem.kt b/app/src/main/kotlin/com/pitchedapps/frost/views/AccountItem.kt index d7a7de0e..8c7435ea 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/views/AccountItem.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/views/AccountItem.kt @@ -33,7 +33,7 @@ import com.bumptech.glide.request.RequestListener import com.bumptech.glide.request.target.Target import com.mikepenz.google_material_typeface_library.GoogleMaterial import com.pitchedapps.frost.R -import com.pitchedapps.frost.dbflow.CookieModel +import com.pitchedapps.frost.db.CookieModel import com.pitchedapps.frost.facebook.profilePictureUrl import com.pitchedapps.frost.glide.FrostGlide import com.pitchedapps.frost.glide.GlideApp diff --git a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostJSI.kt b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostJSI.kt index 50a5e2e1..663acfbb 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostJSI.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostJSI.kt @@ -21,7 +21,7 @@ import android.webkit.JavascriptInterface import com.pitchedapps.frost.activities.MainActivity import com.pitchedapps.frost.contracts.MainActivityContract import com.pitchedapps.frost.contracts.VideoViewHolder -import com.pitchedapps.frost.dbflow.CookieModel +import com.pitchedapps.frost.db.CookieModel import com.pitchedapps.frost.facebook.FbCookie import com.pitchedapps.frost.utils.L import com.pitchedapps.frost.utils.Prefs diff --git a/app/src/main/kotlin/com/pitchedapps/frost/web/LoginWebView.kt b/app/src/main/kotlin/com/pitchedapps/frost/web/LoginWebView.kt index c21ce93b..11a53c36 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/web/LoginWebView.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/web/LoginWebView.kt @@ -29,7 +29,7 @@ import android.webkit.WebView import ca.allanwang.kau.utils.fadeIn import ca.allanwang.kau.utils.isVisible import ca.allanwang.kau.utils.launchMain -import com.pitchedapps.frost.dbflow.CookieModel +import com.pitchedapps.frost.db.CookieModel import com.pitchedapps.frost.facebook.FB_LOGIN_URL import com.pitchedapps.frost.facebook.FB_USER_MATCHER import com.pitchedapps.frost.facebook.FbCookie diff --git a/app/src/schemas/com.pitchedapps.frost.db.FrostPrivateDatabase/1.json b/app/src/schemas/com.pitchedapps.frost.db.FrostPrivateDatabase/1.json new file mode 100644 index 00000000..7bc2e5e9 --- /dev/null +++ b/app/src/schemas/com.pitchedapps.frost.db.FrostPrivateDatabase/1.json @@ -0,0 +1,45 @@ +{ + "formatVersion": 1, + "database": { + "version": 1, + "identityHash": "6af67964d00193c6a3aa2a20200ea0ea", + "entities": [ + { + "tableName": "cookies", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `cookie` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cookie", + "columnName": "cookie", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"6af67964d00193c6a3aa2a20200ea0ea\")" + ] + } +} \ No newline at end of file diff --git a/app/src/schemas/com.pitchedapps.frost.db.FrostPublicDatabase/1.json b/app/src/schemas/com.pitchedapps.frost.db.FrostPublicDatabase/1.json new file mode 100644 index 00000000..687d0bc1 --- /dev/null +++ b/app/src/schemas/com.pitchedapps.frost.db.FrostPublicDatabase/1.json @@ -0,0 +1,39 @@ +{ + "formatVersion": 1, + "database": { + "version": 1, + "identityHash": "fde868470836ff9230f1d406922d7563", + "entities": [ + { + "tableName": "tabs", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`position` INTEGER NOT NULL, `tab` TEXT NOT NULL, PRIMARY KEY(`position`))", + "fields": [ + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "tab", + "columnName": "tab", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "position" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"fde868470836ff9230f1d406922d7563\")" + ] + } +} \ No newline at end of file diff --git a/build.gradle b/build.gradle index 64e324d2..22a0102b 100644 --- a/build.gradle +++ b/build.gradle @@ -8,7 +8,7 @@ buildscript { dependencies { classpath "ca.allanwang:kau:${KAU}" - classpath 'com.android.tools.build.jetifier:jetifier-processor:1.0.0-beta02' +// classpath 'com.android.tools.build.jetifier:jetifier-processor:1.0.0-beta02' classpath "com.android.tools.build:gradle:${ANDROID_GRADLE}" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${KOTLIN}" classpath "com.bugsnag:bugsnag-android-gradle-plugin:${BUGSNAG_PLUGIN}" diff --git a/docs/Changelog.md b/docs/Changelog.md index e58b8f5f..4d90b7fb 100644 --- a/docs/Changelog.md +++ b/docs/Changelog.md @@ -1,5 +1,9 @@ # Changelog +## v2.2.3 +* Add ability to hide stories +* Remove fbclid from urls + ## v2.2.2 * New marketplace shortcut * Fix crash when internet disconnects (may still need app restart) diff --git a/gradle.properties b/gradle.properties index f95fed2b..98f29dbe 100644 --- a/gradle.properties +++ b/gradle.properties @@ -15,7 +15,7 @@ APP_ID=Frost APP_GROUP=com.pitchedapps KAU=4.0.0-alpha02 -KOTLIN=1.3.11 +KOTLIN=1.3.21 # https://mvnrepository.com/artifact/com.android.tools.build/gradle?repo=google ANDROID_GRADLE=3.2.1 @@ -57,6 +57,8 @@ MATERIAL_DRAWER_KT=2.0.1 OKHTTP=3.12.1 # http://robolectric.org/getting-started/ ROBOELECTRIC=4.1 +# https://developer.android.com/jetpack/androidx/releases/room +ROOM=2.1.0-alpha04 # https://github.com/davemorrissey/subsampling-scale-image-view#quick-start SCALE_IMAGE_VIEW=3.10.0 # https://github.com/umano/AndroidSlidingUpPanel#importing-the-library -- cgit v1.2.3 From ee9a9a696420d5da05b4f306898fff29e9abe1a4 Mon Sep 17 00:00:00 2001 From: Allan Wang Date: Tue, 5 Mar 2019 18:44:18 -0500 Subject: Create working dao without suspension --- .../kotlin/com/pitchedapps/frost/db/FrostDatabaseTest.kt | 12 ++++++------ app/src/main/kotlin/com/pitchedapps/frost/FrostApp.kt | 8 -------- app/src/main/kotlin/com/pitchedapps/frost/db/CookiesDb.kt | 8 ++++---- app/src/main/kotlin/com/pitchedapps/frost/db/FbTabsDb.kt | 8 ++++---- 4 files changed, 14 insertions(+), 22 deletions(-) diff --git a/app/src/androidTest/kotlin/com/pitchedapps/frost/db/FrostDatabaseTest.kt b/app/src/androidTest/kotlin/com/pitchedapps/frost/db/FrostDatabaseTest.kt index 6b68e804..2d94ed97 100644 --- a/app/src/androidTest/kotlin/com/pitchedapps/frost/db/FrostDatabaseTest.kt +++ b/app/src/androidTest/kotlin/com/pitchedapps/frost/db/FrostDatabaseTest.kt @@ -5,10 +5,11 @@ import androidx.room.Room import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import kotlinx.coroutines.runBlocking -import org.junit.After -import org.junit.Before -import org.junit.Test +import org.junit.Rule import org.junit.runner.RunWith +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test import kotlin.test.assertEquals @RunWith(AndroidJUnit4::class) @@ -16,9 +17,8 @@ class FrostDatabaseTest { private lateinit var db: FrostDatabase - @Before + @BeforeTest fun before() { - val context = ApplicationProvider.getApplicationContext() val privateDb = Room.inMemoryDatabaseBuilder( context, FrostPrivateDatabase::class.java @@ -29,7 +29,7 @@ class FrostDatabaseTest { db = FrostDatabase(privateDb, publicDb) } - @After + @AfterTest fun after() { db.close() } diff --git a/app/src/main/kotlin/com/pitchedapps/frost/FrostApp.kt b/app/src/main/kotlin/com/pitchedapps/frost/FrostApp.kt index a7cd2777..ba92a345 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/FrostApp.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/FrostApp.kt @@ -44,7 +44,6 @@ import com.raizlabs.android.dbflow.config.DatabaseConfig import com.raizlabs.android.dbflow.config.FlowConfig import com.raizlabs.android.dbflow.config.FlowManager import com.raizlabs.android.dbflow.runtime.ContentResolverNotifier -import com.squareup.sqldelight.android.AndroidSqliteDriver import java.util.Random import kotlin.reflect.KClass @@ -115,13 +114,6 @@ class FrostApp : Application() { .thumbnail(old).into(imageView) } }) - val driver = AndroidSqliteDriver(Database.Schema, this, "test.db") - val db = Database(driver) - db.transaction { - db.frostQueries.selectAll().executeAsList().forEach { - it.id - } - } if (BuildConfig.DEBUG) registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks { override fun onActivityPaused(activity: Activity) {} diff --git a/app/src/main/kotlin/com/pitchedapps/frost/db/CookiesDb.kt b/app/src/main/kotlin/com/pitchedapps/frost/db/CookiesDb.kt index 9deb57da..0b8283fd 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/db/CookiesDb.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/db/CookiesDb.kt @@ -60,16 +60,16 @@ data class CookieEntity( interface CookieDao { @Query("SELECT * FROM cookies") - suspend fun selectAll(): List + fun selectAll(): List @Query("SELECT * FROM cookies WHERE id = :id") - suspend fun selectById(id: Long): CookieEntity? + fun selectById(id: Long): CookieEntity? @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun insertCookie(cookie: CookieEntity) + fun insertCookie(cookie: CookieEntity) @Query("DELETE FROM cookies WHERE id = :id") - suspend fun deleteById(id: Long) + fun deleteById(id: Long) } @Database(version = CookiesDb.VERSION) diff --git a/app/src/main/kotlin/com/pitchedapps/frost/db/FbTabsDb.kt b/app/src/main/kotlin/com/pitchedapps/frost/db/FbTabsDb.kt index 2e2a9d62..ab01e025 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/db/FbTabsDb.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/db/FbTabsDb.kt @@ -45,16 +45,16 @@ data class FbTabEntity(@androidx.room.PrimaryKey var position: Int, var tab: FbI interface FbTabDao { @Query("SELECT * FROM tabs ORDER BY position ASC") - suspend fun _selectAll(): List + fun _selectAll(): List @Query("DELETE FROM tabs") - suspend fun _deleteAll() + fun _deleteAll() @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun _insertAll(items: List) + fun _insertAll(items: List) @Transaction - suspend fun _save(items: List) { + fun _save(items: List) { _deleteAll() _insertAll(items) } -- cgit v1.2.3 From e82b74a687f5b5bff6f7cc16e1ba504583a8db32 Mon Sep 17 00:00:00 2001 From: Allan Wang Date: Tue, 5 Mar 2019 18:46:42 -0500 Subject: Move helper functions outside of interface --- .../main/kotlin/com/pitchedapps/frost/db/FbTabsDb.kt | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/app/src/main/kotlin/com/pitchedapps/frost/db/FbTabsDb.kt b/app/src/main/kotlin/com/pitchedapps/frost/db/FbTabsDb.kt index ab01e025..4fc38ec9 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/db/FbTabsDb.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/db/FbTabsDb.kt @@ -58,19 +58,19 @@ interface FbTabDao { _deleteAll() _insertAll(items) } +} -// suspend fun save(items: List) { -// _save((items.takeIf { it.isNotEmpty() } ?: defaultTabs()).mapIndexed { index, fbItem -> -// FbTabEntity( -// index, -// fbItem -// ) -// }) -// } -// -// suspend fun selectAll(): List = _selectAll().map { it.tab }.takeIf { it.isNotEmpty() } ?: defaultTabs() +suspend fun FbTabDao.save(items: List) { + _save((items.takeIf { it.isNotEmpty() } ?: defaultTabs()).mapIndexed { index, fbItem -> + FbTabEntity( + index, + fbItem + ) + }) } +suspend fun FbTabDao.selectAll(): List = _selectAll().map { it.tab }.takeIf { it.isNotEmpty() } ?: defaultTabs() + object FbItemConverter { @androidx.room.TypeConverter @JvmStatic -- cgit v1.2.3 From 8841728780438444bf51f1a2c3b0d961e49908d2 Mon Sep 17 00:00:00 2001 From: Allan Wang Date: Tue, 5 Mar 2019 18:52:47 -0500 Subject: Change dependencies and add back suspensions --- app/build.gradle | 4 ++-- app/src/main/kotlin/com/pitchedapps/frost/db/CookiesDb.kt | 8 ++++---- app/src/main/kotlin/com/pitchedapps/frost/db/FbTabsDb.kt | 8 ++++---- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 4c22ab97..82f0c6b1 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -258,8 +258,8 @@ dependencies { implementation "com.sothree.slidinguppanel:library:${SLIDING_PANEL}" implementation "androidx.room:room-coroutines:${ROOM}" - implementation "android.arch.persistence.room:runtime:${ROOM}" - kapt "android.arch.persistence.room:compiler:${ROOM}" + implementation "androidx.room:room-runtime:${ROOM}" + kapt "androidx.room:room-compiler:${ROOM}" testImplementation "androidx.room:room-testing:${ROOM}" } diff --git a/app/src/main/kotlin/com/pitchedapps/frost/db/CookiesDb.kt b/app/src/main/kotlin/com/pitchedapps/frost/db/CookiesDb.kt index 0b8283fd..9deb57da 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/db/CookiesDb.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/db/CookiesDb.kt @@ -60,16 +60,16 @@ data class CookieEntity( interface CookieDao { @Query("SELECT * FROM cookies") - fun selectAll(): List + suspend fun selectAll(): List @Query("SELECT * FROM cookies WHERE id = :id") - fun selectById(id: Long): CookieEntity? + suspend fun selectById(id: Long): CookieEntity? @Insert(onConflict = OnConflictStrategy.REPLACE) - fun insertCookie(cookie: CookieEntity) + suspend fun insertCookie(cookie: CookieEntity) @Query("DELETE FROM cookies WHERE id = :id") - fun deleteById(id: Long) + suspend fun deleteById(id: Long) } @Database(version = CookiesDb.VERSION) diff --git a/app/src/main/kotlin/com/pitchedapps/frost/db/FbTabsDb.kt b/app/src/main/kotlin/com/pitchedapps/frost/db/FbTabsDb.kt index 4fc38ec9..ff64f5eb 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/db/FbTabsDb.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/db/FbTabsDb.kt @@ -45,16 +45,16 @@ data class FbTabEntity(@androidx.room.PrimaryKey var position: Int, var tab: FbI interface FbTabDao { @Query("SELECT * FROM tabs ORDER BY position ASC") - fun _selectAll(): List + suspend fun _selectAll(): List @Query("DELETE FROM tabs") - fun _deleteAll() + suspend fun _deleteAll() @Insert(onConflict = OnConflictStrategy.REPLACE) - fun _insertAll(items: List) + suspend fun _insertAll(items: List) @Transaction - fun _save(items: List) { + suspend fun _save(items: List) { _deleteAll() _insertAll(items) } -- cgit v1.2.3 From b5d442ba3c86500b23d5bff8f1eb80ab51d1ccfa Mon Sep 17 00:00:00 2001 From: Allan Wang Date: Tue, 5 Mar 2019 19:13:57 -0500 Subject: Add more cookie db tests --- .../com/pitchedapps/frost/db/FrostDatabaseTest.kt | 41 ++++++++++++++++++++-- .../kotlin/com/pitchedapps/frost/db/FbTabsDb.kt | 31 +++++++++------- 2 files changed, 58 insertions(+), 14 deletions(-) diff --git a/app/src/androidTest/kotlin/com/pitchedapps/frost/db/FrostDatabaseTest.kt b/app/src/androidTest/kotlin/com/pitchedapps/frost/db/FrostDatabaseTest.kt index 2d94ed97..dcc96c2d 100644 --- a/app/src/androidTest/kotlin/com/pitchedapps/frost/db/FrostDatabaseTest.kt +++ b/app/src/androidTest/kotlin/com/pitchedapps/frost/db/FrostDatabaseTest.kt @@ -5,7 +5,6 @@ import androidx.room.Room import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import kotlinx.coroutines.runBlocking -import org.junit.Rule import org.junit.runner.RunWith import kotlin.test.AfterTest import kotlin.test.BeforeTest @@ -35,7 +34,7 @@ class FrostDatabaseTest { } @Test - fun basic() { + fun basicCookie() { val cookie = CookieEntity(id = 1234L, name = "testName", cookie = "testCookie") runBlocking { db.cookieDao().insertCookie(cookie) @@ -43,4 +42,42 @@ class FrostDatabaseTest { assertEquals(listOf(cookie), cookies, "Cookie mismatch") } } + + @Test + fun deleteCookie() { + val cookie = CookieEntity(id = 1234L, name = "testName", cookie = "testCookie") + + runBlocking { + db.cookieDao().insertCookie(cookie) + db.cookieDao().deleteById(cookie.id + 1) + assertEquals( + listOf(cookie), + db.cookieDao().selectAll(), + "Cookie list should be the same after inexistent deletion" + ) + db.cookieDao().deleteById(cookie.id) + assertEquals(emptyList(), db.cookieDao().selectAll(), "Cookie list should be empty after deletion") + } + } + + @Test + fun insertCookie() { + val cookie = CookieEntity(id = 1234L, name = "testName", cookie = "testCookie") + runBlocking { + db.cookieDao().insertCookie(cookie) + assertEquals(listOf(cookie), db.cookieDao().selectAll(), "Cookie insertion failed") + db.cookieDao().insertCookie(cookie.copy(name = "testName2")) + assertEquals( + listOf(cookie.copy(name = "testName2")), + db.cookieDao().selectAll(), + "Cookie replacement failed" + ) + db.cookieDao().insertCookie(cookie.copy(id = 123L)) + assertEquals( + setOf(cookie.copy(id = 123L), cookie.copy(name = "testName2")), + db.cookieDao().selectAll().toSet(), + "New cookie insertion failed" + ) + } + } } \ No newline at end of file diff --git a/app/src/main/kotlin/com/pitchedapps/frost/db/FbTabsDb.kt b/app/src/main/kotlin/com/pitchedapps/frost/db/FbTabsDb.kt index ff64f5eb..44e62938 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/db/FbTabsDb.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/db/FbTabsDb.kt @@ -33,6 +33,8 @@ import com.raizlabs.android.dbflow.kotlinextensions.fastSave import com.raizlabs.android.dbflow.kotlinextensions.from import com.raizlabs.android.dbflow.kotlinextensions.select import com.raizlabs.android.dbflow.structure.BaseModel +import kotlinx.coroutines.NonCancellable +import kotlinx.coroutines.withContext /** * Created by Allan Wang on 2017-05-30. @@ -52,21 +54,26 @@ interface FbTabDao { @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun _insertAll(items: List) - - @Transaction - suspend fun _save(items: List) { - _deleteAll() - _insertAll(items) - } } +/** + * Saving tabs operates by deleting all db items and saving the new list. + * Transactions can't be done with suspensions in room as switching threads during the process + * may result in a deadlock. + * In this case, there may be a chance that the 'transaction' completes partially, + * but we'll just fallback to the default anyways. + */ suspend fun FbTabDao.save(items: List) { - _save((items.takeIf { it.isNotEmpty() } ?: defaultTabs()).mapIndexed { index, fbItem -> - FbTabEntity( - index, - fbItem - ) - }) + withContext(NonCancellable) { + _deleteAll() + val entities = (items.takeIf { it.isNotEmpty() } ?: defaultTabs()).mapIndexed { index, fbItem -> + FbTabEntity( + index, + fbItem + ) + } + _insertAll(entities) + } } suspend fun FbTabDao.selectAll(): List = _selectAll().map { it.tab }.takeIf { it.isNotEmpty() } ?: defaultTabs() -- cgit v1.2.3 From cd0e7549d4a9876923649b83bbc82dab9caa0232 Mon Sep 17 00:00:00 2001 From: Allan Wang Date: Tue, 5 Mar 2019 19:21:17 -0500 Subject: Split db tests per dao --- .../kotlin/com/pitchedapps/frost/db/BaseDbTest.kt | 32 +++++++++ .../com/pitchedapps/frost/db/CookieDbTest.kt | 67 +++++++++++++++++ .../com/pitchedapps/frost/db/FrostDatabaseTest.kt | 83 ---------------------- 3 files changed, 99 insertions(+), 83 deletions(-) create mode 100644 app/src/androidTest/kotlin/com/pitchedapps/frost/db/BaseDbTest.kt create mode 100644 app/src/androidTest/kotlin/com/pitchedapps/frost/db/CookieDbTest.kt delete mode 100644 app/src/androidTest/kotlin/com/pitchedapps/frost/db/FrostDatabaseTest.kt diff --git a/app/src/androidTest/kotlin/com/pitchedapps/frost/db/BaseDbTest.kt b/app/src/androidTest/kotlin/com/pitchedapps/frost/db/BaseDbTest.kt new file mode 100644 index 00000000..6621ea95 --- /dev/null +++ b/app/src/androidTest/kotlin/com/pitchedapps/frost/db/BaseDbTest.kt @@ -0,0 +1,32 @@ +package com.pitchedapps.frost.db + +import android.content.Context +import androidx.room.Room +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.runner.RunWith +import kotlin.test.AfterTest +import kotlin.test.BeforeTest + +@RunWith(AndroidJUnit4::class) +abstract class BaseDbTest { + + protected lateinit var db: FrostDatabase + + @BeforeTest + fun before() { + val context = ApplicationProvider.getApplicationContext() + val privateDb = Room.inMemoryDatabaseBuilder( + context, FrostPrivateDatabase::class.java + ).build() + val publicDb = Room.inMemoryDatabaseBuilder( + context, FrostPublicDatabase::class.java + ).build() + db = FrostDatabase(privateDb, publicDb) + } + + @AfterTest + fun after() { + db.close() + } +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/pitchedapps/frost/db/CookieDbTest.kt b/app/src/androidTest/kotlin/com/pitchedapps/frost/db/CookieDbTest.kt new file mode 100644 index 00000000..351490e2 --- /dev/null +++ b/app/src/androidTest/kotlin/com/pitchedapps/frost/db/CookieDbTest.kt @@ -0,0 +1,67 @@ +package com.pitchedapps.frost.db + +import kotlinx.coroutines.runBlocking +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull + +class CookieDbTest : BaseDbTest() { + + @Test + fun basicCookie() { + val cookie = CookieEntity(id = 1234L, name = "testName", cookie = "testCookie") + runBlocking { + db.cookieDao().insertCookie(cookie) + val cookies = db.cookieDao().selectAll() + assertEquals(listOf(cookie), cookies, "Cookie mismatch") + } + } + + @Test + fun deleteCookie() { + val cookie = CookieEntity(id = 1234L, name = "testName", cookie = "testCookie") + + runBlocking { + db.cookieDao().insertCookie(cookie) + db.cookieDao().deleteById(cookie.id + 1) + assertEquals( + listOf(cookie), + db.cookieDao().selectAll(), + "Cookie list should be the same after inexistent deletion" + ) + db.cookieDao().deleteById(cookie.id) + assertEquals(emptyList(), db.cookieDao().selectAll(), "Cookie list should be empty after deletion") + } + } + + @Test + fun insertReplaceCookie() { + val cookie = CookieEntity(id = 1234L, name = "testName", cookie = "testCookie") + runBlocking { + db.cookieDao().insertCookie(cookie) + assertEquals(listOf(cookie), db.cookieDao().selectAll(), "Cookie insertion failed") + db.cookieDao().insertCookie(cookie.copy(name = "testName2")) + assertEquals( + listOf(cookie.copy(name = "testName2")), + db.cookieDao().selectAll(), + "Cookie replacement failed" + ) + db.cookieDao().insertCookie(cookie.copy(id = 123L)) + assertEquals( + setOf(cookie.copy(id = 123L), cookie.copy(name = "testName2")), + db.cookieDao().selectAll().toSet(), + "New cookie insertion failed" + ) + } + } + + @Test + fun selectCookie() { + val cookie = CookieEntity(id = 1234L, name = "testName", cookie = "testCookie") + runBlocking { + db.cookieDao().insertCookie(cookie) + assertEquals(cookie, db.cookieDao().selectById(cookie.id), "Cookie selection failed") + assertNull(db.cookieDao().selectById(cookie.id + 1), "Inexistent cookie selection failed") + } + } +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/pitchedapps/frost/db/FrostDatabaseTest.kt b/app/src/androidTest/kotlin/com/pitchedapps/frost/db/FrostDatabaseTest.kt deleted file mode 100644 index dcc96c2d..00000000 --- a/app/src/androidTest/kotlin/com/pitchedapps/frost/db/FrostDatabaseTest.kt +++ /dev/null @@ -1,83 +0,0 @@ -package com.pitchedapps.frost.db - -import android.content.Context -import androidx.room.Room -import androidx.test.core.app.ApplicationProvider -import androidx.test.ext.junit.runners.AndroidJUnit4 -import kotlinx.coroutines.runBlocking -import org.junit.runner.RunWith -import kotlin.test.AfterTest -import kotlin.test.BeforeTest -import kotlin.test.Test -import kotlin.test.assertEquals - -@RunWith(AndroidJUnit4::class) -class FrostDatabaseTest { - - private lateinit var db: FrostDatabase - - @BeforeTest - fun before() { - val context = ApplicationProvider.getApplicationContext() - val privateDb = Room.inMemoryDatabaseBuilder( - context, FrostPrivateDatabase::class.java - ).build() - val publicDb = Room.inMemoryDatabaseBuilder( - context, FrostPublicDatabase::class.java - ).build() - db = FrostDatabase(privateDb, publicDb) - } - - @AfterTest - fun after() { - db.close() - } - - @Test - fun basicCookie() { - val cookie = CookieEntity(id = 1234L, name = "testName", cookie = "testCookie") - runBlocking { - db.cookieDao().insertCookie(cookie) - val cookies = db.cookieDao().selectAll() - assertEquals(listOf(cookie), cookies, "Cookie mismatch") - } - } - - @Test - fun deleteCookie() { - val cookie = CookieEntity(id = 1234L, name = "testName", cookie = "testCookie") - - runBlocking { - db.cookieDao().insertCookie(cookie) - db.cookieDao().deleteById(cookie.id + 1) - assertEquals( - listOf(cookie), - db.cookieDao().selectAll(), - "Cookie list should be the same after inexistent deletion" - ) - db.cookieDao().deleteById(cookie.id) - assertEquals(emptyList(), db.cookieDao().selectAll(), "Cookie list should be empty after deletion") - } - } - - @Test - fun insertCookie() { - val cookie = CookieEntity(id = 1234L, name = "testName", cookie = "testCookie") - runBlocking { - db.cookieDao().insertCookie(cookie) - assertEquals(listOf(cookie), db.cookieDao().selectAll(), "Cookie insertion failed") - db.cookieDao().insertCookie(cookie.copy(name = "testName2")) - assertEquals( - listOf(cookie.copy(name = "testName2")), - db.cookieDao().selectAll(), - "Cookie replacement failed" - ) - db.cookieDao().insertCookie(cookie.copy(id = 123L)) - assertEquals( - setOf(cookie.copy(id = 123L), cookie.copy(name = "testName2")), - db.cookieDao().selectAll().toSet(), - "New cookie insertion failed" - ) - } - } -} \ No newline at end of file -- cgit v1.2.3 From 5c4400975450c9739f0986561075983e08afae89 Mon Sep 17 00:00:00 2001 From: Allan Wang Date: Tue, 5 Mar 2019 20:03:55 -0500 Subject: Add fb tab test --- .../com/pitchedapps/frost/db/FbTabsDbTest.kt | 32 ++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 app/src/androidTest/kotlin/com/pitchedapps/frost/db/FbTabsDbTest.kt diff --git a/app/src/androidTest/kotlin/com/pitchedapps/frost/db/FbTabsDbTest.kt b/app/src/androidTest/kotlin/com/pitchedapps/frost/db/FbTabsDbTest.kt new file mode 100644 index 00000000..a2dce692 --- /dev/null +++ b/app/src/androidTest/kotlin/com/pitchedapps/frost/db/FbTabsDbTest.kt @@ -0,0 +1,32 @@ +package com.pitchedapps.frost.db + +import com.pitchedapps.frost.facebook.FbItem +import com.pitchedapps.frost.facebook.defaultTabs +import kotlinx.coroutines.runBlocking +import kotlin.test.Test +import kotlin.test.assertEquals + +class FbTabsDbTest : BaseDbTest() { + + /** + * Note that order is also preserved here + */ + @Test + fun save() { + val tabs = listOf(FbItem.ACTIVITY_LOG, FbItem.BIRTHDAYS, FbItem.EVENTS, FbItem.MARKETPLACE, FbItem.ACTIVITY_LOG) + runBlocking { + db.tabDao().save(tabs) + assertEquals(tabs, db.tabDao().selectAll(), "Tab saving failed") + val newTabs = listOf(FbItem.PAGES, FbItem.MENU) + db.tabDao().save(newTabs) + assertEquals(newTabs, db.tabDao().selectAll(), "Tab saving does not delete preexisting items") + } + } + + @Test + fun defaultRetrieve() { + runBlocking { + assertEquals(defaultTabs(), db.tabDao().selectAll(), "Default retrieval failed") + } + } +} \ No newline at end of file -- cgit v1.2.3 From 65bb9233b2a0d8734c1d13e8f3a01bee0f6c3b17 Mon Sep 17 00:00:00 2001 From: Allan Wang Date: Tue, 5 Mar 2019 21:06:24 -0500 Subject: Convert fbcookies to room entities --- app/build.gradle | 6 +-- .../main/kotlin/com/pitchedapps/frost/FrostApp.kt | 3 ++ .../kotlin/com/pitchedapps/frost/StartActivity.kt | 19 ++++++-- .../frost/activities/BaseMainActivity.kt | 54 +++++++++++++--------- .../pitchedapps/frost/activities/LoginActivity.kt | 15 +++--- .../kotlin/com/pitchedapps/frost/db/CookiesDb.kt | 50 ++++---------------- .../kotlin/com/pitchedapps/frost/db/Database.kt | 14 ++++++ .../com/pitchedapps/frost/facebook/FbCookie.kt | 31 ++++++++----- .../frost/facebook/parsers/FrostParser.kt | 3 +- .../frost/facebook/parsers/MessageParser.kt | 3 +- .../frost/facebook/parsers/NotifParser.kt | 3 +- .../frost/services/FrostNotifications.kt | 7 +-- .../frost/services/NotificationService.kt | 11 +++-- .../pitchedapps/frost/settings/Notifications.kt | 11 +++-- .../com/pitchedapps/frost/utils/Downloader.kt | 7 +-- .../kotlin/com/pitchedapps/frost/utils/Utils.kt | 10 ++-- .../com/pitchedapps/frost/views/AccountItem.kt | 4 +- .../pitchedapps/frost/views/FrostVideoViewer.kt | 11 ++++- .../kotlin/com/pitchedapps/frost/web/FrostJSI.kt | 3 +- .../com/pitchedapps/frost/web/LoginWebView.kt | 16 ++++--- 20 files changed, 158 insertions(+), 123 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 82f0c6b1..d5dcf87e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -204,9 +204,9 @@ dependencies { implementation "androidx.core:core-ktx:${KTX}" -// implementation "org.koin:koin-android:${KOIN}" -// testImplementation "org.koin:koin-test:${KOIN}" -// androidTestImplementation "org.koin:koin-test:${KOIN}" + implementation "org.koin:koin-android:${KOIN}" + testImplementation "org.koin:koin-test:${KOIN}" + androidTestImplementation "org.koin:koin-test:${KOIN}" // androidTestImplementation "io.mockk:mockk:${MOCKK}" diff --git a/app/src/main/kotlin/com/pitchedapps/frost/FrostApp.kt b/app/src/main/kotlin/com/pitchedapps/frost/FrostApp.kt index ba92a345..7669ef46 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/FrostApp.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/FrostApp.kt @@ -31,6 +31,7 @@ import com.mikepenz.materialdrawer.util.AbstractDrawerImageLoader import com.mikepenz.materialdrawer.util.DrawerImageLoader import com.pitchedapps.frost.db.CookiesDb import com.pitchedapps.frost.db.FbTabsDb +import com.pitchedapps.frost.db.FrostDatabase import com.pitchedapps.frost.db.NotificationDb import com.pitchedapps.frost.glide.GlideApp import com.pitchedapps.frost.services.scheduleNotifications @@ -44,6 +45,7 @@ import com.raizlabs.android.dbflow.config.DatabaseConfig import com.raizlabs.android.dbflow.config.FlowConfig import com.raizlabs.android.dbflow.config.FlowManager import com.raizlabs.android.dbflow.runtime.ContentResolverNotifier +import org.koin.android.ext.android.startKoin import java.util.Random import kotlin.reflect.KClass @@ -132,6 +134,7 @@ class FrostApp : Application() { L.d { "Activity ${activity.localClassName} created" } } }) + startKoin(this, listOf(FrostDatabase.module(this))) } private fun initBugsnag() { diff --git a/app/src/main/kotlin/com/pitchedapps/frost/StartActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/StartActivity.kt index 86596843..fe859f95 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/StartActivity.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/StartActivity.kt @@ -32,6 +32,8 @@ import com.mikepenz.google_material_typeface_library.GoogleMaterial import com.pitchedapps.frost.activities.LoginActivity import com.pitchedapps.frost.activities.MainActivity import com.pitchedapps.frost.activities.SelectorActivity +import com.pitchedapps.frost.db.CookieDao +import com.pitchedapps.frost.db.CookieEntity import com.pitchedapps.frost.db.CookieModel import com.pitchedapps.frost.db.loadFbCookiesSync import com.pitchedapps.frost.facebook.FbCookie @@ -43,6 +45,7 @@ import com.pitchedapps.frost.utils.loadAssets import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import org.koin.android.ext.android.inject import java.util.ArrayList /** @@ -50,6 +53,8 @@ import java.util.ArrayList */ class StartActivity : KauBaseActivity() { + private val cookieDao: CookieDao by inject() + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -67,12 +72,11 @@ class StartActivity : KauBaseActivity() { launch { try { + migrate() FbCookie.switchBackUser() - val cookies = ArrayList(withContext(Dispatchers.IO) { - loadFbCookiesSync() - }) + val cookies = ArrayList(cookieDao.selectAll()) L.i { "Cookies loaded at time ${System.currentTimeMillis()}" } - L._d { "Cookies: ${cookies.joinToString("\t", transform = CookieModel::toSensitiveString)}" } + L._d { "Cookies: ${cookies.joinToString("\t", transform = CookieEntity::toSensitiveString)}" } loadAssets() when { cookies.isEmpty() -> launchNewTask() @@ -90,6 +94,13 @@ class StartActivity : KauBaseActivity() { } } + /** + * Migrate from dbflow to room + */ + private suspend fun migrate() { + + } + private fun showInvalidWebView() = showInvalidView(R.string.error_webview) diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseMainActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseMainActivity.kt index 33fc2078..4669418d 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseMainActivity.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseMainActivity.kt @@ -69,9 +69,11 @@ import com.pitchedapps.frost.contracts.FileChooserContract import com.pitchedapps.frost.contracts.FileChooserDelegate import com.pitchedapps.frost.contracts.MainActivityContract import com.pitchedapps.frost.contracts.VideoViewHolder +import com.pitchedapps.frost.db.CookieDao +import com.pitchedapps.frost.db.FbTabDao import com.pitchedapps.frost.db.TAB_COUNT -import com.pitchedapps.frost.db.loadFbCookie -import com.pitchedapps.frost.db.loadFbTabs +import com.pitchedapps.frost.db.currentCookie +import com.pitchedapps.frost.db.selectAll import com.pitchedapps.frost.enums.MainActivityLayout import com.pitchedapps.frost.facebook.FbCookie import com.pitchedapps.frost.facebook.FbItem @@ -109,6 +111,7 @@ import kotlinx.android.synthetic.main.view_main_toolbar.* import kotlinx.android.synthetic.main.view_main_viewpager.* import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.launch +import org.koin.android.ext.android.inject /** * Created by Allan Wang on 20/12/17. @@ -123,6 +126,8 @@ abstract class BaseMainActivity : BaseActivity(), MainActivityContract, protected lateinit var adapter: SectionsPagerAdapter override val frameWrapper: FrameLayout get() = frame_wrapper val viewPager: FrostViewPager get() = container + val cookieDao: CookieDao by inject() + val tabDao: FbTabDao by inject() /* * Components with the same id in multiple layout files @@ -151,9 +156,11 @@ abstract class BaseMainActivity : BaseActivity(), MainActivityContract, background(viewPager) } setSupportActionBar(toolbar) - adapter = SectionsPagerAdapter(loadFbTabs()) - viewPager.adapter = adapter - viewPager.offscreenPageLimit = TAB_COUNT + launch { + adapter = SectionsPagerAdapter(tabDao.selectAll()) + viewPager.adapter = adapter + viewPager.offscreenPageLimit = TAB_COUNT + } tabs.setBackgroundColor(Prefs.mainActivityLayout.backgroundColor()) onNestedCreate(savedInstanceState) L.i { "Main finished loading UI in ${System.currentTimeMillis() - start} ms" } @@ -274,27 +281,28 @@ abstract class BaseMainActivity : BaseActivity(), MainActivityContract, if (current) launchWebOverlay(FbItem.PROFILE.url) else when (profile.identifier) { -2L -> { - val currentCookie = loadFbCookie(Prefs.userId) - if (currentCookie == null) { - toast(R.string.account_not_found) - launch { + // TODO no backpressure support + launch { + val currentCookie = cookieDao.currentCookie() + if (currentCookie == null) { + toast(R.string.account_not_found) FbCookie.reset() launchLogin(cookies(), true) - } - } else { - materialDialogThemed { - title(R.string.kau_logout) - content( - String.format( - string(R.string.kau_logout_confirm_as_x), currentCookie.name - ?: Prefs.userId.toString() + } else { + materialDialogThemed { + title(R.string.kau_logout) + content( + String.format( + string(R.string.kau_logout_confirm_as_x), + currentCookie.name ?: Prefs.userId.toString() + ) ) - ) - positiveText(R.string.kau_yes) - negativeText(R.string.kau_no) - onPositive { _, _ -> - launch { - FbCookie.logout(this@BaseMainActivity) + positiveText(R.string.kau_yes) + negativeText(R.string.kau_no) + onPositive { _, _ -> + this@BaseMainActivity.launch { + FbCookie.logout(this@BaseMainActivity) + } } } } diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/LoginActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/LoginActivity.kt index 7ff22a5a..5649cc73 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/activities/LoginActivity.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/LoginActivity.kt @@ -32,9 +32,8 @@ import com.bumptech.glide.load.engine.GlideException import com.bumptech.glide.request.RequestListener import com.bumptech.glide.request.target.Target import com.pitchedapps.frost.R -import com.pitchedapps.frost.db.CookieModel -import com.pitchedapps.frost.db.loadFbCookiesSuspend -import com.pitchedapps.frost.db.saveFbCookie +import com.pitchedapps.frost.db.CookieDao +import com.pitchedapps.frost.db.CookieEntity import com.pitchedapps.frost.facebook.FbCookie import com.pitchedapps.frost.facebook.FbItem import com.pitchedapps.frost.facebook.profilePictureUrl @@ -58,6 +57,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.withContext import kotlinx.coroutines.withTimeout +import org.koin.android.ext.android.inject import java.net.UnknownHostException import kotlin.coroutines.resume @@ -71,6 +71,7 @@ class LoginActivity : BaseActivity() { private val swipeRefresh: SwipeRefreshLayout by bindView(R.id.swipe_refresh) private val textview: AppCompatTextView by bindView(R.id.textview) private val profile: ImageView by bindView(R.id.profile) + private val cookieDao: CookieDao by inject() private lateinit var profileLoader: RequestManager private val refreshChannel = Channel(10) @@ -109,7 +110,7 @@ class LoginActivity : BaseActivity() { refreshChannel.offer(refreshing) } - private suspend fun loadInfo(cookie: CookieModel): Unit = withMainContext { + private suspend fun loadInfo(cookie: CookieEntity): Unit = withMainContext { refresh(true) val imageDeferred = async { loadProfile(cookie.id) } @@ -134,7 +135,7 @@ class LoginActivity : BaseActivity() { * The user may have logged into an account that is already in the database * We will let the db handle duplicates and load it now after the new account has been saved */ - val cookies = ArrayList(loadFbCookiesSuspend()) + val cookies = ArrayList(cookieDao.selectAll()) delay(1000) if (Showcase.intro) launchNewTask(cookies, true) @@ -171,7 +172,7 @@ class LoginActivity : BaseActivity() { } } - private suspend fun loadUsername(cookie: CookieModel): String = withContext(Dispatchers.IO) { + private suspend fun loadUsername(cookie: CookieEntity): String = withContext(Dispatchers.IO) { val result: String = try { withTimeout(5000) { frostJsoup(cookie.cookie, FbItem.PROFILE.url).title() @@ -184,7 +185,7 @@ class LoginActivity : BaseActivity() { if (cookie.name?.isNotBlank() == false && result != cookie.name) { cookie.name = result - saveFbCookie(cookie) + cookieDao.insertCookie(cookie) } cookie.name ?: "" diff --git a/app/src/main/kotlin/com/pitchedapps/frost/db/CookiesDb.kt b/app/src/main/kotlin/com/pitchedapps/frost/db/CookiesDb.kt index 9deb57da..fb240fd3 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/db/CookiesDb.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/db/CookiesDb.kt @@ -23,6 +23,7 @@ import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query import com.pitchedapps.frost.utils.L +import com.pitchedapps.frost.utils.Prefs import com.raizlabs.android.dbflow.annotation.ConflictAction import com.raizlabs.android.dbflow.annotation.Database import com.raizlabs.android.dbflow.annotation.PrimaryKey @@ -48,12 +49,12 @@ import kotlinx.coroutines.withContext data class CookieEntity( @androidx.room.PrimaryKey var id: Long, - var name: String, - var cookie: String + var name: String?, + var cookie: String? ) : Parcelable { - override fun toString(): String = "CookieModel(${hashCode()})" + override fun toString(): String = "CookieEntity(${hashCode()})" - fun toSensitiveString(): String = "CookieModel(id=$id, name=$name, cookie=$cookie)" + fun toSensitiveString(): String = "CookieEntity(id=$id, name=$name, cookie=$cookie)" } @Dao @@ -72,6 +73,8 @@ interface CookieDao { suspend fun deleteById(id: Long) } +suspend fun CookieDao.currentCookie() = selectById(Prefs.userId) + @Database(version = CookiesDb.VERSION) object CookiesDb { const val NAME = "Cookies" @@ -86,41 +89,4 @@ data class CookieModel(@PrimaryKey var id: Long = -1L, var name: String? = null, override fun toString(): String = "CookieModel(${hashCode()})" fun toSensitiveString(): String = "CookieModel(id=$id, name=$name, cookie=$cookie)" -} - -fun loadFbCookie(id: Long): CookieModel? = - (select from CookieModel::class where (CookieModel_Table.id eq id)).querySingle() - -fun loadFbCookie(name: String): CookieModel? = - (select from CookieModel::class where (CookieModel_Table.name eq name)).querySingle() - -/** - * Loads cookies sorted by name - */ -fun loadFbCookiesAsync(callback: (cookies: List) -> Unit) { - (select from CookieModel::class).orderBy(CookieModel_Table.name, true).async() - .queryListResultCallback { _, tResult -> callback(tResult) }.execute() -} - -fun loadFbCookiesSync(): List = - (select from CookieModel::class).orderBy(CookieModel_Table.name, true).queryList() - -// TODO temp method until dbflow supports coroutines -suspend fun loadFbCookiesSuspend(): List = withContext(Dispatchers.IO) { - loadFbCookiesSync() -} - -inline fun saveFbCookie(cookie: CookieModel, crossinline callback: (() -> Unit) = {}) { - cookie.async save { - L.d { "Fb cookie saved" } - L._d { cookie.toSensitiveString() } - callback() - } -} - -fun removeCookie(id: Long) { - loadFbCookie(id)?.async?.delete { - L.d { "Fb cookie deleted" } - L._d { id } - } -} +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/pitchedapps/frost/db/Database.kt b/app/src/main/kotlin/com/pitchedapps/frost/db/Database.kt index 34de5e07..161ed93d 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/db/Database.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/db/Database.kt @@ -5,6 +5,8 @@ import androidx.room.Database import androidx.room.Room import androidx.room.RoomDatabase import androidx.room.TypeConverters +import org.koin.dsl.module.module +import org.koin.standalone.StandAloneContext interface FrostPrivateDao { fun cookieDao(): CookieDao @@ -58,5 +60,17 @@ class FrostDatabase(private val privateDb: FrostPrivateDatabase, private val pub ).build() return FrostDatabase(privateDb, publicDb) } + + fun module(context: Context) = module { + single { create(context) } + single { get().cookieDao() } + single { get().tabDao() } + } + + /** + * Get from koin + * For the most part, you can retrieve directly from other koin components + */ + fun get(): FrostDatabase = StandAloneContext.getKoin().koinContext.get() } } diff --git a/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbCookie.kt b/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbCookie.kt index 349b415c..6f79da43 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbCookie.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbCookie.kt @@ -19,10 +19,10 @@ package com.pitchedapps.frost.facebook import android.app.Activity import android.content.Context import android.webkit.CookieManager -import com.pitchedapps.frost.db.CookieModel -import com.pitchedapps.frost.db.loadFbCookie +import com.pitchedapps.frost.db.CookieDao +import com.pitchedapps.frost.db.CookieEntity +import com.pitchedapps.frost.db.FrostDatabase import com.pitchedapps.frost.db.removeCookie -import com.pitchedapps.frost.db.saveFbCookie import com.pitchedapps.frost.utils.L import com.pitchedapps.frost.utils.Prefs import com.pitchedapps.frost.utils.cookies @@ -50,6 +50,10 @@ object FbCookie { inline val webCookie: String? get() = CookieManager.getInstance().getCookie(COOKIE_DOMAIN) + private val cookieDao: CookieDao by lazy { + FrostDatabase.get().cookieDao() + } + private suspend fun CookieManager.suspendSetWebCookie(cookie: String?): Boolean { cookie ?: return true return withContext(NonCancellable) { @@ -77,12 +81,12 @@ object FbCookie { } } - fun save(id: Long) { + suspend fun save(id: Long) { L.d { "New cookie found" } Prefs.userId = id CookieManager.getInstance().flush() - val cookie = CookieModel(Prefs.userId, "", webCookie) - saveFbCookie(cookie) + val cookie = CookieEntity(Prefs.userId, null, webCookie) + cookieDao.insertCookie(cookie) } suspend fun reset() { @@ -93,11 +97,12 @@ object FbCookie { } } - suspend fun switchUser(id: Long) = switchUser(loadFbCookie(id)) - - suspend fun switchUser(name: String) = switchUser(loadFbCookie(name)) + suspend fun switchUser(id: Long) { + val cookie = cookieDao.selectById(id) ?: return L.e { "No cookie for id" } + switchUser(cookie) + } - suspend fun switchUser(cookie: CookieModel?) { + suspend fun switchUser(cookie: CookieEntity?) { if (cookie == null) { L.d { "Switching User; null cookie" } return @@ -114,7 +119,7 @@ object FbCookie { * and launch the proper login page */ suspend fun logout(context: Context) { - val cookies = arrayListOf() + val cookies = arrayListOf() if (context is Activity) cookies.addAll(context.cookies().filter { it.id != Prefs.userId }) logout(Prefs.userId) @@ -126,7 +131,9 @@ object FbCookie { */ suspend fun logout(id: Long) { L.d { "Logging out user" } - removeCookie(id) + cookieDao.deleteById(id) + L.d { "Fb cookie deleted" } + L._d { id } reset() } diff --git a/app/src/main/kotlin/com/pitchedapps/frost/facebook/parsers/FrostParser.kt b/app/src/main/kotlin/com/pitchedapps/frost/facebook/parsers/FrostParser.kt index 57e7cc94..bf89a103 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/facebook/parsers/FrostParser.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/facebook/parsers/FrostParser.kt @@ -16,6 +16,7 @@ */ package com.pitchedapps.frost.facebook.parsers +import com.pitchedapps.frost.db.CookieEntity import com.pitchedapps.frost.db.CookieModel import com.pitchedapps.frost.facebook.FB_CSS_URL_MATCHER import com.pitchedapps.frost.facebook.formattedFbUrl @@ -81,7 +82,7 @@ data class ParseResponse(val cookie: String, val data: T) { } interface ParseNotification { - fun getUnreadNotifications(data: CookieModel): List + fun getUnreadNotifications(data: CookieEntity): List } internal fun List.toJsonString(tag: String, indent: Int) = StringBuilder().apply { diff --git a/app/src/main/kotlin/com/pitchedapps/frost/facebook/parsers/MessageParser.kt b/app/src/main/kotlin/com/pitchedapps/frost/facebook/parsers/MessageParser.kt index b2b59234..866cab10 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/facebook/parsers/MessageParser.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/facebook/parsers/MessageParser.kt @@ -16,6 +16,7 @@ */ package com.pitchedapps.frost.facebook.parsers +import com.pitchedapps.frost.db.CookieEntity import com.pitchedapps.frost.db.CookieModel import com.pitchedapps.frost.facebook.FB_EPOCH_MATCHER import com.pitchedapps.frost.facebook.FB_MESSAGE_NOTIF_ID_MATCHER @@ -54,7 +55,7 @@ data class FrostMessages( append("}") }.toString() - override fun getUnreadNotifications(data: CookieModel) = + override fun getUnreadNotifications(data: CookieEntity) = threads.asSequence().filter(FrostThread::unread).map { with(it) { NotificationContent( diff --git a/app/src/main/kotlin/com/pitchedapps/frost/facebook/parsers/NotifParser.kt b/app/src/main/kotlin/com/pitchedapps/frost/facebook/parsers/NotifParser.kt index 0474e35e..3449ac48 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/facebook/parsers/NotifParser.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/facebook/parsers/NotifParser.kt @@ -16,6 +16,7 @@ */ package com.pitchedapps.frost.facebook.parsers +import com.pitchedapps.frost.db.CookieEntity import com.pitchedapps.frost.db.CookieModel import com.pitchedapps.frost.facebook.FB_EPOCH_MATCHER import com.pitchedapps.frost.facebook.FB_NOTIF_ID_MATCHER @@ -43,7 +44,7 @@ data class FrostNotifs( append("}") }.toString() - override fun getUnreadNotifications(data: CookieModel) = + override fun getUnreadNotifications(data: CookieEntity) = notifs.asSequence().filter(FrostNotif::unread).map { with(it) { NotificationContent( diff --git a/app/src/main/kotlin/com/pitchedapps/frost/services/FrostNotifications.kt b/app/src/main/kotlin/com/pitchedapps/frost/services/FrostNotifications.kt index 994f9a18..abd871b3 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/services/FrostNotifications.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/services/FrostNotifications.kt @@ -31,6 +31,7 @@ import ca.allanwang.kau.utils.string import com.pitchedapps.frost.BuildConfig import com.pitchedapps.frost.R import com.pitchedapps.frost.activities.FrostWebActivity +import com.pitchedapps.frost.db.CookieEntity import com.pitchedapps.frost.db.CookieModel import com.pitchedapps.frost.db.NotificationModel import com.pitchedapps.frost.db.lastNotificationTime @@ -116,7 +117,7 @@ enum class NotificationType( * Returns the number of notifications generated, * or -1 if an error occurred */ - fun fetch(context: Context, data: CookieModel): Int { + fun fetch(context: Context, data: CookieEntity): Int { val response = try { parser.parse(data.cookie) } catch (ignored: Exception) { @@ -161,7 +162,7 @@ enum class NotificationType( return notifs.size } - fun debugNotification(context: Context, data: CookieModel) { + fun debugNotification(context: Context, data: CookieEntity) { val content = NotificationContent( data, System.currentTimeMillis(), @@ -247,7 +248,7 @@ enum class NotificationType( * Notification data holder */ data class NotificationContent( - val data: CookieModel, + val data: CookieEntity, val id: Long, val href: String, val title: String? = null, // defaults to frost title diff --git a/app/src/main/kotlin/com/pitchedapps/frost/services/NotificationService.kt b/app/src/main/kotlin/com/pitchedapps/frost/services/NotificationService.kt index a895900f..088d8e0a 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/services/NotificationService.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/services/NotificationService.kt @@ -21,8 +21,8 @@ import androidx.core.app.NotificationManagerCompat import ca.allanwang.kau.utils.string import com.pitchedapps.frost.BuildConfig import com.pitchedapps.frost.R -import com.pitchedapps.frost.db.CookieModel -import com.pitchedapps.frost.db.loadFbCookiesSync +import com.pitchedapps.frost.db.CookieDao +import com.pitchedapps.frost.db.CookieEntity import com.pitchedapps.frost.utils.L import com.pitchedapps.frost.utils.Prefs import com.pitchedapps.frost.utils.frostEvent @@ -31,6 +31,7 @@ import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlinx.coroutines.yield +import org.koin.android.ext.android.inject /** * Created by Allan Wang on 2017-06-14. @@ -42,6 +43,8 @@ import kotlinx.coroutines.yield */ class NotificationService : BaseJobService() { + val cookieDao: CookieDao by inject() + override fun onStopJob(params: JobParameters?): Boolean { super.onStopJob(params) prepareFinish(true) @@ -81,7 +84,7 @@ class NotificationService : BaseJobService() { private suspend fun sendNotifications(params: JobParameters?): Unit = withContext(Dispatchers.Default) { val currentId = Prefs.userId - val cookies = loadFbCookiesSync() + val cookies = cookieDao.selectAll() yield() val jobId = params?.extras?.getInt(NOTIFICATION_PARAM_ID, -1) ?: -1 var notifCount = 0 @@ -107,7 +110,7 @@ class NotificationService : BaseJobService() { * Implemented fetch to also notify when an error occurs * Also normalized the output to return the number of notifications received */ - private fun fetch(jobId: Int, type: NotificationType, cookie: CookieModel): Int { + private fun fetch(jobId: Int, type: NotificationType, cookie: CookieEntity): Int { val count = type.fetch(this, cookie) if (count < 0) { if (jobId == NOTIFICATION_JOB_NOW) diff --git a/app/src/main/kotlin/com/pitchedapps/frost/settings/Notifications.kt b/app/src/main/kotlin/com/pitchedapps/frost/settings/Notifications.kt index 774b0e7f..3444d0b0 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/settings/Notifications.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/settings/Notifications.kt @@ -29,14 +29,15 @@ import ca.allanwang.kau.utils.string import com.pitchedapps.frost.BuildConfig import com.pitchedapps.frost.R import com.pitchedapps.frost.activities.SettingsActivity +import com.pitchedapps.frost.db.FrostDatabase import com.pitchedapps.frost.db.NotificationModel -import com.pitchedapps.frost.db.loadFbCookiesAsync import com.pitchedapps.frost.services.fetchNotifications import com.pitchedapps.frost.services.scheduleNotifications import com.pitchedapps.frost.utils.Prefs import com.pitchedapps.frost.utils.frostSnackbar import com.pitchedapps.frost.utils.materialDialogThemed import com.pitchedapps.frost.views.Keywords +import kotlinx.coroutines.launch /** * Created by Allan Wang on 2017-06-29. @@ -171,8 +172,12 @@ fun SettingsActivity.getNotificationPrefs(): KPrefAdapterBuilder.() -> Unit = { if (BuildConfig.DEBUG) { plainText(R.string.reset_notif_epoch) { onClick = { - loadFbCookiesAsync { cookies -> - cookies.map { NotificationModel(it.id) }.forEach { it.save() } + launch { + FrostDatabase.get() + .cookieDao() + .selectAll() + .map { NotificationModel(it.id) } + .forEach { it.save() } } } } diff --git a/app/src/main/kotlin/com/pitchedapps/frost/utils/Downloader.kt b/app/src/main/kotlin/com/pitchedapps/frost/utils/Downloader.kt index 254297a6..64094d1e 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/utils/Downloader.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/utils/Downloader.kt @@ -29,7 +29,7 @@ import ca.allanwang.kau.utils.showAppInfo import ca.allanwang.kau.utils.string import ca.allanwang.kau.utils.toast import com.pitchedapps.frost.R -import com.pitchedapps.frost.db.loadFbCookie +import com.pitchedapps.frost.db.CookieEntity import com.pitchedapps.frost.facebook.USER_AGENT_BASIC /** @@ -38,6 +38,7 @@ import com.pitchedapps.frost.facebook.USER_AGENT_BASIC * With reference to Stack Overflow */ fun Context.frostDownload( + cookie: CookieEntity, url: String?, userAgent: String = USER_AGENT_BASIC, contentDisposition: String? = null, @@ -45,10 +46,11 @@ fun Context.frostDownload( contentLength: Long = 0L ) { url ?: return - frostDownload(Uri.parse(url), userAgent, contentDisposition, mimeType, contentLength) + frostDownload(cookie, Uri.parse(url), userAgent, contentDisposition, mimeType, contentLength) } fun Context.frostDownload( + cookie: CookieEntity, uri: Uri?, userAgent: String = USER_AGENT_BASIC, contentDisposition: String? = null, @@ -75,7 +77,6 @@ fun Context.frostDownload( if (!granted) return@kauRequestPermissions val request = DownloadManager.Request(uri) request.setMimeType(mimeType) - val cookie = loadFbCookie(Prefs.userId) ?: return@kauRequestPermissions val title = URLUtil.guessFileName(uri.toString(), contentDisposition, mimeType) request.addRequestHeader("Cookie", cookie.cookie) request.addRequestHeader("User-Agent", userAgent) diff --git a/app/src/main/kotlin/com/pitchedapps/frost/utils/Utils.kt b/app/src/main/kotlin/com/pitchedapps/frost/utils/Utils.kt index 1222e93b..5a104458 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/utils/Utils.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/utils/Utils.kt @@ -62,7 +62,7 @@ import com.pitchedapps.frost.activities.TabCustomizerActivity import com.pitchedapps.frost.activities.WebOverlayActivity import com.pitchedapps.frost.activities.WebOverlayActivityBase import com.pitchedapps.frost.activities.WebOverlayBasicActivity -import com.pitchedapps.frost.db.CookieModel +import com.pitchedapps.frost.db.CookieEntity import com.pitchedapps.frost.facebook.FACEBOOK_COM import com.pitchedapps.frost.facebook.FBCDN_NET import com.pitchedapps.frost.facebook.FbCookie @@ -103,7 +103,7 @@ internal inline val Context.ctxCoroutine: CoroutineScope get() = this as? CoroutineScope ?: GlobalScope inline fun Context.launchNewTask( - cookieList: ArrayList = arrayListOf(), + cookieList: ArrayList = arrayListOf(), clearStack: Boolean = false ) { startActivity(clearStack, intentBuilder = { @@ -111,13 +111,13 @@ inline fun Context.launchNewTask( }) } -fun Context.launchLogin(cookieList: ArrayList, clearStack: Boolean = true) { +fun Context.launchLogin(cookieList: ArrayList, clearStack: Boolean = true) { if (cookieList.isNotEmpty()) launchNewTask(cookieList, clearStack) else launchNewTask(clearStack = clearStack) } -fun Activity.cookies(): ArrayList { - return intent?.getParcelableArrayListExtra(EXTRA_COOKIES) ?: arrayListOf() +fun Activity.cookies(): ArrayList { + return intent?.getParcelableArrayListExtra(EXTRA_COOKIES) ?: arrayListOf() } /** diff --git a/app/src/main/kotlin/com/pitchedapps/frost/views/AccountItem.kt b/app/src/main/kotlin/com/pitchedapps/frost/views/AccountItem.kt index 8c7435ea..07eaed0a 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/views/AccountItem.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/views/AccountItem.kt @@ -33,7 +33,7 @@ import com.bumptech.glide.request.RequestListener import com.bumptech.glide.request.target.Target import com.mikepenz.google_material_typeface_library.GoogleMaterial import com.pitchedapps.frost.R -import com.pitchedapps.frost.db.CookieModel +import com.pitchedapps.frost.db.CookieEntity import com.pitchedapps.frost.facebook.profilePictureUrl import com.pitchedapps.frost.glide.FrostGlide import com.pitchedapps.frost.glide.GlideApp @@ -42,7 +42,7 @@ import com.pitchedapps.frost.utils.Prefs /** * Created by Allan Wang on 2017-06-05. */ -class AccountItem(val cookie: CookieModel?) : KauIItem +class AccountItem(val cookie: CookieEntity?) : KauIItem (R.layout.view_account, { ViewHolder(it) }, R.id.item_account) { override fun bindView(viewHolder: ViewHolder, payloads: MutableList) { diff --git a/app/src/main/kotlin/com/pitchedapps/frost/views/FrostVideoViewer.kt b/app/src/main/kotlin/com/pitchedapps/frost/views/FrostVideoViewer.kt index c2535940..288e2b40 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/views/FrostVideoViewer.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/views/FrostVideoViewer.kt @@ -32,6 +32,7 @@ import ca.allanwang.kau.utils.inflate import ca.allanwang.kau.utils.isColorDark import ca.allanwang.kau.utils.isGone import ca.allanwang.kau.utils.isVisible +import ca.allanwang.kau.utils.launchMain import ca.allanwang.kau.utils.setIcon import ca.allanwang.kau.utils.setMenuIcons import ca.allanwang.kau.utils.visible @@ -39,8 +40,12 @@ import ca.allanwang.kau.utils.withMinAlpha import com.devbrackets.android.exomedia.listener.VideoControlsVisibilityListener import com.mikepenz.google_material_typeface_library.GoogleMaterial import com.pitchedapps.frost.R +import com.pitchedapps.frost.db.CookieModel_Table.cookie +import com.pitchedapps.frost.db.FrostDatabase +import com.pitchedapps.frost.db.currentCookie import com.pitchedapps.frost.utils.L import com.pitchedapps.frost.utils.Prefs +import com.pitchedapps.frost.utils.ctxCoroutine import com.pitchedapps.frost.utils.frostDownload import kotlinx.android.synthetic.main.view_video.view.* @@ -96,7 +101,11 @@ class FrostVideoViewer @JvmOverloads constructor( video_toolbar.setOnMenuItemClickListener { when (it.itemId) { R.id.action_pip -> video.isExpanded = false - R.id.action_download -> context.frostDownload(video.videoUri) + R.id.action_download -> context.ctxCoroutine.launchMain { + val cookie = FrostDatabase.get().cookieDao().currentCookie() ?: return@launchMain + context.frostDownload(cookie, video.videoUri) + } + } true } diff --git a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostJSI.kt b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostJSI.kt index 663acfbb..4df84b63 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostJSI.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostJSI.kt @@ -21,6 +21,7 @@ import android.webkit.JavascriptInterface import com.pitchedapps.frost.activities.MainActivity import com.pitchedapps.frost.contracts.MainActivityContract import com.pitchedapps.frost.contracts.VideoViewHolder +import com.pitchedapps.frost.db.CookieEntity import com.pitchedapps.frost.db.CookieModel import com.pitchedapps.frost.facebook.FbCookie import com.pitchedapps.frost.utils.L @@ -44,7 +45,7 @@ class FrostJSI(val web: FrostWebView) { private val activity: MainActivity? = context as? MainActivity private val header: SendChannel? = activity?.headerBadgeChannel private val refresh: SendChannel = web.parent.refreshChannel - private val cookies: List = activity?.cookies() ?: arrayListOf() + private val cookies: List = activity?.cookies() ?: arrayListOf() /** * Attempts to load the url in an overlay diff --git a/app/src/main/kotlin/com/pitchedapps/frost/web/LoginWebView.kt b/app/src/main/kotlin/com/pitchedapps/frost/web/LoginWebView.kt index 11a53c36..07aafaf0 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/web/LoginWebView.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/web/LoginWebView.kt @@ -29,6 +29,7 @@ import android.webkit.WebView import ca.allanwang.kau.utils.fadeIn import ca.allanwang.kau.utils.isVisible import ca.allanwang.kau.utils.launchMain +import com.pitchedapps.frost.db.CookieEntity import com.pitchedapps.frost.db.CookieModel import com.pitchedapps.frost.facebook.FB_LOGIN_URL import com.pitchedapps.frost.facebook.FB_USER_MATCHER @@ -52,7 +53,7 @@ class LoginWebView @JvmOverloads constructor( defStyleAttr: Int = 0 ) : WebView(context, attrs, defStyleAttr) { - private val completable: CompletableDeferred = CompletableDeferred() + private val completable: CompletableDeferred = CompletableDeferred() private lateinit var progressCallback: (Int) -> Unit @SuppressLint("SetJavaScriptEnabled") @@ -63,7 +64,7 @@ class LoginWebView @JvmOverloads constructor( webChromeClient = LoginChromeClient() } - suspend fun loadLogin(progressCallback: (Int) -> Unit): CompletableDeferred = coroutineScope { + suspend fun loadLogin(progressCallback: (Int) -> Unit): CompletableDeferred = coroutineScope { this@LoginWebView.progressCallback = progressCallback L.d { "Begin loading login" } launchMain { @@ -78,18 +79,19 @@ class LoginWebView @JvmOverloads constructor( override fun onPageFinished(view: WebView, url: String?) { super.onPageFinished(view, url) - val cookieModel = checkForLogin(url) - if (cookieModel != null) - completable.complete(cookieModel) + val cookie = checkForLogin(url) + if (cookie != null) + completable.complete(cookie) if (!view.isVisible) view.fadeIn() } - fun checkForLogin(url: String?): CookieModel? { + fun checkForLogin(url: String?): CookieEntity? { if (!url.isFacebookUrl) return null val cookie = CookieManager.getInstance().getCookie(url) ?: return null L.d { "Checking cookie for login" } val id = FB_USER_MATCHER.find(cookie)[1]?.toLong() ?: return null - return CookieModel(id, "", cookie) + // TODO set name to null? + return CookieEntity(id, "", cookie) } override fun onPageCommitVisible(view: WebView, url: String?) { -- cgit v1.2.3 From 9a1d9719ad6559054ea1bc4f21f8559559eb9cda Mon Sep 17 00:00:00 2001 From: Allan Wang Date: Tue, 5 Mar 2019 21:25:46 -0500 Subject: Port rest of cookie items --- app/src/main/AndroidManifest.xml | 1 + app/src/main/kotlin/com/pitchedapps/frost/StartActivity.kt | 12 +++++++++--- app/src/main/kotlin/com/pitchedapps/frost/db/CookiesDb.kt | 13 +++---------- .../main/kotlin/com/pitchedapps/frost/facebook/FbCookie.kt | 1 - .../main/kotlin/com/pitchedapps/frost/views/FrostWebView.kt | 12 +++++++++++- .../com.pitchedapps.frost.db.FrostPrivateDatabase/1.json | 11 ++++++----- 6 files changed, 30 insertions(+), 20 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 68e84e4d..ad1fcbdc 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -17,6 +17,7 @@ ) + @Query("DELETE FROM cookies WHERE id = :id") suspend fun deleteById(id: Long) } diff --git a/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbCookie.kt b/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbCookie.kt index 6f79da43..02718b78 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbCookie.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbCookie.kt @@ -22,7 +22,6 @@ import android.webkit.CookieManager import com.pitchedapps.frost.db.CookieDao import com.pitchedapps.frost.db.CookieEntity import com.pitchedapps.frost.db.FrostDatabase -import com.pitchedapps.frost.db.removeCookie import com.pitchedapps.frost.utils.L import com.pitchedapps.frost.utils.Prefs import com.pitchedapps.frost.utils.cookies diff --git a/app/src/main/kotlin/com/pitchedapps/frost/views/FrostWebView.kt b/app/src/main/kotlin/com/pitchedapps/frost/views/FrostWebView.kt index 0e9d94e6..594b0acd 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/views/FrostWebView.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/views/FrostWebView.kt @@ -24,15 +24,19 @@ import android.util.AttributeSet import android.view.View import android.view.ViewGroup import ca.allanwang.kau.utils.AnimHolder +import ca.allanwang.kau.utils.launchMain import com.pitchedapps.frost.contracts.FrostContentContainer import com.pitchedapps.frost.contracts.FrostContentCore import com.pitchedapps.frost.contracts.FrostContentParent +import com.pitchedapps.frost.db.FrostDatabase +import com.pitchedapps.frost.db.currentCookie import com.pitchedapps.frost.facebook.FB_HOME_URL import com.pitchedapps.frost.facebook.FbItem import com.pitchedapps.frost.facebook.USER_AGENT_BASIC import com.pitchedapps.frost.facebook.USER_AGENT_FULL import com.pitchedapps.frost.fragments.WebFragment import com.pitchedapps.frost.utils.Prefs +import com.pitchedapps.frost.utils.ctxCoroutine import com.pitchedapps.frost.utils.frostDownload import com.pitchedapps.frost.web.FrostChromeClient import com.pitchedapps.frost.web.FrostJSI @@ -80,7 +84,13 @@ class FrostWebView @JvmOverloads constructor( webChromeClient = FrostChromeClient(this) addJavascriptInterface(FrostJSI(this), "Frost") setBackgroundColor(Color.TRANSPARENT) - setDownloadListener(context::frostDownload) + val db = FrostDatabase.get() + setDownloadListener { url, userAgent, contentDisposition, mimetype, contentLength -> + context.ctxCoroutine.launchMain { + val cookie = db.cookieDao().currentCookie() ?: return@launchMain + context.frostDownload(cookie, url, userAgent, contentDisposition, mimetype, contentLength) + } + } return this } diff --git a/app/src/schemas/com.pitchedapps.frost.db.FrostPrivateDatabase/1.json b/app/src/schemas/com.pitchedapps.frost.db.FrostPrivateDatabase/1.json index 7bc2e5e9..9816651c 100644 --- a/app/src/schemas/com.pitchedapps.frost.db.FrostPrivateDatabase/1.json +++ b/app/src/schemas/com.pitchedapps.frost.db.FrostPrivateDatabase/1.json @@ -2,11 +2,11 @@ "formatVersion": 1, "database": { "version": 1, - "identityHash": "6af67964d00193c6a3aa2a20200ea0ea", + "identityHash": "ba6f1d7e47823dac6ed1622fec043d5d", "entities": [ { "tableName": "cookies", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `cookie` TEXT NOT NULL, PRIMARY KEY(`id`))", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT, `cookie` TEXT, PRIMARY KEY(`id`))", "fields": [ { "fieldPath": "id", @@ -18,13 +18,13 @@ "fieldPath": "name", "columnName": "name", "affinity": "TEXT", - "notNull": true + "notNull": false }, { "fieldPath": "cookie", "columnName": "cookie", "affinity": "TEXT", - "notNull": true + "notNull": false } ], "primaryKey": { @@ -37,9 +37,10 @@ "foreignKeys": [] } ], + "views": [], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"6af67964d00193c6a3aa2a20200ea0ea\")" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"ba6f1d7e47823dac6ed1622fec043d5d\")" ] } } \ No newline at end of file -- cgit v1.2.3 From 8b70d80070209eb19791eecf207a8fdefea17a4e Mon Sep 17 00:00:00 2001 From: Allan Wang Date: Wed, 6 Mar 2019 17:42:31 -0500 Subject: Make db entities immutable --- .../com/pitchedapps/frost/db/CookieDbTest.kt | 34 +++--- .../com/pitchedapps/frost/db/FbTabsDbTest.kt | 12 +- .../com/pitchedapps/frost/db/NotificationDbTest.kt | 20 ++++ .../pitchedapps/frost/activities/LoginActivity.kt | 3 +- .../kotlin/com/pitchedapps/frost/db/CookiesDb.kt | 6 +- .../kotlin/com/pitchedapps/frost/db/Database.kt | 7 +- .../kotlin/com/pitchedapps/frost/db/FbTabsDb.kt | 28 +++-- .../com/pitchedapps/frost/db/NotificationDb.kt | 64 +++++++++++ .../1.json | 125 ++++++++++++++++++++- 9 files changed, 259 insertions(+), 40 deletions(-) create mode 100644 app/src/androidTest/kotlin/com/pitchedapps/frost/db/NotificationDbTest.kt diff --git a/app/src/androidTest/kotlin/com/pitchedapps/frost/db/CookieDbTest.kt b/app/src/androidTest/kotlin/com/pitchedapps/frost/db/CookieDbTest.kt index 351490e2..20592347 100644 --- a/app/src/androidTest/kotlin/com/pitchedapps/frost/db/CookieDbTest.kt +++ b/app/src/androidTest/kotlin/com/pitchedapps/frost/db/CookieDbTest.kt @@ -6,13 +6,15 @@ import kotlin.test.assertEquals import kotlin.test.assertNull class CookieDbTest : BaseDbTest() { + + private val dao get() = db.cookieDao() @Test fun basicCookie() { val cookie = CookieEntity(id = 1234L, name = "testName", cookie = "testCookie") runBlocking { - db.cookieDao().insertCookie(cookie) - val cookies = db.cookieDao().selectAll() + dao.insertCookie(cookie) + val cookies = dao.selectAll() assertEquals(listOf(cookie), cookies, "Cookie mismatch") } } @@ -22,15 +24,15 @@ class CookieDbTest : BaseDbTest() { val cookie = CookieEntity(id = 1234L, name = "testName", cookie = "testCookie") runBlocking { - db.cookieDao().insertCookie(cookie) - db.cookieDao().deleteById(cookie.id + 1) + dao.insertCookie(cookie) + dao.deleteById(cookie.id + 1) assertEquals( listOf(cookie), - db.cookieDao().selectAll(), + dao.selectAll(), "Cookie list should be the same after inexistent deletion" ) - db.cookieDao().deleteById(cookie.id) - assertEquals(emptyList(), db.cookieDao().selectAll(), "Cookie list should be empty after deletion") + dao.deleteById(cookie.id) + assertEquals(emptyList(), dao.selectAll(), "Cookie list should be empty after deletion") } } @@ -38,18 +40,18 @@ class CookieDbTest : BaseDbTest() { fun insertReplaceCookie() { val cookie = CookieEntity(id = 1234L, name = "testName", cookie = "testCookie") runBlocking { - db.cookieDao().insertCookie(cookie) - assertEquals(listOf(cookie), db.cookieDao().selectAll(), "Cookie insertion failed") - db.cookieDao().insertCookie(cookie.copy(name = "testName2")) + dao.insertCookie(cookie) + assertEquals(listOf(cookie), dao.selectAll(), "Cookie insertion failed") + dao.insertCookie(cookie.copy(name = "testName2")) assertEquals( listOf(cookie.copy(name = "testName2")), - db.cookieDao().selectAll(), + dao.selectAll(), "Cookie replacement failed" ) - db.cookieDao().insertCookie(cookie.copy(id = 123L)) + dao.insertCookie(cookie.copy(id = 123L)) assertEquals( setOf(cookie.copy(id = 123L), cookie.copy(name = "testName2")), - db.cookieDao().selectAll().toSet(), + dao.selectAll().toSet(), "New cookie insertion failed" ) } @@ -59,9 +61,9 @@ class CookieDbTest : BaseDbTest() { fun selectCookie() { val cookie = CookieEntity(id = 1234L, name = "testName", cookie = "testCookie") runBlocking { - db.cookieDao().insertCookie(cookie) - assertEquals(cookie, db.cookieDao().selectById(cookie.id), "Cookie selection failed") - assertNull(db.cookieDao().selectById(cookie.id + 1), "Inexistent cookie selection failed") + dao.insertCookie(cookie) + assertEquals(cookie, dao.selectById(cookie.id), "Cookie selection failed") + assertNull(dao.selectById(cookie.id + 1), "Inexistent cookie selection failed") } } } \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/pitchedapps/frost/db/FbTabsDbTest.kt b/app/src/androidTest/kotlin/com/pitchedapps/frost/db/FbTabsDbTest.kt index a2dce692..91a0bf9a 100644 --- a/app/src/androidTest/kotlin/com/pitchedapps/frost/db/FbTabsDbTest.kt +++ b/app/src/androidTest/kotlin/com/pitchedapps/frost/db/FbTabsDbTest.kt @@ -7,6 +7,8 @@ import kotlin.test.Test import kotlin.test.assertEquals class FbTabsDbTest : BaseDbTest() { + + private val dao get() = db.tabDao() /** * Note that order is also preserved here @@ -15,18 +17,18 @@ class FbTabsDbTest : BaseDbTest() { fun save() { val tabs = listOf(FbItem.ACTIVITY_LOG, FbItem.BIRTHDAYS, FbItem.EVENTS, FbItem.MARKETPLACE, FbItem.ACTIVITY_LOG) runBlocking { - db.tabDao().save(tabs) - assertEquals(tabs, db.tabDao().selectAll(), "Tab saving failed") + dao.save(tabs) + assertEquals(tabs, dao.selectAll(), "Tab saving failed") val newTabs = listOf(FbItem.PAGES, FbItem.MENU) - db.tabDao().save(newTabs) - assertEquals(newTabs, db.tabDao().selectAll(), "Tab saving does not delete preexisting items") + dao.save(newTabs) + assertEquals(newTabs, dao.selectAll(), "Tab saving does not delete preexisting items") } } @Test fun defaultRetrieve() { runBlocking { - assertEquals(defaultTabs(), db.tabDao().selectAll(), "Default retrieval failed") + assertEquals(defaultTabs(), dao.selectAll(), "Default retrieval failed") } } } \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/pitchedapps/frost/db/NotificationDbTest.kt b/app/src/androidTest/kotlin/com/pitchedapps/frost/db/NotificationDbTest.kt new file mode 100644 index 00000000..12092bf6 --- /dev/null +++ b/app/src/androidTest/kotlin/com/pitchedapps/frost/db/NotificationDbTest.kt @@ -0,0 +1,20 @@ +package com.pitchedapps.frost.db + +import com.pitchedapps.frost.facebook.FbItem +import com.pitchedapps.frost.facebook.defaultTabs +import kotlinx.coroutines.runBlocking +import kotlin.test.Test +import kotlin.test.assertEquals + +class NotificationDbTest : BaseDbTest() { + + private val dao get() = db.notifDao() + + /** + * Note that order is also preserved here + */ + @Test + fun save() { + + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/LoginActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/LoginActivity.kt index 5649cc73..27dbc37a 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/activities/LoginActivity.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/LoginActivity.kt @@ -184,8 +184,7 @@ class LoginActivity : BaseActivity() { } if (cookie.name?.isNotBlank() == false && result != cookie.name) { - cookie.name = result - cookieDao.insertCookie(cookie) + cookieDao.insertCookie(cookie.copy(name = result)) } cookie.name ?: "" diff --git a/app/src/main/kotlin/com/pitchedapps/frost/db/CookiesDb.kt b/app/src/main/kotlin/com/pitchedapps/frost/db/CookiesDb.kt index 128abae3..34a88011 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/db/CookiesDb.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/db/CookiesDb.kt @@ -38,9 +38,9 @@ import kotlinx.android.parcel.Parcelize @Parcelize data class CookieEntity( @androidx.room.PrimaryKey - var id: Long, - var name: String?, - var cookie: String? + val id: Long, + val name: String?, + val cookie: String? ) : Parcelable { override fun toString(): String = "CookieEntity(${hashCode()})" diff --git a/app/src/main/kotlin/com/pitchedapps/frost/db/Database.kt b/app/src/main/kotlin/com/pitchedapps/frost/db/Database.kt index 161ed93d..a37c2ee9 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/db/Database.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/db/Database.kt @@ -10,9 +10,14 @@ import org.koin.standalone.StandAloneContext interface FrostPrivateDao { fun cookieDao(): CookieDao + fun notifDao(): NotificationDao } -@Database(entities = [CookieEntity::class], version = 1, exportSchema = true) +@Database( + entities = [CookieEntity::class, NotificationInfoEntity::class, NotificationEntity::class], + version = 1, + exportSchema = true +) abstract class FrostPrivateDatabase : RoomDatabase(), FrostPrivateDao { companion object { const val DATABASE_NAME = "frost-priv-db" diff --git a/app/src/main/kotlin/com/pitchedapps/frost/db/FbTabsDb.kt b/app/src/main/kotlin/com/pitchedapps/frost/db/FbTabsDb.kt index 44e62938..582d57fb 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/db/FbTabsDb.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/db/FbTabsDb.kt @@ -33,7 +33,7 @@ import com.raizlabs.android.dbflow.kotlinextensions.fastSave import com.raizlabs.android.dbflow.kotlinextensions.from import com.raizlabs.android.dbflow.kotlinextensions.select import com.raizlabs.android.dbflow.structure.BaseModel -import kotlinx.coroutines.NonCancellable +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext /** @@ -41,42 +41,48 @@ import kotlinx.coroutines.withContext */ @Entity(tableName = "tabs") -data class FbTabEntity(@androidx.room.PrimaryKey var position: Int, var tab: FbItem) +data class FbTabEntity(@androidx.room.PrimaryKey val position: Int, val tab: FbItem) @Dao interface FbTabDao { @Query("SELECT * FROM tabs ORDER BY position ASC") - suspend fun _selectAll(): List + fun _selectAll(): List @Query("DELETE FROM tabs") - suspend fun _deleteAll() + fun _deleteAll() @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun _insertAll(items: List) + fun _insertAll(items: List) + + @Transaction + fun _save(items: List) { + _deleteAll() + _insertAll(items) + } } /** * Saving tabs operates by deleting all db items and saving the new list. * Transactions can't be done with suspensions in room as switching threads during the process * may result in a deadlock. - * In this case, there may be a chance that the 'transaction' completes partially, - * but we'll just fallback to the default anyways. + * That's why we disallow thread switching within the transaction, but wrap the entire thing in a coroutine */ suspend fun FbTabDao.save(items: List) { - withContext(NonCancellable) { - _deleteAll() + withContext(Dispatchers.IO) { val entities = (items.takeIf { it.isNotEmpty() } ?: defaultTabs()).mapIndexed { index, fbItem -> FbTabEntity( index, fbItem ) } - _insertAll(entities) + _save(entities) } } -suspend fun FbTabDao.selectAll(): List = _selectAll().map { it.tab }.takeIf { it.isNotEmpty() } ?: defaultTabs() +suspend fun FbTabDao.selectAll(): List = withContext(Dispatchers.IO) { + _selectAll().map { it.tab }.takeIf { it.isNotEmpty() } ?: defaultTabs() +} object FbItemConverter { @androidx.room.TypeConverter diff --git a/app/src/main/kotlin/com/pitchedapps/frost/db/NotificationDb.kt b/app/src/main/kotlin/com/pitchedapps/frost/db/NotificationDb.kt index 5b501792..56c3c5ac 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/db/NotificationDb.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/db/NotificationDb.kt @@ -16,6 +16,16 @@ */ package com.pitchedapps.frost.db +import androidx.room.Dao +import androidx.room.Embedded +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.Ignore +import androidx.room.Index +import androidx.room.Insert +import androidx.room.Query +import androidx.room.Relation +import androidx.room.Transaction import com.pitchedapps.frost.utils.L import com.raizlabs.android.dbflow.annotation.ConflictAction import com.raizlabs.android.dbflow.annotation.Database @@ -32,6 +42,60 @@ import com.raizlabs.android.dbflow.sql.SQLiteType import com.raizlabs.android.dbflow.sql.migration.AlterTableMigration import com.raizlabs.android.dbflow.structure.BaseModel +@Entity( + tableName = "notification_info", + foreignKeys = [ForeignKey( + entity = CookieEntity::class, + parentColumns = ["id"], childColumns = ["id"], onDelete = ForeignKey.CASCADE + )] +) +data class NotificationInfoEntity( + @androidx.room.PrimaryKey val id: Long, + val epoch: Long, + val epochIm: Long +) + +@Entity( + tableName = "notifications", + foreignKeys = [ForeignKey( + entity = NotificationInfoEntity::class, + parentColumns = ["id"], + childColumns = ["userId"], + onDelete = ForeignKey.CASCADE + )], + indices = [Index("userId")] +) +data class NotificationEntity( + @androidx.room.PrimaryKey var id: Long, + val userId: Long, + val href: String, + val title: String?, + val text: String, + val timestamp: Long, + val profileUrl: String? +) { + @Ignore + val notifId = Math.abs(id.toInt()) +} + +data class NotificationInfo( + @Embedded + val info: NotificationInfoEntity, + @Relation(parentColumn = "id", entityColumn = "userId") + val notifications: List = emptyList() +) + +@Dao +interface NotificationDao { + + @Query("SELECT * FROM notification_info WHERE id = :id") + fun selectById(id: Long): NotificationInfo? + + @Transaction + @Insert + fun insertInfo(info: NotificationInfoEntity) +} + /** * Created by Allan Wang on 2017-05-30. */ diff --git a/app/src/schemas/com.pitchedapps.frost.db.FrostPrivateDatabase/1.json b/app/src/schemas/com.pitchedapps.frost.db.FrostPrivateDatabase/1.json index 9816651c..f0909a17 100644 --- a/app/src/schemas/com.pitchedapps.frost.db.FrostPrivateDatabase/1.json +++ b/app/src/schemas/com.pitchedapps.frost.db.FrostPrivateDatabase/1.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 1, - "identityHash": "ba6f1d7e47823dac6ed1622fec043d5d", + "identityHash": "d0911840f629ef359aaf8ac8635dc571", "entities": [ { "tableName": "cookies", @@ -35,12 +35,133 @@ }, "indices": [], "foreignKeys": [] + }, + { + "tableName": "notification_info", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `epoch` INTEGER NOT NULL, `epochIm` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`id`) REFERENCES `cookies`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "epoch", + "columnName": "epoch", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "epochIm", + "columnName": "epochIm", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [ + { + "table": "cookies", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "notifications", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `userId` INTEGER NOT NULL, `href` TEXT NOT NULL, `title` TEXT, `text` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `profileUrl` TEXT, PRIMARY KEY(`id`), FOREIGN KEY(`userId`) REFERENCES `notification_info`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "href", + "columnName": "href", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "profileUrl", + "columnName": "profileUrl", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_notifications_userId", + "unique": false, + "columnNames": [ + "userId" + ], + "createSql": "CREATE INDEX `index_notifications_userId` ON `${TABLE_NAME}` (`userId`)" + } + ], + "foreignKeys": [ + { + "table": "notification_info", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "id" + ] + } + ] } ], "views": [], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"ba6f1d7e47823dac6ed1622fec043d5d\")" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"d0911840f629ef359aaf8ac8635dc571\")" ] } } \ No newline at end of file -- cgit v1.2.3 From b417cc51b28d558195c4cc075d0e6ce8192bf270 Mon Sep 17 00:00:00 2001 From: Allan Wang Date: Wed, 6 Mar 2019 23:35:04 -0500 Subject: Update notif database --- .../com/pitchedapps/frost/db/NotificationDbTest.kt | 81 ++++++++++++++- .../kotlin/com/pitchedapps/frost/db/CookiesDb.kt | 6 +- .../kotlin/com/pitchedapps/frost/db/Database.kt | 2 +- .../com/pitchedapps/frost/db/NotificationDb.kt | 112 +++++++++++++++------ .../frost/services/FrostNotifications.kt | 1 + .../1.json | 79 +++++---------- .../1.json | 1 + 7 files changed, 190 insertions(+), 92 deletions(-) diff --git a/app/src/androidTest/kotlin/com/pitchedapps/frost/db/NotificationDbTest.kt b/app/src/androidTest/kotlin/com/pitchedapps/frost/db/NotificationDbTest.kt index 12092bf6..2e9f1875 100644 --- a/app/src/androidTest/kotlin/com/pitchedapps/frost/db/NotificationDbTest.kt +++ b/app/src/androidTest/kotlin/com/pitchedapps/frost/db/NotificationDbTest.kt @@ -1,20 +1,93 @@ package com.pitchedapps.frost.db -import com.pitchedapps.frost.facebook.FbItem -import com.pitchedapps.frost.facebook.defaultTabs +import android.database.sqlite.SQLiteConstraintException +import com.pitchedapps.frost.services.NOTIF_CHANNEL_GENERAL +import com.pitchedapps.frost.services.NotificationContent import kotlinx.coroutines.runBlocking import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertTrue class NotificationDbTest : BaseDbTest() { private val dao get() = db.notifDao() + private fun cookie(id: Long) = CookieEntity(id, "name$id", "cookie$id") + + private fun notifContent(id: Long, cookie: CookieEntity, time: Long = id) = NotificationContent( + data = cookie, + id = id, + href = "", + title = null, + text = "", + timestamp = time, + profileUrl = null + ) + + @Test + fun saveAndRetrieve() { + val cookie = cookie(12345L) + // Unique unsorted ids + val notifs = listOf(0L, 4L, 2L, 6L, 99L, 3L).map { notifContent(it, cookie) } + runBlocking { + db.cookieDao().insertCookie(cookie) + dao.saveNotifications(NOTIF_CHANNEL_GENERAL, notifs) + val dbNotifs = dao.selectNotifications(cookie.id, NOTIF_CHANNEL_GENERAL) + assertEquals(notifs.sortedByDescending { it.timestamp }, dbNotifs, "Incorrect notification list received") + } + } + /** - * Note that order is also preserved here + * Primary key is both id and userId, in the event that the same notification to multiple users has the same id */ @Test - fun save() { + fun primaryKeyCheck() { + runBlocking { + val cookie1 = cookie(12345L) + val cookie2 = cookie(12L) + val notifs1 = (0L..2L).map { notifContent(it, cookie1) } + val notifs2 = notifs1.map { it.copy(data = cookie2) } + db.cookieDao().insertCookie(cookie1) + db.cookieDao().insertCookie(cookie2) + dao.saveNotifications(NOTIF_CHANNEL_GENERAL, notifs1) + dao.saveNotifications(NOTIF_CHANNEL_GENERAL, notifs2) + } + } + @Test + fun cascadeDeletion() { + val cookie = cookie(12345L) + // Unique unsorted ids + val notifs = listOf(0L, 4L, 2L, 6L, 99L, 3L).map { notifContent(it, cookie) } + runBlocking { + db.cookieDao().insertCookie(cookie) + dao.saveNotifications(NOTIF_CHANNEL_GENERAL, notifs) + db.cookieDao().deleteById(cookie.id) + val dbNotifs = dao.selectNotifications(cookie.id, NOTIF_CHANNEL_GENERAL) + assertTrue(dbNotifs.isEmpty(), "Cascade deletion failed") + } + } + + @Test + fun latestEpoch() { + val cookie = cookie(12345L) + // Unique unsorted ids + val notifs = listOf(0L, 4L, 2L, 6L, 99L, 3L).map { notifContent(it, cookie) } + runBlocking { + assertEquals(-1L, dao.latestEpoch(cookie.id, NOTIF_CHANNEL_GENERAL), "Default epoch failed") + db.cookieDao().insertCookie(cookie) + dao.saveNotifications(NOTIF_CHANNEL_GENERAL, notifs) + assertEquals(99L, dao.latestEpoch(cookie.id, NOTIF_CHANNEL_GENERAL), "Latest epoch failed") + } + } + + @Test + fun insertionWithInvalidCookies() { + assertFailsWith(SQLiteConstraintException::class) { + runBlocking { + dao.saveNotifications(NOTIF_CHANNEL_GENERAL, listOf(notifContent(1L, cookie(2L)))) + } + } } } \ No newline at end of file diff --git a/app/src/main/kotlin/com/pitchedapps/frost/db/CookiesDb.kt b/app/src/main/kotlin/com/pitchedapps/frost/db/CookiesDb.kt index 34a88011..d5347f18 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/db/CookiesDb.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/db/CookiesDb.kt @@ -17,6 +17,7 @@ package com.pitchedapps.frost.db import android.os.Parcelable +import androidx.room.ColumnInfo import androidx.room.Dao import androidx.room.Entity import androidx.room.Insert @@ -38,6 +39,7 @@ import kotlinx.android.parcel.Parcelize @Parcelize data class CookieEntity( @androidx.room.PrimaryKey + @ColumnInfo(name = "cookie_id") val id: Long, val name: String?, val cookie: String? @@ -53,7 +55,7 @@ interface CookieDao { @Query("SELECT * FROM cookies") suspend fun selectAll(): List - @Query("SELECT * FROM cookies WHERE id = :id") + @Query("SELECT * FROM cookies WHERE cookie_id = :id") suspend fun selectById(id: Long): CookieEntity? @Insert(onConflict = OnConflictStrategy.REPLACE) @@ -62,7 +64,7 @@ interface CookieDao { @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertCookies(cookies: List) - @Query("DELETE FROM cookies WHERE id = :id") + @Query("DELETE FROM cookies WHERE cookie_id = :id") suspend fun deleteById(id: Long) } diff --git a/app/src/main/kotlin/com/pitchedapps/frost/db/Database.kt b/app/src/main/kotlin/com/pitchedapps/frost/db/Database.kt index a37c2ee9..1ce9e7c2 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/db/Database.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/db/Database.kt @@ -14,7 +14,7 @@ interface FrostPrivateDao { } @Database( - entities = [CookieEntity::class, NotificationInfoEntity::class, NotificationEntity::class], + entities = [CookieEntity::class, NotificationEntity::class], version = 1, exportSchema = true ) diff --git a/app/src/main/kotlin/com/pitchedapps/frost/db/NotificationDb.kt b/app/src/main/kotlin/com/pitchedapps/frost/db/NotificationDb.kt index 56c3c5ac..9622ec47 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/db/NotificationDb.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/db/NotificationDb.kt @@ -16,16 +16,17 @@ */ package com.pitchedapps.frost.db +import androidx.room.ColumnInfo import androidx.room.Dao import androidx.room.Embedded import androidx.room.Entity import androidx.room.ForeignKey -import androidx.room.Ignore import androidx.room.Index import androidx.room.Insert +import androidx.room.OnConflictStrategy import androidx.room.Query -import androidx.room.Relation import androidx.room.Transaction +import com.pitchedapps.frost.services.NotificationContent import com.pitchedapps.frost.utils.L import com.raizlabs.android.dbflow.annotation.ConflictAction import com.raizlabs.android.dbflow.annotation.Database @@ -41,59 +42,108 @@ import com.raizlabs.android.dbflow.kotlinextensions.where import com.raizlabs.android.dbflow.sql.SQLiteType import com.raizlabs.android.dbflow.sql.migration.AlterTableMigration import com.raizlabs.android.dbflow.structure.BaseModel - -@Entity( - tableName = "notification_info", - foreignKeys = [ForeignKey( - entity = CookieEntity::class, - parentColumns = ["id"], childColumns = ["id"], onDelete = ForeignKey.CASCADE - )] -) -data class NotificationInfoEntity( - @androidx.room.PrimaryKey val id: Long, - val epoch: Long, - val epochIm: Long -) +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext @Entity( tableName = "notifications", + primaryKeys = ["notif_id", "userId"], foreignKeys = [ForeignKey( - entity = NotificationInfoEntity::class, - parentColumns = ["id"], + entity = CookieEntity::class, + parentColumns = ["cookie_id"], childColumns = ["userId"], onDelete = ForeignKey.CASCADE )], - indices = [Index("userId")] + indices = [Index("notif_id"), Index("userId")] ) data class NotificationEntity( - @androidx.room.PrimaryKey var id: Long, + @ColumnInfo(name = "notif_id") + val id: Long, val userId: Long, val href: String, val title: String?, val text: String, val timestamp: Long, - val profileUrl: String? + val profileUrl: String?, + // Type essentially refers to channel + val type: String ) { - @Ignore - val notifId = Math.abs(id.toInt()) + constructor( + type: String, + content: NotificationContent + ) : this( + content.id, + content.data.id, + content.href, + content.title, + content.text, + content.timestamp, + content.profileUrl, + type + ) } -data class NotificationInfo( +data class NotificationContentEntity( @Embedded - val info: NotificationInfoEntity, - @Relation(parentColumn = "id", entityColumn = "userId") - val notifications: List = emptyList() -) + val cookie: CookieEntity, + @Embedded + val notif: NotificationEntity +) { + fun toNotifContent() = NotificationContent( + data = cookie, + id = notif.id, + href = notif.href, + title = notif.title, + text = notif.text, + timestamp = notif.timestamp, + profileUrl = notif.profileUrl + ) +} @Dao interface NotificationDao { - @Query("SELECT * FROM notification_info WHERE id = :id") - fun selectById(id: Long): NotificationInfo? + /** + * Note that notifications are guaranteed to be ordered by descending timestamp + */ + @Transaction + @Query("SELECT * FROM cookies INNER JOIN notifications ON cookie_id = userId WHERE userId = :userId AND type = :type ORDER BY timestamp DESC") + fun _selectNotifications(userId: Long, type: String): List + @Query("SELECT timestamp FROM notifications WHERE userId = :userId AND type = :type ORDER BY timestamp DESC LIMIT 1") + fun _selectEpoch(userId: Long, type: String): Long? + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun _insertNotifications(notifs: List) + + @Query("DELETE FROM notifications WHERE userId = :userId AND type = :type") + fun _deleteNotifications(userId: Long, type: String) + + /** + * It is assumed that the notification batch comes from the same user + */ @Transaction - @Insert - fun insertInfo(info: NotificationInfoEntity) + fun _saveNotifications(type: String, notifs: List) { + val userId = notifs.firstOrNull()?.data?.id ?: return + val entities = notifs.map { NotificationEntity(type, it) } + _deleteNotifications(userId, type) + _insertNotifications(entities) + } +} + +suspend fun NotificationDao.selectNotifications(userId: Long, type: String): List = + withContext(Dispatchers.IO) { + _selectNotifications(userId, type).map { it.toNotifContent() } + } + +suspend fun NotificationDao.saveNotifications(type: String, notifs: List) { + withContext(Dispatchers.IO) { + _saveNotifications(type, notifs) + } +} + +suspend fun NotificationDao.latestEpoch(userId: Long, type: String): Long = withContext(Dispatchers.IO) { + _selectEpoch(userId, type) ?: -1 } /** diff --git a/app/src/main/kotlin/com/pitchedapps/frost/services/FrostNotifications.kt b/app/src/main/kotlin/com/pitchedapps/frost/services/FrostNotifications.kt index abd871b3..7da3c128 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/services/FrostNotifications.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/services/FrostNotifications.kt @@ -248,6 +248,7 @@ enum class NotificationType( * Notification data holder */ data class NotificationContent( + // TODO replace data with userId? val data: CookieEntity, val id: Long, val href: String, diff --git a/app/src/schemas/com.pitchedapps.frost.db.FrostPrivateDatabase/1.json b/app/src/schemas/com.pitchedapps.frost.db.FrostPrivateDatabase/1.json index f0909a17..c382bce7 100644 --- a/app/src/schemas/com.pitchedapps.frost.db.FrostPrivateDatabase/1.json +++ b/app/src/schemas/com.pitchedapps.frost.db.FrostPrivateDatabase/1.json @@ -2,15 +2,15 @@ "formatVersion": 1, "database": { "version": 1, - "identityHash": "d0911840f629ef359aaf8ac8635dc571", + "identityHash": "77eff76407f59b690b8877cc22fac42f", "entities": [ { "tableName": "cookies", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT, `cookie` TEXT, PRIMARY KEY(`id`))", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`cookie_id` INTEGER NOT NULL, `name` TEXT, `cookie` TEXT, PRIMARY KEY(`cookie_id`))", "fields": [ { "fieldPath": "id", - "columnName": "id", + "columnName": "cookie_id", "affinity": "INTEGER", "notNull": true }, @@ -29,64 +29,20 @@ ], "primaryKey": { "columnNames": [ - "id" + "cookie_id" ], "autoGenerate": false }, "indices": [], "foreignKeys": [] }, - { - "tableName": "notification_info", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `epoch` INTEGER NOT NULL, `epochIm` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`id`) REFERENCES `cookies`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "epoch", - "columnName": "epoch", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "epochIm", - "columnName": "epochIm", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "id" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [ - { - "table": "cookies", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "id" - ], - "referencedColumns": [ - "id" - ] - } - ] - }, { "tableName": "notifications", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `userId` INTEGER NOT NULL, `href` TEXT NOT NULL, `title` TEXT, `text` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `profileUrl` TEXT, PRIMARY KEY(`id`), FOREIGN KEY(`userId`) REFERENCES `notification_info`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`notif_id` INTEGER NOT NULL, `userId` INTEGER NOT NULL, `href` TEXT NOT NULL, `title` TEXT, `text` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `profileUrl` TEXT, `type` TEXT NOT NULL, PRIMARY KEY(`notif_id`, `userId`), FOREIGN KEY(`userId`) REFERENCES `cookies`(`cookie_id`) ON UPDATE NO ACTION ON DELETE CASCADE )", "fields": [ { "fieldPath": "id", - "columnName": "id", + "columnName": "notif_id", "affinity": "INTEGER", "notNull": true }, @@ -125,15 +81,30 @@ "columnName": "profileUrl", "affinity": "TEXT", "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true } ], "primaryKey": { "columnNames": [ - "id" + "notif_id", + "userId" ], "autoGenerate": false }, "indices": [ + { + "name": "index_notifications_notif_id", + "unique": false, + "columnNames": [ + "notif_id" + ], + "createSql": "CREATE INDEX `index_notifications_notif_id` ON `${TABLE_NAME}` (`notif_id`)" + }, { "name": "index_notifications_userId", "unique": false, @@ -145,14 +116,14 @@ ], "foreignKeys": [ { - "table": "notification_info", + "table": "cookies", "onDelete": "CASCADE", "onUpdate": "NO ACTION", "columns": [ "userId" ], "referencedColumns": [ - "id" + "cookie_id" ] } ] @@ -161,7 +132,7 @@ "views": [], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"d0911840f629ef359aaf8ac8635dc571\")" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"77eff76407f59b690b8877cc22fac42f\")" ] } } \ No newline at end of file diff --git a/app/src/schemas/com.pitchedapps.frost.db.FrostPublicDatabase/1.json b/app/src/schemas/com.pitchedapps.frost.db.FrostPublicDatabase/1.json index 687d0bc1..fe2aa83e 100644 --- a/app/src/schemas/com.pitchedapps.frost.db.FrostPublicDatabase/1.json +++ b/app/src/schemas/com.pitchedapps.frost.db.FrostPublicDatabase/1.json @@ -31,6 +31,7 @@ "foreignKeys": [] } ], + "views": [], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"fde868470836ff9230f1d406922d7563\")" -- cgit v1.2.3 From 7f1f2247de1d61354adfd2cec011cc475a20f683 Mon Sep 17 00:00:00 2001 From: Allan Wang Date: Thu, 7 Mar 2019 00:12:24 -0500 Subject: Migrate to dao and add filter to title --- .../com/pitchedapps/frost/db/NotificationDbTest.kt | 51 +++++++++++++++++---- .../com/pitchedapps/frost/db/NotificationDb.kt | 26 ++++++----- .../frost/services/FrostNotifications.kt | 52 +++++++++++++--------- .../frost/services/NotificationService.kt | 4 +- 4 files changed, 91 insertions(+), 42 deletions(-) diff --git a/app/src/androidTest/kotlin/com/pitchedapps/frost/db/NotificationDbTest.kt b/app/src/androidTest/kotlin/com/pitchedapps/frost/db/NotificationDbTest.kt index 2e9f1875..176d0d3a 100644 --- a/app/src/androidTest/kotlin/com/pitchedapps/frost/db/NotificationDbTest.kt +++ b/app/src/androidTest/kotlin/com/pitchedapps/frost/db/NotificationDbTest.kt @@ -1,12 +1,12 @@ package com.pitchedapps.frost.db -import android.database.sqlite.SQLiteConstraintException import com.pitchedapps.frost.services.NOTIF_CHANNEL_GENERAL +import com.pitchedapps.frost.services.NOTIF_CHANNEL_MESSAGES import com.pitchedapps.frost.services.NotificationContent import kotlinx.coroutines.runBlocking import kotlin.test.Test import kotlin.test.assertEquals -import kotlin.test.assertFailsWith +import kotlin.test.assertFalse import kotlin.test.assertTrue class NotificationDbTest : BaseDbTest() { @@ -38,6 +38,40 @@ class NotificationDbTest : BaseDbTest() { } } + @Test + fun selectConditions() { + runBlocking { + val cookie1 = cookie(12345L) + val cookie2 = cookie(12L) + val notifs1 = (0L..2L).map { notifContent(it, cookie1) } + val notifs2 = (5L..10L).map { notifContent(it, cookie2) } + db.cookieDao().insertCookie(cookie1) + db.cookieDao().insertCookie(cookie2) + dao.saveNotifications(NOTIF_CHANNEL_GENERAL, notifs1) + dao.saveNotifications(NOTIF_CHANNEL_MESSAGES, notifs2) + assertEquals( + emptyList(), + dao.selectNotifications(cookie1.id, NOTIF_CHANNEL_MESSAGES), + "Filtering by type did not work for cookie1" + ) + assertEquals( + notifs1.sortedByDescending { it.timestamp }, + dao.selectNotifications(cookie1.id, NOTIF_CHANNEL_GENERAL), + "Selection for cookie1 failed" + ) + assertEquals( + emptyList(), + dao.selectNotifications(cookie2.id, NOTIF_CHANNEL_GENERAL), + "Filtering by type did not work for cookie2" + ) + assertEquals( + notifs2.sortedByDescending { it.timestamp }, + dao.selectNotifications(cookie2.id, NOTIF_CHANNEL_MESSAGES), + "Selection for cookie2 failed" + ) + } + } + /** * Primary key is both id and userId, in the event that the same notification to multiple users has the same id */ @@ -50,8 +84,8 @@ class NotificationDbTest : BaseDbTest() { val notifs2 = notifs1.map { it.copy(data = cookie2) } db.cookieDao().insertCookie(cookie1) db.cookieDao().insertCookie(cookie2) - dao.saveNotifications(NOTIF_CHANNEL_GENERAL, notifs1) - dao.saveNotifications(NOTIF_CHANNEL_GENERAL, notifs2) + assertTrue(dao.saveNotifications(NOTIF_CHANNEL_GENERAL, notifs1), "Notif1 save failed") + assertTrue(dao.saveNotifications(NOTIF_CHANNEL_GENERAL, notifs2), "Notif2 save failed") } } @@ -84,10 +118,11 @@ class NotificationDbTest : BaseDbTest() { @Test fun insertionWithInvalidCookies() { - assertFailsWith(SQLiteConstraintException::class) { - runBlocking { - dao.saveNotifications(NOTIF_CHANNEL_GENERAL, listOf(notifContent(1L, cookie(2L)))) - } + runBlocking { + assertFalse( + dao.saveNotifications(NOTIF_CHANNEL_GENERAL, listOf(notifContent(1L, cookie(2L)))), + "Notif save should not have passed without relevant cookie entries" + ) } } } \ No newline at end of file diff --git a/app/src/main/kotlin/com/pitchedapps/frost/db/NotificationDb.kt b/app/src/main/kotlin/com/pitchedapps/frost/db/NotificationDb.kt index 9622ec47..60ae2ae7 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/db/NotificationDb.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/db/NotificationDb.kt @@ -136,9 +136,19 @@ suspend fun NotificationDao.selectNotifications(userId: Long, type: String): Lis _selectNotifications(userId, type).map { it.toNotifContent() } } -suspend fun NotificationDao.saveNotifications(type: String, notifs: List) { - withContext(Dispatchers.IO) { - _saveNotifications(type, notifs) +/** + * Returns true if successful, given that there are constraints to the insertion + */ +suspend fun NotificationDao.saveNotifications(type: String, notifs: List): Boolean { + if (notifs.isEmpty()) return true + return withContext(Dispatchers.IO) { + try { + _saveNotifications(type, notifs) + true + } catch (e: Exception) { + L.e(e) { "Notif save failed" } + false + } } } @@ -175,12 +185,4 @@ data class NotificationModel( fun lastNotificationTime(id: Long): NotificationModel = (select from NotificationModel::class where (NotificationModel_Table.id eq id)).querySingle() - ?: NotificationModel(id = id) - -fun saveNotificationTime(notificationModel: NotificationModel, callback: (() -> Unit)? = null) { - notificationModel.async save { - L.d { "Fb notification model saved" } - L._d { notificationModel } - callback?.invoke() - } -} + ?: NotificationModel(id = id) \ No newline at end of file diff --git a/app/src/main/kotlin/com/pitchedapps/frost/services/FrostNotifications.kt b/app/src/main/kotlin/com/pitchedapps/frost/services/FrostNotifications.kt index 7da3c128..eb81ff04 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/services/FrostNotifications.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/services/FrostNotifications.kt @@ -32,9 +32,11 @@ import com.pitchedapps.frost.BuildConfig import com.pitchedapps.frost.R import com.pitchedapps.frost.activities.FrostWebActivity import com.pitchedapps.frost.db.CookieEntity -import com.pitchedapps.frost.db.CookieModel +import com.pitchedapps.frost.db.FrostDatabase import com.pitchedapps.frost.db.NotificationModel import com.pitchedapps.frost.db.lastNotificationTime +import com.pitchedapps.frost.db.latestEpoch +import com.pitchedapps.frost.db.saveNotifications import com.pitchedapps.frost.enums.OverlayContext import com.pitchedapps.frost.facebook.FbItem import com.pitchedapps.frost.facebook.parsers.FrostParser @@ -65,8 +67,8 @@ enum class NotificationType( private val overlayContext: OverlayContext, private val fbItem: FbItem, private val parser: FrostParser, + // Legacy; remove with dbflow private val getTime: (notif: NotificationModel) -> Long, - private val putTime: (notif: NotificationModel, time: Long) -> NotificationModel, private val ringtone: () -> String ) { @@ -76,7 +78,6 @@ enum class NotificationType( FbItem.NOTIFICATIONS, NotifParser, NotificationModel::epoch, - { notif, time -> notif.copy(epoch = time) }, Prefs::notificationRingtone ) { @@ -90,7 +91,6 @@ enum class NotificationType( FbItem.MESSAGES, MessageParser, NotificationModel::epochIm, - { notif, time -> notif.copy(epochIm = time) }, Prefs::messageRingtone ); @@ -117,7 +117,8 @@ enum class NotificationType( * Returns the number of notifications generated, * or -1 if an error occurred */ - fun fetch(context: Context, data: CookieEntity): Int { + suspend fun fetch(context: Context, data: CookieEntity): Int { + val notifDao = FrostDatabase.get().notifDao() val response = try { parser.parse(data.cookie) } catch (ignored: Exception) { @@ -128,35 +129,44 @@ enum class NotificationType( return -1 } val notifContents = response.data.getUnreadNotifications(data).filter { notif -> - val text = notif.text - Prefs.notificationKeywords.none { text.contains(it, true) } + val inText = notif.text.let { text -> + Prefs.notificationKeywords.none { text.contains(it, true) } + } + val inTitle = notif.title?.let { title -> + Prefs.notificationKeywords.none { title.contains(it, true) } + } ?: false + inText || inTitle } if (notifContents.isEmpty()) return 0 val userId = data.id - val prevNotifTime = lastNotificationTime(userId) - val prevLatestEpoch = getTime(prevNotifTime) + // Legacy, remove with dbflow + val prevLatestEpoch = + notifDao.latestEpoch(userId, channelId).takeIf { it != -1L } ?: getTime(lastNotificationTime(userId)) L.v { "Notif $name prev epoch $prevLatestEpoch" } - var newLatestEpoch = prevLatestEpoch - val notifs = mutableListOf() - notifContents.forEach { notif -> - L.v { "Notif timestamp ${notif.timestamp}" } - if (notif.timestamp <= prevLatestEpoch) return@forEach - notifs.add(createNotification(context, notif)) - if (notif.timestamp > newLatestEpoch) - newLatestEpoch = notif.timestamp - } - if (newLatestEpoch > prevLatestEpoch) - putTime(prevNotifTime, newLatestEpoch).save() - L.d { "Notif $name new epoch ${getTime(lastNotificationTime(userId))}" } if (prevLatestEpoch == -1L && !BuildConfig.DEBUG) { L.d { "Skipping first notification fetch" } return 0 // do not notify the first time } + + val newNotifContents = notifContents.filter { it.timestamp > prevLatestEpoch } + + if (newNotifContents.isEmpty()) { + L.d { "No new notifs found for $name" } + return 0 + } + + L.d { "Notif $name new epoch ${newNotifContents.map { it.timestamp }.max()}" } + + val notifs = newNotifContents.map { createNotification(context, it) } + + notifDao.saveNotifications(channelId, newNotifContents) + frostEvent("Notifications", "Type" to name, "Count" to notifs.size) if (notifs.size > 1) summaryNotification(context, userId, notifs.size).notify(context) val ringtone = ringtone() notifs.forEachIndexed { i, notif -> + // Ring at most twice notif.withAlert(i < 2, ringtone).notify(context) } return notifs.size diff --git a/app/src/main/kotlin/com/pitchedapps/frost/services/NotificationService.kt b/app/src/main/kotlin/com/pitchedapps/frost/services/NotificationService.kt index 088d8e0a..e1db5fa6 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/services/NotificationService.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/services/NotificationService.kt @@ -23,6 +23,7 @@ import com.pitchedapps.frost.BuildConfig import com.pitchedapps.frost.R import com.pitchedapps.frost.db.CookieDao import com.pitchedapps.frost.db.CookieEntity +import com.pitchedapps.frost.db.NotificationDao import com.pitchedapps.frost.utils.L import com.pitchedapps.frost.utils.Prefs import com.pitchedapps.frost.utils.frostEvent @@ -44,6 +45,7 @@ import org.koin.android.ext.android.inject class NotificationService : BaseJobService() { val cookieDao: CookieDao by inject() + val notifDao: NotificationDao by inject() override fun onStopJob(params: JobParameters?): Boolean { super.onStopJob(params) @@ -110,7 +112,7 @@ class NotificationService : BaseJobService() { * Implemented fetch to also notify when an error occurs * Also normalized the output to return the number of notifications received */ - private fun fetch(jobId: Int, type: NotificationType, cookie: CookieEntity): Int { + private suspend fun fetch(jobId: Int, type: NotificationType, cookie: CookieEntity): Int { val count = type.fetch(this, cookie) if (count < 0) { if (jobId == NOTIFICATION_JOB_NOW) -- cgit v1.2.3 From e96160d7f7cf45cdb8673928ac9e2fbca63002bd Mon Sep 17 00:00:00 2001 From: Allan Wang Date: Thu, 7 Mar 2019 00:16:22 -0500 Subject: Do not send notifications if db save fails --- .../kotlin/com/pitchedapps/frost/services/FrostNotifications.kt | 9 ++++++--- .../kotlin/com/pitchedapps/frost/services/NotificationService.kt | 1 - 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/app/src/main/kotlin/com/pitchedapps/frost/services/FrostNotifications.kt b/app/src/main/kotlin/com/pitchedapps/frost/services/FrostNotifications.kt index eb81ff04..14b6ec39 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/services/FrostNotifications.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/services/FrostNotifications.kt @@ -155,11 +155,14 @@ enum class NotificationType( return 0 } - L.d { "Notif $name new epoch ${newNotifContents.map { it.timestamp }.max()}" } + L.d { "${newNotifContents.size} new notifs found for $name" } - val notifs = newNotifContents.map { createNotification(context, it) } + if (!notifDao.saveNotifications(channelId, newNotifContents)) { + L.d { "Skip notifs for $name as saving failed" } + return 0 + } - notifDao.saveNotifications(channelId, newNotifContents) + val notifs = newNotifContents.map { createNotification(context, it) } frostEvent("Notifications", "Type" to name, "Count" to notifs.size) if (notifs.size > 1) diff --git a/app/src/main/kotlin/com/pitchedapps/frost/services/NotificationService.kt b/app/src/main/kotlin/com/pitchedapps/frost/services/NotificationService.kt index e1db5fa6..8b100c19 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/services/NotificationService.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/services/NotificationService.kt @@ -45,7 +45,6 @@ import org.koin.android.ext.android.inject class NotificationService : BaseJobService() { val cookieDao: CookieDao by inject() - val notifDao: NotificationDao by inject() override fun onStopJob(params: JobParameters?): Boolean { super.onStopJob(params) -- cgit v1.2.3 From cf5fd28ec74069746b49757b6a95a36851a56105 Mon Sep 17 00:00:00 2001 From: Allan Wang Date: Thu, 7 Mar 2019 00:22:15 -0500 Subject: Move legacy notif model to internal call --- .../main/kotlin/com/pitchedapps/frost/db/NotificationDb.kt | 14 ++++++++++---- .../com/pitchedapps/frost/services/FrostNotifications.kt | 9 +-------- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/app/src/main/kotlin/com/pitchedapps/frost/db/NotificationDb.kt b/app/src/main/kotlin/com/pitchedapps/frost/db/NotificationDb.kt index 60ae2ae7..7f41fbf8 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/db/NotificationDb.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/db/NotificationDb.kt @@ -26,6 +26,8 @@ import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query import androidx.room.Transaction +import com.pitchedapps.frost.services.NOTIF_CHANNEL_GENERAL +import com.pitchedapps.frost.services.NOTIF_CHANNEL_MESSAGES import com.pitchedapps.frost.services.NotificationContent import com.pitchedapps.frost.utils.L import com.raizlabs.android.dbflow.annotation.ConflictAction @@ -33,10 +35,8 @@ import com.raizlabs.android.dbflow.annotation.Database import com.raizlabs.android.dbflow.annotation.Migration import com.raizlabs.android.dbflow.annotation.PrimaryKey import com.raizlabs.android.dbflow.annotation.Table -import com.raizlabs.android.dbflow.kotlinextensions.async import com.raizlabs.android.dbflow.kotlinextensions.eq import com.raizlabs.android.dbflow.kotlinextensions.from -import com.raizlabs.android.dbflow.kotlinextensions.save import com.raizlabs.android.dbflow.kotlinextensions.select import com.raizlabs.android.dbflow.kotlinextensions.where import com.raizlabs.android.dbflow.sql.SQLiteType @@ -153,7 +153,13 @@ suspend fun NotificationDao.saveNotifications(type: String, notifs: List it.epoch + NOTIF_CHANNEL_MESSAGES -> it.epochIm + else -> -1L + } + } } /** @@ -183,6 +189,6 @@ data class NotificationModel( var epochIm: Long = -1L ) : BaseModel() -fun lastNotificationTime(id: Long): NotificationModel = +private fun lastNotificationTime(id: Long): NotificationModel = (select from NotificationModel::class where (NotificationModel_Table.id eq id)).querySingle() ?: NotificationModel(id = id) \ No newline at end of file diff --git a/app/src/main/kotlin/com/pitchedapps/frost/services/FrostNotifications.kt b/app/src/main/kotlin/com/pitchedapps/frost/services/FrostNotifications.kt index 14b6ec39..631912a3 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/services/FrostNotifications.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/services/FrostNotifications.kt @@ -33,8 +33,6 @@ import com.pitchedapps.frost.R import com.pitchedapps.frost.activities.FrostWebActivity import com.pitchedapps.frost.db.CookieEntity import com.pitchedapps.frost.db.FrostDatabase -import com.pitchedapps.frost.db.NotificationModel -import com.pitchedapps.frost.db.lastNotificationTime import com.pitchedapps.frost.db.latestEpoch import com.pitchedapps.frost.db.saveNotifications import com.pitchedapps.frost.enums.OverlayContext @@ -67,8 +65,6 @@ enum class NotificationType( private val overlayContext: OverlayContext, private val fbItem: FbItem, private val parser: FrostParser, - // Legacy; remove with dbflow - private val getTime: (notif: NotificationModel) -> Long, private val ringtone: () -> String ) { @@ -77,7 +73,6 @@ enum class NotificationType( OverlayContext.NOTIFICATION, FbItem.NOTIFICATIONS, NotifParser, - NotificationModel::epoch, Prefs::notificationRingtone ) { @@ -90,7 +85,6 @@ enum class NotificationType( OverlayContext.MESSAGE, FbItem.MESSAGES, MessageParser, - NotificationModel::epochIm, Prefs::messageRingtone ); @@ -140,8 +134,7 @@ enum class NotificationType( if (notifContents.isEmpty()) return 0 val userId = data.id // Legacy, remove with dbflow - val prevLatestEpoch = - notifDao.latestEpoch(userId, channelId).takeIf { it != -1L } ?: getTime(lastNotificationTime(userId)) + val prevLatestEpoch = notifDao.latestEpoch(userId, channelId) L.v { "Notif $name prev epoch $prevLatestEpoch" } if (prevLatestEpoch == -1L && !BuildConfig.DEBUG) { L.d { "Skipping first notification fetch" } -- cgit v1.2.3 From d7ee076bdc1a179a0fdb45b812642b8656d74415 Mon Sep 17 00:00:00 2001 From: Allan Wang Date: Thu, 7 Mar 2019 00:32:17 -0500 Subject: Allow migration for fbtabs --- .../kotlin/com/pitchedapps/frost/StartActivity.kt | 12 ++++++--- .../frost/activities/TabCustomizerActivity.kt | 30 +++++++++++++++------- .../kotlin/com/pitchedapps/frost/db/FbTabsDb.kt | 17 +----------- 3 files changed, 30 insertions(+), 29 deletions(-) diff --git a/app/src/main/kotlin/com/pitchedapps/frost/StartActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/StartActivity.kt index e81df3fa..3b7418e1 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/StartActivity.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/StartActivity.kt @@ -35,7 +35,9 @@ import com.pitchedapps.frost.activities.SelectorActivity import com.pitchedapps.frost.db.CookieDao import com.pitchedapps.frost.db.CookieEntity import com.pitchedapps.frost.db.CookieModel +import com.pitchedapps.frost.db.FbTabDao import com.pitchedapps.frost.db.FbTabModel +import com.pitchedapps.frost.db.save import com.pitchedapps.frost.facebook.FbCookie import com.pitchedapps.frost.utils.EXTRA_COOKIES import com.pitchedapps.frost.utils.L @@ -56,6 +58,7 @@ import java.util.ArrayList class StartActivity : KauBaseActivity() { private val cookieDao: CookieDao by inject() + private val tabDao: FbTabDao by inject() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -98,13 +101,14 @@ class StartActivity : KauBaseActivity() { /** * Migrate from dbflow to room + * TODO delete dbflow data */ private suspend fun migrate() = withContext(Dispatchers.IO) { if (cookieDao.selectAll().isNotEmpty()) return@withContext - val cookies = (select from CookieModel::class).queryList() - cookieDao.insertCookies(cookies.map { CookieEntity(it.id, it.name, it.cookie) }) - // TODO - val tabs = (select from FbTabModel::class).queryList() + val cookies = (select from CookieModel::class).queryList().map { CookieEntity(it.id, it.name, it.cookie) } + cookieDao.insertCookies(cookies) + val tabs = (select from FbTabModel::class).queryList().map(FbTabModel::tab) + tabDao.save(tabs) } private fun showInvalidWebView() = diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/TabCustomizerActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/TabCustomizerActivity.kt index 73e6419e..a5dceb3a 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/activities/TabCustomizerActivity.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/TabCustomizerActivity.kt @@ -25,6 +25,7 @@ import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.RecyclerView import ca.allanwang.kau.kotlin.lazyContext +import ca.allanwang.kau.utils.launchMain import ca.allanwang.kau.utils.scaleXY import ca.allanwang.kau.utils.setIcon import ca.allanwang.kau.utils.withAlpha @@ -33,14 +34,18 @@ import com.mikepenz.fastadapter_extensions.drag.ItemTouchCallback import com.mikepenz.fastadapter_extensions.drag.SimpleDragCallback import com.mikepenz.google_material_typeface_library.GoogleMaterial import com.pitchedapps.frost.R +import com.pitchedapps.frost.db.FbTabDao import com.pitchedapps.frost.db.TAB_COUNT -import com.pitchedapps.frost.db.loadFbTabs import com.pitchedapps.frost.db.save +import com.pitchedapps.frost.db.selectAll import com.pitchedapps.frost.facebook.FbItem import com.pitchedapps.frost.iitems.TabIItem import com.pitchedapps.frost.utils.Prefs import com.pitchedapps.frost.utils.setFrostColors import kotlinx.android.synthetic.main.activity_tab_customizer.* +import kotlinx.coroutines.NonCancellable +import kotlinx.coroutines.launch +import org.koin.android.ext.android.inject import java.util.Collections /** @@ -48,6 +53,8 @@ import java.util.Collections */ class TabCustomizerActivity : BaseActivity() { + private val tabDao: FbTabDao by inject() + private val adapter = FastItemAdapter() private val wobble = lazyContext { AnimationUtils.loadAnimation(it, R.anim.rotate_delta) } @@ -65,12 +72,14 @@ class TabCustomizerActivity : BaseActivity() { divider.setBackgroundColor(Prefs.textColor.withAlpha(30)) instructions.setTextColor(Prefs.textColor) - val tabs = loadFbTabs().toMutableList() - val remaining = FbItem.values().filter { it.name[0] != '_' }.toMutableList() - remaining.removeAll(tabs) - tabs.addAll(remaining) + launch { + val tabs = tabDao.selectAll().toMutableList() + val remaining = FbItem.values().filter { it.name[0] != '_' }.toMutableList() + remaining.removeAll(tabs) + tabs.addAll(remaining) + adapter.add(tabs.map(::TabIItem)) + } - adapter.add(tabs.map(::TabIItem)) bindSwapper(adapter, tab_recycler) adapter.withOnClickListener { view, _, _, _ -> view!!.wobble(); true } @@ -80,9 +89,12 @@ class TabCustomizerActivity : BaseActivity() { fab_save.setIcon(GoogleMaterial.Icon.gmd_check, Prefs.iconColor) fab_save.backgroundTintList = ColorStateList.valueOf(Prefs.accentColor) fab_save.setOnClickListener { - adapter.adapterItems.subList(0, TAB_COUNT).map(TabIItem::item).save() - setResult(Activity.RESULT_OK) - finish() + launchMain(NonCancellable) { + val tabs = adapter.adapterItems.subList(0, TAB_COUNT).map(TabIItem::item) + tabDao.save(tabs) + setResult(Activity.RESULT_OK) + finish() + } } fab_cancel.setIcon(GoogleMaterial.Icon.gmd_close, Prefs.iconColor) fab_cancel.backgroundTintList = ColorStateList.valueOf(Prefs.accentColor) diff --git a/app/src/main/kotlin/com/pitchedapps/frost/db/FbTabsDb.kt b/app/src/main/kotlin/com/pitchedapps/frost/db/FbTabsDb.kt index 582d57fb..c2bb0837 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/db/FbTabsDb.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/db/FbTabsDb.kt @@ -103,19 +103,4 @@ object FbTabsDb { } @Table(database = FbTabsDb::class, allFields = true) -data class FbTabModel(@PrimaryKey var position: Int = -1, var tab: FbItem = FbItem.FEED) : BaseModel() - -/** - * Load tabs synchronously - * Note that tab length should never be a big number anyways - */ -fun loadFbTabs(): List { - val tabs: List? = (select from (FbTabModel::class)).orderBy(FbTabModel_Table.position, true).queryList() - if (tabs?.size == TAB_COUNT) return tabs.map(FbTabModel::tab) - L.d { "No tabs (${tabs?.size}); loading default" } - return defaultTabs() -} - -fun List.save() { - database().beginTransactionAsync(mapIndexed(::FbTabModel).fastSave().build()).execute() -} +data class FbTabModel(@PrimaryKey var position: Int = -1, var tab: FbItem = FbItem.FEED) : BaseModel() \ No newline at end of file -- cgit v1.2.3 From f717953debf1cd023b2f612e0ea9b2eede4c37c3 Mon Sep 17 00:00:00 2001 From: Allan Wang Date: Thu, 7 Mar 2019 00:36:11 -0500 Subject: Delete unused dbutils --- .../main/kotlin/com/pitchedapps/frost/FrostApp.kt | 2 +- .../kotlin/com/pitchedapps/frost/db/DbUtils.kt | 38 ---------------------- 2 files changed, 1 insertion(+), 39 deletions(-) delete mode 100644 app/src/main/kotlin/com/pitchedapps/frost/db/DbUtils.kt diff --git a/app/src/main/kotlin/com/pitchedapps/frost/FrostApp.kt b/app/src/main/kotlin/com/pitchedapps/frost/FrostApp.kt index 7669ef46..41c6ff4b 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/FrostApp.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/FrostApp.kt @@ -83,7 +83,7 @@ class FrostApp : Application() { ) Showcase.initialize(this, "${BuildConfig.APPLICATION_ID}.showcase") Prefs.initialize(this, "${BuildConfig.APPLICATION_ID}.prefs") - // if (LeakCanary.isInAnalyzerProcess(this)) return +// if (LeakCanary.isInAnalyzerProcess(this)) return // refWatcher = LeakCanary.install(this) initBugsnag() KL.shouldLog = { BuildConfig.DEBUG } diff --git a/app/src/main/kotlin/com/pitchedapps/frost/db/DbUtils.kt b/app/src/main/kotlin/com/pitchedapps/frost/db/DbUtils.kt deleted file mode 100644 index 03aac059..00000000 --- a/app/src/main/kotlin/com/pitchedapps/frost/db/DbUtils.kt +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright 2018 Allan Wang - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package com.pitchedapps.frost.db - -import android.content.Context -import com.pitchedapps.frost.utils.L -import com.raizlabs.android.dbflow.config.FlowManager -import com.raizlabs.android.dbflow.structure.database.transaction.FastStoreModelTransaction - -/** - * Created by Allan Wang on 2017-05-30. - */ - -object DbUtils { - fun db(name: String) = FlowManager.getDatabase(name) - fun dbName(name: String) = "$name.db" - fun deleteDatabase(c: Context, name: String) = c.deleteDatabase(dbName(name)) -} - -inline fun List.replace(dbName: String) { - L.d { "Replacing $dbName.db" } - DbUtils.db(dbName).reset() - FastStoreModelTransaction.saveBuilder(FlowManager.getModelAdapter(T::class.java)).addAll(this).build() -} -- cgit v1.2.3 From eabc8e3393cbd6b3dba5c70a5920075b8158d84e Mon Sep 17 00:00:00 2001 From: Allan Wang Date: Thu, 7 Mar 2019 03:12:27 -0500 Subject: Add cache entityt --- .../kotlin/com/pitchedapps/frost/db/CacheDb.kt | 56 ++++++++++++++++++++++ .../kotlin/com/pitchedapps/frost/db/Database.kt | 3 +- 2 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 app/src/main/kotlin/com/pitchedapps/frost/db/CacheDb.kt diff --git a/app/src/main/kotlin/com/pitchedapps/frost/db/CacheDb.kt b/app/src/main/kotlin/com/pitchedapps/frost/db/CacheDb.kt new file mode 100644 index 00000000..bd6bff4b --- /dev/null +++ b/app/src/main/kotlin/com/pitchedapps/frost/db/CacheDb.kt @@ -0,0 +1,56 @@ +/* + * Copyright 2018 Allan Wang + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.pitchedapps.frost.db + +import android.os.Parcelable +import androidx.room.Dao +import androidx.room.Entity +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import kotlinx.android.parcel.Parcelize + +/** + * Created by Allan Wang on 2017-05-30. + */ + +/** + * Generic cache to store serialized content + */ +@Entity(tableName = "frost_cache") +@Parcelize +data class CacheEntity( + @androidx.room.PrimaryKey + val id: String, + val lastUpdated: Long, + val contents: String +) : Parcelable + +@Dao +interface CacheDao { + + @Query("SELECT * FROM frost_cache WHERE id = :id") + suspend fun selectById(id: Long): CacheEntity? + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertCache(cache: CacheEntity) + + @Query("DELETE FROM frost_cache WHERE id = :id") + suspend fun deleteById(id: Long) +} + +suspend fun CacheDao.save(id: String, contents: String) = insertCache(CacheEntity(id, System.currentTimeMillis(), contents)) \ No newline at end of file diff --git a/app/src/main/kotlin/com/pitchedapps/frost/db/Database.kt b/app/src/main/kotlin/com/pitchedapps/frost/db/Database.kt index 1ce9e7c2..b83fce52 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/db/Database.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/db/Database.kt @@ -11,10 +11,11 @@ import org.koin.standalone.StandAloneContext interface FrostPrivateDao { fun cookieDao(): CookieDao fun notifDao(): NotificationDao + fun cacheDao(): CacheDao } @Database( - entities = [CookieEntity::class, NotificationEntity::class], + entities = [CookieEntity::class, NotificationEntity::class, CacheEntity::class], version = 1, exportSchema = true ) -- cgit v1.2.3 From f1878133d8af686ce8c27acffe28f26e9dda5165 Mon Sep 17 00:00:00 2001 From: Allan Wang Date: Thu, 7 Mar 2019 04:00:20 -0500 Subject: Add koin test --- app/build.gradle | 1 + .../com/pitchedapps/frost/db/DatabaseTest.kt | 38 ++++++++++++++++++++++ .../kotlin/com/pitchedapps/frost/db/CacheDb.kt | 3 +- .../kotlin/com/pitchedapps/frost/db/Database.kt | 2 ++ .../kotlin/com/pitchedapps/frost/db/FbTabsDb.kt | 5 --- .../1.json | 36 ++++++++++++++++++-- gradle.properties | 2 +- 7 files changed, 78 insertions(+), 9 deletions(-) create mode 100644 app/src/androidTest/kotlin/com/pitchedapps/frost/db/DatabaseTest.kt diff --git a/app/build.gradle b/app/build.gradle index d5dcf87e..74e8015c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -178,6 +178,7 @@ dependencies { androidTestImplementation kauDependency.espresso androidTestImplementation kauDependency.testRules androidTestImplementation kauDependency.testRunner + androidTestImplementation "org.jetbrains.kotlin:kotlin-reflect:${KOTLIN}" testImplementation kauDependency.kotlinTest testImplementation "org.jetbrains.kotlin:kotlin-reflect:${KOTLIN}" diff --git a/app/src/androidTest/kotlin/com/pitchedapps/frost/db/DatabaseTest.kt b/app/src/androidTest/kotlin/com/pitchedapps/frost/db/DatabaseTest.kt new file mode 100644 index 00000000..1f1a201b --- /dev/null +++ b/app/src/androidTest/kotlin/com/pitchedapps/frost/db/DatabaseTest.kt @@ -0,0 +1,38 @@ +package com.pitchedapps.frost.db + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.runner.RunWith +import org.koin.error.NoBeanDefFoundException +import org.koin.standalone.get +import org.koin.test.KoinTest +import kotlin.reflect.KClass +import kotlin.reflect.full.functions +import kotlin.test.Test +import kotlin.test.assertTrue + +@RunWith(AndroidJUnit4::class) +class DatabaseTest : KoinTest { + + inline fun hasKoin() = hasKoin(T::class) + + fun hasKoin(klazz: KClass): Boolean = + try { + get(clazz = klazz) + true + } catch (e: NoBeanDefFoundException) { + false + } + + /** + * Database and all daos should be loaded as components + */ + @Test + fun testKoins() { + hasKoin() + val members = FrostDatabase::class.java.kotlin.functions.filter { it.name.endsWith("Dao") } + .mapNotNull { it.returnType.classifier as? KClass<*> } + assertTrue(members.isNotEmpty(), "Failed to find dao interfaces") + val missingKoins = (members + FrostDatabase::class).filter { !hasKoin(it) } + assertTrue(missingKoins.isEmpty(), "Missing koins: $missingKoins") + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/pitchedapps/frost/db/CacheDb.kt b/app/src/main/kotlin/com/pitchedapps/frost/db/CacheDb.kt index bd6bff4b..4d6bc938 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/db/CacheDb.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/db/CacheDb.kt @@ -53,4 +53,5 @@ interface CacheDao { suspend fun deleteById(id: Long) } -suspend fun CacheDao.save(id: String, contents: String) = insertCache(CacheEntity(id, System.currentTimeMillis(), contents)) \ No newline at end of file +suspend fun CacheDao.save(id: String, contents: String) = + insertCache(CacheEntity(id, System.currentTimeMillis(), contents)) \ No newline at end of file diff --git a/app/src/main/kotlin/com/pitchedapps/frost/db/Database.kt b/app/src/main/kotlin/com/pitchedapps/frost/db/Database.kt index b83fce52..29296494 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/db/Database.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/db/Database.kt @@ -71,6 +71,8 @@ class FrostDatabase(private val privateDb: FrostPrivateDatabase, private val pub single { create(context) } single { get().cookieDao() } single { get().tabDao() } + single { get().cacheDao() } + single { get().notifDao() } } /** diff --git a/app/src/main/kotlin/com/pitchedapps/frost/db/FbTabsDb.kt b/app/src/main/kotlin/com/pitchedapps/frost/db/FbTabsDb.kt index c2bb0837..f4e74509 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/db/FbTabsDb.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/db/FbTabsDb.kt @@ -24,14 +24,9 @@ import androidx.room.Query import androidx.room.Transaction import com.pitchedapps.frost.facebook.FbItem import com.pitchedapps.frost.facebook.defaultTabs -import com.pitchedapps.frost.utils.L import com.raizlabs.android.dbflow.annotation.Database import com.raizlabs.android.dbflow.annotation.PrimaryKey import com.raizlabs.android.dbflow.annotation.Table -import com.raizlabs.android.dbflow.kotlinextensions.database -import com.raizlabs.android.dbflow.kotlinextensions.fastSave -import com.raizlabs.android.dbflow.kotlinextensions.from -import com.raizlabs.android.dbflow.kotlinextensions.select import com.raizlabs.android.dbflow.structure.BaseModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext diff --git a/app/src/schemas/com.pitchedapps.frost.db.FrostPrivateDatabase/1.json b/app/src/schemas/com.pitchedapps.frost.db.FrostPrivateDatabase/1.json index c382bce7..72b86db3 100644 --- a/app/src/schemas/com.pitchedapps.frost.db.FrostPrivateDatabase/1.json +++ b/app/src/schemas/com.pitchedapps.frost.db.FrostPrivateDatabase/1.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 1, - "identityHash": "77eff76407f59b690b8877cc22fac42f", + "identityHash": "099ffebd0f0fe80199c0f6413a549ebb", "entities": [ { "tableName": "cookies", @@ -127,12 +127,44 @@ ] } ] + }, + { + "tableName": "frost_cache", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `lastUpdated` INTEGER NOT NULL, `contents` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastUpdated", + "columnName": "lastUpdated", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contents", + "columnName": "contents", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] } ], "views": [], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"77eff76407f59b690b8877cc22fac42f\")" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"099ffebd0f0fe80199c0f6413a549ebb\")" ] } } \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 98f29dbe..146591bb 100644 --- a/gradle.properties +++ b/gradle.properties @@ -18,7 +18,7 @@ KAU=4.0.0-alpha02 KOTLIN=1.3.21 # https://mvnrepository.com/artifact/com.android.tools.build/gradle?repo=google -ANDROID_GRADLE=3.2.1 +ANDROID_GRADLE=3.3.2 # https://github.com/diffplug/spotless/blob/master/plugin-gradle/CHANGES.md SPOTLESS=3.17.0 -- cgit v1.2.3 From 12f491737ec2e2d774a816e84170ff352d1b6cd6 Mon Sep 17 00:00:00 2001 From: Allan Wang Date: Thu, 7 Mar 2019 14:59:40 -0500 Subject: Add cache test --- .../kotlin/com/pitchedapps/frost/db/CacheDbTest.kt | 32 ++++++++++++++++++ .../kotlin/com/pitchedapps/frost/db/CacheDb.kt | 38 +++++++++++++++++----- .../com/pitchedapps/frost/db/NotificationDb.kt | 2 +- .../1.json | 29 ++++++++++++++--- 4 files changed, 86 insertions(+), 15 deletions(-) create mode 100644 app/src/androidTest/kotlin/com/pitchedapps/frost/db/CacheDbTest.kt diff --git a/app/src/androidTest/kotlin/com/pitchedapps/frost/db/CacheDbTest.kt b/app/src/androidTest/kotlin/com/pitchedapps/frost/db/CacheDbTest.kt new file mode 100644 index 00000000..780bbd3e --- /dev/null +++ b/app/src/androidTest/kotlin/com/pitchedapps/frost/db/CacheDbTest.kt @@ -0,0 +1,32 @@ +package com.pitchedapps.frost.db + +import kotlinx.coroutines.runBlocking +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue +import kotlin.test.fail + +class CacheDbTest : BaseDbTest() { + + private val dao get() = db.cacheDao() + private val cookieDao get() = db.cookieDao() + + private fun cookie(id: Long) = CookieEntity(id, "name$id", "cookie$id") + + @Test + fun save() { + val cookie = cookie(1L) + val type = "test" + val content = "long test".repeat(10000) + runBlocking { + cookieDao.insertCookie(cookie) + dao.save(cookie.id, type, content) + val cache = dao.select(cookie.id, type) ?: fail("Cache not found") + assertEquals(content, cache.contents, "Content mismatch") + assertTrue( + System.currentTimeMillis() - cache.lastUpdated < 500, + "Cache retrieval took over 500ms (${System.currentTimeMillis() - cache.lastUpdated})" + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/pitchedapps/frost/db/CacheDb.kt b/app/src/main/kotlin/com/pitchedapps/frost/db/CacheDb.kt index 4d6bc938..8c3c9c6b 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/db/CacheDb.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/db/CacheDb.kt @@ -19,9 +19,11 @@ package com.pitchedapps.frost.db import android.os.Parcelable import androidx.room.Dao import androidx.room.Entity +import androidx.room.ForeignKey import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query +import com.pitchedapps.frost.utils.L import kotlinx.android.parcel.Parcelize /** @@ -31,11 +33,20 @@ import kotlinx.android.parcel.Parcelize /** * Generic cache to store serialized content */ -@Entity(tableName = "frost_cache") +@Entity( + tableName = "frost_cache", + primaryKeys = ["id", "type"], + foreignKeys = [ForeignKey( + entity = CookieEntity::class, + parentColumns = ["cookie_id"], + childColumns = ["id"], + onDelete = ForeignKey.CASCADE + )] +) @Parcelize data class CacheEntity( - @androidx.room.PrimaryKey - val id: String, + val id: Long, + val type: String, val lastUpdated: Long, val contents: String ) : Parcelable @@ -43,15 +54,24 @@ data class CacheEntity( @Dao interface CacheDao { - @Query("SELECT * FROM frost_cache WHERE id = :id") - suspend fun selectById(id: Long): CacheEntity? + @Query("SELECT * FROM frost_cache WHERE id = :id AND type = :type") + suspend fun select(id: Long, type: String): CacheEntity? @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertCache(cache: CacheEntity) - @Query("DELETE FROM frost_cache WHERE id = :id") - suspend fun deleteById(id: Long) + @Query("DELETE FROM frost_cache WHERE id = :id AND type = :type") + suspend fun delete(id: Long, type: String) } -suspend fun CacheDao.save(id: String, contents: String) = - insertCache(CacheEntity(id, System.currentTimeMillis(), contents)) \ No newline at end of file +/** + * Returns true if successful, given that there are constraints to the insertion + */ +suspend fun CacheDao.save(id: Long, type: String, contents: String): Boolean = + try { + insertCache(CacheEntity(id, type, System.currentTimeMillis(), contents)) + true + } catch (e: Exception) { + L.e(e) { "Cache save failed for $type" } + false + } \ No newline at end of file diff --git a/app/src/main/kotlin/com/pitchedapps/frost/db/NotificationDb.kt b/app/src/main/kotlin/com/pitchedapps/frost/db/NotificationDb.kt index 7f41fbf8..d2771754 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/db/NotificationDb.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/db/NotificationDb.kt @@ -146,7 +146,7 @@ suspend fun NotificationDao.saveNotifications(type: String, notifs: List Date: Thu, 7 Mar 2019 15:26:35 -0500 Subject: Optimize imports --- app/src/androidTest/kotlin/com/pitchedapps/frost/db/CookieDbTest.kt | 2 +- app/src/androidTest/kotlin/com/pitchedapps/frost/db/FbTabsDbTest.kt | 2 +- app/src/main/kotlin/com/pitchedapps/frost/activities/MainActivity.kt | 3 +-- app/src/main/kotlin/com/pitchedapps/frost/db/CookiesDb.kt | 2 -- .../main/kotlin/com/pitchedapps/frost/facebook/parsers/FrostParser.kt | 1 - .../kotlin/com/pitchedapps/frost/facebook/parsers/MessageParser.kt | 1 - .../main/kotlin/com/pitchedapps/frost/facebook/parsers/NotifParser.kt | 1 - .../main/kotlin/com/pitchedapps/frost/services/NotificationService.kt | 1 - app/src/main/kotlin/com/pitchedapps/frost/views/FrostVideoViewer.kt | 1 - app/src/main/kotlin/com/pitchedapps/frost/web/FrostJSI.kt | 1 - app/src/main/kotlin/com/pitchedapps/frost/web/LoginWebView.kt | 1 - .../test/kotlin/com/pitchedapps/frost/debugger/OfflineWebsiteTest.kt | 2 +- 12 files changed, 4 insertions(+), 14 deletions(-) diff --git a/app/src/androidTest/kotlin/com/pitchedapps/frost/db/CookieDbTest.kt b/app/src/androidTest/kotlin/com/pitchedapps/frost/db/CookieDbTest.kt index 20592347..5ec771f5 100644 --- a/app/src/androidTest/kotlin/com/pitchedapps/frost/db/CookieDbTest.kt +++ b/app/src/androidTest/kotlin/com/pitchedapps/frost/db/CookieDbTest.kt @@ -6,7 +6,7 @@ import kotlin.test.assertEquals import kotlin.test.assertNull class CookieDbTest : BaseDbTest() { - + private val dao get() = db.cookieDao() @Test diff --git a/app/src/androidTest/kotlin/com/pitchedapps/frost/db/FbTabsDbTest.kt b/app/src/androidTest/kotlin/com/pitchedapps/frost/db/FbTabsDbTest.kt index 91a0bf9a..752112f9 100644 --- a/app/src/androidTest/kotlin/com/pitchedapps/frost/db/FbTabsDbTest.kt +++ b/app/src/androidTest/kotlin/com/pitchedapps/frost/db/FbTabsDbTest.kt @@ -7,7 +7,7 @@ import kotlin.test.Test import kotlin.test.assertEquals class FbTabsDbTest : BaseDbTest() { - + private val dao get() = db.tabDao() /** diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/MainActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/MainActivity.kt index a1dba417..75c9537b 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/activities/MainActivity.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/MainActivity.kt @@ -86,8 +86,7 @@ class MainActivity : BaseMainActivity() { (tab.customView as BadgedIcon).badgeText = null } }) - headerBadgeChannel.subscribeDuringJob(this, Dispatchers.IO) { - html -> + headerBadgeChannel.subscribeDuringJob(this, Dispatchers.IO) { html -> try { val doc = Jsoup.parse(html) if (doc.select("[data-sigil=count]").isEmpty()) diff --git a/app/src/main/kotlin/com/pitchedapps/frost/db/CookiesDb.kt b/app/src/main/kotlin/com/pitchedapps/frost/db/CookiesDb.kt index d5347f18..c6c983fb 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/db/CookiesDb.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/db/CookiesDb.kt @@ -82,6 +82,4 @@ data class CookieModel(@PrimaryKey var id: Long = -1L, var name: String? = null, BaseModel(), Parcelable { override fun toString(): String = "CookieModel(${hashCode()})" - - fun toSensitiveString(): String = "CookieModel(id=$id, name=$name, cookie=$cookie)" } \ No newline at end of file diff --git a/app/src/main/kotlin/com/pitchedapps/frost/facebook/parsers/FrostParser.kt b/app/src/main/kotlin/com/pitchedapps/frost/facebook/parsers/FrostParser.kt index bf89a103..4c494e0b 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/facebook/parsers/FrostParser.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/facebook/parsers/FrostParser.kt @@ -17,7 +17,6 @@ package com.pitchedapps.frost.facebook.parsers import com.pitchedapps.frost.db.CookieEntity -import com.pitchedapps.frost.db.CookieModel import com.pitchedapps.frost.facebook.FB_CSS_URL_MATCHER import com.pitchedapps.frost.facebook.formattedFbUrl import com.pitchedapps.frost.facebook.get diff --git a/app/src/main/kotlin/com/pitchedapps/frost/facebook/parsers/MessageParser.kt b/app/src/main/kotlin/com/pitchedapps/frost/facebook/parsers/MessageParser.kt index 866cab10..80ed8ee8 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/facebook/parsers/MessageParser.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/facebook/parsers/MessageParser.kt @@ -17,7 +17,6 @@ package com.pitchedapps.frost.facebook.parsers import com.pitchedapps.frost.db.CookieEntity -import com.pitchedapps.frost.db.CookieModel import com.pitchedapps.frost.facebook.FB_EPOCH_MATCHER import com.pitchedapps.frost.facebook.FB_MESSAGE_NOTIF_ID_MATCHER import com.pitchedapps.frost.facebook.FbItem diff --git a/app/src/main/kotlin/com/pitchedapps/frost/facebook/parsers/NotifParser.kt b/app/src/main/kotlin/com/pitchedapps/frost/facebook/parsers/NotifParser.kt index 3449ac48..199fc685 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/facebook/parsers/NotifParser.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/facebook/parsers/NotifParser.kt @@ -17,7 +17,6 @@ package com.pitchedapps.frost.facebook.parsers import com.pitchedapps.frost.db.CookieEntity -import com.pitchedapps.frost.db.CookieModel import com.pitchedapps.frost.facebook.FB_EPOCH_MATCHER import com.pitchedapps.frost.facebook.FB_NOTIF_ID_MATCHER import com.pitchedapps.frost.facebook.FbItem diff --git a/app/src/main/kotlin/com/pitchedapps/frost/services/NotificationService.kt b/app/src/main/kotlin/com/pitchedapps/frost/services/NotificationService.kt index 8b100c19..2e994577 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/services/NotificationService.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/services/NotificationService.kt @@ -23,7 +23,6 @@ import com.pitchedapps.frost.BuildConfig import com.pitchedapps.frost.R import com.pitchedapps.frost.db.CookieDao import com.pitchedapps.frost.db.CookieEntity -import com.pitchedapps.frost.db.NotificationDao import com.pitchedapps.frost.utils.L import com.pitchedapps.frost.utils.Prefs import com.pitchedapps.frost.utils.frostEvent diff --git a/app/src/main/kotlin/com/pitchedapps/frost/views/FrostVideoViewer.kt b/app/src/main/kotlin/com/pitchedapps/frost/views/FrostVideoViewer.kt index 288e2b40..75b80ae1 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/views/FrostVideoViewer.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/views/FrostVideoViewer.kt @@ -40,7 +40,6 @@ import ca.allanwang.kau.utils.withMinAlpha import com.devbrackets.android.exomedia.listener.VideoControlsVisibilityListener import com.mikepenz.google_material_typeface_library.GoogleMaterial import com.pitchedapps.frost.R -import com.pitchedapps.frost.db.CookieModel_Table.cookie import com.pitchedapps.frost.db.FrostDatabase import com.pitchedapps.frost.db.currentCookie import com.pitchedapps.frost.utils.L diff --git a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostJSI.kt b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostJSI.kt index 4df84b63..0d980ba0 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostJSI.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostJSI.kt @@ -22,7 +22,6 @@ import com.pitchedapps.frost.activities.MainActivity import com.pitchedapps.frost.contracts.MainActivityContract import com.pitchedapps.frost.contracts.VideoViewHolder import com.pitchedapps.frost.db.CookieEntity -import com.pitchedapps.frost.db.CookieModel import com.pitchedapps.frost.facebook.FbCookie import com.pitchedapps.frost.utils.L import com.pitchedapps.frost.utils.Prefs diff --git a/app/src/main/kotlin/com/pitchedapps/frost/web/LoginWebView.kt b/app/src/main/kotlin/com/pitchedapps/frost/web/LoginWebView.kt index 07aafaf0..4c7572fb 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/web/LoginWebView.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/web/LoginWebView.kt @@ -30,7 +30,6 @@ import ca.allanwang.kau.utils.fadeIn import ca.allanwang.kau.utils.isVisible import ca.allanwang.kau.utils.launchMain import com.pitchedapps.frost.db.CookieEntity -import com.pitchedapps.frost.db.CookieModel import com.pitchedapps.frost.facebook.FB_LOGIN_URL import com.pitchedapps.frost.facebook.FB_USER_MATCHER import com.pitchedapps.frost.facebook.FbCookie diff --git a/app/src/test/kotlin/com/pitchedapps/frost/debugger/OfflineWebsiteTest.kt b/app/src/test/kotlin/com/pitchedapps/frost/debugger/OfflineWebsiteTest.kt index a2bafba9..0ad60126 100644 --- a/app/src/test/kotlin/com/pitchedapps/frost/debugger/OfflineWebsiteTest.kt +++ b/app/src/test/kotlin/com/pitchedapps/frost/debugger/OfflineWebsiteTest.kt @@ -27,9 +27,9 @@ import org.junit.Assume.assumeTrue import java.io.File import java.util.zip.ZipFile import kotlin.test.AfterTest -import kotlin.test.Test import kotlin.test.BeforeTest import kotlin.test.Ignore +import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNotNull import kotlin.test.assertTrue -- cgit v1.2.3 From e617c95e2b4acfcfbeb1a72a658f19e69eaa3d6c Mon Sep 17 00:00:00 2001 From: Allan Wang Date: Thu, 7 Mar 2019 17:28:32 -0500 Subject: Rename some methods --- .../kotlin/com/pitchedapps/frost/db/CacheDbTest.kt | 2 +- .../com/pitchedapps/frost/db/CookieDbTest.kt | 12 +- .../com/pitchedapps/frost/db/NotificationDbTest.kt | 14 +- .../kotlin/com/pitchedapps/frost/StartActivity.kt | 6 +- .../pitchedapps/frost/activities/LoginActivity.kt | 2 +- .../kotlin/com/pitchedapps/frost/db/CookiesDb.kt | 4 +- .../kotlin/com/pitchedapps/frost/db/Database.kt | 5 +- .../com/pitchedapps/frost/facebook/FbCookie.kt | 2 +- .../main/kotlin/com/pitchedapps/frost/utils/L.kt | 6 + .../1.json | 189 --------------------- .../1.json | 40 ----- 11 files changed, 32 insertions(+), 250 deletions(-) delete mode 100644 app/src/schemas/com.pitchedapps.frost.db.FrostPrivateDatabase/1.json delete mode 100644 app/src/schemas/com.pitchedapps.frost.db.FrostPublicDatabase/1.json diff --git a/app/src/androidTest/kotlin/com/pitchedapps/frost/db/CacheDbTest.kt b/app/src/androidTest/kotlin/com/pitchedapps/frost/db/CacheDbTest.kt index 780bbd3e..1fe7bbc4 100644 --- a/app/src/androidTest/kotlin/com/pitchedapps/frost/db/CacheDbTest.kt +++ b/app/src/androidTest/kotlin/com/pitchedapps/frost/db/CacheDbTest.kt @@ -19,7 +19,7 @@ class CacheDbTest : BaseDbTest() { val type = "test" val content = "long test".repeat(10000) runBlocking { - cookieDao.insertCookie(cookie) + cookieDao.save(cookie) dao.save(cookie.id, type, content) val cache = dao.select(cookie.id, type) ?: fail("Cache not found") assertEquals(content, cache.contents, "Content mismatch") diff --git a/app/src/androidTest/kotlin/com/pitchedapps/frost/db/CookieDbTest.kt b/app/src/androidTest/kotlin/com/pitchedapps/frost/db/CookieDbTest.kt index 5ec771f5..122e3205 100644 --- a/app/src/androidTest/kotlin/com/pitchedapps/frost/db/CookieDbTest.kt +++ b/app/src/androidTest/kotlin/com/pitchedapps/frost/db/CookieDbTest.kt @@ -13,7 +13,7 @@ class CookieDbTest : BaseDbTest() { fun basicCookie() { val cookie = CookieEntity(id = 1234L, name = "testName", cookie = "testCookie") runBlocking { - dao.insertCookie(cookie) + dao.save(cookie) val cookies = dao.selectAll() assertEquals(listOf(cookie), cookies, "Cookie mismatch") } @@ -24,7 +24,7 @@ class CookieDbTest : BaseDbTest() { val cookie = CookieEntity(id = 1234L, name = "testName", cookie = "testCookie") runBlocking { - dao.insertCookie(cookie) + dao.save(cookie) dao.deleteById(cookie.id + 1) assertEquals( listOf(cookie), @@ -40,15 +40,15 @@ class CookieDbTest : BaseDbTest() { fun insertReplaceCookie() { val cookie = CookieEntity(id = 1234L, name = "testName", cookie = "testCookie") runBlocking { - dao.insertCookie(cookie) + dao.save(cookie) assertEquals(listOf(cookie), dao.selectAll(), "Cookie insertion failed") - dao.insertCookie(cookie.copy(name = "testName2")) + dao.save(cookie.copy(name = "testName2")) assertEquals( listOf(cookie.copy(name = "testName2")), dao.selectAll(), "Cookie replacement failed" ) - dao.insertCookie(cookie.copy(id = 123L)) + dao.save(cookie.copy(id = 123L)) assertEquals( setOf(cookie.copy(id = 123L), cookie.copy(name = "testName2")), dao.selectAll().toSet(), @@ -61,7 +61,7 @@ class CookieDbTest : BaseDbTest() { fun selectCookie() { val cookie = CookieEntity(id = 1234L, name = "testName", cookie = "testCookie") runBlocking { - dao.insertCookie(cookie) + dao.save(cookie) assertEquals(cookie, dao.selectById(cookie.id), "Cookie selection failed") assertNull(dao.selectById(cookie.id + 1), "Inexistent cookie selection failed") } diff --git a/app/src/androidTest/kotlin/com/pitchedapps/frost/db/NotificationDbTest.kt b/app/src/androidTest/kotlin/com/pitchedapps/frost/db/NotificationDbTest.kt index 176d0d3a..25c29db4 100644 --- a/app/src/androidTest/kotlin/com/pitchedapps/frost/db/NotificationDbTest.kt +++ b/app/src/androidTest/kotlin/com/pitchedapps/frost/db/NotificationDbTest.kt @@ -31,7 +31,7 @@ class NotificationDbTest : BaseDbTest() { // Unique unsorted ids val notifs = listOf(0L, 4L, 2L, 6L, 99L, 3L).map { notifContent(it, cookie) } runBlocking { - db.cookieDao().insertCookie(cookie) + db.cookieDao().save(cookie) dao.saveNotifications(NOTIF_CHANNEL_GENERAL, notifs) val dbNotifs = dao.selectNotifications(cookie.id, NOTIF_CHANNEL_GENERAL) assertEquals(notifs.sortedByDescending { it.timestamp }, dbNotifs, "Incorrect notification list received") @@ -45,8 +45,8 @@ class NotificationDbTest : BaseDbTest() { val cookie2 = cookie(12L) val notifs1 = (0L..2L).map { notifContent(it, cookie1) } val notifs2 = (5L..10L).map { notifContent(it, cookie2) } - db.cookieDao().insertCookie(cookie1) - db.cookieDao().insertCookie(cookie2) + db.cookieDao().save(cookie1) + db.cookieDao().save(cookie2) dao.saveNotifications(NOTIF_CHANNEL_GENERAL, notifs1) dao.saveNotifications(NOTIF_CHANNEL_MESSAGES, notifs2) assertEquals( @@ -82,8 +82,8 @@ class NotificationDbTest : BaseDbTest() { val cookie2 = cookie(12L) val notifs1 = (0L..2L).map { notifContent(it, cookie1) } val notifs2 = notifs1.map { it.copy(data = cookie2) } - db.cookieDao().insertCookie(cookie1) - db.cookieDao().insertCookie(cookie2) + db.cookieDao().save(cookie1) + db.cookieDao().save(cookie2) assertTrue(dao.saveNotifications(NOTIF_CHANNEL_GENERAL, notifs1), "Notif1 save failed") assertTrue(dao.saveNotifications(NOTIF_CHANNEL_GENERAL, notifs2), "Notif2 save failed") } @@ -95,7 +95,7 @@ class NotificationDbTest : BaseDbTest() { // Unique unsorted ids val notifs = listOf(0L, 4L, 2L, 6L, 99L, 3L).map { notifContent(it, cookie) } runBlocking { - db.cookieDao().insertCookie(cookie) + db.cookieDao().save(cookie) dao.saveNotifications(NOTIF_CHANNEL_GENERAL, notifs) db.cookieDao().deleteById(cookie.id) val dbNotifs = dao.selectNotifications(cookie.id, NOTIF_CHANNEL_GENERAL) @@ -110,7 +110,7 @@ class NotificationDbTest : BaseDbTest() { val notifs = listOf(0L, 4L, 2L, 6L, 99L, 3L).map { notifContent(it, cookie) } runBlocking { assertEquals(-1L, dao.latestEpoch(cookie.id, NOTIF_CHANNEL_GENERAL), "Default epoch failed") - db.cookieDao().insertCookie(cookie) + db.cookieDao().save(cookie) dao.saveNotifications(NOTIF_CHANNEL_GENERAL, notifs) assertEquals(99L, dao.latestEpoch(cookie.id, NOTIF_CHANNEL_GENERAL), "Latest epoch failed") } diff --git a/app/src/main/kotlin/com/pitchedapps/frost/StartActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/StartActivity.kt index 3b7418e1..24e9c548 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/StartActivity.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/StartActivity.kt @@ -38,6 +38,7 @@ import com.pitchedapps.frost.db.CookieModel import com.pitchedapps.frost.db.FbTabDao import com.pitchedapps.frost.db.FbTabModel import com.pitchedapps.frost.db.save +import com.pitchedapps.frost.db.selectAll import com.pitchedapps.frost.facebook.FbCookie import com.pitchedapps.frost.utils.EXTRA_COOKIES import com.pitchedapps.frost.utils.L @@ -94,6 +95,7 @@ class StartActivity : KauBaseActivity() { }) } } catch (e: Exception) { + L._e(e) { "Load start failed" } showInvalidWebView() } } @@ -106,9 +108,11 @@ class StartActivity : KauBaseActivity() { private suspend fun migrate() = withContext(Dispatchers.IO) { if (cookieDao.selectAll().isNotEmpty()) return@withContext val cookies = (select from CookieModel::class).queryList().map { CookieEntity(it.id, it.name, it.cookie) } - cookieDao.insertCookies(cookies) + cookieDao.save(cookies) val tabs = (select from FbTabModel::class).queryList().map(FbTabModel::tab) tabDao.save(tabs) + L._d { "Migrated cookies ${cookieDao.selectAll()}" } + L._d { "Migrated tabs ${tabDao.selectAll()}" } } private fun showInvalidWebView() = diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/LoginActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/LoginActivity.kt index 27dbc37a..e5a50543 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/activities/LoginActivity.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/LoginActivity.kt @@ -184,7 +184,7 @@ class LoginActivity : BaseActivity() { } if (cookie.name?.isNotBlank() == false && result != cookie.name) { - cookieDao.insertCookie(cookie.copy(name = result)) + cookieDao.save(cookie.copy(name = result)) } cookie.name ?: "" diff --git a/app/src/main/kotlin/com/pitchedapps/frost/db/CookiesDb.kt b/app/src/main/kotlin/com/pitchedapps/frost/db/CookiesDb.kt index c6c983fb..7e929370 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/db/CookiesDb.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/db/CookiesDb.kt @@ -59,10 +59,10 @@ interface CookieDao { suspend fun selectById(id: Long): CookieEntity? @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun insertCookie(cookie: CookieEntity) + suspend fun save(cookie: CookieEntity) @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun insertCookies(cookies: List) + suspend fun save(cookies: List) @Query("DELETE FROM cookies WHERE cookie_id = :id") suspend fun deleteById(id: Long) diff --git a/app/src/main/kotlin/com/pitchedapps/frost/db/Database.kt b/app/src/main/kotlin/com/pitchedapps/frost/db/Database.kt index 29296494..ae96b696 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/db/Database.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/db/Database.kt @@ -5,6 +5,7 @@ import androidx.room.Database import androidx.room.Room import androidx.room.RoomDatabase import androidx.room.TypeConverters +import org.koin.core.Koin import org.koin.dsl.module.module import org.koin.standalone.StandAloneContext @@ -59,11 +60,11 @@ class FrostDatabase(private val privateDb: FrostPrivateDatabase, private val pub val privateDb = Room.databaseBuilder( context, FrostPrivateDatabase::class.java, FrostPrivateDatabase.DATABASE_NAME - ).build() + ).fallbackToDestructiveMigration().build() val publicDb = Room.databaseBuilder( context, FrostPublicDatabase::class.java, FrostPublicDatabase.DATABASE_NAME - ).build() + ).fallbackToDestructiveMigration().build() return FrostDatabase(privateDb, publicDb) } diff --git a/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbCookie.kt b/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbCookie.kt index 02718b78..6afbea4b 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbCookie.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbCookie.kt @@ -85,7 +85,7 @@ object FbCookie { Prefs.userId = id CookieManager.getInstance().flush() val cookie = CookieEntity(Prefs.userId, null, webCookie) - cookieDao.insertCookie(cookie) + cookieDao.save(cookie) } suspend fun reset() { diff --git a/app/src/main/kotlin/com/pitchedapps/frost/utils/L.kt b/app/src/main/kotlin/com/pitchedapps/frost/utils/L.kt index 8364c34e..7d2e0e08 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/utils/L.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/utils/L.kt @@ -20,6 +20,7 @@ import android.util.Log import ca.allanwang.kau.logging.KauLogger import com.bugsnag.android.Bugsnag import com.pitchedapps.frost.BuildConfig +import java.lang.Exception /** * Created by Allan Wang on 2017-05-28. @@ -50,6 +51,11 @@ object L : KauLogger("Frost", { d(message) } + inline fun _e(e: Throwable?, message: () -> Any?) { + if (BuildConfig.DEBUG) + e(e, message) + } + override fun logImpl(priority: Int, message: String?, t: Throwable?) { if (BuildConfig.DEBUG) super.logImpl(priority, message, t) diff --git a/app/src/schemas/com.pitchedapps.frost.db.FrostPrivateDatabase/1.json b/app/src/schemas/com.pitchedapps.frost.db.FrostPrivateDatabase/1.json deleted file mode 100644 index 60d5cddd..00000000 --- a/app/src/schemas/com.pitchedapps.frost.db.FrostPrivateDatabase/1.json +++ /dev/null @@ -1,189 +0,0 @@ -{ - "formatVersion": 1, - "database": { - "version": 1, - "identityHash": "0a9d994786b7e07fea95c11d9210ce0e", - "entities": [ - { - "tableName": "cookies", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`cookie_id` INTEGER NOT NULL, `name` TEXT, `cookie` TEXT, PRIMARY KEY(`cookie_id`))", - "fields": [ - { - "fieldPath": "id", - "columnName": "cookie_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "name", - "columnName": "name", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "cookie", - "columnName": "cookie", - "affinity": "TEXT", - "notNull": false - } - ], - "primaryKey": { - "columnNames": [ - "cookie_id" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "notifications", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`notif_id` INTEGER NOT NULL, `userId` INTEGER NOT NULL, `href` TEXT NOT NULL, `title` TEXT, `text` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `profileUrl` TEXT, `type` TEXT NOT NULL, PRIMARY KEY(`notif_id`, `userId`), FOREIGN KEY(`userId`) REFERENCES `cookies`(`cookie_id`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "id", - "columnName": "notif_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "userId", - "columnName": "userId", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "href", - "columnName": "href", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "title", - "columnName": "title", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "text", - "columnName": "text", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "timestamp", - "columnName": "timestamp", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "profileUrl", - "columnName": "profileUrl", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "type", - "columnName": "type", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "notif_id", - "userId" - ], - "autoGenerate": false - }, - "indices": [ - { - "name": "index_notifications_notif_id", - "unique": false, - "columnNames": [ - "notif_id" - ], - "createSql": "CREATE INDEX `index_notifications_notif_id` ON `${TABLE_NAME}` (`notif_id`)" - }, - { - "name": "index_notifications_userId", - "unique": false, - "columnNames": [ - "userId" - ], - "createSql": "CREATE INDEX `index_notifications_userId` ON `${TABLE_NAME}` (`userId`)" - } - ], - "foreignKeys": [ - { - "table": "cookies", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "userId" - ], - "referencedColumns": [ - "cookie_id" - ] - } - ] - }, - { - "tableName": "frost_cache", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `type` TEXT NOT NULL, `lastUpdated` INTEGER NOT NULL, `contents` TEXT NOT NULL, PRIMARY KEY(`id`, `type`), FOREIGN KEY(`id`) REFERENCES `cookies`(`cookie_id`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "type", - "columnName": "type", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "lastUpdated", - "columnName": "lastUpdated", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "contents", - "columnName": "contents", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "id", - "type" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [ - { - "table": "cookies", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "id" - ], - "referencedColumns": [ - "cookie_id" - ] - } - ] - } - ], - "views": [], - "setupQueries": [ - "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"0a9d994786b7e07fea95c11d9210ce0e\")" - ] - } -} \ No newline at end of file diff --git a/app/src/schemas/com.pitchedapps.frost.db.FrostPublicDatabase/1.json b/app/src/schemas/com.pitchedapps.frost.db.FrostPublicDatabase/1.json deleted file mode 100644 index fe2aa83e..00000000 --- a/app/src/schemas/com.pitchedapps.frost.db.FrostPublicDatabase/1.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "formatVersion": 1, - "database": { - "version": 1, - "identityHash": "fde868470836ff9230f1d406922d7563", - "entities": [ - { - "tableName": "tabs", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`position` INTEGER NOT NULL, `tab` TEXT NOT NULL, PRIMARY KEY(`position`))", - "fields": [ - { - "fieldPath": "position", - "columnName": "position", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "tab", - "columnName": "tab", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "position" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - } - ], - "views": [], - "setupQueries": [ - "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"fde868470836ff9230f1d406922d7563\")" - ] - } -} \ No newline at end of file -- cgit v1.2.3 From b769e36802c15f521665a353e8ed5e637efeef1f Mon Sep 17 00:00:00 2001 From: Allan Wang Date: Thu, 7 Mar 2019 17:29:53 -0500 Subject: Disable fallback --- app/src/main/kotlin/com/pitchedapps/frost/db/Database.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/kotlin/com/pitchedapps/frost/db/Database.kt b/app/src/main/kotlin/com/pitchedapps/frost/db/Database.kt index ae96b696..5fc986e3 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/db/Database.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/db/Database.kt @@ -60,11 +60,11 @@ class FrostDatabase(private val privateDb: FrostPrivateDatabase, private val pub val privateDb = Room.databaseBuilder( context, FrostPrivateDatabase::class.java, FrostPrivateDatabase.DATABASE_NAME - ).fallbackToDestructiveMigration().build() + ).build() val publicDb = Room.databaseBuilder( context, FrostPublicDatabase::class.java, FrostPublicDatabase.DATABASE_NAME - ).fallbackToDestructiveMigration().build() + ).build() return FrostDatabase(privateDb, publicDb) } -- cgit v1.2.3 From adf9f2f6f23de17aae51435f792de16a0680f286 Mon Sep 17 00:00:00 2001 From: Allan Wang Date: Thu, 7 Mar 2019 18:15:46 -0500 Subject: Rebind main activity logic --- .../frost/activities/BaseMainActivity.kt | 72 ++++++-- .../pitchedapps/frost/activities/MainActivity.kt | 20 +-- .../1.json | 189 +++++++++++++++++++++ .../1.json | 40 +++++ 4 files changed, 291 insertions(+), 30 deletions(-) create mode 100644 app/src/schemas/com.pitchedapps.frost.db.FrostPrivateDatabase/1.json create mode 100644 app/src/schemas/com.pitchedapps.frost.db.FrostPublicDatabase/1.json diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseMainActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseMainActivity.kt index 4669418d..ca30401a 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseMainActivity.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseMainActivity.kt @@ -123,7 +123,10 @@ abstract class BaseMainActivity : BaseActivity(), MainActivityContract, FileChooserContract by FileChooserDelegate(), VideoViewHolder, SearchViewHolder { - protected lateinit var adapter: SectionsPagerAdapter + /** + * Note that tabs themselves are initialized through a coroutine during onCreate + */ + protected val adapter: SectionsPagerAdapter = SectionsPagerAdapter() override val frameWrapper: FrameLayout get() = frame_wrapper val viewPager: FrostViewPager get() = container val cookieDao: CookieDao by inject() @@ -136,6 +139,8 @@ abstract class BaseMainActivity : BaseActivity(), MainActivityContract, val appBar: AppBarLayout by bindView(R.id.appbar) val coordinator: CoordinatorLayout by bindView(R.id.main_content) + protected var lastPosition = -1 + override var videoViewer: FrostVideoViewer? = null private lateinit var drawer: Drawer private lateinit var drawerHeader: AccountHeader @@ -156,14 +161,14 @@ abstract class BaseMainActivity : BaseActivity(), MainActivityContract, background(viewPager) } setSupportActionBar(toolbar) - launch { - adapter = SectionsPagerAdapter(tabDao.selectAll()) - viewPager.adapter = adapter - viewPager.offscreenPageLimit = TAB_COUNT - } + viewPager.adapter = adapter + viewPager.offscreenPageLimit = TAB_COUNT tabs.setBackgroundColor(Prefs.mainActivityLayout.backgroundColor()) onNestedCreate(savedInstanceState) L.i { "Main finished loading UI in ${System.currentTimeMillis() - start} ms" } + launch { + adapter.setPages(tabDao.selectAll()) + } controlWebview = WebView(this) if (BuildConfig.VERSION_CODE > Prefs.versionCode) { Prefs.prevVersionCode = Prefs.versionCode @@ -462,16 +467,12 @@ abstract class BaseMainActivity : BaseActivity(), MainActivityContract, override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) - outState.putStringArrayList(STATE_FORCE_FALLBACK, ArrayList(adapter.forcedFallbacks)) + adapter.saveInstanceState(outState) } override fun onRestoreInstanceState(savedInstanceState: Bundle) { super.onRestoreInstanceState(savedInstanceState) - adapter.forcedFallbacks.clear() - adapter.forcedFallbacks.addAll( - savedInstanceState.getStringArrayList(STATE_FORCE_FALLBACK) - ?: emptyList() - ) + adapter.restoreInstanceState(savedInstanceState) } override fun onResume() { @@ -526,9 +527,47 @@ abstract class BaseMainActivity : BaseActivity(), MainActivityContract, runOnUiThread { adapter.reloadFragment(fragment) } } - inner class SectionsPagerAdapter(val pages: List) : FragmentPagerAdapter(supportFragmentManager) { + inner class SectionsPagerAdapter : FragmentPagerAdapter(supportFragmentManager) { + + private val pages: MutableList = mutableListOf() - val forcedFallbacks = mutableSetOf() + private val forcedFallbacks = mutableSetOf() + + /** + * Update page list and prompt reload + */ + fun setPages(pages: List) { + this.pages.clear() + this.pages.addAll(pages) + notifyDataSetChanged() + tabs.removeAllTabs() + this.pages.forEachIndexed { index, fbItem -> + tabs.addTab( + tabs.newTab() + .setCustomView(BadgedIcon(this@BaseMainActivity).apply { iicon = fbItem.icon }.also { + it.setAllAlpha(if (index == 0) SELECTED_TAB_ALPHA else UNSELECTED_TAB_ALPHA) + }) + ) + } + lastPosition = 0 + viewPager.setCurrentItem(0, false) + viewPager.post { + if (!fragmentChannel.isClosedForSend) + fragmentChannel.offer(0) + } //trigger hook so title is set + } + + fun saveInstanceState(outState: Bundle) { + outState.putStringArrayList(STATE_FORCE_FALLBACK, ArrayList(forcedFallbacks)) + } + + fun restoreInstanceState(savedInstanceState: Bundle) { + forcedFallbacks.clear() + forcedFallbacks.addAll( + savedInstanceState.getStringArrayList(STATE_FORCE_FALLBACK) + ?: emptyList() + ) + } fun reloadFragment(fragment: BaseFragment) { if (fragment is WebFragment) return @@ -567,4 +606,9 @@ abstract class BaseMainActivity : BaseActivity(), MainActivityContract, PointF(0f, toolbar.height.toFloat()) else PointF(0f, 0f) + + companion object { + const val SELECTED_TAB_ALPHA = 255f + const val UNSELECTED_TAB_ALPHA = 128f + } } diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/MainActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/MainActivity.kt index 75c9537b..34674cb0 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/activities/MainActivity.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/MainActivity.kt @@ -35,7 +35,6 @@ class MainActivity : BaseMainActivity() { override val fragmentChannel = BroadcastChannel(10) override val headerBadgeChannel = BroadcastChannel(Channel.CONFLATED) - var lastPosition = -1 override fun onNestedCreate(savedInstanceState: Bundle?) { setupTabs() @@ -54,23 +53,18 @@ class MainActivity : BaseMainActivity() { override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) { super.onPageScrolled(position, positionOffset, positionOffsetPixels) - val delta = positionOffset * (255 - 128).toFloat() + val delta = positionOffset * (SELECTED_TAB_ALPHA - UNSELECTED_TAB_ALPHA) tabsForEachView { tabPosition, view -> view.setAllAlpha( when (tabPosition) { - position -> 255.0f - delta - position + 1 -> 128.0f + delta - else -> 128f + position -> SELECTED_TAB_ALPHA - delta + position + 1 -> UNSELECTED_TAB_ALPHA + delta + else -> UNSELECTED_TAB_ALPHA } ) } } }) - viewPager.post { - if (!fragmentChannel.isClosedForSend) - fragmentChannel.offer(0) - lastPosition = 0 - } //trigger hook so title is set } private fun setupTabs() { @@ -115,11 +109,5 @@ class MainActivity : BaseMainActivity() { L.e(e) { "Header badge error" } } } - adapter.pages.forEach { - tabs.addTab( - tabs.newTab() - .setCustomView(BadgedIcon(this).apply { iicon = it.icon }) - ) - } } } diff --git a/app/src/schemas/com.pitchedapps.frost.db.FrostPrivateDatabase/1.json b/app/src/schemas/com.pitchedapps.frost.db.FrostPrivateDatabase/1.json new file mode 100644 index 00000000..60d5cddd --- /dev/null +++ b/app/src/schemas/com.pitchedapps.frost.db.FrostPrivateDatabase/1.json @@ -0,0 +1,189 @@ +{ + "formatVersion": 1, + "database": { + "version": 1, + "identityHash": "0a9d994786b7e07fea95c11d9210ce0e", + "entities": [ + { + "tableName": "cookies", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`cookie_id` INTEGER NOT NULL, `name` TEXT, `cookie` TEXT, PRIMARY KEY(`cookie_id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "cookie_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "cookie", + "columnName": "cookie", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "cookie_id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "notifications", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`notif_id` INTEGER NOT NULL, `userId` INTEGER NOT NULL, `href` TEXT NOT NULL, `title` TEXT, `text` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `profileUrl` TEXT, `type` TEXT NOT NULL, PRIMARY KEY(`notif_id`, `userId`), FOREIGN KEY(`userId`) REFERENCES `cookies`(`cookie_id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "notif_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "href", + "columnName": "href", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "profileUrl", + "columnName": "profileUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "notif_id", + "userId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_notifications_notif_id", + "unique": false, + "columnNames": [ + "notif_id" + ], + "createSql": "CREATE INDEX `index_notifications_notif_id` ON `${TABLE_NAME}` (`notif_id`)" + }, + { + "name": "index_notifications_userId", + "unique": false, + "columnNames": [ + "userId" + ], + "createSql": "CREATE INDEX `index_notifications_userId` ON `${TABLE_NAME}` (`userId`)" + } + ], + "foreignKeys": [ + { + "table": "cookies", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "cookie_id" + ] + } + ] + }, + { + "tableName": "frost_cache", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `type` TEXT NOT NULL, `lastUpdated` INTEGER NOT NULL, `contents` TEXT NOT NULL, PRIMARY KEY(`id`, `type`), FOREIGN KEY(`id`) REFERENCES `cookies`(`cookie_id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastUpdated", + "columnName": "lastUpdated", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contents", + "columnName": "contents", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id", + "type" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [ + { + "table": "cookies", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "id" + ], + "referencedColumns": [ + "cookie_id" + ] + } + ] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"0a9d994786b7e07fea95c11d9210ce0e\")" + ] + } +} \ No newline at end of file diff --git a/app/src/schemas/com.pitchedapps.frost.db.FrostPublicDatabase/1.json b/app/src/schemas/com.pitchedapps.frost.db.FrostPublicDatabase/1.json new file mode 100644 index 00000000..fe2aa83e --- /dev/null +++ b/app/src/schemas/com.pitchedapps.frost.db.FrostPublicDatabase/1.json @@ -0,0 +1,40 @@ +{ + "formatVersion": 1, + "database": { + "version": 1, + "identityHash": "fde868470836ff9230f1d406922d7563", + "entities": [ + { + "tableName": "tabs", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`position` INTEGER NOT NULL, `tab` TEXT NOT NULL, PRIMARY KEY(`position`))", + "fields": [ + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "tab", + "columnName": "tab", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "position" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"fde868470836ff9230f1d406922d7563\")" + ] + } +} \ No newline at end of file -- cgit v1.2.3 From 7b5df27ea3051676ca29e5960d72a36eebdcde03 Mon Sep 17 00:00:00 2001 From: Allan Wang Date: Thu, 7 Mar 2019 18:18:10 -0500 Subject: Set page limit based on tab count --- .../kotlin/com/pitchedapps/frost/activities/BaseMainActivity.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseMainActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseMainActivity.kt index ca30401a..1f96f076 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseMainActivity.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseMainActivity.kt @@ -71,7 +71,6 @@ import com.pitchedapps.frost.contracts.MainActivityContract import com.pitchedapps.frost.contracts.VideoViewHolder import com.pitchedapps.frost.db.CookieDao import com.pitchedapps.frost.db.FbTabDao -import com.pitchedapps.frost.db.TAB_COUNT import com.pitchedapps.frost.db.currentCookie import com.pitchedapps.frost.db.selectAll import com.pitchedapps.frost.enums.MainActivityLayout @@ -162,12 +161,13 @@ abstract class BaseMainActivity : BaseActivity(), MainActivityContract, } setSupportActionBar(toolbar) viewPager.adapter = adapter - viewPager.offscreenPageLimit = TAB_COUNT tabs.setBackgroundColor(Prefs.mainActivityLayout.backgroundColor()) onNestedCreate(savedInstanceState) L.i { "Main finished loading UI in ${System.currentTimeMillis() - start} ms" } launch { - adapter.setPages(tabDao.selectAll()) + val tabs = tabDao.selectAll() + adapter.setPages(tabs) + viewPager.offscreenPageLimit = tabs.size } controlWebview = WebView(this) if (BuildConfig.VERSION_CODE > Prefs.versionCode) { -- cgit v1.2.3 From f038ed790f74d496098575d4e9112f958d9cd6c9 Mon Sep 17 00:00:00 2001 From: Allan Wang Date: Thu, 7 Mar 2019 18:29:24 -0500 Subject: Fix tab customizer activity --- .../com/pitchedapps/frost/activities/TabCustomizerActivity.kt | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/TabCustomizerActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/TabCustomizerActivity.kt index a5dceb3a..7781e190 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/activities/TabCustomizerActivity.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/TabCustomizerActivity.kt @@ -40,6 +40,7 @@ import com.pitchedapps.frost.db.save import com.pitchedapps.frost.db.selectAll import com.pitchedapps.frost.facebook.FbItem import com.pitchedapps.frost.iitems.TabIItem +import com.pitchedapps.frost.utils.L import com.pitchedapps.frost.utils.Prefs import com.pitchedapps.frost.utils.setFrostColors import kotlinx.android.synthetic.main.activity_tab_customizer.* @@ -74,15 +75,17 @@ class TabCustomizerActivity : BaseActivity() { launch { val tabs = tabDao.selectAll().toMutableList() + L.d { "Tabs $tabs" } val remaining = FbItem.values().filter { it.name[0] != '_' }.toMutableList() remaining.removeAll(tabs) tabs.addAll(remaining) - adapter.add(tabs.map(::TabIItem)) - } + adapter.set(tabs.map(::TabIItem)) + + bindSwapper(adapter, tab_recycler) - bindSwapper(adapter, tab_recycler) + adapter.withOnClickListener { view, _, _, _ -> view!!.wobble(); true } - adapter.withOnClickListener { view, _, _, _ -> view!!.wobble(); true } + } setResult(Activity.RESULT_CANCELED) -- cgit v1.2.3 From 9edbab9845b8a1182dee35121a6348537fc657f7 Mon Sep 17 00:00:00 2001 From: Allan Wang Date: Thu, 7 Mar 2019 18:47:17 -0500 Subject: Delete dbflow data on migration --- app/src/main/kotlin/com/pitchedapps/frost/StartActivity.kt | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/app/src/main/kotlin/com/pitchedapps/frost/StartActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/StartActivity.kt index 24e9c548..24b848fe 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/StartActivity.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/StartActivity.kt @@ -108,11 +108,17 @@ class StartActivity : KauBaseActivity() { private suspend fun migrate() = withContext(Dispatchers.IO) { if (cookieDao.selectAll().isNotEmpty()) return@withContext val cookies = (select from CookieModel::class).queryList().map { CookieEntity(it.id, it.name, it.cookie) } - cookieDao.save(cookies) + if (cookies.isNotEmpty()) { + cookieDao.save(cookies) + L._d { "Migrated cookies ${cookieDao.selectAll()}" } + } val tabs = (select from FbTabModel::class).queryList().map(FbTabModel::tab) - tabDao.save(tabs) - L._d { "Migrated cookies ${cookieDao.selectAll()}" } - L._d { "Migrated tabs ${tabDao.selectAll()}" } + if (tabs.isNotEmpty()) { + tabDao.save(tabs) + L._d { "Migrated tabs ${tabDao.selectAll()}" } + } + deleteDatabase("Cookies.db") + deleteDatabase("FrostTabs.db") } private fun showInvalidWebView() = -- cgit v1.2.3 From 2056d9cb72ef8088a6993cbbb48d5bbb41ffddf2 Mon Sep 17 00:00:00 2001 From: Allan Wang Date: Thu, 7 Mar 2019 18:56:42 -0500 Subject: Make cookie name null by default --- .../com/pitchedapps/frost/activities/LoginActivity.kt | 15 ++++++++------- .../main/kotlin/com/pitchedapps/frost/web/LoginWebView.kt | 3 +-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/LoginActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/LoginActivity.kt index e5a50543..610b2796 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/activities/LoginActivity.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/LoginActivity.kt @@ -116,7 +116,7 @@ class LoginActivity : BaseActivity() { val imageDeferred = async { loadProfile(cookie.id) } val nameDeferred = async { loadUsername(cookie) } - val name: String = nameDeferred.await() + val name: String? = nameDeferred.await() val foundImage: Boolean = imageDeferred.await() L._d { "Logged in and received data" } @@ -127,7 +127,7 @@ class LoginActivity : BaseActivity() { L._i { cookie } } - textview.text = String.format(getString(R.string.welcome), name) + textview.text = String.format(getString(R.string.welcome), name ?: "") textview.fadeIn() frostEvent("Login", "success" to true) @@ -172,22 +172,23 @@ class LoginActivity : BaseActivity() { } } - private suspend fun loadUsername(cookie: CookieEntity): String = withContext(Dispatchers.IO) { - val result: String = try { + private suspend fun loadUsername(cookie: CookieEntity): String? = withContext(Dispatchers.IO) { + val result: String? = try { withTimeout(5000) { frostJsoup(cookie.cookie, FbItem.PROFILE.url).title() } } catch (e: Exception) { if (e !is UnknownHostException) e.logFrostEvent("Fetch username failed") - "" + null } - if (cookie.name?.isNotBlank() == false && result != cookie.name) { + if (result != null) { cookieDao.save(cookie.copy(name = result)) + return@withContext result } - cookie.name ?: "" + return@withContext cookie.name } override fun backConsumer(): Boolean { diff --git a/app/src/main/kotlin/com/pitchedapps/frost/web/LoginWebView.kt b/app/src/main/kotlin/com/pitchedapps/frost/web/LoginWebView.kt index 4c7572fb..4b891800 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/web/LoginWebView.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/web/LoginWebView.kt @@ -89,8 +89,7 @@ class LoginWebView @JvmOverloads constructor( val cookie = CookieManager.getInstance().getCookie(url) ?: return null L.d { "Checking cookie for login" } val id = FB_USER_MATCHER.find(cookie)[1]?.toLong() ?: return null - // TODO set name to null? - return CookieEntity(id, "", cookie) + return CookieEntity(id, null, cookie) } override fun onPageCommitVisible(view: WebView, url: String?) { -- cgit v1.2.3 From 0f500024a3b4196623dba9ef75e7f604bec32040 Mon Sep 17 00:00:00 2001 From: Allan Wang Date: Thu, 7 Mar 2019 19:01:30 -0500 Subject: Move viewpager page setup to adapter --- .../kotlin/com/pitchedapps/frost/activities/BaseMainActivity.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseMainActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseMainActivity.kt index 1f96f076..458e97c9 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseMainActivity.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseMainActivity.kt @@ -32,6 +32,7 @@ import android.webkit.WebView import android.widget.FrameLayout import androidx.annotation.StringRes import androidx.coordinatorlayout.widget.CoordinatorLayout +import androidx.core.view.size import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentPagerAdapter import ca.allanwang.kau.searchview.SearchItem @@ -165,9 +166,7 @@ abstract class BaseMainActivity : BaseActivity(), MainActivityContract, onNestedCreate(savedInstanceState) L.i { "Main finished loading UI in ${System.currentTimeMillis() - start} ms" } launch { - val tabs = tabDao.selectAll() - adapter.setPages(tabs) - viewPager.offscreenPageLimit = tabs.size + adapter.setPages(tabDao.selectAll()) } controlWebview = WebView(this) if (BuildConfig.VERSION_CODE > Prefs.versionCode) { @@ -551,6 +550,7 @@ abstract class BaseMainActivity : BaseActivity(), MainActivityContract, } lastPosition = 0 viewPager.setCurrentItem(0, false) + viewPager.offscreenPageLimit = pages.size viewPager.post { if (!fragmentChannel.isClosedForSend) fragmentChannel.offer(0) -- cgit v1.2.3 From 47ac9c218e92ec25b412e9c8c66812e53341052d Mon Sep 17 00:00:00 2001 From: Allan Wang Date: Thu, 7 Mar 2019 19:03:09 -0500 Subject: Specify coroutine scopes in base main activity --- .../main/kotlin/com/pitchedapps/frost/activities/BaseMainActivity.kt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseMainActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseMainActivity.kt index 458e97c9..90fc8ea7 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseMainActivity.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseMainActivity.kt @@ -32,7 +32,6 @@ import android.webkit.WebView import android.widget.FrameLayout import androidx.annotation.StringRes import androidx.coordinatorlayout.widget.CoordinatorLayout -import androidx.core.view.size import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentPagerAdapter import ca.allanwang.kau.searchview.SearchItem @@ -286,7 +285,7 @@ abstract class BaseMainActivity : BaseActivity(), MainActivityContract, else when (profile.identifier) { -2L -> { // TODO no backpressure support - launch { + this@BaseMainActivity.launch { val currentCookie = cookieDao.currentCookie() if (currentCookie == null) { toast(R.string.account_not_found) @@ -315,7 +314,7 @@ abstract class BaseMainActivity : BaseActivity(), MainActivityContract, -3L -> launchNewTask(clearStack = false) -4L -> launchNewTask(cookies(), false) else -> { - launch { + this@BaseMainActivity.launch { FbCookie.switchUser(profile.identifier) tabsForEachView { _, view -> view.badgeText = null } refreshAll() -- cgit v1.2.3 From cac563f6c6a2656c74527bfa2c8b5780765baf69 Mon Sep 17 00:00:00 2001 From: Allan Wang Date: Thu, 7 Mar 2019 19:25:11 -0500 Subject: Prepare generic db --- .../kotlin/com/pitchedapps/frost/db/GenericDb.kt | 72 ++++++++++++++++++++++ .../com/pitchedapps/frost/db/NotificationDb.kt | 3 + .../pitchedapps/frost/settings/Notifications.kt | 7 +-- 3 files changed, 76 insertions(+), 6 deletions(-) create mode 100644 app/src/main/kotlin/com/pitchedapps/frost/db/GenericDb.kt diff --git a/app/src/main/kotlin/com/pitchedapps/frost/db/GenericDb.kt b/app/src/main/kotlin/com/pitchedapps/frost/db/GenericDb.kt new file mode 100644 index 00000000..c191b673 --- /dev/null +++ b/app/src/main/kotlin/com/pitchedapps/frost/db/GenericDb.kt @@ -0,0 +1,72 @@ +/* + * Copyright 2018 Allan Wang + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.pitchedapps.frost.db + +import androidx.room.Dao +import androidx.room.Entity +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.PrimaryKey +import androidx.room.Query +import com.pitchedapps.frost.facebook.FbItem +import com.pitchedapps.frost.facebook.defaultTabs +import com.pitchedapps.frost.utils.L + +/** + * Created by Allan Wang on 2017-05-30. + */ + +/** + * Generic cache to store serialized content + */ +@Entity(tableName = "frost_generic") +data class GenericEntity( + @PrimaryKey + val type: String, + val contents: String +) + +@Dao +interface GenericDao { + + @Query("SELECT contents FROM frost_generic WHERE type = :type") + suspend fun select(type: String): String? + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun save(entity: GenericEntity) + + @Query("DELETE FROM frost_generic WHERE type = :type") + suspend fun delete(type: String) + + suspend fun saveTabs(tabs: List) { + val content = tabs.joinToString(",") { it.name } + save(GenericEntity(TYPE_TABS, content)) + } + + suspend fun getTabs(): List { + val allTabs = FbItem.values.map { it.name to it }.toMap() + return select(TYPE_TABS) + ?.split(",") + ?.mapNotNull { allTabs[it] } + ?.takeIf { it.isNotEmpty() } + ?: defaultTabs() + } + + companion object { + const val TYPE_TABS = "generic_tabs" + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/pitchedapps/frost/db/NotificationDb.kt b/app/src/main/kotlin/com/pitchedapps/frost/db/NotificationDb.kt index d2771754..01baa43e 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/db/NotificationDb.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/db/NotificationDb.kt @@ -119,6 +119,9 @@ interface NotificationDao { @Query("DELETE FROM notifications WHERE userId = :userId AND type = :type") fun _deleteNotifications(userId: Long, type: String) + @Query("DELETE FROM notifications") + suspend fun deleteAll() + /** * It is assumed that the notification batch comes from the same user */ diff --git a/app/src/main/kotlin/com/pitchedapps/frost/settings/Notifications.kt b/app/src/main/kotlin/com/pitchedapps/frost/settings/Notifications.kt index 3444d0b0..dafb259f 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/settings/Notifications.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/settings/Notifications.kt @@ -30,7 +30,6 @@ import com.pitchedapps.frost.BuildConfig import com.pitchedapps.frost.R import com.pitchedapps.frost.activities.SettingsActivity import com.pitchedapps.frost.db.FrostDatabase -import com.pitchedapps.frost.db.NotificationModel import com.pitchedapps.frost.services.fetchNotifications import com.pitchedapps.frost.services.scheduleNotifications import com.pitchedapps.frost.utils.Prefs @@ -173,11 +172,7 @@ fun SettingsActivity.getNotificationPrefs(): KPrefAdapterBuilder.() -> Unit = { plainText(R.string.reset_notif_epoch) { onClick = { launch { - FrostDatabase.get() - .cookieDao() - .selectAll() - .map { NotificationModel(it.id) } - .forEach { it.save() } + FrostDatabase.get().notifDao().deleteAll() } } } -- cgit v1.2.3 From 3f5d2cf2a55d28528c88e118f09a91fd6c59ac43 Mon Sep 17 00:00:00 2001 From: Allan Wang Date: Thu, 7 Mar 2019 19:36:09 -0500 Subject: Replace tab dao with generic dao --- .../com/pitchedapps/frost/db/FbTabsDbTest.kt | 34 ------------ .../com/pitchedapps/frost/db/GenericDbTest.kt | 46 ++++++++++++++++ .../kotlin/com/pitchedapps/frost/StartActivity.kt | 12 ++--- .../frost/activities/BaseMainActivity.kt | 8 +-- .../frost/activities/TabCustomizerActivity.kt | 12 ++--- .../kotlin/com/pitchedapps/frost/db/Database.kt | 7 ++- .../kotlin/com/pitchedapps/frost/db/FbTabsDb.kt | 63 ---------------------- .../kotlin/com/pitchedapps/frost/db/GenericDb.kt | 29 +++++----- .../1.json | 20 +++---- 9 files changed, 89 insertions(+), 142 deletions(-) delete mode 100644 app/src/androidTest/kotlin/com/pitchedapps/frost/db/FbTabsDbTest.kt create mode 100644 app/src/androidTest/kotlin/com/pitchedapps/frost/db/GenericDbTest.kt diff --git a/app/src/androidTest/kotlin/com/pitchedapps/frost/db/FbTabsDbTest.kt b/app/src/androidTest/kotlin/com/pitchedapps/frost/db/FbTabsDbTest.kt deleted file mode 100644 index 752112f9..00000000 --- a/app/src/androidTest/kotlin/com/pitchedapps/frost/db/FbTabsDbTest.kt +++ /dev/null @@ -1,34 +0,0 @@ -package com.pitchedapps.frost.db - -import com.pitchedapps.frost.facebook.FbItem -import com.pitchedapps.frost.facebook.defaultTabs -import kotlinx.coroutines.runBlocking -import kotlin.test.Test -import kotlin.test.assertEquals - -class FbTabsDbTest : BaseDbTest() { - - private val dao get() = db.tabDao() - - /** - * Note that order is also preserved here - */ - @Test - fun save() { - val tabs = listOf(FbItem.ACTIVITY_LOG, FbItem.BIRTHDAYS, FbItem.EVENTS, FbItem.MARKETPLACE, FbItem.ACTIVITY_LOG) - runBlocking { - dao.save(tabs) - assertEquals(tabs, dao.selectAll(), "Tab saving failed") - val newTabs = listOf(FbItem.PAGES, FbItem.MENU) - dao.save(newTabs) - assertEquals(newTabs, dao.selectAll(), "Tab saving does not delete preexisting items") - } - } - - @Test - fun defaultRetrieve() { - runBlocking { - assertEquals(defaultTabs(), dao.selectAll(), "Default retrieval failed") - } - } -} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/pitchedapps/frost/db/GenericDbTest.kt b/app/src/androidTest/kotlin/com/pitchedapps/frost/db/GenericDbTest.kt new file mode 100644 index 00000000..9979eca4 --- /dev/null +++ b/app/src/androidTest/kotlin/com/pitchedapps/frost/db/GenericDbTest.kt @@ -0,0 +1,46 @@ +package com.pitchedapps.frost.db + +import com.pitchedapps.frost.facebook.FbItem +import com.pitchedapps.frost.facebook.defaultTabs +import kotlinx.coroutines.runBlocking +import kotlin.test.Test +import kotlin.test.assertEquals + +class GenericDbTest : BaseDbTest() { + + private val dao get() = db.genericDao() + + /** + * Note that order is also preserved here + */ + @Test + fun save() { + val tabs = listOf(FbItem.ACTIVITY_LOG, FbItem.BIRTHDAYS, FbItem.EVENTS, FbItem.MARKETPLACE, FbItem.ACTIVITY_LOG) + runBlocking { + dao.saveTabs(tabs) + assertEquals(tabs, dao.getTabs(), "Tab saving failed") + val newTabs = listOf(FbItem.PAGES, FbItem.MENU) + dao.saveTabs(newTabs) + assertEquals(newTabs, dao.getTabs(), "Tab overwrite failed") + } + } + + @Test + fun defaultRetrieve() { + runBlocking { + assertEquals(defaultTabs(), dao.getTabs(), "Default retrieval failed") + } + } + + @Test + fun ignoreErrors() { + runBlocking { + dao.save(GenericEntity(GenericDao.TYPE_TABS, "${FbItem.ACTIVITY_LOG.name},unknown,${FbItem.EVENTS.name}")) + assertEquals( + listOf(FbItem.ACTIVITY_LOG, FbItem.EVENTS), + dao.getTabs(), + "Tab fetching does not ignore unknown names" + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/pitchedapps/frost/StartActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/StartActivity.kt index 24b848fe..87244864 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/StartActivity.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/StartActivity.kt @@ -35,10 +35,10 @@ import com.pitchedapps.frost.activities.SelectorActivity import com.pitchedapps.frost.db.CookieDao import com.pitchedapps.frost.db.CookieEntity import com.pitchedapps.frost.db.CookieModel -import com.pitchedapps.frost.db.FbTabDao import com.pitchedapps.frost.db.FbTabModel -import com.pitchedapps.frost.db.save -import com.pitchedapps.frost.db.selectAll +import com.pitchedapps.frost.db.GenericDao +import com.pitchedapps.frost.db.getTabs +import com.pitchedapps.frost.db.saveTabs import com.pitchedapps.frost.facebook.FbCookie import com.pitchedapps.frost.utils.EXTRA_COOKIES import com.pitchedapps.frost.utils.L @@ -59,7 +59,7 @@ import java.util.ArrayList class StartActivity : KauBaseActivity() { private val cookieDao: CookieDao by inject() - private val tabDao: FbTabDao by inject() + private val genericDao: GenericDao by inject() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -114,8 +114,8 @@ class StartActivity : KauBaseActivity() { } val tabs = (select from FbTabModel::class).queryList().map(FbTabModel::tab) if (tabs.isNotEmpty()) { - tabDao.save(tabs) - L._d { "Migrated tabs ${tabDao.selectAll()}" } + genericDao.saveTabs(tabs) + L._d { "Migrated tabs ${genericDao.getTabs()}" } } deleteDatabase("Cookies.db") deleteDatabase("FrostTabs.db") diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseMainActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseMainActivity.kt index 90fc8ea7..c43c31a2 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseMainActivity.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseMainActivity.kt @@ -70,9 +70,9 @@ import com.pitchedapps.frost.contracts.FileChooserDelegate import com.pitchedapps.frost.contracts.MainActivityContract import com.pitchedapps.frost.contracts.VideoViewHolder import com.pitchedapps.frost.db.CookieDao -import com.pitchedapps.frost.db.FbTabDao +import com.pitchedapps.frost.db.GenericDao import com.pitchedapps.frost.db.currentCookie -import com.pitchedapps.frost.db.selectAll +import com.pitchedapps.frost.db.getTabs import com.pitchedapps.frost.enums.MainActivityLayout import com.pitchedapps.frost.facebook.FbCookie import com.pitchedapps.frost.facebook.FbItem @@ -129,7 +129,7 @@ abstract class BaseMainActivity : BaseActivity(), MainActivityContract, override val frameWrapper: FrameLayout get() = frame_wrapper val viewPager: FrostViewPager get() = container val cookieDao: CookieDao by inject() - val tabDao: FbTabDao by inject() + val genericDao: GenericDao by inject() /* * Components with the same id in multiple layout files @@ -165,7 +165,7 @@ abstract class BaseMainActivity : BaseActivity(), MainActivityContract, onNestedCreate(savedInstanceState) L.i { "Main finished loading UI in ${System.currentTimeMillis() - start} ms" } launch { - adapter.setPages(tabDao.selectAll()) + adapter.setPages(genericDao.getTabs()) } controlWebview = WebView(this) if (BuildConfig.VERSION_CODE > Prefs.versionCode) { diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/TabCustomizerActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/TabCustomizerActivity.kt index 7781e190..a380157f 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/activities/TabCustomizerActivity.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/TabCustomizerActivity.kt @@ -34,10 +34,10 @@ import com.mikepenz.fastadapter_extensions.drag.ItemTouchCallback import com.mikepenz.fastadapter_extensions.drag.SimpleDragCallback import com.mikepenz.google_material_typeface_library.GoogleMaterial import com.pitchedapps.frost.R -import com.pitchedapps.frost.db.FbTabDao +import com.pitchedapps.frost.db.GenericDao import com.pitchedapps.frost.db.TAB_COUNT -import com.pitchedapps.frost.db.save -import com.pitchedapps.frost.db.selectAll +import com.pitchedapps.frost.db.getTabs +import com.pitchedapps.frost.db.saveTabs import com.pitchedapps.frost.facebook.FbItem import com.pitchedapps.frost.iitems.TabIItem import com.pitchedapps.frost.utils.L @@ -54,7 +54,7 @@ import java.util.Collections */ class TabCustomizerActivity : BaseActivity() { - private val tabDao: FbTabDao by inject() + private val genericDao: GenericDao by inject() private val adapter = FastItemAdapter() @@ -74,7 +74,7 @@ class TabCustomizerActivity : BaseActivity() { instructions.setTextColor(Prefs.textColor) launch { - val tabs = tabDao.selectAll().toMutableList() + val tabs = genericDao.getTabs().toMutableList() L.d { "Tabs $tabs" } val remaining = FbItem.values().filter { it.name[0] != '_' }.toMutableList() remaining.removeAll(tabs) @@ -94,7 +94,7 @@ class TabCustomizerActivity : BaseActivity() { fab_save.setOnClickListener { launchMain(NonCancellable) { val tabs = adapter.adapterItems.subList(0, TAB_COUNT).map(TabIItem::item) - tabDao.save(tabs) + genericDao.saveTabs(tabs) setResult(Activity.RESULT_OK) finish() } diff --git a/app/src/main/kotlin/com/pitchedapps/frost/db/Database.kt b/app/src/main/kotlin/com/pitchedapps/frost/db/Database.kt index 5fc986e3..0c009634 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/db/Database.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/db/Database.kt @@ -27,11 +27,10 @@ abstract class FrostPrivateDatabase : RoomDatabase(), FrostPrivateDao { } interface FrostPublicDao { - fun tabDao(): FbTabDao + fun genericDao(): GenericDao } -@Database(entities = [FbTabEntity::class], version = 1, exportSchema = true) -@TypeConverters(FbItemConverter::class) +@Database(entities = [GenericEntity::class], version = 1, exportSchema = true) abstract class FrostPublicDatabase : RoomDatabase(), FrostPublicDao { companion object { const val DATABASE_NAME = "frost-db" @@ -71,9 +70,9 @@ class FrostDatabase(private val privateDb: FrostPrivateDatabase, private val pub fun module(context: Context) = module { single { create(context) } single { get().cookieDao() } - single { get().tabDao() } single { get().cacheDao() } single { get().notifDao() } + single { get().genericDao() } } /** diff --git a/app/src/main/kotlin/com/pitchedapps/frost/db/FbTabsDb.kt b/app/src/main/kotlin/com/pitchedapps/frost/db/FbTabsDb.kt index f4e74509..c63be794 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/db/FbTabsDb.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/db/FbTabsDb.kt @@ -16,79 +16,16 @@ */ package com.pitchedapps.frost.db -import androidx.room.Dao -import androidx.room.Entity -import androidx.room.Insert -import androidx.room.OnConflictStrategy -import androidx.room.Query -import androidx.room.Transaction import com.pitchedapps.frost.facebook.FbItem -import com.pitchedapps.frost.facebook.defaultTabs import com.raizlabs.android.dbflow.annotation.Database import com.raizlabs.android.dbflow.annotation.PrimaryKey import com.raizlabs.android.dbflow.annotation.Table import com.raizlabs.android.dbflow.structure.BaseModel -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext /** * Created by Allan Wang on 2017-05-30. */ -@Entity(tableName = "tabs") -data class FbTabEntity(@androidx.room.PrimaryKey val position: Int, val tab: FbItem) - -@Dao -interface FbTabDao { - - @Query("SELECT * FROM tabs ORDER BY position ASC") - fun _selectAll(): List - - @Query("DELETE FROM tabs") - fun _deleteAll() - - @Insert(onConflict = OnConflictStrategy.REPLACE) - fun _insertAll(items: List) - - @Transaction - fun _save(items: List) { - _deleteAll() - _insertAll(items) - } -} - -/** - * Saving tabs operates by deleting all db items and saving the new list. - * Transactions can't be done with suspensions in room as switching threads during the process - * may result in a deadlock. - * That's why we disallow thread switching within the transaction, but wrap the entire thing in a coroutine - */ -suspend fun FbTabDao.save(items: List) { - withContext(Dispatchers.IO) { - val entities = (items.takeIf { it.isNotEmpty() } ?: defaultTabs()).mapIndexed { index, fbItem -> - FbTabEntity( - index, - fbItem - ) - } - _save(entities) - } -} - -suspend fun FbTabDao.selectAll(): List = withContext(Dispatchers.IO) { - _selectAll().map { it.tab }.takeIf { it.isNotEmpty() } ?: defaultTabs() -} - -object FbItemConverter { - @androidx.room.TypeConverter - @JvmStatic - fun fromItem(item: FbItem): String = item.name - - @androidx.room.TypeConverter - @JvmStatic - fun toItem(value: String): FbItem = FbItem.valueOf(value) -} - const val TAB_COUNT = 4 @Database(version = FbTabsDb.VERSION) diff --git a/app/src/main/kotlin/com/pitchedapps/frost/db/GenericDb.kt b/app/src/main/kotlin/com/pitchedapps/frost/db/GenericDb.kt index c191b673..c1f05092 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/db/GenericDb.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/db/GenericDb.kt @@ -24,7 +24,6 @@ import androidx.room.PrimaryKey import androidx.room.Query import com.pitchedapps.frost.facebook.FbItem import com.pitchedapps.frost.facebook.defaultTabs -import com.pitchedapps.frost.utils.L /** * Created by Allan Wang on 2017-05-30. @@ -52,21 +51,21 @@ interface GenericDao { @Query("DELETE FROM frost_generic WHERE type = :type") suspend fun delete(type: String) - suspend fun saveTabs(tabs: List) { - val content = tabs.joinToString(",") { it.name } - save(GenericEntity(TYPE_TABS, content)) - } - - suspend fun getTabs(): List { - val allTabs = FbItem.values.map { it.name to it }.toMap() - return select(TYPE_TABS) - ?.split(",") - ?.mapNotNull { allTabs[it] } - ?.takeIf { it.isNotEmpty() } - ?: defaultTabs() - } - companion object { const val TYPE_TABS = "generic_tabs" } +} + +suspend fun GenericDao.saveTabs(tabs: List) { + val content = tabs.joinToString(",") { it.name } + save(GenericEntity(GenericDao.TYPE_TABS, content)) +} + +suspend fun GenericDao.getTabs(): List { + val allTabs = FbItem.values.map { it.name to it }.toMap() + return select(GenericDao.TYPE_TABS) + ?.split(",") + ?.mapNotNull { allTabs[it] } + ?.takeIf { it.isNotEmpty() } + ?: defaultTabs() } \ No newline at end of file diff --git a/app/src/schemas/com.pitchedapps.frost.db.FrostPublicDatabase/1.json b/app/src/schemas/com.pitchedapps.frost.db.FrostPublicDatabase/1.json index fe2aa83e..4a523c62 100644 --- a/app/src/schemas/com.pitchedapps.frost.db.FrostPublicDatabase/1.json +++ b/app/src/schemas/com.pitchedapps.frost.db.FrostPublicDatabase/1.json @@ -2,28 +2,28 @@ "formatVersion": 1, "database": { "version": 1, - "identityHash": "fde868470836ff9230f1d406922d7563", + "identityHash": "ee4d2fe4052ad3a1892be17681816c2c", "entities": [ { - "tableName": "tabs", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`position` INTEGER NOT NULL, `tab` TEXT NOT NULL, PRIMARY KEY(`position`))", + "tableName": "frost_generic", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` TEXT NOT NULL, `contents` TEXT NOT NULL, PRIMARY KEY(`type`))", "fields": [ { - "fieldPath": "position", - "columnName": "position", - "affinity": "INTEGER", + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", "notNull": true }, { - "fieldPath": "tab", - "columnName": "tab", + "fieldPath": "contents", + "columnName": "contents", "affinity": "TEXT", "notNull": true } ], "primaryKey": { "columnNames": [ - "position" + "type" ], "autoGenerate": false }, @@ -34,7 +34,7 @@ "views": [], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"fde868470836ff9230f1d406922d7563\")" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"ee4d2fe4052ad3a1892be17681816c2c\")" ] } } \ No newline at end of file -- cgit v1.2.3 From b1c7358c24ab95825d18293636251f276637ba25 Mon Sep 17 00:00:00 2001 From: Allan Wang Date: Thu, 7 Mar 2019 19:43:26 -0500 Subject: Apply spotless --- .../kotlin/com/pitchedapps/frost/db/BaseDbTest.kt | 18 +++++++++++++++++- .../kotlin/com/pitchedapps/frost/db/CacheDbTest.kt | 18 +++++++++++++++++- .../kotlin/com/pitchedapps/frost/db/CookieDbTest.kt | 18 +++++++++++++++++- .../kotlin/com/pitchedapps/frost/db/DatabaseTest.kt | 18 +++++++++++++++++- .../kotlin/com/pitchedapps/frost/db/GenericDbTest.kt | 18 +++++++++++++++++- .../com/pitchedapps/frost/db/NotificationDbTest.kt | 18 +++++++++++++++++- .../frost/activities/TabCustomizerActivity.kt | 1 - .../main/kotlin/com/pitchedapps/frost/db/CacheDb.kt | 2 +- .../main/kotlin/com/pitchedapps/frost/db/CookiesDb.kt | 2 +- .../main/kotlin/com/pitchedapps/frost/db/Database.kt | 18 ++++++++++++++++-- .../main/kotlin/com/pitchedapps/frost/db/FbTabsDb.kt | 2 +- .../main/kotlin/com/pitchedapps/frost/db/GenericDb.kt | 2 +- .../kotlin/com/pitchedapps/frost/db/NotificationDb.kt | 2 +- app/src/main/kotlin/com/pitchedapps/frost/utils/L.kt | 1 - .../com/pitchedapps/frost/views/FrostVideoViewer.kt | 1 - 15 files changed, 123 insertions(+), 16 deletions(-) diff --git a/app/src/androidTest/kotlin/com/pitchedapps/frost/db/BaseDbTest.kt b/app/src/androidTest/kotlin/com/pitchedapps/frost/db/BaseDbTest.kt index 6621ea95..bae56e2f 100644 --- a/app/src/androidTest/kotlin/com/pitchedapps/frost/db/BaseDbTest.kt +++ b/app/src/androidTest/kotlin/com/pitchedapps/frost/db/BaseDbTest.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2019 Allan Wang + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ package com.pitchedapps.frost.db import android.content.Context @@ -29,4 +45,4 @@ abstract class BaseDbTest { fun after() { db.close() } -} \ No newline at end of file +} diff --git a/app/src/androidTest/kotlin/com/pitchedapps/frost/db/CacheDbTest.kt b/app/src/androidTest/kotlin/com/pitchedapps/frost/db/CacheDbTest.kt index 1fe7bbc4..417c6678 100644 --- a/app/src/androidTest/kotlin/com/pitchedapps/frost/db/CacheDbTest.kt +++ b/app/src/androidTest/kotlin/com/pitchedapps/frost/db/CacheDbTest.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2019 Allan Wang + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ package com.pitchedapps.frost.db import kotlinx.coroutines.runBlocking @@ -29,4 +45,4 @@ class CacheDbTest : BaseDbTest() { ) } } -} \ No newline at end of file +} diff --git a/app/src/androidTest/kotlin/com/pitchedapps/frost/db/CookieDbTest.kt b/app/src/androidTest/kotlin/com/pitchedapps/frost/db/CookieDbTest.kt index 122e3205..327ead86 100644 --- a/app/src/androidTest/kotlin/com/pitchedapps/frost/db/CookieDbTest.kt +++ b/app/src/androidTest/kotlin/com/pitchedapps/frost/db/CookieDbTest.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2019 Allan Wang + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ package com.pitchedapps.frost.db import kotlinx.coroutines.runBlocking @@ -66,4 +82,4 @@ class CookieDbTest : BaseDbTest() { assertNull(dao.selectById(cookie.id + 1), "Inexistent cookie selection failed") } } -} \ No newline at end of file +} diff --git a/app/src/androidTest/kotlin/com/pitchedapps/frost/db/DatabaseTest.kt b/app/src/androidTest/kotlin/com/pitchedapps/frost/db/DatabaseTest.kt index 1f1a201b..c79d212e 100644 --- a/app/src/androidTest/kotlin/com/pitchedapps/frost/db/DatabaseTest.kt +++ b/app/src/androidTest/kotlin/com/pitchedapps/frost/db/DatabaseTest.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2019 Allan Wang + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ package com.pitchedapps.frost.db import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -35,4 +51,4 @@ class DatabaseTest : KoinTest { val missingKoins = (members + FrostDatabase::class).filter { !hasKoin(it) } assertTrue(missingKoins.isEmpty(), "Missing koins: $missingKoins") } -} \ No newline at end of file +} diff --git a/app/src/androidTest/kotlin/com/pitchedapps/frost/db/GenericDbTest.kt b/app/src/androidTest/kotlin/com/pitchedapps/frost/db/GenericDbTest.kt index 9979eca4..add9f509 100644 --- a/app/src/androidTest/kotlin/com/pitchedapps/frost/db/GenericDbTest.kt +++ b/app/src/androidTest/kotlin/com/pitchedapps/frost/db/GenericDbTest.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2019 Allan Wang + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ package com.pitchedapps.frost.db import com.pitchedapps.frost.facebook.FbItem @@ -43,4 +59,4 @@ class GenericDbTest : BaseDbTest() { ) } } -} \ No newline at end of file +} diff --git a/app/src/androidTest/kotlin/com/pitchedapps/frost/db/NotificationDbTest.kt b/app/src/androidTest/kotlin/com/pitchedapps/frost/db/NotificationDbTest.kt index 25c29db4..9167c7bf 100644 --- a/app/src/androidTest/kotlin/com/pitchedapps/frost/db/NotificationDbTest.kt +++ b/app/src/androidTest/kotlin/com/pitchedapps/frost/db/NotificationDbTest.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2019 Allan Wang + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ package com.pitchedapps.frost.db import com.pitchedapps.frost.services.NOTIF_CHANNEL_GENERAL @@ -125,4 +141,4 @@ class NotificationDbTest : BaseDbTest() { ) } } -} \ No newline at end of file +} diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/TabCustomizerActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/TabCustomizerActivity.kt index a380157f..6ad7d3f2 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/activities/TabCustomizerActivity.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/TabCustomizerActivity.kt @@ -84,7 +84,6 @@ class TabCustomizerActivity : BaseActivity() { bindSwapper(adapter, tab_recycler) adapter.withOnClickListener { view, _, _, _ -> view!!.wobble(); true } - } setResult(Activity.RESULT_CANCELED) diff --git a/app/src/main/kotlin/com/pitchedapps/frost/db/CacheDb.kt b/app/src/main/kotlin/com/pitchedapps/frost/db/CacheDb.kt index 8c3c9c6b..4906f60a 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/db/CacheDb.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/db/CacheDb.kt @@ -74,4 +74,4 @@ suspend fun CacheDao.save(id: Long, type: String, contents: String): Boolean = } catch (e: Exception) { L.e(e) { "Cache save failed for $type" } false - } \ No newline at end of file + } diff --git a/app/src/main/kotlin/com/pitchedapps/frost/db/CookiesDb.kt b/app/src/main/kotlin/com/pitchedapps/frost/db/CookiesDb.kt index 7e929370..5aadbb02 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/db/CookiesDb.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/db/CookiesDb.kt @@ -82,4 +82,4 @@ data class CookieModel(@PrimaryKey var id: Long = -1L, var name: String? = null, BaseModel(), Parcelable { override fun toString(): String = "CookieModel(${hashCode()})" -} \ No newline at end of file +} diff --git a/app/src/main/kotlin/com/pitchedapps/frost/db/Database.kt b/app/src/main/kotlin/com/pitchedapps/frost/db/Database.kt index 0c009634..e1b1d4c4 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/db/Database.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/db/Database.kt @@ -1,11 +1,25 @@ +/* + * Copyright 2019 Allan Wang + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ package com.pitchedapps.frost.db import android.content.Context import androidx.room.Database import androidx.room.Room import androidx.room.RoomDatabase -import androidx.room.TypeConverters -import org.koin.core.Koin import org.koin.dsl.module.module import org.koin.standalone.StandAloneContext diff --git a/app/src/main/kotlin/com/pitchedapps/frost/db/FbTabsDb.kt b/app/src/main/kotlin/com/pitchedapps/frost/db/FbTabsDb.kt index c63be794..a704ce82 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/db/FbTabsDb.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/db/FbTabsDb.kt @@ -35,4 +35,4 @@ object FbTabsDb { } @Table(database = FbTabsDb::class, allFields = true) -data class FbTabModel(@PrimaryKey var position: Int = -1, var tab: FbItem = FbItem.FEED) : BaseModel() \ No newline at end of file +data class FbTabModel(@PrimaryKey var position: Int = -1, var tab: FbItem = FbItem.FEED) : BaseModel() diff --git a/app/src/main/kotlin/com/pitchedapps/frost/db/GenericDb.kt b/app/src/main/kotlin/com/pitchedapps/frost/db/GenericDb.kt index c1f05092..4177ae86 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/db/GenericDb.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/db/GenericDb.kt @@ -68,4 +68,4 @@ suspend fun GenericDao.getTabs(): List { ?.mapNotNull { allTabs[it] } ?.takeIf { it.isNotEmpty() } ?: defaultTabs() -} \ No newline at end of file +} diff --git a/app/src/main/kotlin/com/pitchedapps/frost/db/NotificationDb.kt b/app/src/main/kotlin/com/pitchedapps/frost/db/NotificationDb.kt index 01baa43e..afeeefcd 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/db/NotificationDb.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/db/NotificationDb.kt @@ -194,4 +194,4 @@ data class NotificationModel( private fun lastNotificationTime(id: Long): NotificationModel = (select from NotificationModel::class where (NotificationModel_Table.id eq id)).querySingle() - ?: NotificationModel(id = id) \ No newline at end of file + ?: NotificationModel(id = id) diff --git a/app/src/main/kotlin/com/pitchedapps/frost/utils/L.kt b/app/src/main/kotlin/com/pitchedapps/frost/utils/L.kt index 7d2e0e08..7c8c1895 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/utils/L.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/utils/L.kt @@ -20,7 +20,6 @@ import android.util.Log import ca.allanwang.kau.logging.KauLogger import com.bugsnag.android.Bugsnag import com.pitchedapps.frost.BuildConfig -import java.lang.Exception /** * Created by Allan Wang on 2017-05-28. diff --git a/app/src/main/kotlin/com/pitchedapps/frost/views/FrostVideoViewer.kt b/app/src/main/kotlin/com/pitchedapps/frost/views/FrostVideoViewer.kt index 75b80ae1..4d88ad3d 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/views/FrostVideoViewer.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/views/FrostVideoViewer.kt @@ -104,7 +104,6 @@ class FrostVideoViewer @JvmOverloads constructor( val cookie = FrostDatabase.get().cookieDao().currentCookie() ?: return@launchMain context.frostDownload(cookie, video.videoUri) } - } true } -- cgit v1.2.3 From e5eb928feacba8d1c634ad8e5d85259ae161cabd Mon Sep 17 00:00:00 2001 From: Allan Wang Date: Sat, 30 Mar 2019 22:30:16 -0400 Subject: Add thread progress --- .../com/pitchedapps/frost/db/NotificationDb.kt | 2 +- .../kotlin/com/pitchedapps/frost/db/ThreadDb.kt | 115 +++++++++++++++++++++ 2 files changed, 116 insertions(+), 1 deletion(-) create mode 100644 app/src/main/kotlin/com/pitchedapps/frost/db/ThreadDb.kt diff --git a/app/src/main/kotlin/com/pitchedapps/frost/db/NotificationDb.kt b/app/src/main/kotlin/com/pitchedapps/frost/db/NotificationDb.kt index afeeefcd..e1f7fc76 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/db/NotificationDb.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/db/NotificationDb.kt @@ -192,6 +192,6 @@ data class NotificationModel( var epochIm: Long = -1L ) : BaseModel() -private fun lastNotificationTime(id: Long): NotificationModel = +internal fun lastNotificationTime(id: Long): NotificationModel = (select from NotificationModel::class where (NotificationModel_Table.id eq id)).querySingle() ?: NotificationModel(id = id) diff --git a/app/src/main/kotlin/com/pitchedapps/frost/db/ThreadDb.kt b/app/src/main/kotlin/com/pitchedapps/frost/db/ThreadDb.kt new file mode 100644 index 00000000..d7c91211 --- /dev/null +++ b/app/src/main/kotlin/com/pitchedapps/frost/db/ThreadDb.kt @@ -0,0 +1,115 @@ +/* + * Copyright 2018 Allan Wang + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.pitchedapps.frost.db + +import androidx.room.Dao +import androidx.room.Embedded +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.Index +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Transaction +import com.pitchedapps.frost.db.CookieModel_Table.cookie +import com.pitchedapps.frost.facebook.parsers.FrostThread +import com.pitchedapps.frost.utils.L +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +@Entity( + tableName = "threads", + primaryKeys = ["thread_id", "userId"], + foreignKeys = [ForeignKey( + entity = CookieEntity::class, + parentColumns = ["cookie_id"], + childColumns = ["userId"], + onDelete = ForeignKey.CASCADE + )], + indices = [Index("thread_id"), Index("userId")] +) +data class ThreadEntity( + @Embedded(prefix = "thread_") + val thread: FrostThread, + val userId: Long +) + +data class ThreadContentEntity( + @Embedded + val cookie: CookieEntity, + @Embedded(prefix = "thread_") + val thread: FrostThread +) + +@Dao +interface ThreadDao { + + /** + * Note that notifications are guaranteed to be ordered by descending timestamp + */ + @Transaction + @Query("SELECT * FROM cookies INNER JOIN threads ON cookie_id = userId WHERE userId = :userId ORDER BY thread_time DESC") + fun _selectThreads(userId: Long): List + + @Query("SELECT thread_time FROM threads WHERE userId = :userId ORDER BY thread_time DESC LIMIT 1") + fun _selectEpoch(userId: Long): Long? + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun _insertThreads(notifs: List) + + @Query("DELETE FROM threads WHERE userId = :userId ") + fun _deleteThreads(userId: Long) + + @Query("DELETE FROM threads") + suspend fun deleteAll() + + /** + * It is assumed that the notification batch comes from the same user + */ + @Transaction + fun _saveThreads(userId: Long, notifs: List) { + val entities = notifs.map { ThreadEntity(it, userId) } + _deleteThreads(userId) + _insertThreads(entities) + } +} + +suspend fun ThreadDao.selectThreads(userId: Long): List = + withContext(Dispatchers.IO) { + _selectThreads(userId) + } + +/** + * Returns true if successful, given that there are constraints to the insertion + */ +suspend fun ThreadDao.saveThreads(userId: Long, threads: List): Boolean { + if (threads.isEmpty()) return true + return withContext(Dispatchers.IO) { + try { + _saveThreads(userId, threads) + true + } catch (e: Exception) { + L.e(e) { "Thread save failed" } + false + } + } +} + +suspend fun ThreadDao.latestEpoch(userId: Long, type: String): Long = + withContext(Dispatchers.IO) { + _selectEpoch(userId) ?: lastNotificationTime(userId).epochIm + } -- cgit v1.2.3 From 4739e6f58df1116babac69896764e83db551f583 Mon Sep 17 00:00:00 2001 From: Allan Wang Date: Sun, 21 Apr 2019 20:26:01 -0400 Subject: Merge properties --- docs/Changelog.md | 6 ------ gradle.properties | 8 -------- 2 files changed, 14 deletions(-) diff --git a/docs/Changelog.md b/docs/Changelog.md index 3d72ec4f..7a6bbfab 100644 --- a/docs/Changelog.md +++ b/docs/Changelog.md @@ -1,10 +1,5 @@ # Changelog -<<<<<<< HEAD -## v2.2.3 -* Add ability to hide stories -* Remove fbclid from urls -======= ## v2.2.4 * Show top bar to allow sharing posts * Fix unmuting videos when autoplay is enabled @@ -18,7 +13,6 @@ * Remove round icon settings as they are the default in Facebook * Update theme * Update translations ->>>>>>> dev ## v2.2.2 * New marketplace shortcut diff --git a/gradle.properties b/gradle.properties index c080daa0..4c0ace29 100644 --- a/gradle.properties +++ b/gradle.properties @@ -14,11 +14,7 @@ org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryErro APP_ID=Frost APP_GROUP=com.pitchedapps -<<<<<<< HEAD -KAU=4.0.0-alpha02 -======= KAU=4.0.0 ->>>>>>> dev KOTLIN=1.3.21 # https://mvnrepository.com/artifact/com.android.tools.build/gradle?repo=google @@ -60,13 +56,9 @@ MATERIAL_DRAWER_KT=2.0.1 # https://github.com/square/okhttp/releases OKHTTP=3.14.0 # http://robolectric.org/getting-started/ -<<<<<<< HEAD -ROBOELECTRIC=4.1 # https://developer.android.com/jetpack/androidx/releases/room ROOM=2.1.0-alpha04 -======= ROBOELECTRIC=4.2 ->>>>>>> dev # https://github.com/davemorrissey/subsampling-scale-image-view#quick-start SCALE_IMAGE_VIEW=3.10.0 # https://github.com/umano/AndroidSlidingUpPanel#importing-the-library -- cgit v1.2.3 From 5002792f2c79a7479c531736a4a9c40c0b1fe116 Mon Sep 17 00:00:00 2001 From: Allan Wang Date: Sun, 21 Apr 2019 21:02:55 -0400 Subject: Wrap all db calls using our own context --- .../kotlin/com/pitchedapps/frost/StartActivity.kt | 2 + .../pitchedapps/frost/activities/LoginActivity.kt | 2 + .../kotlin/com/pitchedapps/frost/db/CacheDb.kt | 19 +++- .../kotlin/com/pitchedapps/frost/db/CookiesDb.kt | 15 ++- .../kotlin/com/pitchedapps/frost/db/DaoUtils.kt | 28 +++++ .../kotlin/com/pitchedapps/frost/db/GenericDb.kt | 14 +-- .../com/pitchedapps/frost/db/NotificationDb.kt | 17 ++- .../kotlin/com/pitchedapps/frost/db/ThreadDb.kt | 115 --------------------- .../com/pitchedapps/frost/facebook/FbCookie.kt | 3 + .../frost/services/NotificationService.kt | 1 + .../pitchedapps/frost/settings/Notifications.kt | 1 + .../com/pitchedapps/frost/web/DebugWebView.kt | 2 + 12 files changed, 78 insertions(+), 141 deletions(-) create mode 100644 app/src/main/kotlin/com/pitchedapps/frost/db/DaoUtils.kt delete mode 100644 app/src/main/kotlin/com/pitchedapps/frost/db/ThreadDb.kt diff --git a/app/src/main/kotlin/com/pitchedapps/frost/StartActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/StartActivity.kt index 87244864..18ae4b0b 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/StartActivity.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/StartActivity.kt @@ -38,7 +38,9 @@ import com.pitchedapps.frost.db.CookieModel import com.pitchedapps.frost.db.FbTabModel import com.pitchedapps.frost.db.GenericDao import com.pitchedapps.frost.db.getTabs +import com.pitchedapps.frost.db.save import com.pitchedapps.frost.db.saveTabs +import com.pitchedapps.frost.db.selectAll import com.pitchedapps.frost.facebook.FbCookie import com.pitchedapps.frost.utils.EXTRA_COOKIES import com.pitchedapps.frost.utils.L diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/LoginActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/LoginActivity.kt index b80f06f7..ed207896 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/activities/LoginActivity.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/LoginActivity.kt @@ -34,6 +34,8 @@ import com.bumptech.glide.request.target.Target import com.pitchedapps.frost.R import com.pitchedapps.frost.db.CookieDao import com.pitchedapps.frost.db.CookieEntity +import com.pitchedapps.frost.db.save +import com.pitchedapps.frost.db.selectAll import com.pitchedapps.frost.facebook.FbCookie import com.pitchedapps.frost.facebook.FbItem import com.pitchedapps.frost.facebook.profilePictureUrl diff --git a/app/src/main/kotlin/com/pitchedapps/frost/db/CacheDb.kt b/app/src/main/kotlin/com/pitchedapps/frost/db/CacheDb.kt index 4906f60a..f0dacdc7 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/db/CacheDb.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/db/CacheDb.kt @@ -55,23 +55,32 @@ data class CacheEntity( interface CacheDao { @Query("SELECT * FROM frost_cache WHERE id = :id AND type = :type") - suspend fun select(id: Long, type: String): CacheEntity? + fun _select(id: Long, type: String): CacheEntity? @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun insertCache(cache: CacheEntity) + fun _insertCache(cache: CacheEntity) @Query("DELETE FROM frost_cache WHERE id = :id AND type = :type") - suspend fun delete(id: Long, type: String) + fun _delete(id: Long, type: String) +} + +suspend fun CacheDao.select(id: Long, type: String) = dao { + _select(id, type) +} + +suspend fun CacheDao.delete(id: Long, type: String) = dao { + _delete(id, type) } /** * Returns true if successful, given that there are constraints to the insertion */ -suspend fun CacheDao.save(id: Long, type: String, contents: String): Boolean = +suspend fun CacheDao.save(id: Long, type: String, contents: String): Boolean = dao { try { - insertCache(CacheEntity(id, type, System.currentTimeMillis(), contents)) + _insertCache(CacheEntity(id, type, System.currentTimeMillis(), contents)) true } catch (e: Exception) { L.e(e) { "Cache save failed for $type" } false } +} diff --git a/app/src/main/kotlin/com/pitchedapps/frost/db/CookiesDb.kt b/app/src/main/kotlin/com/pitchedapps/frost/db/CookiesDb.kt index 5aadbb02..b81ce365 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/db/CookiesDb.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/db/CookiesDb.kt @@ -53,21 +53,26 @@ data class CookieEntity( interface CookieDao { @Query("SELECT * FROM cookies") - suspend fun selectAll(): List + fun _selectAll(): List @Query("SELECT * FROM cookies WHERE cookie_id = :id") - suspend fun selectById(id: Long): CookieEntity? + fun _selectById(id: Long): CookieEntity? @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun save(cookie: CookieEntity) + fun _save(cookie: CookieEntity) @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun save(cookies: List) + fun _save(cookies: List) @Query("DELETE FROM cookies WHERE cookie_id = :id") - suspend fun deleteById(id: Long) + fun _deleteById(id: Long) } +suspend fun CookieDao.selectAll() = dao { _selectAll() } +suspend fun CookieDao.selectById(id: Long) = dao { _selectById(id) } +suspend fun CookieDao.save(cookie: CookieEntity) = dao { _save(cookie) } +suspend fun CookieDao.save(cookies: List) = dao { _save(cookies) } +suspend fun CookieDao.deleteById(id: Long) = dao { _deleteById(id) } suspend fun CookieDao.currentCookie() = selectById(Prefs.userId) @Database(version = CookiesDb.VERSION) diff --git a/app/src/main/kotlin/com/pitchedapps/frost/db/DaoUtils.kt b/app/src/main/kotlin/com/pitchedapps/frost/db/DaoUtils.kt new file mode 100644 index 00000000..c31aa9b7 --- /dev/null +++ b/app/src/main/kotlin/com/pitchedapps/frost/db/DaoUtils.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2019 Allan Wang + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.pitchedapps.frost.db + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +/** + * Wraps dao calls to work with coroutines + * Non transactional queries were supposed to be fixed in https://issuetracker.google.com/issues/69474692, + * but it still requires dispatch from a non ui thread. + * This avoids that constraint + */ +suspend inline fun dao(crossinline block: () -> T) = withContext(Dispatchers.IO) { block() } diff --git a/app/src/main/kotlin/com/pitchedapps/frost/db/GenericDb.kt b/app/src/main/kotlin/com/pitchedapps/frost/db/GenericDb.kt index 4177ae86..f36c8af9 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/db/GenericDb.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/db/GenericDb.kt @@ -43,27 +43,27 @@ data class GenericEntity( interface GenericDao { @Query("SELECT contents FROM frost_generic WHERE type = :type") - suspend fun select(type: String): String? + fun _select(type: String): String? @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun save(entity: GenericEntity) + fun _save(entity: GenericEntity) @Query("DELETE FROM frost_generic WHERE type = :type") - suspend fun delete(type: String) + fun _delete(type: String) companion object { const val TYPE_TABS = "generic_tabs" } } -suspend fun GenericDao.saveTabs(tabs: List) { +suspend fun GenericDao.saveTabs(tabs: List) = dao { val content = tabs.joinToString(",") { it.name } - save(GenericEntity(GenericDao.TYPE_TABS, content)) + _save(GenericEntity(GenericDao.TYPE_TABS, content)) } -suspend fun GenericDao.getTabs(): List { +suspend fun GenericDao.getTabs(): List = dao { val allTabs = FbItem.values.map { it.name to it }.toMap() - return select(GenericDao.TYPE_TABS) + _select(GenericDao.TYPE_TABS) ?.split(",") ?.mapNotNull { allTabs[it] } ?.takeIf { it.isNotEmpty() } diff --git a/app/src/main/kotlin/com/pitchedapps/frost/db/NotificationDb.kt b/app/src/main/kotlin/com/pitchedapps/frost/db/NotificationDb.kt index e1f7fc76..8936d682 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/db/NotificationDb.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/db/NotificationDb.kt @@ -42,8 +42,6 @@ import com.raizlabs.android.dbflow.kotlinextensions.where import com.raizlabs.android.dbflow.sql.SQLiteType import com.raizlabs.android.dbflow.sql.migration.AlterTableMigration import com.raizlabs.android.dbflow.structure.BaseModel -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext @Entity( tableName = "notifications", @@ -120,7 +118,7 @@ interface NotificationDao { fun _deleteNotifications(userId: Long, type: String) @Query("DELETE FROM notifications") - suspend fun deleteAll() + fun _deleteAll() /** * It is assumed that the notification batch comes from the same user @@ -134,17 +132,18 @@ interface NotificationDao { } } -suspend fun NotificationDao.selectNotifications(userId: Long, type: String): List = - withContext(Dispatchers.IO) { - _selectNotifications(userId, type).map { it.toNotifContent() } - } +suspend fun NotificationDao.deleteAll() = dao { _deleteAll() } + +suspend fun NotificationDao.selectNotifications(userId: Long, type: String): List = dao { + _selectNotifications(userId, type).map { it.toNotifContent() } +} /** * Returns true if successful, given that there are constraints to the insertion */ suspend fun NotificationDao.saveNotifications(type: String, notifs: List): Boolean { if (notifs.isEmpty()) return true - return withContext(Dispatchers.IO) { + return dao { try { _saveNotifications(type, notifs) true @@ -155,7 +154,7 @@ suspend fun NotificationDao.saveNotifications(type: String, notifs: List it.epoch diff --git a/app/src/main/kotlin/com/pitchedapps/frost/db/ThreadDb.kt b/app/src/main/kotlin/com/pitchedapps/frost/db/ThreadDb.kt deleted file mode 100644 index d7c91211..00000000 --- a/app/src/main/kotlin/com/pitchedapps/frost/db/ThreadDb.kt +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Copyright 2018 Allan Wang - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package com.pitchedapps.frost.db - -import androidx.room.Dao -import androidx.room.Embedded -import androidx.room.Entity -import androidx.room.ForeignKey -import androidx.room.Index -import androidx.room.Insert -import androidx.room.OnConflictStrategy -import androidx.room.Query -import androidx.room.Transaction -import com.pitchedapps.frost.db.CookieModel_Table.cookie -import com.pitchedapps.frost.facebook.parsers.FrostThread -import com.pitchedapps.frost.utils.L -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext - -@Entity( - tableName = "threads", - primaryKeys = ["thread_id", "userId"], - foreignKeys = [ForeignKey( - entity = CookieEntity::class, - parentColumns = ["cookie_id"], - childColumns = ["userId"], - onDelete = ForeignKey.CASCADE - )], - indices = [Index("thread_id"), Index("userId")] -) -data class ThreadEntity( - @Embedded(prefix = "thread_") - val thread: FrostThread, - val userId: Long -) - -data class ThreadContentEntity( - @Embedded - val cookie: CookieEntity, - @Embedded(prefix = "thread_") - val thread: FrostThread -) - -@Dao -interface ThreadDao { - - /** - * Note that notifications are guaranteed to be ordered by descending timestamp - */ - @Transaction - @Query("SELECT * FROM cookies INNER JOIN threads ON cookie_id = userId WHERE userId = :userId ORDER BY thread_time DESC") - fun _selectThreads(userId: Long): List - - @Query("SELECT thread_time FROM threads WHERE userId = :userId ORDER BY thread_time DESC LIMIT 1") - fun _selectEpoch(userId: Long): Long? - - @Insert(onConflict = OnConflictStrategy.REPLACE) - fun _insertThreads(notifs: List) - - @Query("DELETE FROM threads WHERE userId = :userId ") - fun _deleteThreads(userId: Long) - - @Query("DELETE FROM threads") - suspend fun deleteAll() - - /** - * It is assumed that the notification batch comes from the same user - */ - @Transaction - fun _saveThreads(userId: Long, notifs: List) { - val entities = notifs.map { ThreadEntity(it, userId) } - _deleteThreads(userId) - _insertThreads(entities) - } -} - -suspend fun ThreadDao.selectThreads(userId: Long): List = - withContext(Dispatchers.IO) { - _selectThreads(userId) - } - -/** - * Returns true if successful, given that there are constraints to the insertion - */ -suspend fun ThreadDao.saveThreads(userId: Long, threads: List): Boolean { - if (threads.isEmpty()) return true - return withContext(Dispatchers.IO) { - try { - _saveThreads(userId, threads) - true - } catch (e: Exception) { - L.e(e) { "Thread save failed" } - false - } - } -} - -suspend fun ThreadDao.latestEpoch(userId: Long, type: String): Long = - withContext(Dispatchers.IO) { - _selectEpoch(userId) ?: lastNotificationTime(userId).epochIm - } diff --git a/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbCookie.kt b/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbCookie.kt index 6afbea4b..0c1da3a3 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbCookie.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbCookie.kt @@ -22,6 +22,9 @@ import android.webkit.CookieManager import com.pitchedapps.frost.db.CookieDao import com.pitchedapps.frost.db.CookieEntity import com.pitchedapps.frost.db.FrostDatabase +import com.pitchedapps.frost.db.deleteById +import com.pitchedapps.frost.db.save +import com.pitchedapps.frost.db.selectById import com.pitchedapps.frost.utils.L import com.pitchedapps.frost.utils.Prefs import com.pitchedapps.frost.utils.cookies diff --git a/app/src/main/kotlin/com/pitchedapps/frost/services/NotificationService.kt b/app/src/main/kotlin/com/pitchedapps/frost/services/NotificationService.kt index 2e994577..760c681a 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/services/NotificationService.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/services/NotificationService.kt @@ -23,6 +23,7 @@ import com.pitchedapps.frost.BuildConfig import com.pitchedapps.frost.R import com.pitchedapps.frost.db.CookieDao import com.pitchedapps.frost.db.CookieEntity +import com.pitchedapps.frost.db.selectAll import com.pitchedapps.frost.utils.L import com.pitchedapps.frost.utils.Prefs import com.pitchedapps.frost.utils.frostEvent diff --git a/app/src/main/kotlin/com/pitchedapps/frost/settings/Notifications.kt b/app/src/main/kotlin/com/pitchedapps/frost/settings/Notifications.kt index dafb259f..c58710b5 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/settings/Notifications.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/settings/Notifications.kt @@ -30,6 +30,7 @@ import com.pitchedapps.frost.BuildConfig import com.pitchedapps.frost.R import com.pitchedapps.frost.activities.SettingsActivity import com.pitchedapps.frost.db.FrostDatabase +import com.pitchedapps.frost.db.deleteAll import com.pitchedapps.frost.services.fetchNotifications import com.pitchedapps.frost.services.scheduleNotifications import com.pitchedapps.frost.utils.Prefs diff --git a/app/src/main/kotlin/com/pitchedapps/frost/web/DebugWebView.kt b/app/src/main/kotlin/com/pitchedapps/frost/web/DebugWebView.kt index 6511ef9f..c66180ed 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/web/DebugWebView.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/web/DebugWebView.kt @@ -58,6 +58,7 @@ class DebugWebView @JvmOverloads constructor( settings.userAgentString = USER_AGENT_MOBILE setLayerType(View.LAYER_TYPE_HARDWARE, null) webViewClient = DebugClient() + @Suppress("DEPRECATION") isDrawingCacheEnabled = true } @@ -72,6 +73,7 @@ class DebugWebView @JvmOverloads constructor( } try { output.outputStream().use { + @Suppress("DEPRECATION") drawingCache.compress(Bitmap.CompressFormat.PNG, 100, it) } L.d { "Created screenshot at ${output.absolutePath}" } -- cgit v1.2.3 From 51e71caf4063702d88ad07b71a2cf9257175f71e Mon Sep 17 00:00:00 2001 From: Allan Wang Date: Sun, 21 Apr 2019 21:07:52 -0400 Subject: Move project to travis.com --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 30191f8f..5d2330d2 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Frost-for-Facebook [![Releaes Version](https://img.shields.io/github/release/AllanWang/Frost-for-Facebook.svg)](https://github.com/AllanWang/Frost-for-Facebook/releases) -[![Build Status](https://travis-ci.org/AllanWang/Frost-for-Facebook.svg?branch=dev)](https://travis-ci.org/AllanWang/Frost-for-Facebook) +[![Build Status](https://travis-ci.com/AllanWang/Frost-for-Facebook.svg?branch=dev)](https://travis-ci.com/AllanWang/Frost-for-Facebook) [![Crowdin](https://d322cqt584bo4o.cloudfront.net/frost-for-facebook/localized.svg)](https://crowdin.com/project/frost-for-facebook) [![ZenHub](https://img.shields.io/badge/Shipping%20faster%20with-ZenHub-45529A.svg)](https://app.zenhub.com/workspace/o/allanwang/frost-for-facebook/boards) [![BugSnag](https://img.shields.io/badge/Bug%20tracking%20with-BugSnag-37C2D9.svg)](https://www.bugsnag.com/) -- cgit v1.2.3 From 2fc18d0803c86a3e6892842136e9342dca31a53f Mon Sep 17 00:00:00 2001 From: Allan Wang Date: Sun, 21 Apr 2019 21:32:29 -0400 Subject: Add initial class --- .../kotlin/com/pitchedapps/frost/widgets/NotificationWidget.kt | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 app/src/main/kotlin/com/pitchedapps/frost/widgets/NotificationWidget.kt diff --git a/app/src/main/kotlin/com/pitchedapps/frost/widgets/NotificationWidget.kt b/app/src/main/kotlin/com/pitchedapps/frost/widgets/NotificationWidget.kt new file mode 100644 index 00000000..308772d0 --- /dev/null +++ b/app/src/main/kotlin/com/pitchedapps/frost/widgets/NotificationWidget.kt @@ -0,0 +1,7 @@ +package com.pitchedapps.frost.widgets + +import android.appwidget.AppWidgetProvider + +class NotificationWidget : AppWidgetProvider() { + +} \ No newline at end of file -- cgit v1.2.3 From 7f371a95320a1d8aae29f8ca15f7fd972367b60e Mon Sep 17 00:00:00 2001 From: Allan Wang Date: Mon, 22 Apr 2019 03:21:30 -0400 Subject: Update parsers and add widget items --- app/src/main/AndroidManifest.xml | 14 +++ .../kotlin/com/pitchedapps/frost/db/Database.kt | 12 +- .../com/pitchedapps/frost/db/NotificationDb.kt | 9 +- .../frost/facebook/parsers/MessageParser.kt | 3 +- .../frost/facebook/parsers/NotifParser.kt | 3 +- .../frost/services/FrostNotifications.kt | 6 +- .../frost/widgets/NotificationWidget.kt | 135 ++++++++++++++++++++- .../main/res/layout/widget_notification_item.xml | 53 ++++++++ app/src/main/res/layout/widget_notifications.xml | 22 ++++ app/src/main/res/values/dimens.xml | 2 + app/src/main/res/xml/notification_widget_info.xml | 4 + .../1.json | 12 +- 12 files changed, 262 insertions(+), 13 deletions(-) create mode 100644 app/src/main/res/layout/widget_notification_item.xml create mode 100644 app/src/main/res/layout/widget_notifications.xml create mode 100644 app/src/main/res/xml/notification_widget_info.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ceb309f3..1ca91d62 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -173,6 +173,20 @@ + + + + + + + + + RoomDatabase.Builder.frostBuild() = if (BuildConfig.DEBUG) { + fallbackToDestructiveMigration().build() + } else { + build() + } + fun create(context: Context): FrostDatabase { val privateDb = Room.databaseBuilder( context, FrostPrivateDatabase::class.java, FrostPrivateDatabase.DATABASE_NAME - ).build() + ).frostBuild() val publicDb = Room.databaseBuilder( context, FrostPublicDatabase::class.java, FrostPublicDatabase.DATABASE_NAME - ).build() + ).frostBuild() return FrostDatabase(privateDb, publicDb) } diff --git a/app/src/main/kotlin/com/pitchedapps/frost/db/NotificationDb.kt b/app/src/main/kotlin/com/pitchedapps/frost/db/NotificationDb.kt index 8936d682..532bb435 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/db/NotificationDb.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/db/NotificationDb.kt @@ -64,7 +64,8 @@ data class NotificationEntity( val timestamp: Long, val profileUrl: String?, // Type essentially refers to channel - val type: String + val type: String, + val unread: Boolean ) { constructor( type: String, @@ -77,7 +78,8 @@ data class NotificationEntity( content.text, content.timestamp, content.profileUrl, - type + type, + content.unread ) } @@ -94,7 +96,8 @@ data class NotificationContentEntity( title = notif.title, text = notif.text, timestamp = notif.timestamp, - profileUrl = notif.profileUrl + profileUrl = notif.profileUrl, + unread = notif.unread ) } diff --git a/app/src/main/kotlin/com/pitchedapps/frost/facebook/parsers/MessageParser.kt b/app/src/main/kotlin/com/pitchedapps/frost/facebook/parsers/MessageParser.kt index 80ed8ee8..9979cd2b 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/facebook/parsers/MessageParser.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/facebook/parsers/MessageParser.kt @@ -64,7 +64,8 @@ data class FrostMessages( title = title, text = content ?: "", timestamp = time, - profileUrl = img + profileUrl = img, + unread = unread ) } }.toList() diff --git a/app/src/main/kotlin/com/pitchedapps/frost/facebook/parsers/NotifParser.kt b/app/src/main/kotlin/com/pitchedapps/frost/facebook/parsers/NotifParser.kt index 199fc685..c22524ad 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/facebook/parsers/NotifParser.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/facebook/parsers/NotifParser.kt @@ -53,7 +53,8 @@ data class FrostNotifs( title = null, text = content, timestamp = time, - profileUrl = img + profileUrl = img, + unread = unread ) } }.toList() diff --git a/app/src/main/kotlin/com/pitchedapps/frost/services/FrostNotifications.kt b/app/src/main/kotlin/com/pitchedapps/frost/services/FrostNotifications.kt index 6f039784..68ed859c 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/services/FrostNotifications.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/services/FrostNotifications.kt @@ -181,7 +181,8 @@ enum class NotificationType( "Debug Notif", "Test 123", System.currentTimeMillis() / 1000, - "https://www.iconexperience.com/_img/v_collection_png/256x256/shadow/dog.png" + "https://www.iconexperience.com/_img/v_collection_png/256x256/shadow/dog.png", + false ) createNotification(context, content).notify(context) } @@ -266,7 +267,8 @@ data class NotificationContent( val title: String? = null, // defaults to frost title val text: String, val timestamp: Long, - val profileUrl: String? + val profileUrl: String?, + val unread: Boolean ) { val notifId = Math.abs(id.toInt()) diff --git a/app/src/main/kotlin/com/pitchedapps/frost/widgets/NotificationWidget.kt b/app/src/main/kotlin/com/pitchedapps/frost/widgets/NotificationWidget.kt index 308772d0..e7a17c8e 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/widgets/NotificationWidget.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/widgets/NotificationWidget.kt @@ -1,7 +1,140 @@ +/* + * Copyright 2019 Allan Wang + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ package com.pitchedapps.frost.widgets +import android.appwidget.AppWidgetManager import android.appwidget.AppWidgetProvider +import android.content.Context +import android.content.Intent +import android.widget.RemoteViews +import android.widget.RemoteViewsService +import androidx.annotation.ColorRes +import ca.allanwang.kau.utils.ContextHelper +import ca.allanwang.kau.utils.withAlpha +import com.bumptech.glide.request.target.AppWidgetTarget +import com.pitchedapps.frost.R +import com.pitchedapps.frost.db.NotificationDao +import com.pitchedapps.frost.db.selectNotifications +import com.pitchedapps.frost.glide.FrostGlide +import com.pitchedapps.frost.glide.GlideApp +import com.pitchedapps.frost.services.NOTIF_CHANNEL_GENERAL +import com.pitchedapps.frost.services.NotificationContent +import com.pitchedapps.frost.utils.Prefs +import com.pitchedapps.frost.widgets.NotificationWidget.Companion.NOTIF_WIDGET_IDS +import com.pitchedapps.frost.widgets.NotificationWidget.Companion.NOTIF_WIDGET_TYPE +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.runBlocking +import org.koin.standalone.KoinComponent +import org.koin.standalone.inject +import kotlin.coroutines.CoroutineContext class NotificationWidget : AppWidgetProvider() { + override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) { + super.onUpdate(context, appWidgetManager, appWidgetIds) + val views = RemoteViews(context.packageName, com.pitchedapps.frost.R.layout.widget_notifications) + val intent = NotificationWidgetService.createIntent(context, NOTIF_CHANNEL_GENERAL, appWidgetIds) + for (id in appWidgetIds) { + views.setRemoteAdapter(R.id.widget_notification_list, intent) + appWidgetManager.updateAppWidget(id, views) + } + } -} \ No newline at end of file + companion object { + const val NOTIF_WIDGET_TYPE = "notif_widget_type" + const val NOTIF_WIDGET_IDS = "notif_widget_ids" + } +} + +class NotificationWidgetService : RemoteViewsService() { + override fun onGetViewFactory(intent: Intent): RemoteViewsFactory = NotificationWidgetDataProvider(this, intent) + + companion object { + fun createIntent(context: Context, type: String, appWidgetIds: IntArray): Intent = + Intent(context, NotificationWidgetService::class.java) + .putExtra(NOTIF_WIDGET_TYPE, type) + .putExtra(NOTIF_WIDGET_IDS, appWidgetIds) + } +} + +class NotificationWidgetDataProvider(val context: Context, val intent: Intent) : RemoteViewsService.RemoteViewsFactory, + CoroutineScope, KoinComponent { + + private val notifDao: NotificationDao by inject() + @Volatile + private var content: List = emptyList() + + private val type = intent.getStringExtra(NOTIF_WIDGET_TYPE) + + private val widgetIds = intent.getIntArrayExtra(NOTIF_WIDGET_IDS) + + private lateinit var job: Job + override val coroutineContext: CoroutineContext + get() = ContextHelper.dispatcher + job + + private suspend fun loadNotifications() { + content = notifDao.selectNotifications(Prefs.userId, type) + } + + override fun onCreate() { + job = SupervisorJob() + runBlocking { + loadNotifications() + } + } + + override fun onDataSetChanged() { + runBlocking { + loadNotifications() + } + } + + override fun getLoadingView(): RemoteViews? = null + + override fun getItemId(position: Int): Long = content[position].id + + override fun hasStableIds(): Boolean = true + + override fun getViewAt(position: Int): RemoteViews { + val views = RemoteViews(context.packageName, R.layout.widget_notification_item) + val glide = GlideApp.with(context).asBitmap() + val notif = content[position] + views.setBackgroundColor(R.id.item_frame, Prefs.nativeBgColor(notif.unread)) + views.setTextColor(R.id.item_content, Prefs.textColor) + views.setTextViewText(R.id.item_content, notif.text) + views.setTextColor(R.id.item_date, Prefs.textColor.withAlpha(150)) + views.setTextViewText(R.id.item_date, notif.timestamp.toString()) // TODO + glide.load(notif.profileUrl).transform(FrostGlide.circleCrop) + .into(AppWidgetTarget(context, R.id.item_avatar, views)) + return views + } + + private fun RemoteViews.setBackgroundColor(viewId: Int, @ColorRes color: Int) { + setInt(viewId, "setBackgroundColor", color) + } + + override fun getCount(): Int = content.size + + override fun getViewTypeCount(): Int { + TODO("not implemented") + } + + override fun onDestroy() { + job.cancel() + } +} diff --git a/app/src/main/res/layout/widget_notification_item.xml b/app/src/main/res/layout/widget_notification_item.xml new file mode 100644 index 00000000..d02e2611 --- /dev/null +++ b/app/src/main/res/layout/widget_notification_item.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/widget_notifications.xml b/app/src/main/res/layout/widget_notifications.xml new file mode 100644 index 00000000..fd68e20a --- /dev/null +++ b/app/src/main/res/layout/widget_notifications.xml @@ -0,0 +1,22 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 713bd1b4..53dad5ef 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -9,4 +9,6 @@ 50dp 64dp 20dp + + 18dp diff --git a/app/src/main/res/xml/notification_widget_info.xml b/app/src/main/res/xml/notification_widget_info.xml new file mode 100644 index 00000000..3cb97ed7 --- /dev/null +++ b/app/src/main/res/xml/notification_widget_info.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/src/schemas/com.pitchedapps.frost.db.FrostPrivateDatabase/1.json b/app/src/schemas/com.pitchedapps.frost.db.FrostPrivateDatabase/1.json index 60d5cddd..a0cc2c2a 100644 --- a/app/src/schemas/com.pitchedapps.frost.db.FrostPrivateDatabase/1.json +++ b/app/src/schemas/com.pitchedapps.frost.db.FrostPrivateDatabase/1.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 1, - "identityHash": "0a9d994786b7e07fea95c11d9210ce0e", + "identityHash": "fe8f5b6c27f48d7e0733ee6819f06f40", "entities": [ { "tableName": "cookies", @@ -38,7 +38,7 @@ }, { "tableName": "notifications", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`notif_id` INTEGER NOT NULL, `userId` INTEGER NOT NULL, `href` TEXT NOT NULL, `title` TEXT, `text` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `profileUrl` TEXT, `type` TEXT NOT NULL, PRIMARY KEY(`notif_id`, `userId`), FOREIGN KEY(`userId`) REFERENCES `cookies`(`cookie_id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`notif_id` INTEGER NOT NULL, `userId` INTEGER NOT NULL, `href` TEXT NOT NULL, `title` TEXT, `text` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `profileUrl` TEXT, `type` TEXT NOT NULL, `unread` INTEGER NOT NULL, PRIMARY KEY(`notif_id`, `userId`), FOREIGN KEY(`userId`) REFERENCES `cookies`(`cookie_id`) ON UPDATE NO ACTION ON DELETE CASCADE )", "fields": [ { "fieldPath": "id", @@ -87,6 +87,12 @@ "columnName": "type", "affinity": "TEXT", "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true } ], "primaryKey": { @@ -183,7 +189,7 @@ "views": [], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"0a9d994786b7e07fea95c11d9210ce0e\")" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"fe8f5b6c27f48d7e0733ee6819f06f40\")" ] } } \ No newline at end of file -- cgit v1.2.3 From ba0b4e70d93f91497a7bc157fe7f3838942dfb15 Mon Sep 17 00:00:00 2001 From: Allan Wang Date: Mon, 22 Apr 2019 04:42:52 -0400 Subject: Fix icon loading --- .../com/pitchedapps/frost/db/NotificationDb.kt | 5 +- .../frost/widgets/NotificationWidget.kt | 60 +++++++++------------- app/src/main/res/drawable/ic_refresh_24dp.xml | 5 ++ app/src/main/res/layout/widget_notifications.xml | 20 +++++--- app/src/main/res/values/dimens.xml | 2 +- app/src/main/res/xml/notification_widget_info.xml | 8 +-- 6 files changed, 54 insertions(+), 46 deletions(-) create mode 100644 app/src/main/res/drawable/ic_refresh_24dp.xml diff --git a/app/src/main/kotlin/com/pitchedapps/frost/db/NotificationDb.kt b/app/src/main/kotlin/com/pitchedapps/frost/db/NotificationDb.kt index 532bb435..d4b51c1e 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/db/NotificationDb.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/db/NotificationDb.kt @@ -137,8 +137,11 @@ interface NotificationDao { suspend fun NotificationDao.deleteAll() = dao { _deleteAll() } -suspend fun NotificationDao.selectNotifications(userId: Long, type: String): List = dao { +fun NotificationDao.selectNotificationsSync(userId: Long, type: String): List = _selectNotifications(userId, type).map { it.toNotifContent() } + +suspend fun NotificationDao.selectNotifications(userId: Long, type: String): List = dao { + selectNotificationsSync(userId, type) } /** diff --git a/app/src/main/kotlin/com/pitchedapps/frost/widgets/NotificationWidget.kt b/app/src/main/kotlin/com/pitchedapps/frost/widgets/NotificationWidget.kt index e7a17c8e..6ba0a3d6 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/widgets/NotificationWidget.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/widgets/NotificationWidget.kt @@ -22,37 +22,34 @@ import android.content.Context import android.content.Intent import android.widget.RemoteViews import android.widget.RemoteViewsService -import androidx.annotation.ColorRes -import ca.allanwang.kau.utils.ContextHelper +import androidx.annotation.ColorInt +import ca.allanwang.kau.utils.dimenPixelSize import ca.allanwang.kau.utils.withAlpha -import com.bumptech.glide.request.target.AppWidgetTarget import com.pitchedapps.frost.R import com.pitchedapps.frost.db.NotificationDao -import com.pitchedapps.frost.db.selectNotifications +import com.pitchedapps.frost.db.selectNotificationsSync import com.pitchedapps.frost.glide.FrostGlide import com.pitchedapps.frost.glide.GlideApp import com.pitchedapps.frost.services.NOTIF_CHANNEL_GENERAL import com.pitchedapps.frost.services.NotificationContent +import com.pitchedapps.frost.utils.L import com.pitchedapps.frost.utils.Prefs import com.pitchedapps.frost.widgets.NotificationWidget.Companion.NOTIF_WIDGET_IDS import com.pitchedapps.frost.widgets.NotificationWidget.Companion.NOTIF_WIDGET_TYPE -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Job -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.runBlocking import org.koin.standalone.KoinComponent import org.koin.standalone.inject -import kotlin.coroutines.CoroutineContext class NotificationWidget : AppWidgetProvider() { override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) { super.onUpdate(context, appWidgetManager, appWidgetIds) - val views = RemoteViews(context.packageName, com.pitchedapps.frost.R.layout.widget_notifications) val intent = NotificationWidgetService.createIntent(context, NOTIF_CHANNEL_GENERAL, appWidgetIds) for (id in appWidgetIds) { + val views = RemoteViews(context.packageName, R.layout.widget_notifications) + views.setBackgroundColor(R.id.widget_layout_container, Prefs.bgColor) views.setRemoteAdapter(R.id.widget_notification_list, intent) appWidgetManager.updateAppWidget(id, views) } + appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetIds, R.id.widget_notification_list) } companion object { @@ -61,6 +58,10 @@ class NotificationWidget : AppWidgetProvider() { } } +private fun RemoteViews.setBackgroundColor(viewId: Int, @ColorInt color: Int) { + setInt(viewId, "setBackgroundColor", color) +} + class NotificationWidgetService : RemoteViewsService() { override fun onGetViewFactory(intent: Intent): RemoteViewsFactory = NotificationWidgetDataProvider(this, intent) @@ -73,7 +74,7 @@ class NotificationWidgetService : RemoteViewsService() { } class NotificationWidgetDataProvider(val context: Context, val intent: Intent) : RemoteViewsService.RemoteViewsFactory, - CoroutineScope, KoinComponent { + KoinComponent { private val notifDao: NotificationDao by inject() @Volatile @@ -83,25 +84,20 @@ class NotificationWidgetDataProvider(val context: Context, val intent: Intent) : private val widgetIds = intent.getIntArrayExtra(NOTIF_WIDGET_IDS) - private lateinit var job: Job - override val coroutineContext: CoroutineContext - get() = ContextHelper.dispatcher + job + private val avatarSize = context.dimenPixelSize(R.dimen.avatar_image_size) + + private val glide = GlideApp.with(context).asBitmap() - private suspend fun loadNotifications() { - content = notifDao.selectNotifications(Prefs.userId, type) + private fun loadNotifications() { + content = notifDao.selectNotificationsSync(Prefs.userId, type) + L._d { "Updated notif widget with ${content.size} items" } } override fun onCreate() { - job = SupervisorJob() - runBlocking { - loadNotifications() - } } override fun onDataSetChanged() { - runBlocking { - loadNotifications() - } + loadNotifications() } override fun getLoadingView(): RemoteViews? = null @@ -112,29 +108,23 @@ class NotificationWidgetDataProvider(val context: Context, val intent: Intent) : override fun getViewAt(position: Int): RemoteViews { val views = RemoteViews(context.packageName, R.layout.widget_notification_item) - val glide = GlideApp.with(context).asBitmap() val notif = content[position] + L._d { "View $position $notif" } views.setBackgroundColor(R.id.item_frame, Prefs.nativeBgColor(notif.unread)) views.setTextColor(R.id.item_content, Prefs.textColor) views.setTextViewText(R.id.item_content, notif.text) views.setTextColor(R.id.item_date, Prefs.textColor.withAlpha(150)) views.setTextViewText(R.id.item_date, notif.timestamp.toString()) // TODO - glide.load(notif.profileUrl).transform(FrostGlide.circleCrop) - .into(AppWidgetTarget(context, R.id.item_avatar, views)) +// views.setOnClickPendingIntent() + val avatar = glide.load(notif.profileUrl).transform(FrostGlide.circleCrop).submit(avatarSize, avatarSize).get() + views.setImageViewBitmap(R.id.item_avatar, avatar) return views } - private fun RemoteViews.setBackgroundColor(viewId: Int, @ColorRes color: Int) { - setInt(viewId, "setBackgroundColor", color) - } - override fun getCount(): Int = content.size - override fun getViewTypeCount(): Int { - TODO("not implemented") - } + override fun getViewTypeCount(): Int = 1 override fun onDestroy() { - job.cancel() } -} +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_refresh_24dp.xml b/app/src/main/res/drawable/ic_refresh_24dp.xml new file mode 100644 index 00000000..cc2d1e04 --- /dev/null +++ b/app/src/main/res/drawable/ic_refresh_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/layout/widget_notifications.xml b/app/src/main/res/layout/widget_notifications.xml index fd68e20a..098bb9eb 100644 --- a/app/src/main/res/layout/widget_notifications.xml +++ b/app/src/main/res/layout/widget_notifications.xml @@ -1,22 +1,30 @@ - + + + + android:layout_gravity="center_vertical" + android:src="@drawable/ic_refresh_24dp" /> + - + android:layout_height="0dp" + android:layout_weight="1" /> \ No newline at end of file diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 53dad5ef..847e74cb 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -10,5 +10,5 @@ 64dp 20dp - 18dp + 24dp diff --git a/app/src/main/res/xml/notification_widget_info.xml b/app/src/main/res/xml/notification_widget_info.xml index 3cb97ed7..2b8b4fb5 100644 --- a/app/src/main/res/xml/notification_widget_info.xml +++ b/app/src/main/res/xml/notification_widget_info.xml @@ -1,4 +1,6 @@ - - - \ No newline at end of file + -- cgit v1.2.3 From 9df2a878226fb5f7adcf1af61a1ce87dff826fbc Mon Sep 17 00:00:00 2001 From: Allan Wang Date: Mon, 22 Apr 2019 04:44:19 -0400 Subject: Remove unnecessary id --- .../com/pitchedapps/frost/widgets/NotificationWidget.kt | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/app/src/main/kotlin/com/pitchedapps/frost/widgets/NotificationWidget.kt b/app/src/main/kotlin/com/pitchedapps/frost/widgets/NotificationWidget.kt index 6ba0a3d6..4ddd7225 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/widgets/NotificationWidget.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/widgets/NotificationWidget.kt @@ -34,15 +34,13 @@ import com.pitchedapps.frost.services.NOTIF_CHANNEL_GENERAL import com.pitchedapps.frost.services.NotificationContent import com.pitchedapps.frost.utils.L import com.pitchedapps.frost.utils.Prefs -import com.pitchedapps.frost.widgets.NotificationWidget.Companion.NOTIF_WIDGET_IDS -import com.pitchedapps.frost.widgets.NotificationWidget.Companion.NOTIF_WIDGET_TYPE import org.koin.standalone.KoinComponent import org.koin.standalone.inject class NotificationWidget : AppWidgetProvider() { override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) { super.onUpdate(context, appWidgetManager, appWidgetIds) - val intent = NotificationWidgetService.createIntent(context, NOTIF_CHANNEL_GENERAL, appWidgetIds) + val intent = NotificationWidgetService.createIntent(context, NOTIF_CHANNEL_GENERAL) for (id in appWidgetIds) { val views = RemoteViews(context.packageName, R.layout.widget_notifications) views.setBackgroundColor(R.id.widget_layout_container, Prefs.bgColor) @@ -51,13 +49,10 @@ class NotificationWidget : AppWidgetProvider() { } appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetIds, R.id.widget_notification_list) } - - companion object { - const val NOTIF_WIDGET_TYPE = "notif_widget_type" - const val NOTIF_WIDGET_IDS = "notif_widget_ids" - } } +private const val NOTIF_WIDGET_TYPE = "notif_widget_type" + private fun RemoteViews.setBackgroundColor(viewId: Int, @ColorInt color: Int) { setInt(viewId, "setBackgroundColor", color) } @@ -66,10 +61,9 @@ class NotificationWidgetService : RemoteViewsService() { override fun onGetViewFactory(intent: Intent): RemoteViewsFactory = NotificationWidgetDataProvider(this, intent) companion object { - fun createIntent(context: Context, type: String, appWidgetIds: IntArray): Intent = + fun createIntent(context: Context, type: String): Intent = Intent(context, NotificationWidgetService::class.java) .putExtra(NOTIF_WIDGET_TYPE, type) - .putExtra(NOTIF_WIDGET_IDS, appWidgetIds) } } @@ -82,8 +76,6 @@ class NotificationWidgetDataProvider(val context: Context, val intent: Intent) : private val type = intent.getStringExtra(NOTIF_WIDGET_TYPE) - private val widgetIds = intent.getIntArrayExtra(NOTIF_WIDGET_IDS) - private val avatarSize = context.dimenPixelSize(R.dimen.avatar_image_size) private val glide = GlideApp.with(context).asBitmap() -- cgit v1.2.3 From 7e227078373adf0ef8c243b7e1e05804524cd3ea Mon Sep 17 00:00:00 2001 From: Allan Wang Date: Wed, 24 Apr 2019 19:12:25 -0700 Subject: Add click events to notifications --- .../com/pitchedapps/frost/db/NotificationDbTest.kt | 3 +- .../frost/services/FrostNotifications.kt | 36 +++++++--- .../com/pitchedapps/frost/utils/TimeUtils.kt | 2 + .../kotlin/com/pitchedapps/frost/utils/Utils.kt | 2 +- .../frost/widgets/NotificationWidget.kt | 81 +++++++++++++++++++--- .../main/res/layout/widget_notification_item.xml | 15 ++-- app/src/main/res/layout/widget_notifications.xml | 9 +-- app/src/main/res/values/strings.xml | 10 +++ 8 files changed, 122 insertions(+), 36 deletions(-) create mode 100644 app/src/main/kotlin/com/pitchedapps/frost/utils/TimeUtils.kt diff --git a/app/src/androidTest/kotlin/com/pitchedapps/frost/db/NotificationDbTest.kt b/app/src/androidTest/kotlin/com/pitchedapps/frost/db/NotificationDbTest.kt index 9167c7bf..45a09cbe 100644 --- a/app/src/androidTest/kotlin/com/pitchedapps/frost/db/NotificationDbTest.kt +++ b/app/src/androidTest/kotlin/com/pitchedapps/frost/db/NotificationDbTest.kt @@ -38,7 +38,8 @@ class NotificationDbTest : BaseDbTest() { title = null, text = "", timestamp = time, - profileUrl = null + profileUrl = null, + unread = true ) @Test diff --git a/app/src/main/kotlin/com/pitchedapps/frost/services/FrostNotifications.kt b/app/src/main/kotlin/com/pitchedapps/frost/services/FrostNotifications.kt index 68ed859c..1c37bc29 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/services/FrostNotifications.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/services/FrostNotifications.kt @@ -61,7 +61,7 @@ private val _40_DP = 40.dpToPx * Enum to handle notification creations */ enum class NotificationType( - private val channelId: String, + val channelId: String, private val overlayContext: OverlayContext, private val fbItem: FbItem, private val parser: FrostParser, @@ -95,8 +95,8 @@ enum class NotificationType( */ internal open fun bindRequest(content: NotificationContent, cookie: String): (BaseBundle.() -> Unit)? = null - private fun bindRequest(intent: Intent, content: NotificationContent, cookie: String?) { - cookie ?: return + private fun bindRequest(intent: Intent, content: NotificationContent) { + val cookie = content.data.cookie ?: return val binder = bindRequest(content, cookie) ?: return val bundle = Bundle() bundle.binder() @@ -187,18 +187,34 @@ enum class NotificationType( createNotification(context, content).notify(context) } + /** + * Attach content related data to an intent + */ + fun putContentExtra(intent: Intent, content: NotificationContent): Intent { + // We will show the notification page for dependent urls. We can trigger a click next time + intent.data = Uri.parse(if (content.href.isIndependent) content.href else FbItem.NOTIFICATIONS.url) + bindRequest(intent, content) + return intent + } + + /** + * Create a generic content for the provided type and user id. + * No content related data is added + */ + fun createCommonIntent(context: Context, userId: Long): Intent { + val intent = Intent(context, FrostWebActivity::class.java) + intent.putExtra(ARG_USER_ID, userId) + overlayContext.put(intent) + return intent + } + /** * Create and submit a new notification with the given [content] */ private fun createNotification(context: Context, content: NotificationContent): FrostNotification = with(content) { - val intent = Intent(context, FrostWebActivity::class.java) - // TODO temp fix; we will show notification page for dependent urls. We can trigger a click next time - intent.data = Uri.parse(if (href.isIndependent) href else FbItem.NOTIFICATIONS.url) - intent.putExtra(ARG_USER_ID, data.id) - overlayContext.put(intent) - bindRequest(intent, content, data.cookie) - + val intent = createCommonIntent(context, content.data.id) + putContentExtra(intent, content) val group = "${groupPrefix}_${data.id}" val pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT) val notifBuilder = context.frostNotification(channelId) diff --git a/app/src/main/kotlin/com/pitchedapps/frost/utils/TimeUtils.kt b/app/src/main/kotlin/com/pitchedapps/frost/utils/TimeUtils.kt new file mode 100644 index 00000000..1ca6333b --- /dev/null +++ b/app/src/main/kotlin/com/pitchedapps/frost/utils/TimeUtils.kt @@ -0,0 +1,2 @@ +package com.pitchedapps.frost.utils + diff --git a/app/src/main/kotlin/com/pitchedapps/frost/utils/Utils.kt b/app/src/main/kotlin/com/pitchedapps/frost/utils/Utils.kt index 711d7e18..76ffd8cd 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/utils/Utils.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/utils/Utils.kt @@ -186,7 +186,7 @@ fun MaterialDialog.Builder.theme(): MaterialDialog.Builder { } fun Activity.setFrostTheme(forceTransparent: Boolean = false) { - val isTransparent = (Color.alpha(Prefs.bgColor) != 255) || forceTransparent + val isTransparent = (Color.alpha(Prefs.bgColor) != 255) || (Color.alpha(Prefs.headerColor) != 255) || forceTransparent if (Prefs.bgColor.isColorDark) setTheme(if (isTransparent) R.style.FrostTheme_Transparent else R.style.FrostTheme) else diff --git a/app/src/main/kotlin/com/pitchedapps/frost/widgets/NotificationWidget.kt b/app/src/main/kotlin/com/pitchedapps/frost/widgets/NotificationWidget.kt index 4ddd7225..ae45fa7b 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/widgets/NotificationWidget.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/widgets/NotificationWidget.kt @@ -16,35 +16,69 @@ */ package com.pitchedapps.frost.widgets +import android.app.PendingIntent import android.appwidget.AppWidgetManager import android.appwidget.AppWidgetProvider import android.content.Context import android.content.Intent +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.PorterDuff +import android.graphics.PorterDuffColorFilter +import android.graphics.drawable.Icon +import android.os.Build import android.widget.RemoteViews import android.widget.RemoteViewsService import androidx.annotation.ColorInt +import androidx.annotation.DrawableRes +import androidx.annotation.IdRes import ca.allanwang.kau.utils.dimenPixelSize +import ca.allanwang.kau.utils.string import ca.allanwang.kau.utils.withAlpha import com.pitchedapps.frost.R +import com.pitchedapps.frost.activities.MainActivity import com.pitchedapps.frost.db.NotificationDao import com.pitchedapps.frost.db.selectNotificationsSync import com.pitchedapps.frost.glide.FrostGlide import com.pitchedapps.frost.glide.GlideApp -import com.pitchedapps.frost.services.NOTIF_CHANNEL_GENERAL import com.pitchedapps.frost.services.NotificationContent +import com.pitchedapps.frost.services.NotificationType import com.pitchedapps.frost.utils.L import com.pitchedapps.frost.utils.Prefs import org.koin.standalone.KoinComponent import org.koin.standalone.inject +import java.text.DateFormat class NotificationWidget : AppWidgetProvider() { override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) { super.onUpdate(context, appWidgetManager, appWidgetIds) - val intent = NotificationWidgetService.createIntent(context, NOTIF_CHANNEL_GENERAL) + val type = NotificationType.GENERAL + val userId = Prefs.userId + val intent = NotificationWidgetService.createIntent(context, type, userId) for (id in appWidgetIds) { val views = RemoteViews(context.packageName, R.layout.widget_notifications) - views.setBackgroundColor(R.id.widget_layout_container, Prefs.bgColor) + + views.setBackgroundColor(R.id.widget_layout_toolbar, Prefs.headerColor) + views.setIcon(R.id.img_frost, context, R.drawable.frost_f_24, Prefs.iconColor) + views.setOnClickPendingIntent( + R.id.img_frost, + PendingIntent.getActivity(context, 0, Intent(context, MainActivity::class.java), 0) + ) + + views.setBackgroundColor(R.id.widget_notification_list, Prefs.bgColor) views.setRemoteAdapter(R.id.widget_notification_list, intent) + + val pendingIntentTemplate = PendingIntent.getActivity( + context, + 0, + type.createCommonIntent(context, userId), + PendingIntent.FLAG_UPDATE_CURRENT + ) + + views.setPendingIntentTemplate(R.id.widget_notification_list, pendingIntentTemplate) + appWidgetManager.updateAppWidget(id, views) } appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetIds, R.id.widget_notification_list) @@ -52,18 +86,44 @@ class NotificationWidget : AppWidgetProvider() { } private const val NOTIF_WIDGET_TYPE = "notif_widget_type" +private const val NOTIF_WIDGET_USER_ID = "notif_widget_user_id" +private const val NOTIF_WIDGET_USER_COOKIE = "notif_widget_user_id" -private fun RemoteViews.setBackgroundColor(viewId: Int, @ColorInt color: Int) { +private fun RemoteViews.setBackgroundColor(@IdRes viewId: Int, @ColorInt color: Int) { setInt(viewId, "setBackgroundColor", color) } +/** + * Adds backward compatibility to setting tinted icons + */ +private fun RemoteViews.setIcon(@IdRes viewId: Int, context: Context, @DrawableRes res: Int, @ColorInt color: Int) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val icon = Icon.createWithResource(context, res).setTint(color).setTintMode(PorterDuff.Mode.SRC_IN) + setImageViewIcon(viewId, icon) + } else { + val bitmap = BitmapFactory.decodeResource(context.resources, res) + if (bitmap != null) { + val paint = Paint() + paint.colorFilter = PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN) + val result = Bitmap.createBitmap(bitmap.width, bitmap.height, Bitmap.Config.ARGB_8888) + val canvas = Canvas(result) + canvas.drawBitmap(bitmap, 0f, 0f, paint) + setImageViewBitmap(viewId, result) + } else { + // Fallback to just icon + setImageViewResource(viewId, res) + } + } +} + class NotificationWidgetService : RemoteViewsService() { override fun onGetViewFactory(intent: Intent): RemoteViewsFactory = NotificationWidgetDataProvider(this, intent) companion object { - fun createIntent(context: Context, type: String): Intent = + fun createIntent(context: Context, type: NotificationType, userId: Long): Intent = Intent(context, NotificationWidgetService::class.java) - .putExtra(NOTIF_WIDGET_TYPE, type) + .putExtra(NOTIF_WIDGET_TYPE, type.name) + .putExtra(NOTIF_WIDGET_USER_ID, userId) } } @@ -74,14 +134,16 @@ class NotificationWidgetDataProvider(val context: Context, val intent: Intent) : @Volatile private var content: List = emptyList() - private val type = intent.getStringExtra(NOTIF_WIDGET_TYPE) + private val type = NotificationType.valueOf(intent.getStringExtra(NOTIF_WIDGET_TYPE)) + + private val userId = intent.getLongExtra(NOTIF_WIDGET_USER_ID, -1) private val avatarSize = context.dimenPixelSize(R.dimen.avatar_image_size) private val glide = GlideApp.with(context).asBitmap() private fun loadNotifications() { - content = notifDao.selectNotificationsSync(Prefs.userId, type) + content = notifDao.selectNotificationsSync(userId, type.channelId) L._d { "Updated notif widget with ${content.size} items" } } @@ -107,9 +169,10 @@ class NotificationWidgetDataProvider(val context: Context, val intent: Intent) : views.setTextViewText(R.id.item_content, notif.text) views.setTextColor(R.id.item_date, Prefs.textColor.withAlpha(150)) views.setTextViewText(R.id.item_date, notif.timestamp.toString()) // TODO -// views.setOnClickPendingIntent() + val avatar = glide.load(notif.profileUrl).transform(FrostGlide.circleCrop).submit(avatarSize, avatarSize).get() views.setImageViewBitmap(R.id.item_avatar, avatar) + views.setOnClickFillInIntent(R.id.item_frame, type.putContentExtra(Intent(), notif)) return views } diff --git a/app/src/main/res/layout/widget_notification_item.xml b/app/src/main/res/layout/widget_notification_item.xml index d02e2611..f36f2766 100644 --- a/app/src/main/res/layout/widget_notification_item.xml +++ b/app/src/main/res/layout/widget_notification_item.xml @@ -1,22 +1,19 @@ + android:layout_height="@dimen/avatar_image_size" /> + %s at %s + -- cgit v1.2.3 From 27d1b98330c25e8833279ed48a4d9bd7d7566d10 Mon Sep 17 00:00:00 2001 From: Allan Wang Date: Wed, 24 Apr 2019 19:38:54 -0700 Subject: Create readable time converter --- .../com/pitchedapps/frost/utils/TimeUtils.kt | 48 ++++++++++++++++++++++ .../frost/widgets/NotificationWidget.kt | 7 ++-- app/src/main/res/values/strings.xml | 4 +- 3 files changed, 54 insertions(+), 5 deletions(-) diff --git a/app/src/main/kotlin/com/pitchedapps/frost/utils/TimeUtils.kt b/app/src/main/kotlin/com/pitchedapps/frost/utils/TimeUtils.kt index 1ca6333b..c0feab1e 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/utils/TimeUtils.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/utils/TimeUtils.kt @@ -1,2 +1,50 @@ +/* + * Copyright 2019 Allan Wang + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ package com.pitchedapps.frost.utils +import android.content.Context +import ca.allanwang.kau.utils.string +import com.pitchedapps.frost.R +import java.text.DateFormat +import java.text.SimpleDateFormat +import java.util.Calendar +import java.util.Date +import java.util.Locale + +/** + * Converts time in millis to readable date, + * eg Apr 24 at 7:32 PM + * + * With regards to date modifications in calendars, + * it appears to respect calendar rules; + * see https://stackoverflow.com/a/43227817/4407321 + */ +fun Long.toReadableTime(context: Context): String { + val cal = Calendar.getInstance() + cal.timeInMillis = this + val timeFormatter = SimpleDateFormat.getTimeInstance(DateFormat.SHORT) + val time = timeFormatter.format(Date(this)) + val day = when { + cal >= Calendar.getInstance().apply { add(Calendar.DAY_OF_MONTH, -1) } -> context.string(R.string.today) + cal >= Calendar.getInstance().apply { add(Calendar.DAY_OF_MONTH, -2) } -> context.string(R.string.yesterday) + else -> { + val dayFormatter = SimpleDateFormat("MMM dd", Locale.getDefault()) + dayFormatter.format(Date(this)) + } + } + return context.getString(R.string.time_template, day, time) +} diff --git a/app/src/main/kotlin/com/pitchedapps/frost/widgets/NotificationWidget.kt b/app/src/main/kotlin/com/pitchedapps/frost/widgets/NotificationWidget.kt index ae45fa7b..1897b042 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/widgets/NotificationWidget.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/widgets/NotificationWidget.kt @@ -35,7 +35,6 @@ import androidx.annotation.ColorInt import androidx.annotation.DrawableRes import androidx.annotation.IdRes import ca.allanwang.kau.utils.dimenPixelSize -import ca.allanwang.kau.utils.string import ca.allanwang.kau.utils.withAlpha import com.pitchedapps.frost.R import com.pitchedapps.frost.activities.MainActivity @@ -47,9 +46,9 @@ import com.pitchedapps.frost.services.NotificationContent import com.pitchedapps.frost.services.NotificationType import com.pitchedapps.frost.utils.L import com.pitchedapps.frost.utils.Prefs +import com.pitchedapps.frost.utils.toReadableTime import org.koin.standalone.KoinComponent import org.koin.standalone.inject -import java.text.DateFormat class NotificationWidget : AppWidgetProvider() { override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) { @@ -168,7 +167,7 @@ class NotificationWidgetDataProvider(val context: Context, val intent: Intent) : views.setTextColor(R.id.item_content, Prefs.textColor) views.setTextViewText(R.id.item_content, notif.text) views.setTextColor(R.id.item_date, Prefs.textColor.withAlpha(150)) - views.setTextViewText(R.id.item_date, notif.timestamp.toString()) // TODO + views.setTextViewText(R.id.item_date, notif.timestamp.toReadableTime(context)) val avatar = glide.load(notif.profileUrl).transform(FrostGlide.circleCrop).submit(avatarSize, avatarSize).get() views.setImageViewBitmap(R.id.item_avatar, avatar) @@ -182,4 +181,4 @@ class NotificationWidgetDataProvider(val context: Context, val intent: Intent) : override fun onDestroy() { } -} \ No newline at end of file +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2b6a2ba6..0c2e625c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -69,7 +69,9 @@ For instance: Today at 1:23 PM Mar 13 at 9:00 AM + + The first element is the day, and the second element is the time --> - %s at %s + %1s at %2s -- cgit v1.2.3 From 14578e26fcd39cf671aa4584d3abf311a66afba2 Mon Sep 17 00:00:00 2001 From: Allan Wang Date: Wed, 24 Apr 2019 19:51:37 -0700 Subject: Apply updates from services --- .../com/pitchedapps/frost/activities/BaseMainActivity.kt | 7 ++++++- .../com/pitchedapps/frost/services/NotificationService.kt | 4 ++++ .../com/pitchedapps/frost/widgets/NotificationWidget.kt | 14 ++++++++++++++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseMainActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseMainActivity.kt index c43c31a2..d6f1abe7 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseMainActivity.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseMainActivity.kt @@ -104,6 +104,7 @@ import com.pitchedapps.frost.utils.setFrostColors import com.pitchedapps.frost.views.BadgedIcon import com.pitchedapps.frost.views.FrostVideoViewer import com.pitchedapps.frost.views.FrostViewPager +import com.pitchedapps.frost.widgets.NotificationWidget import kotlinx.android.synthetic.main.activity_frame_wrapper.* import kotlinx.android.synthetic.main.view_main_fab.* import kotlinx.android.synthetic.main.view_main_toolbar.* @@ -450,7 +451,11 @@ abstract class BaseMainActivity : BaseActivity(), MainActivityContract, Runtime.getRuntime().exit(0) return } - if (resultCode and REQUEST_RESTART > 0) return restart() + if (resultCode and REQUEST_RESTART > 0) { + NotificationWidget.forceUpdate(this) + restart() + return + } /* * These results can be stacked */ diff --git a/app/src/main/kotlin/com/pitchedapps/frost/services/NotificationService.kt b/app/src/main/kotlin/com/pitchedapps/frost/services/NotificationService.kt index 760c681a..0eee5558 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/services/NotificationService.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/services/NotificationService.kt @@ -27,6 +27,7 @@ import com.pitchedapps.frost.db.selectAll import com.pitchedapps.frost.utils.L import com.pitchedapps.frost.utils.Prefs import com.pitchedapps.frost.utils.frostEvent +import com.pitchedapps.frost.widgets.NotificationWidget import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.isActive import kotlinx.coroutines.launch @@ -105,6 +106,9 @@ class NotificationService : BaseJobService() { L.i { "Sent $notifCount notifications" } if (notifCount == 0 && jobId == NOTIFICATION_JOB_NOW) generalNotification(665, R.string.no_new_notifications, BuildConfig.DEBUG) + if (notifCount > 0) { + NotificationWidget.forceUpdate(this@NotificationService) + } } /** diff --git a/app/src/main/kotlin/com/pitchedapps/frost/widgets/NotificationWidget.kt b/app/src/main/kotlin/com/pitchedapps/frost/widgets/NotificationWidget.kt index 1897b042..57cc4fe1 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/widgets/NotificationWidget.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/widgets/NotificationWidget.kt @@ -19,6 +19,7 @@ package com.pitchedapps.frost.widgets import android.app.PendingIntent import android.appwidget.AppWidgetManager import android.appwidget.AppWidgetProvider +import android.content.ComponentName import android.content.Context import android.content.Intent import android.graphics.Bitmap @@ -51,6 +52,7 @@ import org.koin.standalone.KoinComponent import org.koin.standalone.inject class NotificationWidget : AppWidgetProvider() { + override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) { super.onUpdate(context, appWidgetManager, appWidgetIds) val type = NotificationType.GENERAL @@ -82,6 +84,18 @@ class NotificationWidget : AppWidgetProvider() { } appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetIds, R.id.widget_notification_list) } + + companion object { + fun forceUpdate(context: Context) { + val manager = AppWidgetManager.getInstance(context) + val ids = manager.getAppWidgetIds(ComponentName(context, NotificationWidget::class.java)) + val intent = Intent().apply { + action = AppWidgetManager.ACTION_APPWIDGET_UPDATE + putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids) + } + context.sendBroadcast(intent) + } + } } private const val NOTIF_WIDGET_TYPE = "notif_widget_type" -- cgit v1.2.3 From e627bc4f1fbf50216f74c53c6a4812beb6d2d055 Mon Sep 17 00:00:00 2001 From: Allan Wang Date: Wed, 24 Apr 2019 23:54:27 -0700 Subject: Remove unnecessary components --- .../frost/widgets/NotificationWidget.kt | 4 -- app/src/main/res/drawable/ic_refresh_24dp.xml | 5 --- .../res/drawable/notification_widget_preview.xml | 49 ++++++++++++++++++++++ app/src/main/res/xml/notification_widget_info.xml | 10 +++-- 4 files changed, 56 insertions(+), 12 deletions(-) delete mode 100644 app/src/main/res/drawable/ic_refresh_24dp.xml create mode 100644 app/src/main/res/drawable/notification_widget_preview.xml diff --git a/app/src/main/kotlin/com/pitchedapps/frost/widgets/NotificationWidget.kt b/app/src/main/kotlin/com/pitchedapps/frost/widgets/NotificationWidget.kt index 57cc4fe1..594da00a 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/widgets/NotificationWidget.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/widgets/NotificationWidget.kt @@ -45,7 +45,6 @@ import com.pitchedapps.frost.glide.FrostGlide import com.pitchedapps.frost.glide.GlideApp import com.pitchedapps.frost.services.NotificationContent import com.pitchedapps.frost.services.NotificationType -import com.pitchedapps.frost.utils.L import com.pitchedapps.frost.utils.Prefs import com.pitchedapps.frost.utils.toReadableTime import org.koin.standalone.KoinComponent @@ -100,7 +99,6 @@ class NotificationWidget : AppWidgetProvider() { private const val NOTIF_WIDGET_TYPE = "notif_widget_type" private const val NOTIF_WIDGET_USER_ID = "notif_widget_user_id" -private const val NOTIF_WIDGET_USER_COOKIE = "notif_widget_user_id" private fun RemoteViews.setBackgroundColor(@IdRes viewId: Int, @ColorInt color: Int) { setInt(viewId, "setBackgroundColor", color) @@ -157,7 +155,6 @@ class NotificationWidgetDataProvider(val context: Context, val intent: Intent) : private fun loadNotifications() { content = notifDao.selectNotificationsSync(userId, type.channelId) - L._d { "Updated notif widget with ${content.size} items" } } override fun onCreate() { @@ -176,7 +173,6 @@ class NotificationWidgetDataProvider(val context: Context, val intent: Intent) : override fun getViewAt(position: Int): RemoteViews { val views = RemoteViews(context.packageName, R.layout.widget_notification_item) val notif = content[position] - L._d { "View $position $notif" } views.setBackgroundColor(R.id.item_frame, Prefs.nativeBgColor(notif.unread)) views.setTextColor(R.id.item_content, Prefs.textColor) views.setTextViewText(R.id.item_content, notif.text) diff --git a/app/src/main/res/drawable/ic_refresh_24dp.xml b/app/src/main/res/drawable/ic_refresh_24dp.xml deleted file mode 100644 index cc2d1e04..00000000 --- a/app/src/main/res/drawable/ic_refresh_24dp.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/notification_widget_preview.xml b/app/src/main/res/drawable/notification_widget_preview.xml new file mode 100644 index 00000000..a4ead7d0 --- /dev/null +++ b/app/src/main/res/drawable/notification_widget_preview.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/notification_widget_info.xml b/app/src/main/res/xml/notification_widget_info.xml index 2b8b4fb5..c14bbfb2 100644 --- a/app/src/main/res/xml/notification_widget_info.xml +++ b/app/src/main/res/xml/notification_widget_info.xml @@ -1,6 +1,10 @@ - + + android:minWidth="180dp" + android:minHeight="250dp" + android:previewImage="@drawable/notification_widget_preview" /> -- cgit v1.2.3 From 2fb01ffb24b0a80560bb7bbc89d2fa6f6bdb65fb Mon Sep 17 00:00:00 2001 From: Allan Wang Date: Thu, 25 Apr 2019 13:05:02 -0700 Subject: Add widget preview --- .../res/drawable/notification_widget_preview.xml | 97 +++++++++++----------- 1 file changed, 48 insertions(+), 49 deletions(-) diff --git a/app/src/main/res/drawable/notification_widget_preview.xml b/app/src/main/res/drawable/notification_widget_preview.xml index a4ead7d0..a03fd362 100644 --- a/app/src/main/res/drawable/notification_widget_preview.xml +++ b/app/src/main/res/drawable/notification_widget_preview.xml @@ -1,49 +1,48 @@ - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file + + + + + + + + + + + + + + + + -- cgit v1.2.3 From cf152584b544425e0ee3ae28d765dc7e55a196af Mon Sep 17 00:00:00 2001 From: Allan Wang Date: Thu, 25 Apr 2019 13:06:37 -0700 Subject: Update changelog --- app/src/main/res/xml/frost_changelog.xml | 12 +++++++++++- docs/Changelog.md | 4 ++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/xml/frost_changelog.xml b/app/src/main/res/xml/frost_changelog.xml index 1895e46f..560b1111 100644 --- a/app/src/main/res/xml/frost_changelog.xml +++ b/app/src/main/res/xml/frost_changelog.xml @@ -6,12 +6,22 @@ --> + + + + + + + + + + + - diff --git a/docs/Changelog.md b/docs/Changelog.md index 7a6bbfab..39e7fa82 100644 --- a/docs/Changelog.md +++ b/docs/Changelog.md @@ -1,5 +1,9 @@ # Changelog +## v2.3.0 +* Converted internals of Facebook data storage; auto migration will only work from 2.2.x to 2.3.x +* Added notification widget + ## v2.2.4 * Show top bar to allow sharing posts * Fix unmuting videos when autoplay is enabled -- cgit v1.2.3