diff --git a/.editorconfig b/.editorconfig index 59dd365a..bc307f93 100644 --- a/.editorconfig +++ b/.editorconfig @@ -6,7 +6,6 @@ insert_final_newline = true trim_trailing_whitespace = true [*.{kt,kts}] -ktlint_code_style = android_studio indent_size = 4 ij_kotlin_allow_trailing_comma=true ij_kotlin_allow_trailing_comma_on_call_site=true diff --git a/.github/workflows/build_debug.yml b/.github/workflows/build_debug.yml index 814aea65..d43cf45a 100644 --- a/.github/workflows/build_debug.yml +++ b/.github/workflows/build_debug.yml @@ -27,51 +27,48 @@ jobs: packages: write steps: - - name: Check out repository - uses: actions/checkout@v4 - with: - submodules: true + - name: Check out repository + uses: actions/checkout@v4 + with: + submodules: true - - name: Validate Gradle Wrapper - uses: gradle/actions/setup-gradle@v4 + - name: Validate Gradle Wrapper + uses: gradle/actions/setup-gradle@v4 - - name: Setup Gradle - uses: gradle/wrapper-validation-action@v3 + - name: Setup Gradle + uses: gradle/wrapper-validation-action@v3 - - name: Set up Java 17 - uses: actions/setup-java@v4 - with: - java-version: 17 - distribution: 'adopt' - cache: gradle + - name: Set up Java 17 + uses: actions/setup-java@v4 + with: + java-version: 17 + distribution: 'adopt' + cache: gradle - - name: Grant execution permission to Gradle Wrapper - run: chmod +x gradlew + - name: Grant execution permission to Gradle Wrapper + run: chmod +x gradlew - - name: Format Code - run: ./gradlew ktlintFormat + - name: Build Debug APK + run: ./gradlew assembleDebug - - name: Build Debug APK - run: ./gradlew assembleDebug + - name: Sign Apk + continue-on-error: true + id: sign_apk + uses: r0adkll/sign-android-release@v1 + with: + releaseDir: app/build/outputs/apk/debug + signingKeyBase64: ${{ secrets.KEY_BASE64 }} + alias: ${{ secrets.KEY_ALIAS }} + keyStorePassword: ${{ secrets.KEYSTORE_PASS }} + keyPassword: ${{ secrets.KEYSTORE_PASS }} - - name: Sign Apk - continue-on-error: true - id: sign_apk - uses: r0adkll/sign-android-release@v1 - with: - releaseDir: app/build/outputs/apk/debug - signingKeyBase64: ${{ secrets.KEY_BASE64 }} - alias: ${{ secrets.KEY_ALIAS }} - keyStorePassword: ${{ secrets.KEYSTORE_PASS }} - keyPassword: ${{ secrets.KEYSTORE_PASS }} + - name: Remove file that aren't signed + continue-on-error: true + run: | + ls | grep 'signed\.apk$' && find . -type f -name '*.apk' ! -name '*-signed.apk' -delete - - name: Remove file that aren't signed - continue-on-error: true - run: | - ls | grep 'signed\.apk$' && find . -type f -name '*.apk' ! -name '*-signed.apk' -delete - - - name: Upload the APK - uses: actions/upload-artifact@v4 - with: - name: debug - path: app/build/outputs/apk/debug/app-debug*.apk + - name: Upload the APK + uses: actions/upload-artifact@v4 + with: + name: debug + path: app/build/outputs/apk/debug/app-debug*.apk diff --git a/.github/workflows/release_build.yml b/.github/workflows/release_build.yml index bed82139..1be76ce5 100644 --- a/.github/workflows/release_build.yml +++ b/.github/workflows/release_build.yml @@ -56,12 +56,34 @@ jobs: keyStorePassword: ${{ secrets.KEYSTORE_PASS }} keyPassword: ${{ secrets.KEYSTORE_PASS }} env: - BUILD_TOOLS_VERSION: "34.0.0" + BUILD_TOOLS_VERSION: "35.0.0" + + - name: Extract Version Code + id: extract_version + run: | + VERSION_CODE=$(grep -oP '(?<=versionCode=)\d+' app/build.gradle.kts) # Adjust path to your build.gradle + echo "::set-output name=version_code::$VERSION_CODE" + echo "Version Code: $VERSION_CODE" + + - name: Read Changelog + id: read_changelog + run: | + CHANGELOG_PATH="metadata/en-US/changelogs/${{ steps.extract_version.outputs.version_code }}.txt" + if [[ -f "$CHANGELOG_PATH" ]]; then + CHANGELOG=$(cat "$CHANGELOG_PATH") + echo "::set-output name=changelog::$CHANGELOG" + else + echo "::set-output name=changelog::No changelog found for this version." + echo "No changelog found at: $CHANGELOG_PATH" + fi - uses: softprops/action-gh-release@v2 name: Create Release id: publish_release with: + body: ${{ steps.read_changelog.outputs.changelog }} + tag_name: ${{ github.ref }} + name: Release ${{ github.ref }} files: ${{steps.sign_app.outputs.signedReleaseFile}} draft: true prerelease: false diff --git a/README.md b/README.md index 99d3585c..c5a23d76 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ [![Github Downloads](https://img.shields.io/github/downloads/Iamlooker/Droid-ify/total.svg?color=%23f5ad64&style=for-the-badge)](https://github.com/Iamlooker/Droid-ify/releases/) [![Github Latest](https://img.shields.io/github/v/release/Iamlooker/Droid-ify?display_name=tag&color=%23f5ad64&style=for-the-badge)](https://github.com/Iamlooker/Droid-ify/releases/latest) [![FDroid Latest](https://img.shields.io/f-droid/v/com.looker.droidify?color=%23f5ad64&style=for-the-badge)](https://f-droid.org/packages/com.looker.droidify) - +
## Features @@ -22,8 +22,10 @@ ## Building and Installing + 1. **Install Android Studio**: - - Download and install [Android Studio](https://developer.android.com/studio) on your computer if you haven't already. + - Download and install [Android Studio](https://developer.android.com/studio) on your computer + if you haven't already. 2. **Clone the Repository**: - Open Android Studio and select "Project from Version Control." @@ -48,6 +50,7 @@ - Your PR will undergo review ## Translations + [![Translation status](https://hosted.weblate.org/widgets/droidify/-/horizontal-auto.svg)](https://hosted.weblate.org/engage/droidify/?utm_source=widget) ## License @@ -67,3 +70,5 @@ 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 . ``` + +
diff --git a/app/build.gradle.kts b/app/build.gradle.kts index abd3ab09..3cae340c 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -10,7 +10,7 @@ plugins { } android { - val latestVersionName = "0.6.4" + val latestVersionName = "0.6.5" namespace = "com.looker.droidify" buildToolsVersion = "35.0.0" compileSdk = 35 @@ -18,7 +18,7 @@ android { minSdk = 23 targetSdk = 35 applicationId = "com.looker.droidify" - versionCode = 640 + versionCode = 650 versionName = latestVersionName vectorDrawables.useSupportLibrary = true testInstrumentationRunner = "com.looker.droidify.TestRunner" @@ -32,7 +32,12 @@ android { kotlinOptions { jvmTarget = "17" - freeCompilerArgs = listOf("-Xcontext-receivers") + freeCompilerArgs = listOf( + "-opt-in=kotlin.RequiresOptIn", + "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi", + "-opt-in=kotlinx.coroutines.FlowPreview", + "-Xcontext-receivers" + ) } ksp { @@ -45,11 +50,11 @@ android { } buildTypes { - getByName("debug") { + debug { applicationIdSuffix = ".debug" resValue("string", "application_name", "Droid-ify-Debug") } - getByName("release") { + release { isMinifyEnabled = true isShrinkResources = true resValue("string", "application_name", "Droid-ify") @@ -79,7 +84,16 @@ android { } packaging { resources { - excludes += listOf("/DebugProbesKt.bin") + excludes += listOf( + "/DebugProbesKt.bin", + "/kotlin/**.kotlin_builtins", + "/kotlin/**.kotlin_metadata", + "/META-INF/**.kotlin_module", + "/META-INF/**.pro", + "/META-INF/**.version", + "/META-INF/{AL2.0,LGPL2.1,LICENSE*}", + "/META-INF/versions/9/previous-**.bin", + ) } } buildFeatures { @@ -87,6 +101,10 @@ android { viewBinding = true buildConfig = true } + dependenciesInfo { + includeInApk = false + includeInBundle = false + } } dependencies { @@ -102,7 +120,7 @@ dependencies { implementation(libs.sqlite.ktx) implementation(libs.image.viewer) - implementation(libs.coil.kt) + implementation(libs.bundles.coil) implementation(libs.datastore.core) implementation(libs.datastore.proto) diff --git a/app/proguard.pro b/app/proguard.pro index 6a2417c5..d1de746e 100644 --- a/app/proguard.pro +++ b/app/proguard.pro @@ -7,68 +7,3 @@ -dontwarn kotlinx.serialization.KSerializer -dontwarn kotlinx.serialization.Serializable -dontwarn org.slf4j.impl.StaticLoggerBinder -# -#-dontwarn com.looker.core.common.BuildConfig -#-dontwarn com.looker.core.common.DeeplinkType$AddRepository -#-dontwarn com.looker.core.common.DeeplinkType$AppDetail -#-dontwarn com.looker.core.common.DeeplinkType -#-dontwarn com.looker.core.common.DeeplinksKt -#-dontwarn com.looker.core.common.Exporter -#-dontwarn com.looker.core.common.NotificationKt -#-dontwarn com.looker.core.common.PermissionsKt -#-dontwarn com.looker.core.common.Scroller -#-dontwarn com.looker.core.common.SdkCheck -#-dontwarn com.looker.core.common.Singleton -#-dontwarn com.looker.core.common.TextKt -#-dontwarn com.looker.core.common.cache.Cache -#-dontwarn com.looker.core.common.cache.Cache -#-dontwarn com.looker.core.common.device.Huawei -#-dontwarn com.looker.core.common.extension.ContextKt -#-dontwarn com.looker.core.common.extension.CursorKt -#-dontwarn com.looker.core.common.extension.DateTimeKt -#-dontwarn com.looker.core.common.extension.FingerprintKt -#-dontwarn com.looker.core.common.extension.FlowKt -#-dontwarn com.looker.core.common.extension.InsetsKt -#-dontwarn com.looker.core.common.extension.IntentKt -#-dontwarn com.looker.core.common.extension.Json -#-dontwarn com.looker.core.common.extension.JsonKt -#-dontwarn com.looker.core.common.extension.KeyToken -#-dontwarn com.looker.core.common.extension.LocaleKt -#-dontwarn com.looker.core.common.extension.NumberKt -#-dontwarn com.looker.core.common.extension.PackageInfoKt -#-dontwarn com.looker.core.common.extension.SQLiteDatabaseKt -#-dontwarn com.looker.core.common.extension.ServiceKt -#-dontwarn com.looker.core.common.extension.ViewKt -#-dontwarn com.looker.core.common.result.Result$Error -#-dontwarn com.looker.core.common.result.Result$Success -#-dontwarn com.looker.core.common.result.Result -#-dontwarn com.looker.core.common.signature.Hash -#-dontwarn com.looker.core.common.signature.HashCheckerKt -# -#-dontwarn com.looker.core.datastore.Settings -#-dontwarn com.looker.core.datastore.SettingsRepository -#-dontwarn com.looker.core.datastore.di.DatastoreModule_ProvidePreferenceDatastoreFactory -#-dontwarn com.looker.core.datastore.di.DatastoreModule_ProvideProtoDatastoreFactory -#-dontwarn com.looker.core.datastore.di.DatastoreModule_ProvideSettingsExporterFactory -#-dontwarn com.looker.core.datastore.di.DatastoreModule_ProvideSettingsRepositoryFactory -#-dontwarn com.looker.core.datastore.extension.PreferencesKt -#-dontwarn com.looker.core.datastore.model.AutoSync -#-dontwarn com.looker.core.datastore.model.InstallerType$Companion -#-dontwarn com.looker.core.datastore.model.InstallerType -#-dontwarn com.looker.core.datastore.model.ProxyPreference -#-dontwarn com.looker.core.datastore.model.ProxyType -#-dontwarn com.looker.core.datastore.model.SortOrder -#-dontwarn com.looker.core.datastore.model.Theme -# -#-dontwarn com.looker.installer.InstallManager -#-dontwarn com.looker.installer.InstallModule_ProvideRootPermissionHandlerFactory -#-dontwarn com.looker.installer.InstallModule_ProvideShizukuPermissionHandlerFactory -#-dontwarn com.looker.installer.InstallModule_ProvidesInstallerFactory -#-dontwarn com.looker.installer.installers.root.RootPermissionHandler -#-dontwarn com.looker.installer.installers.session.SessionInstallerReceiver_GeneratedInjector -#-dontwarn com.looker.installer.installers.shizuku.ShizukuPermissionHandler$State -#-dontwarn com.looker.installer.installers.shizuku.ShizukuPermissionHandler -#-dontwarn com.looker.installer.model.InstallItem -#-dontwarn com.looker.installer.model.InstallItemKt -#-dontwarn com.looker.installer.model.InstallState -#-dontwarn com.looker.installer.notification.InstallNotificationKt diff --git a/app/src/androidTest/kotlin/com/looker/droidify/sync/common/Resource.kt b/app/src/androidTest/kotlin/com/looker/droidify/sync/common/Resource.kt index a6781a56..9ce29c0f 100644 --- a/app/src/androidTest/kotlin/com/looker/droidify/sync/common/Resource.kt +++ b/app/src/androidTest/kotlin/com/looker/droidify/sync/common/Resource.kt @@ -1,7 +1,6 @@ package com.looker.droidify.sync.common import androidx.test.platform.app.InstrumentationRegistry -import java.io.File import java.io.InputStream fun assets(name: String): InputStream { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index c440299f..568c9857 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -50,7 +50,6 @@ diff --git a/app/src/main/kotlin/com/looker/droidify/Droidify.kt b/app/src/main/kotlin/com/looker/droidify/Droidify.kt index edb610a3..90e73997 100644 --- a/app/src/main/kotlin/com/looker/droidify/Droidify.kt +++ b/app/src/main/kotlin/com/looker/droidify/Droidify.kt @@ -13,10 +13,14 @@ import androidx.appcompat.app.AppCompatDelegate import androidx.hilt.work.HiltWorkerFactory import androidx.work.Configuration import androidx.work.NetworkType -import coil.ImageLoader -import coil.ImageLoaderFactory -import coil.disk.DiskCache -import coil.memory.MemoryCache +import coil3.ImageLoader +import coil3.PlatformContext +import coil3.SingletonImageLoader +import coil3.asImage +import coil3.disk.DiskCache +import coil3.disk.directory +import coil3.memory.MemoryCache +import coil3.request.crossfade import com.looker.droidify.content.ProductPreferences import com.looker.droidify.database.Database import com.looker.droidify.datastore.SettingsRepository @@ -35,6 +39,7 @@ import com.looker.droidify.sync.toJobNetworkType import com.looker.droidify.utility.common.Constants import com.looker.droidify.utility.common.SdkCheck import com.looker.droidify.utility.common.cache.Cache +import com.looker.droidify.utility.common.extension.getDrawableCompat import com.looker.droidify.utility.common.extension.getInstalledPackagesCompat import com.looker.droidify.utility.common.extension.jobScheduler import com.looker.droidify.utility.common.log @@ -55,7 +60,7 @@ import kotlin.time.Duration.Companion.INFINITE import kotlin.time.Duration.Companion.hours @HiltAndroidApp -class Droidify : Application(), ImageLoaderFactory, Configuration.Provider { +class Droidify : Application(), SingletonImageLoader.Factory, Configuration.Provider { private val parentJob = SupervisorJob() private val appScope = CoroutineScope(Dispatchers.Default + parentJob) @@ -221,9 +226,14 @@ class Droidify : Application(), ImageLoaderFactory, Configuration.Provider { override fun onReceive(context: Context, intent: Intent) = Unit } - override fun newImageLoader(): ImageLoader { - val memoryCache = MemoryCache.Builder(this) - .maxSizePercent(0.25) + override val workManagerConfiguration: Configuration + get() = Configuration.Builder() + .setWorkerFactory(workerFactory) + .build() + + override fun newImageLoader(context: PlatformContext): ImageLoader { + val memoryCache = MemoryCache.Builder() + .maxSizePercent(context, 0.25) .build() val diskCache = DiskCache.Builder() @@ -234,15 +244,10 @@ class Droidify : Application(), ImageLoaderFactory, Configuration.Provider { return ImageLoader.Builder(this) .memoryCache(memoryCache) .diskCache(diskCache) - .error(R.drawable.ic_cannot_load) + .error(getDrawableCompat(R.drawable.ic_cannot_load).asImage()) .crossfade(350) .build() } - - override val workManagerConfiguration: Configuration - get() = Configuration.Builder() - .setWorkerFactory(workerFactory) - .build() } @RequiresApi(Build.VERSION_CODES.O) diff --git a/app/src/main/kotlin/com/looker/droidify/content/ProductPreferences.kt b/app/src/main/kotlin/com/looker/droidify/content/ProductPreferences.kt index 32c4cd11..f2f11120 100644 --- a/app/src/main/kotlin/com/looker/droidify/content/ProductPreferences.kt +++ b/app/src/main/kotlin/com/looker/droidify/content/ProductPreferences.kt @@ -2,9 +2,9 @@ package com.looker.droidify.content import android.content.Context import android.content.SharedPreferences -import com.looker.core.common.extension.Json -import com.looker.core.common.extension.parseDictionary -import com.looker.core.common.extension.writeDictionary +import com.looker.droidify.utility.common.extension.Json +import com.looker.droidify.utility.common.extension.parseDictionary +import com.looker.droidify.utility.common.extension.writeDictionary import com.looker.droidify.model.ProductPreference import com.looker.droidify.database.Database import com.looker.droidify.utility.serialization.productPreference diff --git a/app/src/main/kotlin/com/looker/droidify/database/CursorOwner.kt b/app/src/main/kotlin/com/looker/droidify/database/CursorOwner.kt index 48a62374..b4a649e9 100644 --- a/app/src/main/kotlin/com/looker/droidify/database/CursorOwner.kt +++ b/app/src/main/kotlin/com/looker/droidify/database/CursorOwner.kt @@ -12,28 +12,29 @@ class CursorOwner : Fragment(), LoaderManager.LoaderCallbacks { sealed class Request { internal abstract val id: Int - data class ProductsAvailable( + class Available( val searchQuery: String, val section: ProductItem.Section, - val order: SortOrder + val order: SortOrder, ) : Request() { override val id: Int get() = 1 } - data class ProductsInstalled( + class Installed( val searchQuery: String, val section: ProductItem.Section, - val order: SortOrder + val order: SortOrder, ) : Request() { override val id: Int get() = 2 } - data class ProductsUpdates( + class Updates( val searchQuery: String, val section: ProductItem.Section, - val order: SortOrder + val order: SortOrder, + val skipSignatureCheck: Boolean, ) : Request() { override val id: Int get() = 3 @@ -52,7 +53,7 @@ class CursorOwner : Fragment(), LoaderManager.LoaderCallbacks { private data class ActiveRequest( val request: Request, val callback: Callback?, - val cursor: Cursor? + val cursor: Cursor?, ) init { @@ -93,7 +94,7 @@ class CursorOwner : Fragment(), LoaderManager.LoaderCallbacks { val request = activeRequests[id]!!.request return QueryLoader(requireContext()) { when (request) { - is Request.ProductsAvailable -> + is Request.Available -> Database.ProductAdapter .query( installed = false, @@ -101,10 +102,10 @@ class CursorOwner : Fragment(), LoaderManager.LoaderCallbacks { searchQuery = request.searchQuery, section = request.section, order = request.order, - signal = it + signal = it, ) - is Request.ProductsInstalled -> + is Request.Installed -> Database.ProductAdapter .query( installed = true, @@ -112,10 +113,10 @@ class CursorOwner : Fragment(), LoaderManager.LoaderCallbacks { searchQuery = request.searchQuery, section = request.section, order = request.order, - signal = it + signal = it, ) - is Request.ProductsUpdates -> + is Request.Updates -> Database.ProductAdapter .query( installed = true, @@ -123,7 +124,8 @@ class CursorOwner : Fragment(), LoaderManager.LoaderCallbacks { searchQuery = request.searchQuery, section = request.section, order = request.order, - signal = it + signal = it, + skipSignatureCheck = request.skipSignatureCheck, ) is Request.Repositories -> Database.RepositoryAdapter.query(it) diff --git a/app/src/main/kotlin/com/looker/droidify/database/Database.kt b/app/src/main/kotlin/com/looker/droidify/database/Database.kt index 9e5c6076..01f0b9fc 100644 --- a/app/src/main/kotlin/com/looker/droidify/database/Database.kt +++ b/app/src/main/kotlin/com/looker/droidify/database/Database.kt @@ -9,18 +9,18 @@ import android.os.CancellationSignal import androidx.core.database.sqlite.transaction import com.fasterxml.jackson.core.JsonGenerator import com.fasterxml.jackson.core.JsonParser -import com.looker.core.common.extension.Json -import com.looker.droidify.utility.common.extension.asSequence -import com.looker.droidify.utility.common.extension.firstOrNull -import com.looker.core.common.extension.parseDictionary -import com.looker.core.common.extension.writeDictionary -import com.looker.droidify.utility.common.log +import com.looker.droidify.BuildConfig import com.looker.droidify.datastore.model.SortOrder import com.looker.droidify.model.InstalledItem import com.looker.droidify.model.Product import com.looker.droidify.model.ProductItem import com.looker.droidify.model.Repository -import com.looker.droidify.BuildConfig +import com.looker.droidify.utility.common.extension.Json +import com.looker.droidify.utility.common.extension.asSequence +import com.looker.droidify.utility.common.extension.firstOrNull +import com.looker.droidify.utility.common.extension.parseDictionary +import com.looker.droidify.utility.common.extension.writeDictionary +import com.looker.droidify.utility.common.log import com.looker.droidify.utility.serialization.product import com.looker.droidify.utility.serialization.productItem import com.looker.droidify.utility.serialization.repository @@ -71,14 +71,20 @@ object Database { get() = "$databasePrefix$innerName" fun formatCreateTable(name: String): String { - return "CREATE TABLE $name (${QueryBuilder.trimQuery(createTable)})" + return buildString(128) { + append("CREATE TABLE ") + append(name) + append(" (") + trimAndJoin(createTable) + append(")") + } } val createIndexPairFormatted: Pair? get() = createIndex?.let { Pair( "CREATE INDEX ${innerName}_index ON $innerName ($it)", - "CREATE INDEX ${name}_index ON $innerName ($it)" + "CREATE INDEX ${name}_index ON $innerName ($it)", ) } } @@ -214,7 +220,7 @@ object Database { Schema.Product, Schema.Category, Schema.Installed, - Schema.Lock + Schema.Lock, ) dropOldTables(db, Schema.Repository, Schema.Product, Schema.Category) this.created = this.created || create @@ -227,7 +233,7 @@ object Database { val sql = db.query( "${table.databasePrefix}sqlite_master", columns = arrayOf("sql"), - selection = Pair("type = ? AND name = ?", arrayOf("table", table.innerName)) + selection = Pair("type = ? AND name = ?", arrayOf("table", table.innerName)), ).use { it.firstOrNull()?.getString(0) }.orEmpty() table.formatCreateTable(table.innerName) != sql } @@ -261,7 +267,7 @@ object Database { val sqls = db.query( "${table.databasePrefix}sqlite_master", columns = arrayOf("name", "sql"), - selection = Pair("type = ? AND tbl_name = ?", arrayOf("index", table.innerName)) + selection = Pair("type = ? AND tbl_name = ?", arrayOf("index", table.innerName)), ) .use { cursor -> cursor.asSequence() @@ -289,7 +295,7 @@ object Database { val tables = db.query( "sqlite_master", columns = arrayOf("name"), - selection = Pair("type = ?", arrayOf("table")) + selection = Pair("type = ?", arrayOf("table")), ) .use { cursor -> cursor.asSequence().mapNotNull { it.getString(0) }.toList() } .filter { !it.startsWith("sqlite_") && !it.startsWith("android_") } @@ -345,7 +351,7 @@ object Database { private fun SQLiteDatabase.insertOrReplace( replace: Boolean, table: String, - contentValues: ContentValues + contentValues: ContentValues, ): Long { return if (replace) { replace(table, null, contentValues) @@ -353,7 +359,7 @@ object Database { insert( table, null, - contentValues + contentValues, ) } } @@ -363,7 +369,7 @@ object Database { columns: Array? = null, selection: Pair>? = null, orderBy: String? = null, - signal: CancellationSignal? = null + signal: CancellationSignal? = null, ): Cursor { return query( false, @@ -375,7 +381,7 @@ object Database { null, orderBy, null, - signal + signal, ) } @@ -397,7 +403,7 @@ object Database { internal fun putWithoutNotification( repository: Repository, shouldReplace: Boolean, - database: SQLiteDatabase + database: SQLiteDatabase, ): Long { return database.insertOrReplace( shouldReplace, @@ -409,7 +415,7 @@ object Database { put(Schema.Repository.ROW_ENABLED, if (repository.enabled) 1 else 0) put(Schema.Repository.ROW_DELETED, 0) put(Schema.Repository.ROW_DATA, jsonGenerate(repository::serialize)) - } + }, ) } @@ -442,8 +448,8 @@ object Database { Schema.Repository.name, selection = Pair( "${Schema.Repository.ROW_ID} = ? AND ${Schema.Repository.ROW_DELETED} == 0", - arrayOf(id.toString()) - ) + arrayOf(id.toString()), + ), ).use { it.firstOrNull()?.let(::transform) } } @@ -463,9 +469,9 @@ object Database { selection = Pair( "${Schema.Repository.ROW_ENABLED} != 0 AND " + "${Schema.Repository.ROW_DELETED} == 0", - emptyArray() + emptyArray(), ), - signal = null + signal = null, ).use { it.asSequence().map(::transform).toList() } } @@ -473,7 +479,7 @@ object Database { return db.query( Schema.Repository.name, selection = Pair("${Schema.Repository.ROW_DELETED} == 0", emptyArray()), - signal = null + signal = null, ).use { it.asSequence().map(::transform).toList() } } @@ -489,9 +495,9 @@ object Database { selection = Pair( "${Schema.Repository.ROW_ENABLED} == 0 OR " + "${Schema.Repository.ROW_DELETED} != 0", - emptyArray() + emptyArray(), ), - signal = null + signal = null, ).use { parentCursor -> parentCursor.asSequence().associate { val idIndex = it.getColumnIndexOrThrow(Schema.Repository.ROW_ID) @@ -508,7 +514,7 @@ object Database { put(Schema.Repository.ROW_DELETED, 1) }, "${Schema.Repository.ROW_ID} = ?", - arrayOf(id.toString()) + arrayOf(id.toString()), ) notifyChanged(Subject.Repositories, Subject.Repository(id), Subject.Products) } @@ -519,18 +525,18 @@ object Database { val productsCount = db.delete( Schema.Product.name, "${Schema.Product.ROW_REPOSITORY_ID} IN ($idsString)", - null + null, ) val categoriesCount = db.delete( Schema.Category.name, "${Schema.Category.ROW_REPOSITORY_ID} IN ($idsString)", - null + null, ) if (isDeleted) { db.delete( Schema.Repository.name, "${Schema.Repository.ROW_ID} IN ($id)", - null + null, ) } productsCount != 0 || categoriesCount != 0 @@ -555,7 +561,7 @@ object Database { Schema.Repository.name, selection = Pair("${Schema.Repository.ROW_DELETED} == 0", emptyArray()), orderBy = "${Schema.Repository.ROW_ENABLED} DESC", - signal = signal + signal = signal, ).observable(Subject.Repositories) } @@ -577,26 +583,28 @@ object Database { .map { get(packageName, null) } .flowOn(Dispatchers.IO) - suspend fun getUpdates(): List = withContext(Dispatchers.IO) { - query( - installed = true, - updates = true, - searchQuery = "", - section = ProductItem.Section.All, - order = SortOrder.NAME, - signal = null - ).use { - it.asSequence() - .map(ProductAdapter::transformItem) - .toList() + suspend fun getUpdates(skipSignatureCheck: Boolean): List = + withContext(Dispatchers.IO) { + query( + installed = true, + updates = true, + searchQuery = "", + skipSignatureCheck = skipSignatureCheck, + section = ProductItem.Section.All, + order = SortOrder.NAME, + signal = null, + ).use { + it.asSequence() + .map(ProductAdapter::transformItem) + .toList() + } } - } - fun getUpdatesStream(): Flow> = flowOf(Unit) + fun getUpdatesStream(skipSignatureCheck: Boolean): Flow> = flowOf(Unit) .onCompletion { if (it == null) emitAll(flowCollection(Subject.Products)) } // Crashes due to immediate retrieval of data? .onEach { delay(50) } - .map { getUpdates() } + .map { getUpdates(skipSignatureCheck) } .flowOn(Dispatchers.IO) fun getAll(): List { @@ -618,10 +626,10 @@ object Database { columns = arrayOf( Schema.Product.ROW_REPOSITORY_ID, Schema.Product.ROW_DESCRIPTION, - Schema.Product.ROW_DATA + Schema.Product.ROW_DATA, ), selection = Pair("${Schema.Product.ROW_PACKAGE_NAME} = ?", arrayOf(packageName)), - signal = signal + signal = signal, ).use { it.asSequence().map(::transform).toList() } } @@ -636,24 +644,26 @@ object Database { columns = arrayOf("COUNT (*)"), selection = Pair( "${Schema.Product.ROW_REPOSITORY_ID} = ?", - arrayOf(repositoryId.toString()) - ) + arrayOf(repositoryId.toString()), + ), ).use { it.firstOrNull()?.getInt(0) ?: 0 } } fun query( installed: Boolean, updates: Boolean, + skipSignatureCheck: Boolean = false, searchQuery: String, section: ProductItem.Section, order: SortOrder, - signal: CancellationSignal? + signal: CancellationSignal?, ): Cursor { val builder = QueryBuilder() - val signatureMatches = """installed.${Schema.Installed.ROW_SIGNATURE} IS NOT NULL AND - product.${Schema.Product.ROW_SIGNATURES} LIKE ('%.' || installed.${Schema.Installed.ROW_SIGNATURE} || '.%') AND - product.${Schema.Product.ROW_SIGNATURES} != ''""" + val signatureMatches = if (skipSignatureCheck) "1" + else """installed.${Schema.Installed.ROW_SIGNATURE} IS NOT NULL AND + product.${Schema.Product.ROW_SIGNATURES} LIKE ('%.' || installed.${Schema.Installed.ROW_SIGNATURE} || '.%') AND + product.${Schema.Product.ROW_SIGNATURES} != ''""" builder += """SELECT product.rowid AS _id, product.${Schema.Product.ROW_REPOSITORY_ID}, product.${Schema.Product.ROW_PACKAGE_NAME}, product.${Schema.Product.ROW_NAME}, @@ -741,6 +751,10 @@ object Database { } } + fun transformPackageName(cursor: Cursor): String { + return cursor.getString(cursor.getColumnIndexOrThrow(Schema.Product.ROW_PACKAGE_NAME)) + } + fun transformItem(cursor: Cursor): ProductItem { return cursor.getBlob(cursor.getColumnIndexOrThrow(Schema.Product.ROW_DATA_ITEM)) .jsonParse { @@ -806,10 +820,10 @@ object Database { Schema.Installed.ROW_PACKAGE_NAME, Schema.Installed.ROW_VERSION, Schema.Installed.ROW_VERSION_CODE, - Schema.Installed.ROW_SIGNATURE + Schema.Installed.ROW_SIGNATURE, ), selection = Pair("${Schema.Installed.ROW_PACKAGE_NAME} = ?", arrayOf(packageName)), - signal = signal + signal = signal, ).use { it.firstOrNull()?.let(::transform) } } @@ -822,7 +836,7 @@ object Database { put(Schema.Installed.ROW_VERSION, installedItem.version) put(Schema.Installed.ROW_VERSION_CODE, installedItem.versionCode) put(Schema.Installed.ROW_SIGNATURE, installedItem.signature) - } + }, ) if (notify) { notifyChanged(Subject.Products) @@ -842,7 +856,7 @@ object Database { val count = db.delete( Schema.Installed.name, "${Schema.Installed.ROW_PACKAGE_NAME} = ?", - arrayOf(packageName) + arrayOf(packageName), ) if (count > 0) { notifyChanged(Subject.Products) @@ -854,7 +868,7 @@ object Database { cursor.getString(cursor.getColumnIndexOrThrow(Schema.Installed.ROW_PACKAGE_NAME)), cursor.getString(cursor.getColumnIndexOrThrow(Schema.Installed.ROW_VERSION)), cursor.getLong(cursor.getColumnIndexOrThrow(Schema.Installed.ROW_VERSION_CODE)), - cursor.getString(cursor.getColumnIndexOrThrow(Schema.Installed.ROW_SIGNATURE)) + cursor.getString(cursor.getColumnIndexOrThrow(Schema.Installed.ROW_SIGNATURE)), ) } } @@ -867,7 +881,7 @@ object Database { ContentValues().apply { put(Schema.Lock.ROW_PACKAGE_NAME, lock.first) put(Schema.Lock.ROW_VERSION_CODE, lock.second) - } + }, ) if (notify) { notifyChanged(Subject.Products) @@ -923,9 +937,9 @@ object Database { put(Schema.Product.ROW_DATA, jsonGenerate(product::serialize)) put( Schema.Product.ROW_DATA_ITEM, - jsonGenerate(product.item()::serialize) + jsonGenerate(product.item()::serialize), ) - } + }, ) for (category in product.categories) { db.insertOrReplace( @@ -935,7 +949,7 @@ object Database { put(Schema.Category.ROW_REPOSITORY_ID, product.repositoryId) put(Schema.Category.ROW_PACKAGE_NAME, product.packageName) put(Schema.Category.ROW_NAME, category) - } + }, ) } } @@ -948,20 +962,20 @@ object Database { db.delete( Schema.Product.name, "${Schema.Product.ROW_REPOSITORY_ID} = ?", - arrayOf(repository.id.toString()) + arrayOf(repository.id.toString()), ) db.delete( Schema.Category.name, "${Schema.Category.ROW_REPOSITORY_ID} = ?", - arrayOf(repository.id.toString()) + arrayOf(repository.id.toString()), ) db.execSQL( "INSERT INTO ${Schema.Product.name} SELECT * " + - "FROM ${Schema.Product.temporaryName}" + "FROM ${Schema.Product.temporaryName}", ) db.execSQL( "INSERT INTO ${Schema.Category.name} SELECT * " + - "FROM ${Schema.Category.temporaryName}" + "FROM ${Schema.Category.temporaryName}", ) RepositoryAdapter.putWithoutNotification(repository, true, db) db.execSQL("DROP TABLE IF EXISTS ${Schema.Product.temporaryName}") @@ -970,7 +984,7 @@ object Database { notifyChanged( Subject.Repositories, Subject.Repository(repository.id), - Subject.Products + Subject.Products, ) } else { db.execSQL("DROP TABLE IF EXISTS ${Schema.Product.temporaryName}") diff --git a/app/src/main/kotlin/com/looker/droidify/database/QueryBuilder.kt b/app/src/main/kotlin/com/looker/droidify/database/QueryBuilder.kt index 1ecb92b8..de9c01f2 100644 --- a/app/src/main/kotlin/com/looker/droidify/database/QueryBuilder.kt +++ b/app/src/main/kotlin/com/looker/droidify/database/QueryBuilder.kt @@ -3,26 +3,20 @@ package com.looker.droidify.database import android.database.Cursor import android.database.sqlite.SQLiteDatabase import android.os.CancellationSignal +import com.looker.droidify.BuildConfig import com.looker.droidify.utility.common.extension.asSequence import com.looker.droidify.utility.common.log -import com.looker.droidify.BuildConfig class QueryBuilder { - companion object { - fun trimQuery(query: String): String { - return query.lines().map { it.trim() }.filter { it.isNotEmpty() } - .joinToString(separator = " ") - } - } - private val builder = StringBuilder() + private val builder = StringBuilder(256) private val arguments = mutableListOf() operator fun plusAssign(query: String) { if (builder.isNotEmpty()) { builder.append(" ") } - builder.append(trimQuery(query)) + builder.trimAndJoin(query) } operator fun remAssign(argument: String) { @@ -48,3 +42,53 @@ class QueryBuilder { return db.rawQuery(query, arguments, signal) } } + +fun StringBuilder.trimAndJoin( + input: String, +) { + var isFirstLine = true + var startOfLine = 0 + for (i in input.indices) { + val char = input[i] + when { + char == '\n' -> { + trimAndAppendLine(input, startOfLine, i, this, isFirstLine) + isFirstLine = false + startOfLine = i + 1 + } + + else -> { + if (i == input.lastIndex) { + trimAndAppendLine(input, startOfLine, i + 1, this, isFirstLine) + } + } + } + } +} + +private fun trimAndAppendLine( + input: String, + start: Int, + end: Int, + builder: StringBuilder, + isFirstLine: Boolean, +) { + var lineStart = start + var lineEnd = end - 1 + + while (lineStart <= lineEnd && input[lineStart].isWhitespace()) { + lineStart++ + } + + while (lineEnd >= lineStart && input[lineEnd].isWhitespace()) { + lineEnd-- + } + + if (lineStart <= lineEnd) { + if (!isFirstLine) { + builder.append(' ') + } + builder.append(input, lineStart, lineEnd + 1) + } +} + diff --git a/app/src/main/kotlin/com/looker/droidify/database/RepositoryExporter.kt b/app/src/main/kotlin/com/looker/droidify/database/RepositoryExporter.kt index 822667b7..d4e5666a 100644 --- a/app/src/main/kotlin/com/looker/droidify/database/RepositoryExporter.kt +++ b/app/src/main/kotlin/com/looker/droidify/database/RepositoryExporter.kt @@ -4,12 +4,12 @@ import android.content.Context import android.net.Uri import com.fasterxml.jackson.core.JsonToken import com.looker.droidify.utility.common.Exporter -import com.looker.core.common.extension.Json -import com.looker.core.common.extension.forEach -import com.looker.core.common.extension.forEachKey -import com.looker.core.common.extension.parseDictionary -import com.looker.core.common.extension.writeArray -import com.looker.core.common.extension.writeDictionary +import com.looker.droidify.utility.common.extension.Json +import com.looker.droidify.utility.common.extension.forEach +import com.looker.droidify.utility.common.extension.forEachKey +import com.looker.droidify.utility.common.extension.parseDictionary +import com.looker.droidify.utility.common.extension.writeArray +import com.looker.droidify.utility.common.extension.writeDictionary import com.looker.droidify.di.ApplicationScope import com.looker.droidify.di.IoDispatcher import com.looker.droidify.model.Repository diff --git a/app/src/main/kotlin/com/looker/droidify/datastore/PreferenceSettingsRepository.kt b/app/src/main/kotlin/com/looker/droidify/datastore/PreferenceSettingsRepository.kt index 99dbef2e..ad22ba25 100644 --- a/app/src/main/kotlin/com/looker/droidify/datastore/PreferenceSettingsRepository.kt +++ b/app/src/main/kotlin/com/looker/droidify/datastore/PreferenceSettingsRepository.kt @@ -14,6 +14,7 @@ import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.core.stringSetPreferencesKey import com.looker.droidify.datastore.model.AutoSync import com.looker.droidify.datastore.model.InstallerType +import com.looker.droidify.datastore.model.LegacyInstallerComponent import com.looker.droidify.datastore.model.ProxyPreference import com.looker.droidify.datastore.model.ProxyType import com.looker.droidify.datastore.model.SortOrder @@ -85,6 +86,31 @@ class PreferenceSettingsRepository( override suspend fun setInstallerType(installerType: InstallerType) = INSTALLER_TYPE.update(installerType.name) + override suspend fun setLegacyInstallerComponent(component: LegacyInstallerComponent?) { + when (component) { + null -> { + LEGACY_INSTALLER_COMPONENT_TYPE.update("") + LEGACY_INSTALLER_COMPONENT_CLASS.update("") + LEGACY_INSTALLER_COMPONENT_ACTIVITY.update("") + } + is LegacyInstallerComponent.Component -> { + LEGACY_INSTALLER_COMPONENT_TYPE.update("component") + LEGACY_INSTALLER_COMPONENT_CLASS.update(component.clazz) + LEGACY_INSTALLER_COMPONENT_ACTIVITY.update(component.activity) + } + LegacyInstallerComponent.Unspecified -> { + LEGACY_INSTALLER_COMPONENT_TYPE.update("unspecified") + LEGACY_INSTALLER_COMPONENT_CLASS.update("") + LEGACY_INSTALLER_COMPONENT_ACTIVITY.update("") + } + LegacyInstallerComponent.AlwaysChoose -> { + LEGACY_INSTALLER_COMPONENT_TYPE.update("always_choose") + LEGACY_INSTALLER_COMPONENT_CLASS.update("") + LEGACY_INSTALLER_COMPONENT_ACTIVITY.update("") + } + } + } + override suspend fun setAutoUpdate(allow: Boolean) = AUTO_UPDATE.update(allow) @@ -125,6 +151,18 @@ class PreferenceSettingsRepository( private fun mapSettings(preferences: Preferences): Settings { val installerType = InstallerType.valueOf(preferences[INSTALLER_TYPE] ?: InstallerType.Default.name) + val legacyInstallerComponent = when (preferences[LEGACY_INSTALLER_COMPONENT_TYPE]) { + "component" -> { + preferences[LEGACY_INSTALLER_COMPONENT_CLASS]?.takeIf { it.isNotBlank() }?.let { cls -> + preferences[LEGACY_INSTALLER_COMPONENT_ACTIVITY]?.takeIf { it.isNotBlank() }?.let { act -> + LegacyInstallerComponent.Component(cls, act) + } + } + } + "unspecified" -> LegacyInstallerComponent.Unspecified + "always_choose" -> LegacyInstallerComponent.AlwaysChoose + else -> null + } val language = preferences[LANGUAGE] ?: "system" val incompatibleVersions = preferences[INCOMPATIBLE_VERSIONS] ?: false @@ -154,6 +192,7 @@ class PreferenceSettingsRepository( theme = theme, dynamicTheme = dynamicTheme, installerType = installerType, + legacyInstallerComponent = legacyInstallerComponent, autoUpdate = autoUpdate, autoSync = autoSync, sortOrder = sortOrder, @@ -185,6 +224,9 @@ class PreferenceSettingsRepository( val LAST_CLEAN_UP = longPreferencesKey("key_last_clean_up_time") val FAVOURITE_APPS = stringSetPreferencesKey("key_favourite_apps") val HOME_SCREEN_SWIPING = booleanPreferencesKey("key_home_swiping") + val LEGACY_INSTALLER_COMPONENT_CLASS = stringPreferencesKey("key_legacy_installer_component_class") + val LEGACY_INSTALLER_COMPONENT_ACTIVITY = stringPreferencesKey("key_legacy_installer_component_activity") + val LEGACY_INSTALLER_COMPONENT_TYPE = stringPreferencesKey("key_legacy_installer_component_type") // Enums val THEME = stringPreferencesKey("key_theme") @@ -200,6 +242,28 @@ class PreferenceSettingsRepository( set(UNSTABLE_UPDATES, settings.unstableUpdate) set(THEME, settings.theme.name) set(DYNAMIC_THEME, settings.dynamicTheme) + when (settings.legacyInstallerComponent) { + is LegacyInstallerComponent.Component -> { + set(LEGACY_INSTALLER_COMPONENT_TYPE, "component") + set(LEGACY_INSTALLER_COMPONENT_CLASS, settings.legacyInstallerComponent.clazz) + set(LEGACY_INSTALLER_COMPONENT_ACTIVITY, settings.legacyInstallerComponent.activity) + } + LegacyInstallerComponent.Unspecified -> { + set(LEGACY_INSTALLER_COMPONENT_TYPE, "unspecified") + set(LEGACY_INSTALLER_COMPONENT_CLASS, "") + set(LEGACY_INSTALLER_COMPONENT_ACTIVITY, "") + } + LegacyInstallerComponent.AlwaysChoose -> { + set(LEGACY_INSTALLER_COMPONENT_TYPE, "always_choose") + set(LEGACY_INSTALLER_COMPONENT_CLASS, "") + set(LEGACY_INSTALLER_COMPONENT_ACTIVITY, "") + } + null -> { + set(LEGACY_INSTALLER_COMPONENT_TYPE, "") + set(LEGACY_INSTALLER_COMPONENT_CLASS, "") + set(LEGACY_INSTALLER_COMPONENT_ACTIVITY, "") + } + } set(INSTALLER_TYPE, settings.installerType.name) set(AUTO_UPDATE, settings.autoUpdate) set(AUTO_SYNC, settings.autoSync.name) diff --git a/app/src/main/kotlin/com/looker/droidify/datastore/Settings.kt b/app/src/main/kotlin/com/looker/droidify/datastore/Settings.kt index 64f413a9..bf7a3eca 100644 --- a/app/src/main/kotlin/com/looker/droidify/datastore/Settings.kt +++ b/app/src/main/kotlin/com/looker/droidify/datastore/Settings.kt @@ -3,6 +3,7 @@ package com.looker.droidify.datastore import androidx.datastore.core.Serializer import com.looker.droidify.datastore.model.AutoSync import com.looker.droidify.datastore.model.InstallerType +import com.looker.droidify.datastore.model.LegacyInstallerComponent import com.looker.droidify.datastore.model.ProxyPreference import com.looker.droidify.datastore.model.SortOrder import com.looker.droidify.datastore.model.Theme @@ -29,6 +30,7 @@ data class Settings( val theme: Theme = Theme.SYSTEM, val dynamicTheme: Boolean = false, val installerType: InstallerType = InstallerType.Default, + val legacyInstallerComponent: LegacyInstallerComponent? = null, val autoUpdate: Boolean = false, val autoSync: AutoSync = AutoSync.WIFI_ONLY, val sortOrder: SortOrder = SortOrder.UPDATED, diff --git a/app/src/main/kotlin/com/looker/droidify/datastore/SettingsRepository.kt b/app/src/main/kotlin/com/looker/droidify/datastore/SettingsRepository.kt index b1b54ac7..fad3b23e 100644 --- a/app/src/main/kotlin/com/looker/droidify/datastore/SettingsRepository.kt +++ b/app/src/main/kotlin/com/looker/droidify/datastore/SettingsRepository.kt @@ -3,6 +3,7 @@ package com.looker.droidify.datastore import android.net.Uri import com.looker.droidify.datastore.model.AutoSync import com.looker.droidify.datastore.model.InstallerType +import com.looker.droidify.datastore.model.LegacyInstallerComponent import com.looker.droidify.datastore.model.ProxyType import com.looker.droidify.datastore.model.SortOrder import com.looker.droidify.datastore.model.Theme @@ -37,6 +38,8 @@ interface SettingsRepository { suspend fun setInstallerType(installerType: InstallerType) + suspend fun setLegacyInstallerComponent(component: LegacyInstallerComponent?) + suspend fun setAutoUpdate(allow: Boolean) suspend fun setAutoSync(autoSync: AutoSync) diff --git a/app/src/main/kotlin/com/looker/droidify/datastore/model/LegacyInstallerComponent.kt b/app/src/main/kotlin/com/looker/droidify/datastore/model/LegacyInstallerComponent.kt new file mode 100644 index 00000000..cdf00d9a --- /dev/null +++ b/app/src/main/kotlin/com/looker/droidify/datastore/model/LegacyInstallerComponent.kt @@ -0,0 +1,26 @@ +package com.looker.droidify.datastore.model + +import kotlinx.serialization.Serializable + +@Serializable +sealed class LegacyInstallerComponent { + @Serializable + object Unspecified : LegacyInstallerComponent() + + @Serializable + object AlwaysChoose : LegacyInstallerComponent() + + @Serializable + data class Component( + val clazz: String, + val activity: String, + ) : LegacyInstallerComponent() { + fun update( + newClazz: String? = null, + newActivity: String? = null, + ): Component = copy( + clazz = newClazz ?: clazz, + activity = newActivity ?: activity + ) + } +} diff --git a/app/src/main/kotlin/com/looker/droidify/di/CoroutinesModule.kt b/app/src/main/kotlin/com/looker/droidify/di/CoroutinesModule.kt index 70549734..57d2b117 100644 --- a/app/src/main/kotlin/com/looker/droidify/di/CoroutinesModule.kt +++ b/app/src/main/kotlin/com/looker/droidify/di/CoroutinesModule.kt @@ -1,5 +1,3 @@ -@file:Suppress("unused") - package com.looker.droidify.di import dagger.Module diff --git a/app/src/main/kotlin/com/looker/droidify/domain/model/Repo.kt b/app/src/main/kotlin/com/looker/droidify/domain/model/Repo.kt index d9fcef5f..10809353 100644 --- a/app/src/main/kotlin/com/looker/droidify/domain/model/Repo.kt +++ b/app/src/main/kotlin/com/looker/droidify/domain/model/Repo.kt @@ -22,9 +22,6 @@ data class Repo( } } -val String.isOnion: Boolean - get() = endsWith(".onion") - data class AntiFeature( val id: Long, val name: String, diff --git a/app/src/main/kotlin/com/looker/droidify/graphics/DrawableWrapper.kt b/app/src/main/kotlin/com/looker/droidify/graphics/DrawableWrapper.kt index 72f6b8ca..3d334957 100644 --- a/app/src/main/kotlin/com/looker/droidify/graphics/DrawableWrapper.kt +++ b/app/src/main/kotlin/com/looker/droidify/graphics/DrawableWrapper.kt @@ -51,7 +51,6 @@ open class DrawableWrapper(val drawable: Drawable) : Drawable() { drawable.colorFilter = colorFilter } - @Deprecated("Deprecated in Java") @Suppress("DEPRECATION") override fun getOpacity(): Int = drawable.opacity } diff --git a/app/src/main/kotlin/com/looker/droidify/index/IndexMerger.kt b/app/src/main/kotlin/com/looker/droidify/index/IndexMerger.kt index 65801edd..e544aa5d 100644 --- a/app/src/main/kotlin/com/looker/droidify/index/IndexMerger.kt +++ b/app/src/main/kotlin/com/looker/droidify/index/IndexMerger.kt @@ -3,10 +3,10 @@ package com.looker.droidify.index import android.content.ContentValues import android.database.sqlite.SQLiteDatabase import com.fasterxml.jackson.core.JsonToken -import com.looker.core.common.extension.Json +import com.looker.droidify.utility.common.extension.Json import com.looker.droidify.utility.common.extension.asSequence -import com.looker.core.common.extension.collectNotNull -import com.looker.core.common.extension.writeDictionary +import com.looker.droidify.utility.common.extension.collectNotNull +import com.looker.droidify.utility.common.extension.writeDictionary import com.looker.droidify.model.Product import com.looker.droidify.model.Release import com.looker.droidify.utility.serialization.product @@ -82,9 +82,9 @@ class IndexMerger(file: File) : Closeable { closeTransaction() db.rawQuery( """SELECT product.description, product.data AS pd, releases.data AS rd FROM product - LEFT JOIN releases ON product.package_name = releases.package_name""", + LEFT JOIN releases ON product.package_name = releases.package_name""", null - )?.use { cursor -> + ).use { cursor -> cursor.asSequence().map { currentCursor -> val description = currentCursor.getString(0) val product = Json.factory.createParser(currentCursor.getBlob(1)).use { diff --git a/app/src/main/kotlin/com/looker/droidify/index/IndexV1Parser.kt b/app/src/main/kotlin/com/looker/droidify/index/IndexV1Parser.kt index eff4cb2d..0aeacd98 100644 --- a/app/src/main/kotlin/com/looker/droidify/index/IndexV1Parser.kt +++ b/app/src/main/kotlin/com/looker/droidify/index/IndexV1Parser.kt @@ -5,16 +5,27 @@ import androidx.core.os.ConfigurationCompat.getLocales import androidx.core.os.LocaleListCompat import com.fasterxml.jackson.core.JsonParser import com.fasterxml.jackson.core.JsonToken -import com.looker.droidify.utility.common.SdkCheck -import com.looker.core.common.extension.Json -import com.looker.core.common.extension.collectDistinctNotEmptyStrings -import com.looker.core.common.extension.collectNotNull -import com.looker.core.common.extension.forEach -import com.looker.core.common.extension.forEachKey -import com.looker.core.common.extension.illegal -import com.looker.droidify.utility.common.nullIfEmpty +import com.looker.droidify.utility.common.extension.Json +import com.looker.droidify.utility.common.extension.collectDistinctNotEmptyStrings +import com.looker.droidify.utility.common.extension.collectNotNull +import com.looker.droidify.utility.common.extension.forEach +import com.looker.droidify.utility.common.extension.forEachKey +import com.looker.droidify.utility.common.extension.illegal import com.looker.droidify.model.Product +import com.looker.droidify.model.Product.Donate.Bitcoin +import com.looker.droidify.model.Product.Donate.Liberapay +import com.looker.droidify.model.Product.Donate.Litecoin +import com.looker.droidify.model.Product.Donate.OpenCollective +import com.looker.droidify.model.Product.Donate.Regular +import com.looker.droidify.model.Product.Screenshot.Type.LARGE_TABLET +import com.looker.droidify.model.Product.Screenshot.Type.PHONE +import com.looker.droidify.model.Product.Screenshot.Type.SMALL_TABLET +import com.looker.droidify.model.Product.Screenshot.Type.TV +import com.looker.droidify.model.Product.Screenshot.Type.VIDEO +import com.looker.droidify.model.Product.Screenshot.Type.WEAR import com.looker.droidify.model.Release +import com.looker.droidify.utility.common.SdkCheck +import com.looker.droidify.utility.common.nullIfEmpty import java.io.InputStream object IndexV1Parser { @@ -32,9 +43,12 @@ object IndexV1Parser { } private class Screenshots( + val video: List, val phone: List, val smallTablet: List, - val largeTablet: List + val largeTablet: List, + val wear: List, + val tv: List, ) private class Localized( @@ -90,10 +104,9 @@ object IndexV1Parser { } private fun Map.find(callback: (String, Localized) -> T?): T? { - return getAndCall("en-US", callback) ?: getAndCall("en_US", callback) ?: getAndCall( - "en", - callback - ) + return getAndCall("en-US", callback) + ?: getAndCall("en_US", callback) + ?: getAndCall("en", callback) } private fun Map.findLocalized(callback: (Localized) -> T?): T? { @@ -122,12 +135,11 @@ object IndexV1Parser { internal object DonateComparator : Comparator { private val classes = listOf( - Product.Donate.Regular::class, - Product.Donate.Bitcoin::class, - Product.Donate.Litecoin::class, - Product.Donate.Flattr::class, - Product.Donate.Liberapay::class, - Product.Donate.OpenCollective::class + Regular::class, + Bitcoin::class, + Litecoin::class, + Liberapay::class, + OpenCollective::class ) override fun compare(donate1: Product.Donate, donate2: Product.Donate): Int { @@ -236,14 +248,17 @@ object IndexV1Parser { private const val KEY_PRODUCT_LICENSE = "license" private const val KEY_PRODUCT_DONATE = "donate" private const val KEY_PRODUCT_BITCOIN = "bitcoin" - private const val KEY_PRODUCT_FLATTRID = "flattrID" - private const val KEY_PRODUCT_LIBERAPAYID = "liberapayID" + private const val KEY_PRODUCT_LIBERAPAYID = "liberapay" + private const val KEY_PRODUCT_LITECOIN = "litecoin" private const val KEY_PRODUCT_OPENCOLLECTIVE = "openCollective" private const val KEY_PRODUCT_LOCALIZED = "localized" private const val KEY_PRODUCT_WHATSNEW = "whatsNew" - private const val KEY_PRODUCT_PHONESCREENSHOTS = "phoneScreenshots" - private const val KEY_PRODUCT_SEVENINCHSCREENSHOTS = "sevenInchScreenshots" - private const val KEY_PRODUCT_TENINCHSCREENSHOTS = "tenInchScreenshots" + private const val KEY_PRODUCT_PHONE_SCREENSHOTS = "phoneScreenshots" + private const val KEY_PRODUCT_SEVEN_INCH_SCREENSHOTS = "sevenInchScreenshots" + private const val KEY_PRODUCT_TEN_INCH_SCREENSHOTS = "tenInchScreenshots" + private const val KEY_PRODUCT_WEAR_SCREENSHOTS = "wearScreenshots" + private const val KEY_PRODUCT_TV_SCREENSHOTS = "tvScreenshots" + private const val KEY_PRODUCT_VIDEO = "video" private fun JsonParser.parseProduct(repositoryId: Long): Product { var packageName = "" @@ -293,16 +308,11 @@ object IndexV1Parser { key.string(KEY_PRODUCT_LICENSE) -> licenses += valueAsString.split(',') .filter { it.isNotEmpty() } - key.string(KEY_PRODUCT_DONATE) -> donates += Product.Donate.Regular(valueAsString) - key.string(KEY_PRODUCT_BITCOIN) -> donates += Product.Donate.Bitcoin(valueAsString) - key.string(KEY_PRODUCT_FLATTRID) -> donates += Product.Donate.Flattr(valueAsString) - key.string(KEY_PRODUCT_LIBERAPAYID) -> donates += Product.Donate.Liberapay( - valueAsString - ) - - key.string(KEY_PRODUCT_OPENCOLLECTIVE) -> donates += Product.Donate.OpenCollective( - valueAsString - ) + key.string(KEY_PRODUCT_DONATE) -> donates += Regular(valueAsString) + key.string(KEY_PRODUCT_BITCOIN) -> donates += Bitcoin(valueAsString) + key.string(KEY_PRODUCT_LIBERAPAYID) -> donates += Liberapay(valueAsString) + key.string(KEY_PRODUCT_LITECOIN) -> donates += Litecoin(valueAsString) + key.string(KEY_PRODUCT_OPENCOLLECTIVE) -> donates += OpenCollective(valueAsString) key.dictionary(KEY_PRODUCT_LOCALIZED) -> forEachKey { localizedKey -> if (localizedKey.token == JsonToken.START_OBJECT) { @@ -315,6 +325,9 @@ object IndexV1Parser { var phone = emptyList() var smallTablet = emptyList() var largeTablet = emptyList() + var wear = emptyList() + var tv = emptyList() + var video = emptyList() forEachKey { when { it.string(KEY_PRODUCT_NAME) -> name = valueAsString @@ -322,39 +335,42 @@ object IndexV1Parser { it.string(KEY_PRODUCT_DESCRIPTION) -> description = valueAsString it.string(KEY_PRODUCT_WHATSNEW) -> whatsNew = valueAsString it.string(KEY_PRODUCT_ICON) -> metadataIcon = valueAsString - it.array(KEY_PRODUCT_PHONESCREENSHOTS) -> - phone = - collectDistinctNotEmptyStrings() + it.string(KEY_PRODUCT_VIDEO) -> video = listOf(valueAsString) + it.array(KEY_PRODUCT_PHONE_SCREENSHOTS) -> + phone = collectDistinctNotEmptyStrings() - it.array(KEY_PRODUCT_SEVENINCHSCREENSHOTS) -> - smallTablet = - collectDistinctNotEmptyStrings() + it.array(KEY_PRODUCT_SEVEN_INCH_SCREENSHOTS) -> + smallTablet = collectDistinctNotEmptyStrings() - it.array(KEY_PRODUCT_TENINCHSCREENSHOTS) -> - largeTablet = - collectDistinctNotEmptyStrings() + it.array(KEY_PRODUCT_TEN_INCH_SCREENSHOTS) -> + largeTablet = collectDistinctNotEmptyStrings() + + it.array(KEY_PRODUCT_WEAR_SCREENSHOTS) -> + wear = collectDistinctNotEmptyStrings() + + it.array(KEY_PRODUCT_TV_SCREENSHOTS) -> + tv = collectDistinctNotEmptyStrings() else -> skipChildren() } } + val isScreenshotEmpty = + arrayOf(video, phone, smallTablet, largeTablet, wear, tv) + .any { it.isNotEmpty() } val screenshots = - if (sequenceOf( - phone, - smallTablet, - largeTablet - ).any { it.isNotEmpty() } - ) { - Screenshots(phone, smallTablet, largeTablet) + if (isScreenshotEmpty) { + Screenshots(video, phone, smallTablet, largeTablet, wear, tv) } else { null } localizedMap[locale] = Localized( - name, - summary, - description, - whatsNew, - metadataIcon.nullIfEmpty()?.let { "$locale/$it" }.orEmpty(), - screenshots + name = name, + summary = summary, + description = description, + whatsNew = whatsNew, + metadataIcon = metadataIcon.nullIfEmpty()?.let { "$locale/$it" } + .orEmpty(), + screenshots = screenshots, ) } else { skipChildren() @@ -377,28 +393,14 @@ object IndexV1Parser { } val screenshotPairs = localizedMap.find { key, localized -> localized.screenshots?.let { Pair(key, it) } } - val screenshots = screenshotPairs - ?.let { (key, screenshots) -> - screenshots.phone.asSequence() - .map { Product.Screenshot(key, Product.Screenshot.Type.PHONE, it) } + - screenshots.smallTablet.asSequence() - .map { - Product.Screenshot( - key, - Product.Screenshot.Type.SMALL_TABLET, - it - ) - } + - screenshots.largeTablet.asSequence() - .map { - Product.Screenshot( - key, - Product.Screenshot.Type.LARGE_TABLET, - it - ) - } - } - .orEmpty().toList() + val screenshots = screenshotPairs?.let { (key, screenshots) -> + screenshots.video.map { Product.Screenshot(key, VIDEO, it) } + + screenshots.phone.map { Product.Screenshot(key, PHONE, it) } + + screenshots.smallTablet.map { Product.Screenshot(key, SMALL_TABLET, it) } + + screenshots.largeTablet.map { Product.Screenshot(key, LARGE_TABLET, it) } + + screenshots.wear.map { Product.Screenshot(key, WEAR, it) } + + screenshots.tv.map { Product.Screenshot(key, TV, it) } + }.orEmpty() return Product( repositoryId = repositoryId, packageName = packageName, diff --git a/app/src/main/kotlin/com/looker/droidify/installer/InstallManager.kt b/app/src/main/kotlin/com/looker/droidify/installer/InstallManager.kt index 8420c544..3530757d 100644 --- a/app/src/main/kotlin/com/looker/droidify/installer/InstallManager.kt +++ b/app/src/main/kotlin/com/looker/droidify/installer/InstallManager.kt @@ -16,6 +16,8 @@ import com.looker.droidify.installer.installers.session.SessionInstaller import com.looker.droidify.installer.installers.shizuku.ShizukuInstaller import com.looker.droidify.installer.model.InstallItem import com.looker.droidify.installer.model.InstallState +import com.looker.droidify.installer.notification.createInstallNotification +import com.looker.droidify.installer.notification.installNotification import com.looker.droidify.installer.notification.removeInstallNotification import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.channels.Channel @@ -30,7 +32,7 @@ import kotlinx.coroutines.sync.withLock class InstallManager( private val context: Context, - settingsRepository: SettingsRepository + private val settingsRepository: SettingsRepository ) { private val installItems = Channel() @@ -87,6 +89,13 @@ class InstallManager( }.consumeEach { item -> if (state.value.containsKey(item.packageName)) { updateState { put(item.packageName, InstallState.Installing) } + context.notificationManager?.installNotification( + packageName = item.packageName.name, + notification = context.createInstallNotification( + appName = item.packageName.name, + state = InstallState.Installing, + ) + ) val success = installer.use { it.install(item) } @@ -106,7 +115,7 @@ class InstallManager( private suspend fun setInstaller(installerType: InstallerType) { lock.withLock { _installer = when (installerType) { - InstallerType.LEGACY -> LegacyInstaller(context) + InstallerType.LEGACY -> LegacyInstaller(context, settingsRepository) InstallerType.SESSION -> SessionInstaller(context) InstallerType.SHIZUKU -> ShizukuInstaller(context) InstallerType.ROOT -> RootInstaller(context) diff --git a/app/src/main/kotlin/com/looker/droidify/installer/installers/LegacyInstaller.kt b/app/src/main/kotlin/com/looker/droidify/installer/installers/LegacyInstaller.kt index 3e95d042..825a4c76 100644 --- a/app/src/main/kotlin/com/looker/droidify/installer/installers/LegacyInstaller.kt +++ b/app/src/main/kotlin/com/looker/droidify/installer/installers/LegacyInstaller.kt @@ -1,49 +1,80 @@ package com.looker.droidify.installer.installers +import android.content.ComponentName import android.content.Context import android.content.Intent import android.util.AndroidRuntimeException import androidx.core.net.toUri -import com.looker.droidify.utility.common.SdkCheck -import com.looker.droidify.utility.common.cache.Cache +import com.looker.droidify.R +import com.looker.droidify.datastore.SettingsRepository +import com.looker.droidify.datastore.get +import com.looker.droidify.datastore.model.LegacyInstallerComponent import com.looker.droidify.domain.model.PackageName import com.looker.droidify.installer.model.InstallItem import com.looker.droidify.installer.model.InstallState -import kotlin.coroutines.resume +import com.looker.droidify.utility.common.SdkCheck +import com.looker.droidify.utility.common.cache.Cache +import com.looker.droidify.utility.common.extension.intent +import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.suspendCancellableCoroutine +import kotlin.coroutines.resume @Suppress("DEPRECATION") -internal class LegacyInstaller(private val context: Context) : Installer { +class LegacyInstaller( + private val context: Context, + private val settingsRepository: SettingsRepository +) : Installer { companion object { private const val APK_MIME = "application/vnd.android.package-archive" } override suspend fun install( - installItem: InstallItem - ): InstallState = suspendCancellableCoroutine { cont -> - val (uri, flags) = if (SdkCheck.isNougat) { - Cache.getReleaseUri( - context, - installItem.installFileName - ) to Intent.FLAG_GRANT_READ_URI_PERMISSION + installItem: InstallItem, + ): InstallState { + val installFlag = if (SdkCheck.isNougat) Intent.FLAG_GRANT_READ_URI_PERMISSION else 0 + val fileUri = if (SdkCheck.isNougat) { + Cache.getReleaseUri(context, installItem.installFileName) } else { - val file = Cache.getReleaseFile(context, installItem.installFileName) - file.toUri() to 0 + Cache.getReleaseFile(context, installItem.installFileName).toUri() } - try { - context.startActivity( - Intent(Intent.ACTION_INSTALL_PACKAGE).setDataAndType(uri, APK_MIME).setFlags(flags) - ) - cont.resume(InstallState.Installed) - } catch (e: AndroidRuntimeException) { - context.startActivity( - Intent(Intent.ACTION_INSTALL_PACKAGE).setDataAndType(uri, APK_MIME) - .setFlags(flags or Intent.FLAG_ACTIVITY_NEW_TASK) - ) - cont.resume(InstallState.Installed) - } catch (e: Exception) { - cont.resume(InstallState.Failed) + + val comp = settingsRepository.get { legacyInstallerComponent }.firstOrNull() + + return suspendCancellableCoroutine { cont -> + val intent = Intent(Intent.ACTION_INSTALL_PACKAGE).apply { + setDataAndType(fileUri, APK_MIME) + flags = installFlag + when (comp) { + is LegacyInstallerComponent.Component -> { + component = ComponentName(comp.clazz, comp.activity) + } + else -> { + // For Unspecified and AlwaysChoose, don't set component + } + } + } + + val installIntent = when (comp) { + LegacyInstallerComponent.AlwaysChoose -> Intent.createChooser(intent, context.getString( + R.string.select_installer)) + else -> intent + } + + try { + context.startActivity(installIntent) + cont.resume(InstallState.Installed) + } catch (e: AndroidRuntimeException) { + installIntent.flags = installFlag or Intent.FLAG_ACTIVITY_NEW_TASK + try { + context.startActivity(installIntent) + cont.resume(InstallState.Installed) + } catch (e: Exception) { + cont.resume(InstallState.Failed) + } + } catch (e: Exception) { + cont.resume(InstallState.Failed) + } } } @@ -53,14 +84,14 @@ internal class LegacyInstaller(private val context: Context) : Installer { override fun close() {} } -internal suspend fun Context.uninstallPackage(packageName: PackageName) = +suspend fun Context.uninstallPackage(packageName: PackageName) = suspendCancellableCoroutine { cont -> try { startActivity( - Intent( - Intent.ACTION_UNINSTALL_PACKAGE, - "package:${packageName.name}".toUri() - ).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + intent(Intent.ACTION_UNINSTALL_PACKAGE) { + data = "package:${packageName.name}".toUri() + flags = Intent.FLAG_ACTIVITY_NEW_TASK + } ) cont.resume(Unit) } catch (e: Exception) { diff --git a/app/src/main/kotlin/com/looker/droidify/installer/installers/root/RootInstaller.kt b/app/src/main/kotlin/com/looker/droidify/installer/installers/root/RootInstaller.kt index e0bc8423..368f7bad 100644 --- a/app/src/main/kotlin/com/looker/droidify/installer/installers/root/RootInstaller.kt +++ b/app/src/main/kotlin/com/looker/droidify/installer/installers/root/RootInstaller.kt @@ -1,70 +1,34 @@ package com.looker.droidify.installer.installers.root import android.content.Context -import com.looker.droidify.utility.common.SdkCheck -import com.looker.droidify.utility.common.cache.Cache import com.looker.droidify.domain.model.PackageName import com.looker.droidify.installer.installers.Installer import com.looker.droidify.installer.installers.uninstallPackage import com.looker.droidify.installer.model.InstallItem import com.looker.droidify.installer.model.InstallState +import com.looker.droidify.utility.common.SdkCheck +import com.looker.droidify.utility.common.cache.Cache +import com.topjohnwu.superuser.Shell import kotlinx.coroutines.suspendCancellableCoroutine -import java.io.File import kotlin.coroutines.resume -internal class RootInstaller(private val context: Context) : Installer { - - private companion object { - const val ROOT_INSTALL_PACKAGE = "cat %s | pm install --user %s -t -r -S %s" - const val DELETE_PACKAGE = "%s rm %s" - - val getCurrentUserState: String - get() = if (SdkCheck.isOreo) { - com.topjohnwu.superuser.Shell.cmd("am get-current-user").exec().out[0] - } else { - com.topjohnwu.superuser.Shell.cmd("dumpsys activity | grep -E \"mUserLru\"") - .exec().out[0].trim() - .removePrefix("mUserLru: [").removeSuffix("]") - } - - val String.quote - get() = "\"${this.replace(Regex("""[\\$"`]""")) { c -> "\\${c.value}" }}\"" - - val getUtilBoxPath: String - get() { - listOf("toybox", "busybox").forEach { - val shellResult = com.topjohnwu.superuser.Shell.cmd("which $it").exec() - if (shellResult.out.isNotEmpty()) { - val utilBoxPath = shellResult.out.joinToString("") - if (utilBoxPath.isNotEmpty()) return utilBoxPath.quote - } - } - return "" - } - - fun installCmd(file: File): String = String.format( - ROOT_INSTALL_PACKAGE, - file.absolutePath, - getCurrentUserState, - file.length() - ) - - fun deleteCmd(file: File): String = String.format( - DELETE_PACKAGE, - getUtilBoxPath, - file.absolutePath.quote - ) - } +class RootInstaller(private val context: Context) : Installer { override suspend fun install( - installItem: InstallItem + installItem: InstallItem, ): InstallState = suspendCancellableCoroutine { cont -> val releaseFile = Cache.getReleaseFile(context, installItem.installFileName) - com.topjohnwu.superuser.Shell.cmd(installCmd(releaseFile)).submit { shellResult -> + val installCommand = INSTALL_COMMAND.format( + releaseFile.absolutePath, + currentUser(), + releaseFile.length(), + ) + Shell.cmd(installCommand).submit { shellResult -> val result = if (shellResult.isSuccess) InstallState.Installed else InstallState.Failed cont.resume(result) - com.topjohnwu.superuser.Shell.cmd(deleteCmd(releaseFile)).submit() + val deleteCommand = DELETE_COMMAND.format(utilBox(), releaseFile.absolutePath) + Shell.cmd(deleteCommand).submit() } } @@ -72,4 +36,34 @@ internal class RootInstaller(private val context: Context) : Installer { context.uninstallPackage(packageName) override fun close() {} + +} + +private const val INSTALL_COMMAND = "cat %s | pm install --user %s -t -r -S %s" +private const val DELETE_COMMAND = "%s rm %s" + +/** Returns the path of either toybox or busybox, or empty string if not found. */ +private fun utilBox(): String { + listOf("toybox", "busybox").forEach { + // Returns the path of the requested [command], or empty string if not found + val out = Shell.cmd("which $it").exec().out + if (out.isEmpty()) return "" + if (out.first().contains("not found")) return "" + return out.first() + } + return "" +} + +/** Returns the current user of the device. */ +private fun currentUser() = if (SdkCheck.isOreo) { + Shell.cmd("am get-current-user") + .exec() + .out[0] +} else { + Shell.cmd("dumpsys activity | grep -E \"mUserLru\"") + .exec() + .out[0] + .trim() + .removePrefix("mUserLru: [") + .removeSuffix("]") } diff --git a/app/src/main/kotlin/com/looker/droidify/installer/installers/session/SessionInstaller.kt b/app/src/main/kotlin/com/looker/droidify/installer/installers/session/SessionInstaller.kt index baedffe2..9f4cc18b 100644 --- a/app/src/main/kotlin/com/looker/droidify/installer/installers/session/SessionInstaller.kt +++ b/app/src/main/kotlin/com/looker/droidify/installer/installers/session/SessionInstaller.kt @@ -20,7 +20,7 @@ import com.looker.droidify.installer.model.InstallState import kotlinx.coroutines.suspendCancellableCoroutine import kotlin.coroutines.resume -internal class SessionInstaller(private val context: Context) : Installer { +class SessionInstaller(private val context: Context) : Installer { private val installer = context.packageManager.packageInstaller private val intent = Intent(context, SessionInstallerReceiver::class.java) diff --git a/app/src/main/kotlin/com/looker/droidify/installer/installers/shizuku/ShizukuInstaller.kt b/app/src/main/kotlin/com/looker/droidify/installer/installers/shizuku/ShizukuInstaller.kt index 927e49a2..a70b3a5c 100644 --- a/app/src/main/kotlin/com/looker/droidify/installer/installers/shizuku/ShizukuInstaller.kt +++ b/app/src/main/kotlin/com/looker/droidify/installer/installers/shizuku/ShizukuInstaller.kt @@ -14,7 +14,7 @@ import java.io.BufferedReader import java.io.InputStream import kotlin.coroutines.resume -internal class ShizukuInstaller(private val context: Context) : Installer { +class ShizukuInstaller(private val context: Context) : Installer { companion object { private val SESSION_ID_REGEX = Regex("(?<=\\[).+?(?=])") diff --git a/app/src/main/kotlin/com/looker/droidify/installer/model/InstallItem.kt b/app/src/main/kotlin/com/looker/droidify/installer/model/InstallItem.kt index 0c604daa..8f4ce263 100644 --- a/app/src/main/kotlin/com/looker/droidify/installer/model/InstallItem.kt +++ b/app/src/main/kotlin/com/looker/droidify/installer/model/InstallItem.kt @@ -3,7 +3,7 @@ package com.looker.droidify.installer.model import com.looker.droidify.domain.model.PackageName import com.looker.droidify.domain.model.toPackageName -data class InstallItem( +class InstallItem( val packageName: PackageName, val installFileName: String ) diff --git a/app/src/main/kotlin/com/looker/droidify/model/Product.kt b/app/src/main/kotlin/com/looker/droidify/model/Product.kt index 25554353..6a7a6e6a 100644 --- a/app/src/main/kotlin/com/looker/droidify/model/Product.kt +++ b/app/src/main/kotlin/com/looker/droidify/model/Product.kt @@ -1,7 +1,9 @@ package com.looker.droidify.model -import com.looker.droidify.domain.model.Donation -import com.looker.droidify.domain.model.Screenshots +import android.content.Context +import com.looker.droidify.utility.common.extension.getColorFromAttr +import com.looker.droidify.utility.common.extension.videoPlaceHolder +import com.google.android.material.R as MaterialR data class Product( var repositoryId: Long, @@ -33,29 +35,38 @@ data class Product( data class Regular(val url: String) : Donate() data class Bitcoin(val address: String) : Donate() data class Litecoin(val address: String) : Donate() - data class Flattr(val id: String) : Donate() data class Liberapay(val id: String) : Donate() data class OpenCollective(val id: String) : Donate() } class Screenshot(val locale: String, val type: Type, val path: String) { enum class Type(val jsonName: String) { + VIDEO("video"), PHONE("phone"), SMALL_TABLET("smallTablet"), - LARGE_TABLET("largeTablet") + LARGE_TABLET("largeTablet"), + WEAR("wear"), + TV("tv") } val identifier: String get() = "$locale.${type.name}.$path" fun url( + context: Context, repository: Repository, packageName: String - ): String { + ): Any { + if (type == Type.VIDEO) return context.videoPlaceHolder.apply { + setTintList(context.getColorFromAttr(MaterialR.attr.colorOnSurfaceInverse)) + } val phoneType = when (type) { Type.PHONE -> "phoneScreenshots" Type.SMALL_TABLET -> "sevenInchScreenshots" Type.LARGE_TABLET -> "tenInchScreenshots" + Type.WEAR -> "wearScreenshots" + Type.TV -> "tvScreenshots" + else -> error("Should not be here, video url already returned") } return "${repository.address}/$packageName/$locale/$phoneType/$path" } @@ -115,48 +126,3 @@ fun List>.findSuggested( } ) ) -// -//fun App.toProduct() = Product( -// packageName = metadata.packageName.name, -// name = metadata.name, -// summary = metadata.summary, -// description = metadata.description, -// whatsNew = metadata.whatsNew, -// icon = metadata.icon, -// metadataIcon = "", -// author = Product.Author( -// name = author.name, -// email = author.email, -// web = author.web, -// ), -// source = links.sourceCode, -// changelog = links.changelog, -// web = links.webSite, -// tracker = links.issueTracker, -// added = metadata.added, -// updated = metadata.lastUpdated, -// suggestedVersionCode = metadata.suggestedVersionCode, -// categories = categories, -// antiFeatures = metadata.antiFeatures, -// licenses = listOf(metadata.license), -// donates = donation.toLegacy(), -// screenshots = screenshots.toLegacy(), -// releases = packages -//) - -fun Donation.toLegacy() = buildList { - regularUrl?.let { it.forEach { add(Product.Donate.Regular(it)) } } - bitcoinAddress?.let { add(Product.Donate.Bitcoin(it)) } - flattrId?.let { add(Product.Donate.Flattr(it)) } - litecoinAddress?.let { add(Product.Donate.Litecoin(it)) } - openCollectiveId?.let { add(Product.Donate.OpenCollective(it)) } - liberapayId?.let { add(Product.Donate.Liberapay(it)) } -} - -fun Screenshots.toLegacy() = buildList { - phone.forEach { add(Product.Screenshot("en-US", Product.Screenshot.Type.PHONE, it)) } - sevenInch.forEach { add(Product.Screenshot("en-US", Product.Screenshot.Type.SMALL_TABLET, it)) } - tenInch.forEach { add(Product.Screenshot("en-US", Product.Screenshot.Type.LARGE_TABLET, it)) } - tv.forEach { add(Product.Screenshot("en-US", Product.Screenshot.Type.PHONE, it)) } - wear.forEach { add(Product.Screenshot("en-US", Product.Screenshot.Type.PHONE, it)) } -} diff --git a/app/src/main/kotlin/com/looker/droidify/model/ProductItem.kt b/app/src/main/kotlin/com/looker/droidify/model/ProductItem.kt index 5fc1c998..7b29cf60 100644 --- a/app/src/main/kotlin/com/looker/droidify/model/ProductItem.kt +++ b/app/src/main/kotlin/com/looker/droidify/model/ProductItem.kt @@ -18,16 +18,16 @@ data class ProductItem( var canUpdate: Boolean, var matchRank: Int ) { - sealed class Section : Parcelable { + sealed interface Section : Parcelable { @Parcelize - data object All : Section() + object All : Section @Parcelize - data class Category(val name: String) : Section() + class Category(val name: String) : Section @Parcelize - data class Repository(val id: Long, val name: String) : Section() + class Repository(val id: Long, val name: String) : Section } private val supportedDpi = intArrayOf(120, 160, 240, 320, 480, 640) diff --git a/app/src/main/kotlin/com/looker/droidify/model/Release.kt b/app/src/main/kotlin/com/looker/droidify/model/Release.kt index f9209e03..aba123e3 100644 --- a/app/src/main/kotlin/com/looker/droidify/model/Release.kt +++ b/app/src/main/kotlin/com/looker/droidify/model/Release.kt @@ -31,7 +31,7 @@ data class Release( object MinSdk : Incompatibility() object MaxSdk : Incompatibility() object Platform : Incompatibility() - data class Feature(val feature: String) : Incompatibility() + class Feature(val feature: String) : Incompatibility() } val identifier: String diff --git a/app/src/main/kotlin/com/looker/droidify/model/Repository.kt b/app/src/main/kotlin/com/looker/droidify/model/Repository.kt index f19eeef6..4ea93461 100644 --- a/app/src/main/kotlin/com/looker/droidify/model/Repository.kt +++ b/app/src/main/kotlin/com/looker/droidify/model/Repository.kt @@ -15,7 +15,7 @@ data class Repository( val entityTag: String, val updated: Long, val timestamp: Long, - val authentication: String + val authentication: String, ) { fun edit(address: String, fingerprint: String, authentication: String): Repository { @@ -38,7 +38,7 @@ data class Repository( version: Int, lastModified: String, entityTag: String, - timestamp: Long + timestamp: Long, ): Repository { return copy( mirrors = mirrors, @@ -62,7 +62,7 @@ data class Repository( fun newRepository( address: String, fingerprint: String, - authentication: String + authentication: String, ): Repository { val name = try { URL(address).let { "${it.host}${it.path}" } @@ -79,7 +79,7 @@ data class Repository( version: Int = 21, enabled: Boolean = false, fingerprint: String, - authentication: String = "" + authentication: String = "", ): Repository { return Repository( -1, address, emptyList(), name, description, version, enabled, @@ -150,14 +150,6 @@ data class Repository( " by Netsyms Technologies.", fingerprint = "2581BA7B32D3AB443180C4087CAB6A7E8FB258D3A6E98870ECB3C675E4D64489" ), - defaultRepository( - address = "https://fdroid.bromite.org/fdroid/repo", - name = "Bromite", - description = "The official repository for Bromite. " + - "Bromite is a Chromium with ad blocking and enhanced p" + - "rivacy.", - fingerprint = "E1EE5CD076D7B0DC84CB2B45FB78B86DF2EB39A3B6C56BA3DC292A5E0C3B9504" - ), defaultRepository( address = "https://molly.im/fdroid/foss/fdroid/repo", name = "Molly", @@ -217,7 +209,7 @@ data class Repository( name = "Kali Nethunter", description = "Kali Nethunter's official selection of original b" + "inaries.", - fingerprint = "7E418D34C3AD4F3C37D7E6B0FACE13332364459C862134EB099A3BDA2CCF4494" + fingerprint = "FE7A23DFC003A1CF2D2ADD2469B9C0C49B206BA5DC9EDD6563B3B7EB6A8F5FAB" ), defaultRepository( address = "https://secfirst.org/fdroid/repo", @@ -265,14 +257,14 @@ data class Repository( name = "Threema Libre", description = "The official repository for Threema Libre. R" + "equires Threema Shop license. Threema Libre is an open" + - "-source messanger focused on security and privacy.", + "-source messenger focused on security and privacy.", fingerprint = "5734E753899B25775D90FE85362A49866E05AC4F83C05BEF5A92880D2910639E" ), defaultRepository( address = "https://fdroid.getsession.org/fdroid/repo", name = "Session", description = "The official repository for Session. Session" + - " is an open-source messanger focused on security and privacy.", + " is an open-source messenger focused on security and privacy.", fingerprint = "DB0E5297EB65CC22D6BD93C869943BDCFCB6A07DC69A48A0DD8C7BA698EC04E6" ), defaultRepository( @@ -398,7 +390,7 @@ data class Repository( ), ) - val newlyAdded = listOf( + val newlyAdded: List = listOf( defaultRepository( address = "https://fdroid.ironfoxoss.org/fdroid/repo", name = "IronFox", @@ -412,6 +404,14 @@ data class Repository( description = "The official repository for Total Commander", fingerprint = "3576596CECDD70488D61CFD90799A49B7FFD26A81A8FEF1BADEC88D069FA72C1" ), + defaultRepository( + address = "https://www.cromite.org/fdroid/repo", + name = "Cromite", + description = "The official repository for Cromite. " + + "Cromite is a Chromium fork based on Bromite with " + + "built-in support for ad blocking and an eye for privacy.", + fingerprint = "49F37E74DEE483DCA2B991334FB5A0200787430D0B5F9A783DD5F13695E9517B" + ) ) } } diff --git a/app/src/main/kotlin/com/looker/droidify/network/KtorDownloader.kt b/app/src/main/kotlin/com/looker/droidify/network/KtorDownloader.kt index 3939ccfb..88e91c6a 100644 --- a/app/src/main/kotlin/com/looker/droidify/network/KtorDownloader.kt +++ b/app/src/main/kotlin/com/looker/droidify/network/KtorDownloader.kt @@ -136,7 +136,7 @@ internal class KtorDownloader( private const val CONNECTION_TIMEOUT = 30_000L private const val SOCKET_TIMEOUT = 15_000L -private const val USER_AGENT = "Droid-ify, ${BuildConfig.VERSION_NAME}" +private const val USER_AGENT = "Droid-ify/${BuildConfig.VERSION_NAME}-${BuildConfig.BUILD_TYPE}" private fun HttpClientConfig<*>.userAgentConfig() = install(UserAgent) { agent = USER_AGENT diff --git a/app/src/main/kotlin/com/looker/droidify/service/SyncService.kt b/app/src/main/kotlin/com/looker/droidify/service/SyncService.kt index f94f103a..18afd196 100644 --- a/app/src/main/kotlin/com/looker/droidify/service/SyncService.kt +++ b/app/src/main/kotlin/com/looker/droidify/service/SyncService.kt @@ -22,7 +22,6 @@ import com.looker.droidify.utility.common.extension.getColorFromAttr import com.looker.droidify.utility.common.extension.notificationManager import com.looker.droidify.utility.common.extension.startServiceCompat import com.looker.droidify.utility.common.extension.stopForegroundCompat -import com.looker.droidify.utility.common.log import com.looker.droidify.utility.common.result.Result import com.looker.droidify.utility.common.sdkAbove import com.looker.droidify.datastore.SettingsRepository @@ -53,6 +52,8 @@ import kotlinx.coroutines.withContext import java.lang.ref.WeakReference import javax.inject.Inject import com.looker.droidify.R +import kotlinx.coroutines.FlowPreview +import kotlin.math.roundToInt import android.R as AndroidR import com.looker.droidify.R.string as stringRes import com.looker.droidify.R.style as styleRes @@ -69,15 +70,16 @@ class SyncService : ConnectionService() { private const val MAX_UPDATE_NOTIFICATION = 5 private const val ACTION_CANCEL = "${BuildConfig.APPLICATION_ID}.intent.action.CANCEL" - private val syncState = MutableSharedFlow() + val syncState = MutableSharedFlow() } @Inject lateinit var settingsRepository: SettingsRepository sealed class State(val name: String) { - data class Connecting(val appName: String) : State(appName) - data class Syncing( + class Connecting(appName: String) : State(appName) + + class Syncing( val appName: String, val stage: RepositoryUpdater.Stage, val read: DataSize, @@ -85,6 +87,18 @@ class SyncService : ConnectionService() { ) : State(appName) data object Finish : State("") + + val progress: Int + get() = when (this) { + is Connecting -> Int.MIN_VALUE + Finish -> Int.MAX_VALUE + is Syncing -> when(stage) { + RepositoryUpdater.Stage.DOWNLOAD -> ((read percentBy total) * 0.4F).roundToInt() + RepositoryUpdater.Stage.PROCESS -> 50 + RepositoryUpdater.Stage.MERGE -> 75 + RepositoryUpdater.Stage.COMMIT -> 90 + } + } } private class Task(val repositoryId: Long, val manual: Boolean) @@ -145,7 +159,8 @@ class SyncService : ConnectionService() { } suspend fun updateAllApps() { - updateAllAppsInternal() + val skipSignature = settingsRepository.getInitial().ignoreSignature + updateAllAppsInternal(skipSignature) } fun setUpdateNotificationBlocker(fragment: Fragment?) { @@ -198,6 +213,7 @@ class SyncService : ConnectionService() { private val binder = Binder() override fun onBind(intent: Intent): Binder = binder + @OptIn(FlowPreview::class) override fun onCreate() { super.onCreate() @@ -389,7 +405,8 @@ class SyncService : ConnectionService() { handleUpdates( hasUpdates = hasUpdates, notifyUpdates = setting.notifyUpdate, - autoUpdate = setting.autoUpdate + autoUpdate = setting.autoUpdate, + skipSignature = setting.ignoreSignature, ) } } @@ -470,7 +487,8 @@ class SyncService : ConnectionService() { private suspend fun handleUpdates( hasUpdates: Boolean, notifyUpdates: Boolean, - autoUpdate: Boolean + autoUpdate: Boolean, + skipSignature: Boolean, ) { try { if (!hasUpdates) { @@ -481,15 +499,16 @@ class SyncService : ConnectionService() { return } val blocked = updateNotificationBlockerFragment?.get()?.isAdded == true - val updates = Database.ProductAdapter.getUpdates() + val updates = Database.ProductAdapter.getUpdates(skipSignature) if (!blocked && updates.isNotEmpty()) { if (notifyUpdates) displayUpdatesNotification(updates) - if (autoUpdate) updateAllAppsInternal() + if (autoUpdate) updateAllAppsInternal(skipSignature) } handleUpdates( hasUpdates = false, notifyUpdates = notifyUpdates, - autoUpdate = autoUpdate + autoUpdate = autoUpdate, + skipSignature = skipSignature, ) } finally { withContext(NonCancellable) { @@ -499,10 +518,9 @@ class SyncService : ConnectionService() { } } - private suspend fun updateAllAppsInternal() { - log("Check Running", "Syncing") + private suspend fun updateAllAppsInternal(skipSignature: Boolean) { Database.ProductAdapter - .getUpdates() + .getUpdates(skipSignature) // Update Droid-ify the last .sortedBy { if (it.packageName == packageName) 1 else -1 } .map { diff --git a/app/src/main/kotlin/com/looker/droidify/ui/appDetail/AppDetailAdapter.kt b/app/src/main/kotlin/com/looker/droidify/ui/appDetail/AppDetailAdapter.kt index 4f3a2056..ef31a053 100644 --- a/app/src/main/kotlin/com/looker/droidify/ui/appDetail/AppDetailAdapter.kt +++ b/app/src/main/kotlin/com/looker/droidify/ui/appDetail/AppDetailAdapter.kt @@ -40,7 +40,7 @@ import androidx.core.text.util.LinkifyCompat import androidx.core.view.isVisible import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView -import coil.load +import coil3.load import com.google.android.material.button.MaterialButton import com.google.android.material.card.MaterialCardView import com.google.android.material.imageview.ShapeableImageView @@ -56,6 +56,7 @@ import com.looker.droidify.model.Release import com.looker.droidify.model.Repository import com.looker.droidify.model.findSuggested import com.looker.droidify.network.DataSize +import com.looker.droidify.network.percentBy import com.looker.droidify.utility.PackageItemResolver import com.looker.droidify.utility.common.extension.authentication import com.looker.droidify.utility.common.extension.copyToClipboard @@ -69,6 +70,7 @@ import com.looker.droidify.utility.common.extension.inflate import com.looker.droidify.utility.common.extension.open import com.looker.droidify.utility.common.extension.setTextSizeScaled import com.looker.droidify.utility.common.nullIfEmpty +import com.looker.droidify.utility.common.sdkName import com.looker.droidify.utility.extension.android.Android import com.looker.droidify.utility.extension.resources.TypefaceExtra import com.looker.droidify.utility.extension.resources.sizeScaled @@ -101,7 +103,7 @@ class AppDetailAdapter(private val callbacks: Callbacks) : fun onFavouriteClicked() fun onPreferenceChanged(preference: ProductPreference) fun onPermissionsClick(group: String?, permissions: List) - fun onScreenshotClick(screenshot: Product.Screenshot, parentView: ImageView) + fun onScreenshotClick(position: Int) fun onReleaseClick(release: Release) fun onRequestAddRepository(address: String) fun onUriClick(uri: Uri, shouldConfirm: Boolean): Boolean @@ -313,7 +315,6 @@ class AppDetailAdapter(private val callbacks: Callbacks) : is Product.Donate.Regular -> drawableRes.ic_donate is Product.Donate.Bitcoin -> drawableRes.ic_donate_bitcoin is Product.Donate.Litecoin -> drawableRes.ic_donate_litecoin - is Product.Donate.Flattr -> drawableRes.ic_donate_flattr is Product.Donate.Liberapay -> drawableRes.ic_donate_liberapay is Product.Donate.OpenCollective -> drawableRes.ic_donate_opencollective } @@ -322,7 +323,6 @@ class AppDetailAdapter(private val callbacks: Callbacks) : is Product.Donate.Regular -> context.getString(stringRes.website) is Product.Donate.Bitcoin -> "Bitcoin" is Product.Donate.Litecoin -> "Litecoin" - is Product.Donate.Flattr -> "Flattr" is Product.Donate.Liberapay -> "Liberapay" is Product.Donate.OpenCollective -> "Open Collective" } @@ -331,12 +331,8 @@ class AppDetailAdapter(private val callbacks: Callbacks) : is Product.Donate.Regular -> Uri.parse(donate.url) is Product.Donate.Bitcoin -> Uri.parse("bitcoin:${donate.address}") is Product.Donate.Litecoin -> Uri.parse("litecoin:${donate.address}") - is Product.Donate.Flattr -> Uri.parse( - "https://flattr.com/thing/${donate.id}" - ) - is Product.Donate.Liberapay -> Uri.parse( - "https://liberapay.com/~${donate.id}" + "https://liberapay.com/${donate.id}" ) is Product.Donate.OpenCollective -> Uri.parse( @@ -561,6 +557,7 @@ class AppDetailAdapter(private val callbacks: Callbacks) : val size = itemView.findViewById(R.id.size)!! val signature = itemView.findViewById(R.id.signature)!! val compatibility = itemView.findViewById(R.id.compatibility)!! + val sdkVer = itemView.findViewById(R.id.sdk_ver)!! val statefulViews: Sequence get() = sequenceOf( @@ -571,7 +568,8 @@ class AppDetailAdapter(private val callbacks: Callbacks) : added, size, signature, - compatibility + compatibility, + sdkVer, ) } @@ -1365,12 +1363,10 @@ class AppDetailAdapter(private val callbacks: Callbacks) : ) holder.progress.isIndeterminate = status.total == null if (status.total != null) { - holder.progress.progress = - ( - holder.progress.max.toFloat() * - status.read.value / - status.total.value - ).roundToInt() + holder.progress.setProgressCompat( + status.read.value percentBy status.total.value, + true + ) } } @@ -1430,17 +1426,15 @@ class AppDetailAdapter(private val callbacks: Callbacks) : holder as ScreenShotViewHolder item as Item.ScreenshotItem holder.screenshotsRecycler.run { + setHasFixedSize(true) isNestedScrollingEnabled = false clipToPadding = false setPadding(8.dp, 8.dp, 8.dp, 8.dp) layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false) - adapter = - ScreenshotsAdapter { screenshot, view -> - callbacks.onScreenshotClick(screenshot, view) - }.apply { - setScreenshots(item.repository, item.packageName, item.screenshots) - } + adapter = ScreenshotsAdapter(callbacks::onScreenshotClick).apply { + setScreenshots(item.repository, item.packageName, item.screenshots) + } } } @@ -1626,7 +1620,7 @@ class AppDetailAdapter(private val callbacks: Callbacks) : holder.version.text = context.getString(stringRes.version_FORMAT, item.release.version) - holder.status.apply { + with(holder.status) { isVisible = installed || suggested setText( when { @@ -1637,14 +1631,15 @@ class AppDetailAdapter(private val callbacks: Callbacks) : ) background = context.corneredBackground setPadding(15, 15, 15, 15) - val (background, foreground) = if (installed) { - MaterialR.attr.colorSecondaryContainer to MaterialR.attr.colorOnSecondaryContainer + if (installed) { + backgroundTintList = + context.getColorFromAttr(MaterialR.attr.colorSecondaryContainer) + setTextColor(context.getColorFromAttr(MaterialR.attr.colorOnSecondaryContainer)) } else { - MaterialR.attr.colorPrimaryContainer to MaterialR.attr.colorOnPrimaryContainer + backgroundTintList = + context.getColorFromAttr(MaterialR.attr.colorPrimaryContainer) + setTextColor(context.getColorFromAttr(MaterialR.attr.colorOnPrimaryContainer)) } - backgroundTintList = - context.getColorFromAttr(background) - setTextColor(context.getColorFromAttr(foreground)) } holder.source.text = context.getString(stringRes.provided_by_FORMAT, item.repository.name) @@ -1688,35 +1683,51 @@ class AppDetailAdapter(private val callbacks: Callbacks) : } holder.signature.text = builder } - holder.compatibility.isVisible = incompatibility != null || singlePlatform != null - if (incompatibility != null) { - holder.compatibility.setTextColor( - context.getColorFromAttr(MaterialR.attr.colorError) - ) - holder.compatibility.text = when (incompatibility) { - is Release.Incompatibility.MinSdk, - is Release.Incompatibility.MaxSdk - -> context.getString( - stringRes.incompatible_with_FORMAT, - Android.name - ) + with(holder.compatibility) { + isVisible = incompatibility != null || singlePlatform != null + if (incompatibility != null) { + setTextColor(context.getColorFromAttr(MaterialR.attr.colorError)) + text = when (incompatibility) { + is Release.Incompatibility.MinSdk, + is Release.Incompatibility.MaxSdk -> context.getString( + stringRes.incompatible_with_FORMAT, + Android.name + ) - is Release.Incompatibility.Platform -> context.getString( - stringRes.incompatible_with_FORMAT, - Android.primaryPlatform ?: context.getString(stringRes.unknown) - ) + is Release.Incompatibility.Platform -> context.getString( + stringRes.incompatible_with_FORMAT, + Android.primaryPlatform ?: context.getString(stringRes.unknown) + ) - is Release.Incompatibility.Feature -> context.getString( - stringRes.requires_FORMAT, - incompatibility.feature + is Release.Incompatibility.Feature -> context.getString( + stringRes.requires_FORMAT, + incompatibility.feature + ) + } + } else if (singlePlatform != null) { + setTextColor(context.getColorFromAttr(android.R.attr.textColorSecondary)) + text = context.getString( + stringRes.only_compatible_with_FORMAT, + singlePlatform, ) } - } else if (singlePlatform != null) { - holder.compatibility.setTextColor( - context.getColorFromAttr(android.R.attr.textColorSecondary) + } + with(holder.sdkVer) { + val targetSdkVersion = sdkName.getOrDefault( + item.release.targetSdkVersion, + context.getString( + stringRes.label_unknown_sdk, + item.release.targetSdkVersion, + ), ) - holder.compatibility.text = - context.getString(stringRes.only_compatible_with_FORMAT, singlePlatform) + val minSdkVersion = sdkName.getOrDefault( + item.release.minSdkVersion, + context.getString( + stringRes.label_unknown_sdk, + item.release.minSdkVersion, + ), + ) + text = context.getString(stringRes.label_sdk_version, targetSdkVersion, minSdkVersion) } val enabled = status == Status.Idle holder.statefulViews.forEach { it.isEnabled = enabled } diff --git a/app/src/main/kotlin/com/looker/droidify/ui/appDetail/AppDetailFragment.kt b/app/src/main/kotlin/com/looker/droidify/ui/appDetail/AppDetailFragment.kt index ab17d868..d15c7cc9 100644 --- a/app/src/main/kotlin/com/looker/droidify/ui/appDetail/AppDetailFragment.kt +++ b/app/src/main/kotlin/com/looker/droidify/ui/appDetail/AppDetailFragment.kt @@ -1,5 +1,6 @@ package com.looker.droidify.ui.appDetail +import android.annotation.SuppressLint import android.content.ActivityNotFoundException import android.content.ComponentName import android.content.Intent @@ -8,7 +9,6 @@ import android.os.Bundle import android.provider.Settings import android.view.MenuItem import android.view.View -import android.widget.ImageView import androidx.appcompat.app.AlertDialog import androidx.core.net.toUri import androidx.core.os.bundleOf @@ -20,16 +20,13 @@ import androidx.lifecycle.repeatOnLifecycle import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.SimpleItemAnimator -import coil.load +import coil3.load +import coil3.request.allowHardware import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.looker.droidify.utility.common.cache.Cache -import com.looker.droidify.utility.common.extension.getLauncherActivities -import com.looker.droidify.utility.common.extension.getMutatedIcon -import com.looker.droidify.utility.common.extension.isFirstItemVisible -import com.looker.droidify.utility.common.extension.isSystemApplication -import com.looker.droidify.utility.common.extension.systemBarsPadding -import com.looker.droidify.utility.common.extension.updateAsMutable import com.looker.droidify.content.ProductPreferences +import com.looker.droidify.installer.installers.launchShizuku +import com.looker.droidify.installer.model.InstallState +import com.looker.droidify.installer.model.isCancellable import com.looker.droidify.model.InstalledItem import com.looker.droidify.model.Product import com.looker.droidify.model.ProductPreference @@ -43,11 +40,15 @@ import com.looker.droidify.ui.MessageDialog import com.looker.droidify.ui.ScreenFragment import com.looker.droidify.ui.appDetail.AppDetailViewModel.Companion.ARG_PACKAGE_NAME import com.looker.droidify.ui.appDetail.AppDetailViewModel.Companion.ARG_REPO_ADDRESS -import com.looker.droidify.utility.extension.screenActivity +import com.looker.droidify.utility.common.cache.Cache +import com.looker.droidify.utility.common.extension.getLauncherActivities +import com.looker.droidify.utility.common.extension.getMutatedIcon +import com.looker.droidify.utility.common.extension.isFirstItemVisible +import com.looker.droidify.utility.common.extension.isSystemApplication +import com.looker.droidify.utility.common.extension.systemBarsPadding +import com.looker.droidify.utility.common.extension.updateAsMutable +import com.looker.droidify.utility.extension.mainActivity import com.looker.droidify.utility.extension.startUpdate -import com.looker.droidify.installer.installers.launchShizuku -import com.looker.droidify.installer.model.InstallState -import com.looker.droidify.installer.model.isCancellable import com.stfalcon.imageviewer.StfalconImageViewer import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.delay @@ -89,6 +90,7 @@ class AppDetailFragment() : ScreenFragment(), AppDetailAdapter.Callbacks { private val viewModel: AppDetailViewModel by viewModels() + @SuppressLint("RestrictedApi") private var layoutManagerState: LinearLayoutManager.SavedState? = null private var actions = Pair(emptySet(), null as Action?) @@ -99,6 +101,7 @@ class AppDetailFragment() : ScreenFragment(), AppDetailAdapter.Callbacks { private var recyclerView: RecyclerView? = null private var detailAdapter: AppDetailAdapter? = null + private var imageViewer: StfalconImageViewer.Builder? = null private val downloadConnection = Connection( serviceClass = DownloadService::class.java, @@ -109,11 +112,12 @@ class AppDetailFragment() : ScreenFragment(), AppDetailAdapter.Callbacks { } ) + @SuppressLint("RestrictedApi") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) detailAdapter = AppDetailAdapter(this@AppDetailFragment) - screenActivity.onToolbarCreated(toolbar) + mainActivity.onToolbarCreated(toolbar) toolbar.menu.apply { Action.entries.forEach { action -> add(0, action.id, 0, action.adapterAction.titleResId) @@ -205,10 +209,12 @@ class AppDetailFragment() : ScreenFragment(), AppDetailAdapter.Callbacks { super.onDestroyView() recyclerView = null detailAdapter = null + imageViewer = null downloadConnection.unbind(requireContext()) } + @SuppressLint("RestrictedApi") override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) @@ -446,20 +452,27 @@ class AppDetailFragment() : ScreenFragment(), AppDetailAdapter.Callbacks { .show(childFragmentManager) } - override fun onScreenshotClick(screenshot: Product.Screenshot, parentView: ImageView) { - val product = products - .firstOrNull { (product, _) -> - product.screenshots.find { it === screenshot }?.identifier != null + override fun onScreenshotClick(position: Int) { + if (imageViewer == null) { + val productRepository = products.findSuggested(installed?.installedItem) ?: return + val screenshots = productRepository.first.screenshots.mapNotNull { + if (it.type == Product.Screenshot.Type.VIDEO) null + else it } - ?: return - val screenshots = product.first.screenshots - val position = screenshots.indexOfFirst { screenshot.identifier == it.identifier } - StfalconImageViewer - .Builder(context, screenshots) { view, current -> - view.load(current.url(product.second, viewModel.packageName)) - } - .withStartPosition(position) - .show() + imageViewer = StfalconImageViewer + .Builder(context, screenshots) { view, current -> + val screenshotUrl = current.url( + context = requireContext(), + repository = productRepository.second, + packageName = viewModel.packageName + ) + view.load(screenshotUrl) { + allowHardware(false) + } + } + } + imageViewer?.withStartPosition(position) + imageViewer?.show() } override fun onReleaseClick(release: Release) { @@ -517,7 +530,7 @@ class AppDetailFragment() : ScreenFragment(), AppDetailAdapter.Callbacks { } override fun onRequestAddRepository(address: String) { - screenActivity.navigateAddRepository(address) + mainActivity.navigateAddRepository(address) } override fun onUriClick(uri: Uri, shouldConfirm: Boolean): Boolean { diff --git a/app/src/main/kotlin/com/looker/droidify/ui/appDetail/ScreenshotsAdapter.kt b/app/src/main/kotlin/com/looker/droidify/ui/appDetail/ScreenshotsAdapter.kt index a44e590a..29d02fa0 100644 --- a/app/src/main/kotlin/com/looker/droidify/ui/appDetail/ScreenshotsAdapter.kt +++ b/app/src/main/kotlin/com/looker/droidify/ui/appDetail/ScreenshotsAdapter.kt @@ -5,55 +5,71 @@ import android.graphics.drawable.Drawable import android.view.Gravity import android.view.ViewGroup import android.widget.FrameLayout -import android.widget.ImageView import androidx.recyclerview.widget.RecyclerView -import coil.dispose -import coil.load -import coil.size.Dimension -import coil.size.Scale +import coil3.asImage +import coil3.dispose +import coil3.load +import coil3.request.placeholder +import coil3.size.Scale import com.google.android.material.imageview.ShapeableImageView +import com.looker.droidify.databinding.VideoButtonBinding +import com.looker.droidify.graphics.PaddingDrawable +import com.looker.droidify.model.Product +import com.looker.droidify.model.Repository import com.looker.droidify.utility.common.extension.aspectRatio import com.looker.droidify.utility.common.extension.authentication import com.looker.droidify.utility.common.extension.camera import com.looker.droidify.utility.common.extension.dp -import com.looker.droidify.utility.common.extension.dpToPx import com.looker.droidify.utility.common.extension.getColorFromAttr +import com.looker.droidify.utility.common.extension.layoutInflater +import com.looker.droidify.utility.common.extension.openLink import com.looker.droidify.utility.common.extension.selectableBackground -import com.looker.droidify.graphics.PaddingDrawable -import com.looker.droidify.model.Product -import com.looker.droidify.model.Repository import com.looker.droidify.widget.StableRecyclerAdapter import com.google.android.material.R as MaterialR import com.looker.droidify.R.dimen as dimenRes -class ScreenshotsAdapter(private val onClick: (Product.Screenshot, ImageView) -> Unit) : +class ScreenshotsAdapter(private val onClick: (position: Int) -> Unit) : StableRecyclerAdapter() { - enum class ViewType { SCREENSHOT } + enum class ViewType { SCREENSHOT, VIDEO } - private val items = mutableListOf() + private val items = mutableListOf() - private class ViewHolder(context: Context) : - RecyclerView.ViewHolder(FrameLayout(context)) { - val image: ShapeableImageView = object : ShapeableImageView(context) { - override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { - super.onMeasure(widthMeasureSpec, heightMeasureSpec) - setMeasuredDimension(measuredWidth, measuredHeight) + private inner class VideoViewHolder( + binding: VideoButtonBinding, + ) : RecyclerView.ViewHolder(binding.root) { + val button = binding.videoButton + + init { + with(button) { + layoutParams = RecyclerView.LayoutParams( + RecyclerView.LayoutParams.WRAP_CONTENT, + 150.dp, + ) + setOnClickListener { + val item = items[absoluteAdapterPosition] as Item.VideoItem + it.context?.openLink(item.videoUrl) + } } } + } + + + private inner class ScreenshotViewHolder( + context: Context, + ) : RecyclerView.ViewHolder(FrameLayout(context)) { + val image = ShapeableImageView(context) val placeholderColor = context.getColorFromAttr(MaterialR.attr.colorPrimaryContainer) val radius = context.resources.getDimension(dimenRes.shape_small_corner) val imageShapeModel = image.shapeAppearanceModel.toBuilder() .setAllCornerSizes(radius) .build() - val cameraIcon = context.camera - .apply { setTintList(placeholderColor) } + val cameraIcon = context.camera.apply { setTintList(placeholderColor) } val placeholder: Drawable = PaddingDrawable(cameraIcon, 3f, context.aspectRatio) init { with(image) { layout(0, 0, 0, 0) - adjustViewBounds = true shapeAppearanceModel = imageShapeModel background = context.selectableBackground isFocusable = true @@ -68,6 +84,14 @@ class ScreenshotsAdapter(private val onClick: (Product.Screenshot, ImageView) -> marginEnd = radius.toInt() } foregroundGravity = Gravity.CENTER + setOnClickListener { + val position = if (items.any { it.viewType == ViewType.VIDEO }) { + absoluteAdapterPosition - 1 + } else { + absoluteAdapterPosition + } + onClick(position) + } } } } @@ -75,67 +99,73 @@ class ScreenshotsAdapter(private val onClick: (Product.Screenshot, ImageView) -> fun setScreenshots( repository: Repository, packageName: String, - screenshots: List + screenshots: List, ) { items.clear() - items += screenshots.map { Item.ScreenshotItem(repository, packageName, it) } + items += screenshots.map { + if (it.type == Product.Screenshot.Type.VIDEO) Item.VideoItem(it.path) + else Item.ScreenshotItem(repository, packageName, it) + } notifyItemRangeInserted(0, screenshots.size) } - override val viewTypeClass: Class - get() = ViewType::class.java - - override fun getItemEnumViewType(position: Int): ViewType { - return ViewType.SCREENSHOT - } + override val viewTypeClass: Class get() = ViewType::class.java + override fun getItemCount(): Int = items.size + override fun getItemEnumViewType(position: Int) = items[position].viewType + override fun getItemDescriptor(position: Int): String = items[position].descriptor override fun onCreateViewHolder( parent: ViewGroup, - viewType: ViewType + viewType: ViewType, ): RecyclerView.ViewHolder { - return ViewHolder(parent.context).apply { - image.setOnClickListener { - onClick( - items[absoluteAdapterPosition].screenshot, - it as ImageView - ) - } + return when (viewType) { + ViewType.VIDEO -> VideoViewHolder(VideoButtonBinding.inflate(parent.context.layoutInflater)) + ViewType.SCREENSHOT -> ScreenshotViewHolder(parent.context) } } - override fun getItemDescriptor(position: Int): String = items[position].descriptor - override fun getItemCount(): Int = items.size - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - holder as ViewHolder - val item = items[position] - with(holder.image) { - load(item.screenshot.url(item.repository, item.packageName)) { - size(Dimension.Undefined, Dimension(150.dp.dpToPx.toInt())) - scale(Scale.FIT) - placeholder(holder.placeholder) - error(holder.placeholder) - authentication(item.repository.authentication) + when (getItemEnumViewType(position)) { + ViewType.SCREENSHOT -> { + holder as ScreenshotViewHolder + val item = items[position] as Item.ScreenshotItem + with(holder.image) { + load(item.screenshot.url(context, item.repository, item.packageName)) { + authentication(item.repository.authentication) + scale(Scale.FILL) + placeholder(holder.placeholder) + error(holder.placeholder.asImage()) + } + } } + + ViewType.VIDEO -> {} } } override fun onViewRecycled(holder: RecyclerView.ViewHolder) { super.onViewRecycled(holder) - holder as ViewHolder - holder.image.dispose() + if (holder is ScreenshotViewHolder) holder.image.dispose() } - private sealed class Item { - abstract val descriptor: String + private sealed interface Item { + + val descriptor: String + val viewType: ViewType class ScreenshotItem( val repository: Repository, val packageName: String, - val screenshot: Product.Screenshot - ) : Item() { + val screenshot: Product.Screenshot, + ) : Item { + override val viewType: ViewType get() = ViewType.SCREENSHOT override val descriptor: String get() = "screenshot.${repository.id}.${screenshot.identifier}" } + + class VideoItem(val videoUrl: String) : Item { + override val viewType: ViewType get() = ViewType.VIDEO + override val descriptor: String get() = "video" + } } } diff --git a/app/src/main/kotlin/com/looker/droidify/ui/appList/AppListAdapter.kt b/app/src/main/kotlin/com/looker/droidify/ui/appList/AppListAdapter.kt index ade4a324..3450508d 100644 --- a/app/src/main/kotlin/com/looker/droidify/ui/appList/AppListAdapter.kt +++ b/app/src/main/kotlin/com/looker/droidify/ui/appList/AppListAdapter.kt @@ -2,6 +2,7 @@ package com.looker.droidify.ui.appList import android.annotation.SuppressLint import android.content.Context +import android.content.res.ColorStateList import android.view.Gravity import android.view.View import android.view.ViewGroup @@ -9,7 +10,7 @@ import android.widget.FrameLayout import android.widget.TextView import androidx.core.view.isVisible import androidx.recyclerview.widget.RecyclerView -import coil.load +import coil3.load import com.google.android.material.imageview.ShapeableImageView import com.google.android.material.progressindicator.CircularProgressIndicator import com.looker.droidify.R @@ -22,23 +23,31 @@ import com.looker.droidify.utility.common.extension.dp import com.looker.droidify.utility.common.extension.getColorFromAttr import com.looker.droidify.utility.common.extension.inflate import com.looker.droidify.utility.common.extension.setTextSizeScaled +import com.looker.droidify.utility.common.log import com.looker.droidify.utility.common.nullIfEmpty import com.looker.droidify.utility.extension.resources.TypefaceExtra import com.looker.droidify.widget.CursorRecyclerAdapter +import kotlin.system.measureTimeMillis import com.google.android.material.R as MaterialR class AppListAdapter( private val source: AppListFragment.Source, - private val onClick: (ProductItem) -> Unit + private val onClick: (packageName: String) -> Unit, ) : CursorRecyclerAdapter() { enum class ViewType { PRODUCT, LOADING, EMPTY } - private class ProductViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + private inner class ProductViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { val name = itemView.findViewById(R.id.name)!! val status = itemView.findViewById(R.id.status)!! val summary = itemView.findViewById(R.id.summary)!! val icon = itemView.findViewById(R.id.icon)!! + + init { + itemView.setOnClickListener { + log(measureTimeMillis { onClick(getPackageName(absoluteAdapterPosition)) }, "Bench") + } + } } private class LoadingViewHolder(context: Context) : @@ -82,12 +91,14 @@ class AppListAdapter( } } - var repositories: Map = emptyMap() - @SuppressLint("NotifyDataSetChanged") - set(value) { - field = value - notifyDataSetChanged() + private val repositories: HashMap = HashMap() + + fun updateRepos(repos: List) { + repos.forEach { + repositories[it.id] = it } + notifyDataSetChanged() + } var emptyText: String = "" @SuppressLint("NotifyDataSetChanged") @@ -117,24 +128,31 @@ class AppListAdapter( } } + private fun getPackageName(position: Int): String { + return Database.ProductAdapter.transformPackageName(moveTo(position.coerceAtLeast(0))) + } + private fun getProductItem(position: Int): ProductItem { return Database.ProductAdapter.transformItem(moveTo(position.coerceAtLeast(0))) } override fun onCreateViewHolder( parent: ViewGroup, - viewType: ViewType + viewType: ViewType, ): RecyclerView.ViewHolder { return when (viewType) { - ViewType.PRODUCT -> ProductViewHolder(parent.inflate(R.layout.product_item)).apply { - itemView.setOnClickListener { onClick(getProductItem(absoluteAdapterPosition)) } - } - + ViewType.PRODUCT -> ProductViewHolder(parent.inflate(R.layout.product_item)) ViewType.LOADING -> LoadingViewHolder(parent.context) ViewType.EMPTY -> EmptyViewHolder(parent.context) } } + private var updateBackground: ColorStateList? = null + private var updateForeground: ColorStateList? = null + private var installedBackground: ColorStateList? = null + private var installedForeground: ColorStateList? = null + private var defaultForeground: ColorStateList? = null + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { when (getItemEnumViewType(position)) { ViewType.PRODUCT -> { @@ -142,9 +160,9 @@ class AppListAdapter( val productItem = getProductItem(position) holder.name.text = productItem.name holder.summary.text = productItem.summary - holder.summary.isVisible = - productItem.summary.isNotEmpty() && productItem.name != productItem.summary - val repository: Repository? = repositories[productItem.repositoryId] + holder.summary.isVisible = productItem.summary.isNotEmpty() + && productItem.name != productItem.summary + val repository = repositories[productItem.repositoryId] if (repository != null) { val iconUrl = productItem.icon(view = holder.icon, repository = repository) holder.icon.load(iconUrl) { @@ -161,28 +179,38 @@ class AppListAdapter( val isInstalled = productItem.installedVersion.nullIfEmpty() != null when { productItem.canUpdate -> { - backgroundTintList = - context.getColorFromAttr(MaterialR.attr.colorTertiaryContainer) - setTextColor( - context.getColorFromAttr(MaterialR.attr.colorOnTertiaryContainer) - ) + if (updateBackground == null) { + updateBackground = + context.getColorFromAttr(MaterialR.attr.colorTertiaryContainer) + } + if (updateForeground == null) { + updateForeground = + context.getColorFromAttr(MaterialR.attr.colorOnTertiaryContainer) + } + backgroundTintList = updateBackground + setTextColor(updateForeground) } isInstalled -> { - backgroundTintList = - context.getColorFromAttr(MaterialR.attr.colorSecondaryContainer) - setTextColor( - context.getColorFromAttr(MaterialR.attr.colorOnSecondaryContainer) - ) + if (installedBackground == null) { + installedBackground = + context.getColorFromAttr(MaterialR.attr.colorSecondaryContainer) + } + if (installedForeground == null) { + installedForeground = + context.getColorFromAttr(MaterialR.attr.colorOnSecondaryContainer) + } + backgroundTintList = installedBackground + setTextColor(installedForeground) } else -> { setPadding(0, 0, 0, 0) - setTextColor( - holder.status.context.getColorFromAttr( - MaterialR.attr.colorOnBackground - ) - ) + if (defaultForeground == null) { + defaultForeground = + context.getColorFromAttr(MaterialR.attr.colorOnBackground) + } + setTextColor(defaultForeground) background = null return@with } @@ -191,9 +219,9 @@ class AppListAdapter( 6.dp.let { setPadding(it, it, it, it) } } val enabled = productItem.compatible || productItem.installedVersion.isNotEmpty() - sequenceOf(holder.name, holder.status, holder.summary).forEach { - it.isEnabled = enabled - } + holder.name.isEnabled = enabled + holder.status.isEnabled = enabled + holder.summary.isEnabled = enabled } ViewType.LOADING -> { @@ -204,6 +232,6 @@ class AppListAdapter( holder as EmptyViewHolder holder.text.text = emptyText } - }::class + } } } diff --git a/app/src/main/kotlin/com/looker/droidify/ui/appList/AppListFragment.kt b/app/src/main/kotlin/com/looker/droidify/ui/appList/AppListFragment.kt index 4591d1ff..0c7a7d74 100644 --- a/app/src/main/kotlin/com/looker/droidify/ui/appList/AppListFragment.kt +++ b/app/src/main/kotlin/com/looker/droidify/ui/appList/AppListFragment.kt @@ -14,19 +14,19 @@ import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView -import com.looker.droidify.utility.common.Scroller import com.looker.droidify.R -import com.looker.droidify.R.string as stringRes +import com.looker.droidify.database.CursorOwner +import com.looker.droidify.databinding.RecyclerViewWithFabBinding +import com.looker.droidify.model.ProductItem +import com.looker.droidify.utility.common.Scroller import com.looker.droidify.utility.common.extension.dp import com.looker.droidify.utility.common.extension.isFirstItemVisible import com.looker.droidify.utility.common.extension.systemBarsMargin import com.looker.droidify.utility.common.extension.systemBarsPadding -import com.looker.droidify.model.ProductItem -import com.looker.droidify.database.CursorOwner -import com.looker.droidify.databinding.RecyclerViewWithFabBinding -import com.looker.droidify.utility.extension.screenActivity +import com.looker.droidify.utility.extension.mainActivity import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.launch +import com.looker.droidify.R.string as stringRes @AndroidEntryPoint class AppListFragment() : Fragment(), CursorOwner.Callback { @@ -46,7 +46,7 @@ class AppListFragment() : Fragment(), CursorOwner.Callback { val titleResId: Int, val sections: Boolean, val order: Boolean, - val updateAll: Boolean + val updateAll: Boolean, ) { AVAILABLE(stringRes.available, true, true, false), INSTALLED(stringRes.installed, false, true, false), @@ -63,14 +63,15 @@ class AppListFragment() : Fragment(), CursorOwner.Callback { get() = requireArguments().getString(EXTRA_SOURCE)!!.let(Source::valueOf) private lateinit var recyclerView: RecyclerView - private lateinit var recyclerViewAdapter: AppListAdapter + private lateinit var appListAdapter: AppListAdapter + private var scroller: Scroller? = null private var shortAnimationDuration: Int = 0 private var layoutManagerState: Parcelable? = null override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? + savedInstanceState: Bundle?, ): View { _binding = RecyclerViewWithFabBinding.inflate(inflater, container, false) @@ -83,10 +84,8 @@ class AppListFragment() : Fragment(), CursorOwner.Callback { isMotionEventSplittingEnabled = false setHasFixedSize(true) recycledViewPool.setMaxRecycledViews(AppListAdapter.ViewType.PRODUCT.ordinal, 30) - recyclerViewAdapter = AppListAdapter(source) { - screenActivity.navigateProduct(it.packageName) - } - adapter = recyclerViewAdapter + appListAdapter = AppListAdapter(source, mainActivity::navigateProduct) + adapter = appListAdapter systemBarsPadding() } val fab = binding.scrollUp @@ -103,11 +102,13 @@ class AppListFragment() : Fragment(), CursorOwner.Callback { } systemBarsMargin(16.dp) } else { - text = "" + text = null setIconResource(R.drawable.arrow_up) setOnClickListener { - val scroller = Scroller(requireContext()) - scroller.targetPosition = 0 + if (scroller == null) { + scroller = Scroller(requireContext()) + } + scroller!!.targetPosition = 0 recyclerView.layoutManager?.startSmoothScroll(scroller) } alpha = 0f @@ -138,7 +139,7 @@ class AppListFragment() : Fragment(), CursorOwner.Callback { repeatOnLifecycle(Lifecycle.State.RESUMED) { launch { viewModel.reposStream.collect { repos -> - recyclerViewAdapter.repositories = repos.associateBy { it.id } + appListAdapter.updateRepos(repos) } } launch { @@ -160,12 +161,13 @@ class AppListFragment() : Fragment(), CursorOwner.Callback { super.onDestroyView() viewModel.syncConnection.unbind(requireContext()) _binding = null - screenActivity.cursorOwner.detach(this) + scroller = null + mainActivity.cursorOwner.detach(this) } override fun onCursorData(request: CursorOwner.Request, cursor: Cursor?) { - recyclerViewAdapter.cursor = cursor - recyclerViewAdapter.emptyText = when { + appListAdapter.cursor = cursor + appListAdapter.emptyText = when { cursor == null -> "" viewModel.searchQuery.value.isNotEmpty() -> { getString(stringRes.no_matching_applications_found) @@ -197,7 +199,7 @@ class AppListFragment() : Fragment(), CursorOwner.Callback { private fun updateRequest() { if (view != null) { - screenActivity.cursorOwner.attach(this, viewModel.request(source)) + mainActivity.cursorOwner.attach(this, viewModel.request(source)) } } } diff --git a/app/src/main/kotlin/com/looker/droidify/ui/appList/AppListViewModel.kt b/app/src/main/kotlin/com/looker/droidify/ui/appList/AppListViewModel.kt index 076a2084..18605ee3 100644 --- a/app/src/main/kotlin/com/looker/droidify/ui/appList/AppListViewModel.kt +++ b/app/src/main/kotlin/com/looker/droidify/ui/appList/AppListViewModel.kt @@ -2,40 +2,52 @@ package com.looker.droidify.ui.appList import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.looker.droidify.utility.common.extension.asStateFlow +import com.looker.droidify.database.CursorOwner +import com.looker.droidify.database.CursorOwner.Request.Available +import com.looker.droidify.database.CursorOwner.Request.Installed +import com.looker.droidify.database.CursorOwner.Request.Updates +import com.looker.droidify.database.Database import com.looker.droidify.datastore.SettingsRepository import com.looker.droidify.datastore.get import com.looker.droidify.datastore.model.SortOrder import com.looker.droidify.model.ProductItem import com.looker.droidify.model.ProductItem.Section.All -import com.looker.droidify.database.CursorOwner -import com.looker.droidify.database.Database import com.looker.droidify.service.Connection import com.looker.droidify.service.SyncService +import com.looker.droidify.utility.common.extension.asStateFlow import dagger.hilt.android.lifecycle.HiltViewModel -import javax.inject.Inject import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.flatMapConcat import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch +import javax.inject.Inject @HiltViewModel class AppListViewModel @Inject constructor( - settingsRepository: SettingsRepository + settingsRepository: SettingsRepository, ) : ViewModel() { + private val skipSignatureStream = settingsRepository + .get { ignoreSignature } + .asStateFlow(false) + + val sortOrderFlow = settingsRepository + .get { sortOrder } + .asStateFlow(SortOrder.UPDATED) + val reposStream = Database.RepositoryAdapter .getAllStream() .asStateFlow(emptyList()) - val showUpdateAllButton = Database.ProductAdapter - .getUpdatesStream() - .map { it.isNotEmpty() } - .asStateFlow(false) - - val sortOrderFlow = settingsRepository.get { sortOrder } - .asStateFlow(SortOrder.UPDATED) + @OptIn(ExperimentalCoroutinesApi::class) + val showUpdateAllButton = skipSignatureStream.flatMapConcat { skip -> + Database.ProductAdapter + .getUpdatesStream(skip) + .map { it.isNotEmpty() } + }.asStateFlow(false) private val sections = MutableStateFlow(All) @@ -51,22 +63,23 @@ class AppListViewModel fun request(source: AppListFragment.Source): CursorOwner.Request { return when (source) { - AppListFragment.Source.AVAILABLE -> CursorOwner.Request.ProductsAvailable( - searchQuery.value, - sections.value, - sortOrderFlow.value + AppListFragment.Source.AVAILABLE -> Available( + searchQuery = searchQuery.value, + section = sections.value, + order = sortOrderFlow.value, ) - AppListFragment.Source.INSTALLED -> CursorOwner.Request.ProductsInstalled( - searchQuery.value, - sections.value, - sortOrderFlow.value + AppListFragment.Source.INSTALLED -> Installed( + searchQuery = searchQuery.value, + section = sections.value, + order = sortOrderFlow.value, ) - AppListFragment.Source.UPDATES -> CursorOwner.Request.ProductsUpdates( - searchQuery.value, - sections.value, - sortOrderFlow.value + AppListFragment.Source.UPDATES -> Updates( + searchQuery = searchQuery.value, + section = sections.value, + order = sortOrderFlow.value, + skipSignatureCheck = skipSignatureStream.value, ) } } diff --git a/app/src/main/kotlin/com/looker/droidify/ui/favourites/FavouriteFragmentAdapter.kt b/app/src/main/kotlin/com/looker/droidify/ui/favourites/FavouriteFragmentAdapter.kt index 7806b9bd..4c0ba3fa 100644 --- a/app/src/main/kotlin/com/looker/droidify/ui/favourites/FavouriteFragmentAdapter.kt +++ b/app/src/main/kotlin/com/looker/droidify/ui/favourites/FavouriteFragmentAdapter.kt @@ -4,7 +4,7 @@ import android.view.LayoutInflater import android.view.ViewGroup import androidx.core.view.isVisible import androidx.recyclerview.widget.RecyclerView -import coil.load +import coil3.load import com.looker.droidify.databinding.ProductItemBinding import com.looker.droidify.model.Product import com.looker.droidify.model.Repository diff --git a/app/src/main/kotlin/com/looker/droidify/ui/favourites/FavouritesFragment.kt b/app/src/main/kotlin/com/looker/droidify/ui/favourites/FavouritesFragment.kt index 53e0212b..d9cff3ef 100644 --- a/app/src/main/kotlin/com/looker/droidify/ui/favourites/FavouritesFragment.kt +++ b/app/src/main/kotlin/com/looker/droidify/ui/favourites/FavouritesFragment.kt @@ -15,7 +15,7 @@ import com.looker.droidify.R import com.looker.droidify.utility.common.extension.systemBarsPadding import com.looker.droidify.database.Database import com.looker.droidify.ui.ScreenFragment -import com.looker.droidify.utility.extension.screenActivity +import com.looker.droidify.utility.extension.mainActivity import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch @@ -43,7 +43,7 @@ class FavouritesFragment : ScreenFragment() { isVerticalScrollBarEnabled = false setHasFixedSize(true) recyclerViewAdapter = - FavouriteFragmentAdapter { screenActivity.navigateProduct(it) } + FavouriteFragmentAdapter { mainActivity.navigateProduct(it) } this.adapter = recyclerViewAdapter systemBarsPadding(includeFab = false) recyclerView = this @@ -74,6 +74,6 @@ class FavouritesFragment : ScreenFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - screenActivity.onToolbarCreated(toolbar) + mainActivity.onToolbarCreated(toolbar) } } diff --git a/app/src/main/kotlin/com/looker/droidify/ui/favourites/FavouritesViewModel.kt b/app/src/main/kotlin/com/looker/droidify/ui/favourites/FavouritesViewModel.kt index 02974487..eb253460 100644 --- a/app/src/main/kotlin/com/looker/droidify/ui/favourites/FavouritesViewModel.kt +++ b/app/src/main/kotlin/com/looker/droidify/ui/favourites/FavouritesViewModel.kt @@ -1,21 +1,19 @@ package com.looker.droidify.ui.favourites import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.looker.droidify.utility.common.extension.asStateFlow +import com.looker.droidify.database.Database import com.looker.droidify.datastore.SettingsRepository import com.looker.droidify.datastore.get import com.looker.droidify.model.Product -import com.looker.droidify.database.Database +import com.looker.droidify.utility.common.extension.asStateFlow import dagger.hilt.android.lifecycle.HiltViewModel -import javax.inject.Inject import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map -import kotlinx.coroutines.launch +import javax.inject.Inject @HiltViewModel class FavouritesViewModel @Inject constructor( - private val settingsRepository: SettingsRepository + settingsRepository: SettingsRepository, ) : ViewModel() { val favouriteApps: StateFlow>> = @@ -27,9 +25,4 @@ class FavouritesViewModel @Inject constructor( } }.asStateFlow(emptyList()) - fun updateFavourites(packageName: String) { - viewModelScope.launch { - settingsRepository.toggleFavourites(packageName) - } - } } diff --git a/app/src/main/kotlin/com/looker/droidify/ui/repository/EditRepositoryFragment.kt b/app/src/main/kotlin/com/looker/droidify/ui/repository/EditRepositoryFragment.kt index 3bc14894..8fb86a9d 100644 --- a/app/src/main/kotlin/com/looker/droidify/ui/repository/EditRepositoryFragment.kt +++ b/app/src/main/kotlin/com/looker/droidify/ui/repository/EditRepositoryFragment.kt @@ -15,26 +15,25 @@ import androidx.fragment.app.DialogFragment import androidx.lifecycle.lifecycleScope import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.snackbar.Snackbar -import com.looker.droidify.utility.common.extension.clipboardManager -import com.looker.droidify.utility.common.extension.get -import com.looker.droidify.utility.common.extension.getMutatedIcon -import com.looker.droidify.utility.common.nullIfEmpty -import com.looker.droidify.model.Repository +import com.looker.droidify.R import com.looker.droidify.database.Database import com.looker.droidify.databinding.EditRepositoryBinding +import com.looker.droidify.model.Repository +import com.looker.droidify.network.Downloader +import com.looker.droidify.network.NetworkResponse import com.looker.droidify.service.Connection import com.looker.droidify.service.SyncService import com.looker.droidify.ui.Message import com.looker.droidify.ui.MessageDialog import com.looker.droidify.ui.ScreenFragment -import com.looker.droidify.utility.extension.screenActivity -import com.looker.droidify.network.Downloader -import com.looker.droidify.network.NetworkResponse +import com.looker.droidify.utility.common.extension.clipboardManager +import com.looker.droidify.utility.common.extension.get +import com.looker.droidify.utility.common.extension.getMutatedIcon +import com.looker.droidify.utility.common.nullIfEmpty +import com.looker.droidify.utility.extension.mainActivity import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch import java.net.URI @@ -44,7 +43,6 @@ import java.nio.charset.Charset import java.util.Locale import javax.inject.Inject import kotlin.math.min -import com.looker.droidify.R import com.looker.droidify.R.string as stringRes @AndroidEntryPoint @@ -82,11 +80,9 @@ class EditRepositoryFragment() : ScreenFragment() { syncConnection.bind(requireContext()) - screenActivity.onToolbarCreated(toolbar) + mainActivity.onToolbarCreated(toolbar) toolbar.title = - getString( - if (repoId != null) stringRes.edit_repository else stringRes.add_repository - ) + getString(if (repoId != null) stringRes.edit_repository else stringRes.add_repository) saveMenuItem = toolbar.menu.add(stringRes.save) .setIcon(toolbar.context.getMutatedIcon(R.drawable.ic_save)) @@ -240,6 +236,8 @@ class EditRepositoryFragment() : ScreenFragment() { saveMenuItem = null syncConnection.unbind(requireContext()) + checkJob?.cancel() + checkJob = null _binding = null } @@ -394,22 +392,22 @@ class EditRepositoryFragment() : ScreenFragment() { } private suspend fun checkAddress( - address: String, + rawAddress: String, authentication: String ): String? = coroutineScope { checkInProgress = true invalidateState() - val allAddresses = addressSuffixes.map { "$address/$it" } + address - val pathCheck = allAddresses.map { - async { - downloader.headCall( - url = "$it/index-v1.jar", + val allAddresses = addressSuffixes.map { "$rawAddress/$it" } + rawAddress + allAddresses + .sortedBy { it.length } + .forEach { address -> + val response = downloader.headCall( + url = "$address/index-v1.jar", headers = { authentication(authentication) } - ) is NetworkResponse.Success + ) + if (response is NetworkResponse.Success) return@coroutineScope address } - } - val indexOfValidAddress = pathCheck.awaitAll().indexOf(true) - allAddresses[indexOfValidAddress].nullIfEmpty() + null } private fun onSaveRepositoryProceedInvalidate( @@ -431,7 +429,7 @@ class EditRepositoryFragment() : ScreenFragment() { if (repositoryId == null && changedRepository.enabled) { binder.sync(changedRepository) } - screenActivity.onBackPressedDispatcher.onBackPressed() + mainActivity.onBackPressedDispatcher.onBackPressed() } } else { invalidateState() @@ -470,6 +468,6 @@ class EditRepositoryFragment() : ScreenFragment() { const val EXTRA_REPOSITORY_ID = "repositoryId" const val EXTRA_REPOSITORY_ADDRESS = "repositoryAddress" - val addressSuffixes = listOf("fdroid/repo", "repo") + val addressSuffixes = arrayOf("fdroid/repo", "repo") } } diff --git a/app/src/main/kotlin/com/looker/droidify/ui/repository/RepositoriesFragment.kt b/app/src/main/kotlin/com/looker/droidify/ui/repository/RepositoriesFragment.kt index d4000e49..5cacb480 100644 --- a/app/src/main/kotlin/com/looker/droidify/ui/repository/RepositoriesFragment.kt +++ b/app/src/main/kotlin/com/looker/droidify/ui/repository/RepositoriesFragment.kt @@ -15,7 +15,7 @@ import com.looker.droidify.databinding.RecyclerViewWithFabBinding import com.looker.droidify.service.Connection import com.looker.droidify.service.SyncService import com.looker.droidify.ui.ScreenFragment -import com.looker.droidify.utility.extension.screenActivity +import com.looker.droidify.utility.extension.mainActivity import com.looker.droidify.widget.addDivider class RepositoriesFragment : ScreenFragment(), CursorOwner.Callback { @@ -36,7 +36,7 @@ class RepositoriesFragment : ScreenFragment(), CursorOwner.Callback { binding.scrollUp.apply { setIconResource(R.drawable.ic_add) setText(R.string.add_repository) - setOnClickListener { screenActivity.navigateAddRepository() } + setOnClickListener { mainActivity.navigateAddRepository() } systemBarsMargin(16.dp) } binding.recyclerView.apply { @@ -44,7 +44,7 @@ class RepositoriesFragment : ScreenFragment(), CursorOwner.Callback { isMotionEventSplittingEnabled = false setHasFixedSize(true) adapter = RepositoriesAdapter( - navigate = { screenActivity.navigateRepository(it.id) } + navigate = { mainActivity.navigateRepository(it.id) } ) { repository, isEnabled -> repository.enabled != isEnabled && syncConnection.binder?.setEnabled(repository, isEnabled) == true @@ -79,8 +79,8 @@ class RepositoriesFragment : ScreenFragment(), CursorOwner.Callback { super.onViewCreated(view, savedInstanceState) syncConnection.bind(requireContext()) - screenActivity.cursorOwner.attach(this, CursorOwner.Request.Repositories) - screenActivity.onToolbarCreated(toolbar) + mainActivity.cursorOwner.attach(this, CursorOwner.Request.Repositories) + mainActivity.onToolbarCreated(toolbar) toolbar.title = getString(R.string.repositories) } @@ -89,7 +89,7 @@ class RepositoriesFragment : ScreenFragment(), CursorOwner.Callback { _binding = null syncConnection.unbind(requireContext()) - screenActivity.cursorOwner.detach(this) + mainActivity.cursorOwner.detach(this) } override fun onCursorData(request: CursorOwner.Request, cursor: Cursor?) { diff --git a/app/src/main/kotlin/com/looker/droidify/ui/repository/RepositoryFragment.kt b/app/src/main/kotlin/com/looker/droidify/ui/repository/RepositoryFragment.kt index 9d8c3ecd..0fe79529 100644 --- a/app/src/main/kotlin/com/looker/droidify/ui/repository/RepositoryFragment.kt +++ b/app/src/main/kotlin/com/looker/droidify/ui/repository/RepositoryFragment.kt @@ -22,7 +22,7 @@ import com.looker.droidify.databinding.RepositoryPageBinding import com.looker.droidify.ui.Message import com.looker.droidify.ui.MessageDialog import com.looker.droidify.ui.ScreenFragment -import com.looker.droidify.utility.extension.screenActivity +import com.looker.droidify.utility.extension.mainActivity import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch @@ -53,7 +53,7 @@ class RepositoryFragment() : ScreenFragment() { super.onCreateView(inflater, container, savedInstanceState) _binding = RepositoryPageBinding.inflate(inflater, container, false) viewModel.bindService(requireContext()) - screenActivity.onToolbarCreated(toolbar) + mainActivity.onToolbarCreated(toolbar) toolbar.title = getString(stringRes.repository) val scroll = NestedScrollView(binding.root.context) scroll.addView(binding.root) @@ -149,7 +149,7 @@ class RepositoryFragment() : ScreenFragment() { } editRepoButton.setOnClickListener { - screenActivity.navigateEditRepository(viewModel.id) + mainActivity.navigateEditRepository(viewModel.id) } deleteRepoButton.setOnClickListener { diff --git a/app/src/main/kotlin/com/looker/droidify/ui/settings/SettingsFragment.kt b/app/src/main/kotlin/com/looker/droidify/ui/settings/SettingsFragment.kt index 9f44d849..0196da24 100644 --- a/app/src/main/kotlin/com/looker/droidify/ui/settings/SettingsFragment.kt +++ b/app/src/main/kotlin/com/looker/droidify/ui/settings/SettingsFragment.kt @@ -2,6 +2,7 @@ package com.looker.droidify.ui.settings import android.content.Context import android.content.Intent +import android.content.pm.PackageManager import android.net.Uri import android.os.Bundle import android.view.LayoutInflater @@ -23,13 +24,11 @@ import androidx.lifecycle.repeatOnLifecycle import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.snackbar.Snackbar import com.google.android.material.textfield.TextInputEditText -import com.looker.droidify.utility.common.SdkCheck -import com.looker.droidify.utility.common.extension.getColorFromAttr -import com.looker.droidify.utility.common.extension.homeAsUp -import com.looker.droidify.utility.common.extension.systemBarsPadding -import com.looker.droidify.utility.common.extension.updateAsMutable -import com.looker.droidify.utility.common.isIgnoreBatteryEnabled -import com.looker.droidify.utility.common.requestBatteryFreedom +import com.looker.droidify.BuildConfig +import com.looker.droidify.R +import com.looker.droidify.databinding.EnumTypeBinding +import com.looker.droidify.databinding.SettingsPageBinding +import com.looker.droidify.databinding.SwitchTypeBinding import com.looker.droidify.datastore.Settings import com.looker.droidify.datastore.extension.autoSyncName import com.looker.droidify.datastore.extension.installerName @@ -38,22 +37,25 @@ import com.looker.droidify.datastore.extension.themeName import com.looker.droidify.datastore.extension.toTime import com.looker.droidify.datastore.model.AutoSync import com.looker.droidify.datastore.model.InstallerType +import com.looker.droidify.datastore.model.LegacyInstallerComponent import com.looker.droidify.datastore.model.ProxyType import com.looker.droidify.datastore.model.Theme -import com.looker.droidify.BuildConfig -import com.looker.droidify.databinding.EnumTypeBinding -import com.looker.droidify.databinding.SettingsPageBinding -import com.looker.droidify.databinding.SwitchTypeBinding +import com.looker.droidify.utility.common.SdkCheck +import com.looker.droidify.utility.common.extension.getColorFromAttr +import com.looker.droidify.utility.common.extension.homeAsUp +import com.looker.droidify.utility.common.extension.systemBarsPadding +import com.looker.droidify.utility.common.extension.updateAsMutable +import com.looker.droidify.utility.common.isIgnoreBatteryEnabled +import com.looker.droidify.utility.common.requestBatteryFreedom import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import java.util.Locale import kotlin.time.Duration import kotlin.time.Duration.Companion.days import kotlin.time.Duration.Companion.hours import com.google.android.material.R as MaterialR -import com.looker.droidify.R +import androidx.core.net.toUri @AndroidEntryPoint class SettingsFragment : Fragment() { @@ -119,9 +121,7 @@ class SettingsFragment : Fragment() { ): View { _binding = SettingsPageBinding.inflate(inflater, container, false) binding.nestedScrollView.systemBarsPadding() - if (requireContext().isIgnoreBatteryEnabled()) { - viewModel.allowBackground() - } + viewModel.toggleBackgroundAccess(requireContext().isIgnoreBatteryEnabled()) val toolbar = binding.toolbar toolbar.navigationIcon = toolbar.context.homeAsUp toolbar.setNavigationOnClickListener { activity?.onBackPressedDispatcher?.onBackPressed() } @@ -233,6 +233,56 @@ class SettingsFragment : Fragment() { onClick = { viewModel.setInstaller(requireContext(), it) } ) } + val pm = requireContext().packageManager + legacyInstallerComponent.connect( + titleText = getString(R.string.legacyInstallerComponent), + setting = viewModel.getSetting { legacyInstallerComponent }, + map = { + when (it) { + is LegacyInstallerComponent.Component -> { + val component = it + val appLabel = runCatching { + val info = pm.getApplicationInfo(component.clazz, 0) + pm.getApplicationLabel(info).toString() + }.getOrElse { component.clazz } + "$appLabel (${component.activity})" + } + LegacyInstallerComponent.Unspecified -> getString(R.string.unspecified) + LegacyInstallerComponent.AlwaysChoose -> getString(R.string.always_choose) + null -> getString(R.string.unspecified) + } + }, + ) { component, valueToString -> + val installerOptions = run { + var contentProtocol = "content://" + val intent = Intent(Intent.ACTION_INSTALL_PACKAGE).apply { + setDataAndType(contentProtocol.toUri(), "application/vnd.android.package-archive") + } + val activities = pm.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY) + listOf( + LegacyInstallerComponent.Unspecified, + LegacyInstallerComponent.AlwaysChoose + ) + activities.map { + LegacyInstallerComponent.Component( + clazz = it.activityInfo.packageName, + activity = it.activityInfo.name, + ) + } + } + addSingleCorrectDialog( + initialValue = component ?: LegacyInstallerComponent.Unspecified, + values = installerOptions, + title = R.string.legacyInstallerComponent, + iconRes = R.drawable.ic_apk_install, + valueToString = valueToString, + onClick = { viewModel.setLegacyInstallerComponentComponent(it) }, + ) + } + incompatibleUpdates.connect( + titleText = getString(R.string.incompatible_versions), + contentText = getString(R.string.incompatible_versions_summary), + setting = viewModel.getInitialSetting { incompatibleVersions }, + ) proxyType.connect( titleText = getString(R.string.proxy_type), setting = viewModel.getSetting { proxy.type }, @@ -283,6 +333,7 @@ class SettingsFragment : Fragment() { exportRepos.title.text = getString(R.string.export_repos_title) exportRepos.content.text = getString(R.string.export_repos_DESC) + allowBackgroundWork.root.isVisible = false allowBackgroundWork.title.text = getString(R.string.require_background_access) allowBackgroundWork.content.text = getString(R.string.require_background_access_DESC) @@ -315,8 +366,8 @@ class SettingsFragment : Fragment() { launch { viewModel.settingsFlow.collect { setting -> updateSettings(setting) - binding.allowBackgroundWork.root.isVisible = !viewModel.backgroundTask.first() - && setting.autoSync != AutoSync.NEVER + binding.allowBackgroundWork.root.isVisible = + !viewModel.isBackgroundAllowed && setting.autoSync != AutoSync.NEVER } } } @@ -326,9 +377,7 @@ class SettingsFragment : Fragment() { override fun onResume() { super.onResume() - if (requireContext().isIgnoreBatteryEnabled()) { - viewModel.allowBackground() - } + viewModel.toggleBackgroundAccess(requireContext().isIgnoreBatteryEnabled()) } override fun onDestroyView() { @@ -376,9 +425,7 @@ class SettingsFragment : Fragment() { } allowBackgroundWork.root.setOnClickListener { requireContext().requestBatteryFreedom() - if (requireContext().isIgnoreBatteryEnabled()) { - viewModel.allowBackground() - } + viewModel.toggleBackgroundAccess(requireContext().isIgnoreBatteryEnabled()) } creditFoxy.root.setOnClickListener { openLink(FOXY_DROID_URL) @@ -395,6 +442,9 @@ class SettingsFragment : Fragment() { proxyHost.root.isVisible = allowProxies proxyPort.root.isVisible = allowProxies forceCleanUp.root.isVisible = settings.cleanUpInterval == Duration.INFINITE + + val useLegacyInstaller = settings.installerType == InstallerType.LEGACY + legacyInstallerComponent.root.isVisible = useLegacyInstaller } } diff --git a/app/src/main/kotlin/com/looker/droidify/ui/settings/SettingsViewModel.kt b/app/src/main/kotlin/com/looker/droidify/ui/settings/SettingsViewModel.kt index 77d81b34..6203bc00 100644 --- a/app/src/main/kotlin/com/looker/droidify/ui/settings/SettingsViewModel.kt +++ b/app/src/main/kotlin/com/looker/droidify/ui/settings/SettingsViewModel.kt @@ -7,35 +7,35 @@ import androidx.appcompat.app.AppCompatDelegate import androidx.core.os.LocaleListCompat import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.looker.droidify.R +import com.looker.droidify.database.Database +import com.looker.droidify.database.RepositoryExporter import com.looker.droidify.datastore.Settings import com.looker.droidify.datastore.SettingsRepository import com.looker.droidify.datastore.get import com.looker.droidify.datastore.model.AutoSync import com.looker.droidify.datastore.model.InstallerType -import com.looker.droidify.datastore.model.InstallerType.* +import com.looker.droidify.datastore.model.InstallerType.ROOT +import com.looker.droidify.datastore.model.InstallerType.SHIZUKU +import com.looker.droidify.datastore.model.LegacyInstallerComponent import com.looker.droidify.datastore.model.ProxyType import com.looker.droidify.datastore.model.Theme -import com.looker.droidify.database.Database -import com.looker.droidify.database.RepositoryExporter -import com.looker.droidify.work.CleanUpWorker import com.looker.droidify.installer.installers.isMagiskGranted import com.looker.droidify.installer.installers.isShizukuAlive import com.looker.droidify.installer.installers.isShizukuGranted import com.looker.droidify.installer.installers.isShizukuInstalled import com.looker.droidify.installer.installers.requestPermissionListener +import com.looker.droidify.work.CleanUpWorker import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asSharedFlow -import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import java.util.Locale import javax.inject.Inject import kotlin.time.Duration -import com.looker.droidify.R @HiltViewModel class SettingsViewModel @@ -49,8 +49,8 @@ class SettingsViewModel } val settingsFlow get() = settingsRepository.data - private val _backgroundTask = MutableStateFlow(false) - val backgroundTask = _backgroundTask.asStateFlow() + var isBackgroundAllowed = true + private set private val _snackbarStringId = MutableSharedFlow() val snackbarStringId = _snackbarStringId.asSharedFlow() @@ -59,10 +59,8 @@ class SettingsViewModel fun getInitialSetting(block: Settings.() -> T): Flow = initialSetting.map { it.block() } - fun allowBackground() { - viewModelScope.launch { - _backgroundTask.emit(true) - } + fun toggleBackgroundAccess(enable: Boolean) { + isBackgroundAllowed = enable } fun setLanguage(language: String) { @@ -172,7 +170,7 @@ class SettingsViewModel } else if (isShizukuGranted()) { settingsRepository.setInstallerType(installerType) } else if (!isShizukuGranted()) { - if(requestPermissionListener()) { + if (requestPermissionListener()) { settingsRepository.setInstallerType(installerType) } } @@ -194,6 +192,12 @@ class SettingsViewModel } } + fun setLegacyInstallerComponentComponent(component: LegacyInstallerComponent?) { + viewModelScope.launch { + settingsRepository.setLegacyInstallerComponent(component) + } + } + fun exportSettings(file: Uri) { viewModelScope.launch { settingsRepository.export(file) diff --git a/app/src/main/kotlin/com/looker/droidify/ui/tabsFragment/TabsFragment.kt b/app/src/main/kotlin/com/looker/droidify/ui/tabsFragment/TabsFragment.kt index ebe8a0f1..8872bf19 100644 --- a/app/src/main/kotlin/com/looker/droidify/ui/tabsFragment/TabsFragment.kt +++ b/app/src/main/kotlin/com/looker/droidify/ui/tabsFragment/TabsFragment.kt @@ -14,6 +14,7 @@ import android.widget.FrameLayout import android.widget.TextView import androidx.activity.OnBackPressedCallback import androidx.appcompat.widget.SearchView +import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels @@ -28,24 +29,24 @@ import com.google.android.material.elevation.SurfaceColors import com.google.android.material.shape.MaterialShapeDrawable import com.google.android.material.shape.ShapeAppearanceModel import com.google.android.material.tabs.TabLayoutMediator +import com.looker.droidify.R +import com.looker.droidify.databinding.TabsToolbarBinding +import com.looker.droidify.datastore.model.supportedSortOrders +import com.looker.droidify.datastore.extension.sortOrderName +import com.looker.droidify.datastore.model.SortOrder +import com.looker.droidify.model.ProductItem +import com.looker.droidify.service.Connection +import com.looker.droidify.service.SyncService +import com.looker.droidify.ui.ScreenFragment +import com.looker.droidify.ui.appList.AppListFragment import com.looker.droidify.utility.common.device.Huawei import com.looker.droidify.utility.common.extension.dp import com.looker.droidify.utility.common.extension.getMutatedIcon import com.looker.droidify.utility.common.extension.selectableBackground import com.looker.droidify.utility.common.extension.systemBarsPadding import com.looker.droidify.utility.common.sdkAbove -import com.looker.droidify.datastore.extension.sortOrderName -import com.looker.droidify.datastore.model.SortOrder -import com.looker.droidify.R -import com.looker.droidify.databinding.TabsToolbarBinding -import com.looker.droidify.datastore.model.supportedSortOrders -import com.looker.droidify.model.ProductItem -import com.looker.droidify.service.Connection -import com.looker.droidify.service.SyncService -import com.looker.droidify.ui.ScreenFragment -import com.looker.droidify.ui.appList.AppListFragment import com.looker.droidify.utility.extension.resources.sizeScaled -import com.looker.droidify.utility.extension.screenActivity +import com.looker.droidify.utility.extension.mainActivity import com.looker.droidify.widget.DividerConfiguration import com.looker.droidify.widget.FocusSearchView import com.looker.droidify.widget.StableRecyclerAdapter @@ -152,7 +153,7 @@ class TabsFragment : ScreenFragment() { } } - screenActivity.onToolbarCreated(toolbar) + mainActivity.onToolbarCreated(toolbar) toolbar.title = getString(R.string.application_name) // Move focus from SearchView to Toolbar toolbar.isFocusable = true @@ -204,7 +205,7 @@ class TabsFragment : ScreenFragment() { syncRepositoriesMenuItem = add(0, 0, 0, stringRes.sync_repositories) .setIcon(toolbar.context.getMutatedIcon(R.drawable.ic_sync)) .setOnMenuItemClickListener { - viewModel.sync() + syncConnection.binder?.sync(SyncService.SyncRequest.MANUAL) true } @@ -226,19 +227,19 @@ class TabsFragment : ScreenFragment() { favouritesItem = add(1, 0, 0, stringRes.favourites) .setIcon(toolbar.context.getMutatedIcon(R.drawable.ic_favourite_checked)) .setOnMenuItemClickListener { - view.post { screenActivity.navigateFavourites() } + view.post { mainActivity.navigateFavourites() } true } add(1, 0, 0, stringRes.repositories) .setOnMenuItemClickListener { - view.post { screenActivity.navigateRepositories() } + view.post { mainActivity.navigateRepositories() } true } add(1, 0, 0, stringRes.settings) .setOnMenuItemClickListener { - view.post { screenActivity.navigatePreferences() } + view.post { mainActivity.navigatePreferences() } true } } @@ -295,6 +296,25 @@ class TabsFragment : ScreenFragment() { onBackPressedCallback?.isEnabled = it != BackAction.None } } + launch { + SyncService.syncState.collect { + when (it) { + is SyncService.State.Connecting -> { + tabsBinding.syncState.isVisible = true + tabsBinding.syncState.isIndeterminate = true + } + + SyncService.State.Finish -> { + tabsBinding.syncState.isGone = true + } + + is SyncService.State.Syncing -> { + tabsBinding.syncState.isVisible = true + tabsBinding.syncState.setProgressCompat(it.progress, true) + } + } + } + } } } diff --git a/app/src/main/kotlin/com/looker/droidify/ui/tabsFragment/TabsViewModel.kt b/app/src/main/kotlin/com/looker/droidify/ui/tabsFragment/TabsViewModel.kt index f01e4d9c..606637a7 100644 --- a/app/src/main/kotlin/com/looker/droidify/ui/tabsFragment/TabsViewModel.kt +++ b/app/src/main/kotlin/com/looker/droidify/ui/tabsFragment/TabsViewModel.kt @@ -3,17 +3,12 @@ package com.looker.droidify.ui.tabsFragment import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.looker.droidify.data.local.dao.IndexDao -import com.looker.droidify.data.local.model.RepoEntity -import com.looker.droidify.data.local.model.toRepo import com.looker.droidify.database.Database import com.looker.droidify.datastore.SettingsRepository import com.looker.droidify.datastore.get import com.looker.droidify.datastore.model.SortOrder import com.looker.droidify.domain.model.Fingerprint import com.looker.droidify.model.ProductItem -import com.looker.droidify.sync.Syncable -import com.looker.droidify.sync.v2.model.IndexV2 import com.looker.droidify.ui.tabsFragment.TabsFragment.BackAction import com.looker.droidify.utility.common.extension.asStateFlow import dagger.hilt.android.lifecycle.HiltViewModel @@ -68,9 +63,15 @@ class TabsViewModel @Inject constructor( val backAction = combine( currentSection, isSearchActionItemExpanded, - showSections, - ::calcBackAction, - ).asStateFlow(BackAction.None) + showSections + ) { currentSection, isSearchActionItemExpanded, showSections -> + when { + currentSection != ProductItem.Section.All -> BackAction.ProductAll + isSearchActionItemExpanded -> BackAction.CollapseSearchView + showSections -> BackAction.HideSections + else -> BackAction.None + } + }.asStateFlow(BackAction.None) fun setSection(section: ProductItem.Section) { savedStateHandle[STATE_SECTION] = section @@ -82,33 +83,6 @@ class TabsViewModel @Inject constructor( } } - fun sync() { - viewModelScope.launch { - val repo = RepoEntity( - id = 1, - address = "https://apt.izzysoft.de/fdroid/repo", - name = mapOf("en-US" to "IzzyOnDroid F-Droid Repo"), - description = emptyMap(), - fingerprint = Fingerprint("3BF0D6ABFEAE2F401707B6D966BE743BF0EEE49C2561B9BA39073711F628937A"), - timestamp = 0L, - icon = emptyMap(), - ) - val (_, index) = syncable.sync( - repo.toRepo( - locale = "en-US", - mirrors = emptyList(), - enabled = true, - ), - ) - requireNotNull(index) - indexDao.insertIndex( - fingerprint = repo.fingerprint, - index = index, - expectedRepoId = repo.id, - ) - } - } - private fun calcBackAction( currentSection: ProductItem.Section, isSearchActionItemExpanded: Boolean, diff --git a/app/src/main/kotlin/com/looker/droidify/utility/common/Deeplinks.kt b/app/src/main/kotlin/com/looker/droidify/utility/common/Deeplinks.kt index f6327c33..4fa07a6b 100644 --- a/app/src/main/kotlin/com/looker/droidify/utility/common/Deeplinks.kt +++ b/app/src/main/kotlin/com/looker/droidify/utility/common/Deeplinks.kt @@ -12,7 +12,7 @@ private val supportedExternalHosts = arrayOf( "f-droid.org", "www.f-droid.org", "staging.f-droid.org", - "apt.izzysoft.de" + "apt.izzysoft.de", ) val Intent.deeplinkType: DeeplinkType? @@ -77,7 +77,7 @@ private inline fun invalidDeeplink(message: String): Nothing = throw InvalidDeep sealed interface DeeplinkType { - data class AddRepository(val address: String) : DeeplinkType + class AddRepository(val address: String) : DeeplinkType - data class AppDetail(val packageName: String, val repoAddress: String? = null) : DeeplinkType + class AppDetail(val packageName: String, val repoAddress: String? = null) : DeeplinkType } diff --git a/app/src/main/kotlin/com/looker/droidify/utility/common/SdkCheck.kt b/app/src/main/kotlin/com/looker/droidify/utility/common/SdkCheck.kt index b69e67d0..75d667d8 100644 --- a/app/src/main/kotlin/com/looker/droidify/utility/common/SdkCheck.kt +++ b/app/src/main/kotlin/com/looker/droidify/utility/common/SdkCheck.kt @@ -33,3 +33,28 @@ object SdkCheck { @get:ChecksSdkIntAtLeast(api = Build.VERSION_CODES.N) val isNougat: Boolean get() = sdk >= Build.VERSION_CODES.N } + +val sdkName by lazy { + mapOf( + 16 to "4.1", + 17 to "4.2", + 18 to "4.3", + 19 to "4.4", + 21 to "5.0", + 22 to "5.1", + 23 to "6", + 24 to "7.0", + 25 to "7.1", + 26 to "8.0", + 27 to "8.1", + 28 to "9", + 29 to "10", + 30 to "11", + 31 to "12", + 32 to "12L", + 33 to "13", + 34 to "14", + 35 to "15", + 36 to "16", + ) +} diff --git a/app/src/main/kotlin/com/looker/droidify/utility/common/cache/Cache.kt b/app/src/main/kotlin/com/looker/droidify/utility/common/cache/Cache.kt index c3556252..12337530 100644 --- a/app/src/main/kotlin/com/looker/droidify/utility/common/cache/Cache.kt +++ b/app/src/main/kotlin/com/looker/droidify/utility/common/cache/Cache.kt @@ -46,13 +46,6 @@ object Cache { } } - private fun subPath(dir: File, file: File): String { - val dirPath = "${dir.path}/" - val filePath = file.path - filePath.startsWith(dirPath) || throw RuntimeException() - return filePath.substring(dirPath.length) - } - fun getEmptySpace(context: Context): Long { val dir = context.cacheDir return min(dir.usableSpace, dir.freeSpace) @@ -187,7 +180,7 @@ object Cache { projection: Array?, selection: String?, selectionArgs: Array?, - sortOrder: String? + sortOrder: String?, ): Cursor { val file = getFileAndTypeForUri(uri).first val columns = (projection ?: defaultColumns).mapNotNull { @@ -217,7 +210,7 @@ object Cache { uri: Uri, contentValues: ContentValues?, selection: String?, - selectionArgs: Array? + selectionArgs: Array?, ): Int = unsupported override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor? { diff --git a/app/src/main/kotlin/com/looker/droidify/utility/common/extension/Context.kt b/app/src/main/kotlin/com/looker/droidify/utility/common/extension/Context.kt index 1bd318e0..a457d850 100644 --- a/app/src/main/kotlin/com/looker/droidify/utility/common/extension/Context.kt +++ b/app/src/main/kotlin/com/looker/droidify/utility/common/extension/Context.kt @@ -5,9 +5,9 @@ import android.app.job.JobScheduler import android.content.ClipData import android.content.ClipboardManager import android.content.Context +import android.content.Intent import android.content.res.ColorStateList import android.graphics.drawable.Drawable -import android.net.ConnectivityManager import android.os.PowerManager import android.view.inputmethod.InputMethodManager import androidx.annotation.AttrRes @@ -15,14 +15,12 @@ import androidx.annotation.DrawableRes import androidx.appcompat.content.res.AppCompatResources import androidx.core.content.ContextCompat import androidx.core.content.getSystemService +import androidx.core.net.toUri import com.looker.droidify.R inline val Context.clipboardManager: ClipboardManager? get() = getSystemService() -inline val Context.connectivityManager: ConnectivityManager? - get() = getSystemService() - inline val Context.inputManager: InputMethodManager? get() = getSystemService() @@ -39,6 +37,13 @@ fun Context.copyToClipboard(clip: String) { clipboardManager?.setPrimaryClip(ClipData.newPlainText(null, clip)) } +fun Context.openLink(url: String) { + val intent = intent(Intent.ACTION_VIEW) { + setData(url.toUri()) + } + startActivity(intent) +} + val Context.corneredBackground: Drawable get() = getDrawableCompat(R.drawable.background_border) @@ -57,6 +62,9 @@ val Context.selectableBackground: Drawable val Context.camera: Drawable get() = getDrawableCompat(R.drawable.ic_image) +val Context.videoPlaceHolder: Drawable + get() = getDrawableCompat(R.drawable.ic_video) + val Context.aspectRatio: Float get() = with(resources.displayMetrics) { (heightPixels / widthPixels).toFloat() @@ -75,14 +83,21 @@ private fun Context.getDrawableFromAttr(attrResId: Int): Drawable { } fun Context.getDrawableCompat(@DrawableRes resId: Int = R.drawable.background_border): Drawable = - requireNotNull(AppCompatResources.getDrawable(this, resId)) { "Cannot find drawable, ID: $resId" } + requireNotNull( + AppCompatResources.getDrawable( + this, + resId + ) + ) { "Cannot find drawable, ID: $resId" } fun Context.getColorFromAttr(@AttrRes attrResId: Int): ColorStateList { val typedArray = obtainStyledAttributes(intArrayOf(attrResId)) - val (colorStateList, resId) = try { - Pair(typedArray.getColorStateList(0), typedArray.getResourceId(0, 0)) + return try { + typedArray.getColorStateList(0) ?: run { + val resourceId = typedArray.getResourceId(0, 0) + ContextCompat.getColorStateList(this, resourceId)!! + } } finally { typedArray.recycle() } - return colorStateList ?: ContextCompat.getColorStateList(this, resId)!! } diff --git a/app/src/main/kotlin/com/looker/droidify/utility/common/extension/Json.kt b/app/src/main/kotlin/com/looker/droidify/utility/common/extension/Json.kt index 66451b09..f2f7dd0b 100644 --- a/app/src/main/kotlin/com/looker/droidify/utility/common/extension/Json.kt +++ b/app/src/main/kotlin/com/looker/droidify/utility/common/extension/Json.kt @@ -1,4 +1,4 @@ -package com.looker.core.common.extension +package com.looker.droidify.utility.common.extension import com.fasterxml.jackson.core.JsonFactory import com.fasterxml.jackson.core.JsonGenerator diff --git a/app/src/main/kotlin/com/looker/droidify/utility/common/extension/PackageInfo.kt b/app/src/main/kotlin/com/looker/droidify/utility/common/extension/PackageInfo.kt index d4a350da..ee2c24ee 100644 --- a/app/src/main/kotlin/com/looker/droidify/utility/common/extension/PackageInfo.kt +++ b/app/src/main/kotlin/com/looker/droidify/utility/common/extension/PackageInfo.kt @@ -74,7 +74,6 @@ fun PackageManager.getApplicationInfoCompat( PackageManager.ApplicationInfoFlags.of(0L) ) } else { - @Suppress("DEPRECATION") getApplicationInfo(filePath, 0) } @@ -98,7 +97,6 @@ fun PackageManager.getPackageInfoCompat( PackageManager.PackageInfoFlags.of(signatureFlag.toLong()) ) } else { - @Suppress("DEPRECATION") getPackageInfo(packageName, signatureFlag) } } catch (e: Exception) { @@ -131,7 +129,6 @@ fun PackageManager.getPackageArchiveInfoCompat( PackageManager.PackageInfoFlags.of(signatureFlag.toLong()) ) } else { - @Suppress("DEPRECATION") getPackageArchiveInfo(filePath, signatureFlag) } } catch (e: Exception) { @@ -144,7 +141,6 @@ fun PackageManager.getInstalledPackagesCompat( if (SdkCheck.isTiramisu) { getInstalledPackages(PackageManager.PackageInfoFlags.of(signatureFlag.toLong())) } else { - @Suppress("DEPRECATION") getInstalledPackages(signatureFlag) } } catch (e: Exception) { diff --git a/app/src/main/kotlin/com/looker/droidify/utility/common/extension/View.kt b/app/src/main/kotlin/com/looker/droidify/utility/common/extension/View.kt index 2935c1f4..a32c08fc 100644 --- a/app/src/main/kotlin/com/looker/droidify/utility/common/extension/View.kt +++ b/app/src/main/kotlin/com/looker/droidify/utility/common/extension/View.kt @@ -1,5 +1,6 @@ package com.looker.droidify.utility.common.extension +import android.content.Context import android.util.TypedValue import android.view.LayoutInflater import android.view.View @@ -7,7 +8,9 @@ import android.view.ViewGroup import android.widget.TextView import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView -import coil.request.ImageRequest +import coil3.network.NetworkHeaders +import coil3.network.httpHeaders +import coil3.request.ImageRequest import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.callbackFlow @@ -17,8 +20,13 @@ import kotlinx.coroutines.flow.map import kotlin.math.min import kotlin.math.roundToInt +private val networkHeader by lazy { NetworkHeaders.Builder() } + fun ImageRequest.Builder.authentication(base64: String) { - addHeader("Authorization", base64) + if (base64.isNotEmpty()) { + networkHeader["Authorization"] = base64 + httpHeaders(networkHeader.build()) + } } fun TextView.setTextSizeScaled(size: Int) { @@ -26,6 +34,9 @@ fun TextView.setTextSizeScaled(size: Int) { setTextSize(TypedValue.COMPLEX_UNIT_PX, realSize.toFloat()) } +val Context.layoutInflater: LayoutInflater + get() = LayoutInflater.from(this) + fun ViewGroup.inflate(layoutResId: Int): View { return LayoutInflater.from(context).inflate(layoutResId, this, false) } diff --git a/app/src/main/kotlin/com/looker/droidify/utility/common/signature/HashChecker.kt b/app/src/main/kotlin/com/looker/droidify/utility/common/signature/HashChecker.kt index 652016d0..538983bc 100644 --- a/app/src/main/kotlin/com/looker/droidify/utility/common/signature/HashChecker.kt +++ b/app/src/main/kotlin/com/looker/droidify/utility/common/signature/HashChecker.kt @@ -34,7 +34,7 @@ suspend fun File.calculateHash(hashType: String): String? { } private suspend fun MessageDigest.readBytesFrom( - file: File + file: File, ): ByteArray? = withContext(Dispatchers.IO) { try { if (file.length() < DIRECT_READ_LIMIT) return@withContext digest(file.readBytes()) @@ -57,16 +57,9 @@ private suspend fun MessageDigest.readBytesFrom( // 25 MB private const val DIRECT_READ_LIMIT = 25 * 1024 * 1024 -@Suppress("FunctionName") data class Hash( val type: String, - val hash: String + val hash: String, ) { - - companion object { - fun SHA256(hash: String) = Hash(type = "sha256", hash) - fun MD5(hash: String) = Hash(type = "md5", hash) - } - fun isValid(): Boolean = type.isNotBlank() && hash.isNotBlank() } diff --git a/app/src/main/kotlin/com/looker/droidify/utility/extension/Fragment.kt b/app/src/main/kotlin/com/looker/droidify/utility/extension/Fragment.kt index 371dc526..f62e03ee 100644 --- a/app/src/main/kotlin/com/looker/droidify/utility/extension/Fragment.kt +++ b/app/src/main/kotlin/com/looker/droidify/utility/extension/Fragment.kt @@ -3,5 +3,5 @@ package com.looker.droidify.utility.extension import androidx.fragment.app.Fragment import com.looker.droidify.MainActivity -inline val Fragment.screenActivity: MainActivity +inline val Fragment.mainActivity: MainActivity get() = requireActivity() as MainActivity diff --git a/app/src/main/kotlin/com/looker/droidify/utility/serialization/ProductItemSerialization.kt b/app/src/main/kotlin/com/looker/droidify/utility/serialization/ProductItemSerialization.kt index eb4b47e4..37abbb07 100644 --- a/app/src/main/kotlin/com/looker/droidify/utility/serialization/ProductItemSerialization.kt +++ b/app/src/main/kotlin/com/looker/droidify/utility/serialization/ProductItemSerialization.kt @@ -2,7 +2,7 @@ package com.looker.droidify.utility.serialization import com.fasterxml.jackson.core.JsonGenerator import com.fasterxml.jackson.core.JsonParser -import com.looker.core.common.extension.forEachKey +import com.looker.droidify.utility.common.extension.forEachKey import com.looker.droidify.model.ProductItem fun ProductItem.serialize(generator: JsonGenerator) { diff --git a/app/src/main/kotlin/com/looker/droidify/utility/serialization/ProductPreferenceSerialization.kt b/app/src/main/kotlin/com/looker/droidify/utility/serialization/ProductPreferenceSerialization.kt index cd7b9086..405d955d 100644 --- a/app/src/main/kotlin/com/looker/droidify/utility/serialization/ProductPreferenceSerialization.kt +++ b/app/src/main/kotlin/com/looker/droidify/utility/serialization/ProductPreferenceSerialization.kt @@ -2,7 +2,7 @@ package com.looker.droidify.utility.serialization import com.fasterxml.jackson.core.JsonGenerator import com.fasterxml.jackson.core.JsonParser -import com.looker.core.common.extension.forEachKey +import com.looker.droidify.utility.common.extension.forEachKey import com.looker.droidify.model.ProductPreference fun ProductPreference.serialize(generator: JsonGenerator) { diff --git a/app/src/main/kotlin/com/looker/droidify/utility/serialization/ProductSerialization.kt b/app/src/main/kotlin/com/looker/droidify/utility/serialization/ProductSerialization.kt index 33640b96..fbcba6ba 100644 --- a/app/src/main/kotlin/com/looker/droidify/utility/serialization/ProductSerialization.kt +++ b/app/src/main/kotlin/com/looker/droidify/utility/serialization/ProductSerialization.kt @@ -3,11 +3,11 @@ package com.looker.droidify.utility.serialization import com.fasterxml.jackson.core.JsonGenerator import com.fasterxml.jackson.core.JsonParser import com.fasterxml.jackson.core.JsonToken -import com.looker.core.common.extension.collectNotNull -import com.looker.core.common.extension.collectNotNullStrings -import com.looker.core.common.extension.forEachKey -import com.looker.core.common.extension.writeArray -import com.looker.core.common.extension.writeDictionary +import com.looker.droidify.utility.common.extension.collectNotNull +import com.looker.droidify.utility.common.extension.collectNotNullStrings +import com.looker.droidify.utility.common.extension.forEachKey +import com.looker.droidify.utility.common.extension.writeArray +import com.looker.droidify.utility.common.extension.writeDictionary import com.looker.droidify.model.Product import com.looker.droidify.model.Release @@ -53,11 +53,6 @@ fun Product.serialize(generator: JsonGenerator) { writeStringField(ADDRESS, it.address) } - is Product.Donate.Flattr -> { - writeStringField(TYPE, DONATION_FLATTR) - writeStringField(ID, it.id) - } - is Product.Donate.Liberapay -> { writeStringField(TYPE, DONATION_LIBERAPAY) writeStringField(ID, it.id) @@ -149,7 +144,6 @@ fun JsonParser.product(): Product { DONATION_EMPTY -> Product.Donate.Regular(url) DONATION_BITCOIN -> Product.Donate.Bitcoin(address) DONATION_LITECOIN -> Product.Donate.Litecoin(address) - DONATION_FLATTR -> Product.Donate.Flattr(id) DONATION_LIBERAPAY -> Product.Donate.Liberapay(id) DONATION_OPENCOLLECTIVE -> Product.Donate.OpenCollective(id) else -> null @@ -243,6 +237,5 @@ private const val KEY_EMPTY = "" private const val DONATION_EMPTY = "" private const val DONATION_BITCOIN = "bitcoin" private const val DONATION_LITECOIN = "litecoin" -private const val DONATION_FLATTR = "flattr" private const val DONATION_LIBERAPAY = "liberapay" private const val DONATION_OPENCOLLECTIVE = "openCollective" diff --git a/app/src/main/kotlin/com/looker/droidify/utility/serialization/ReleaseSerialization.kt b/app/src/main/kotlin/com/looker/droidify/utility/serialization/ReleaseSerialization.kt index 30ac26a0..f18730eb 100644 --- a/app/src/main/kotlin/com/looker/droidify/utility/serialization/ReleaseSerialization.kt +++ b/app/src/main/kotlin/com/looker/droidify/utility/serialization/ReleaseSerialization.kt @@ -3,11 +3,11 @@ package com.looker.droidify.utility.serialization import com.fasterxml.jackson.core.JsonGenerator import com.fasterxml.jackson.core.JsonParser import com.fasterxml.jackson.core.JsonToken -import com.looker.core.common.extension.collectNotNull -import com.looker.core.common.extension.collectNotNullStrings -import com.looker.core.common.extension.forEachKey -import com.looker.core.common.extension.writeArray -import com.looker.core.common.extension.writeDictionary +import com.looker.droidify.utility.common.extension.collectNotNull +import com.looker.droidify.utility.common.extension.collectNotNullStrings +import com.looker.droidify.utility.common.extension.forEachKey +import com.looker.droidify.utility.common.extension.writeArray +import com.looker.droidify.utility.common.extension.writeDictionary import com.looker.droidify.model.Release fun Release.serialize(generator: JsonGenerator) { diff --git a/app/src/main/kotlin/com/looker/droidify/utility/serialization/RepositorySerialization.kt b/app/src/main/kotlin/com/looker/droidify/utility/serialization/RepositorySerialization.kt index f0bd4ddd..8fde8e50 100644 --- a/app/src/main/kotlin/com/looker/droidify/utility/serialization/RepositorySerialization.kt +++ b/app/src/main/kotlin/com/looker/droidify/utility/serialization/RepositorySerialization.kt @@ -2,9 +2,9 @@ package com.looker.droidify.utility.serialization import com.fasterxml.jackson.core.JsonGenerator import com.fasterxml.jackson.core.JsonParser -import com.looker.core.common.extension.collectNotNullStrings -import com.looker.core.common.extension.forEachKey -import com.looker.core.common.extension.writeArray +import com.looker.droidify.utility.common.extension.collectNotNullStrings +import com.looker.droidify.utility.common.extension.forEachKey +import com.looker.droidify.utility.common.extension.writeArray import com.looker.droidify.model.Repository fun Repository.serialize(generator: JsonGenerator) { diff --git a/app/src/main/res/color/switch_thumb_tint.xml b/app/src/main/res/color/switch_thumb_tint.xml deleted file mode 100644 index 2ba4d629..00000000 --- a/app/src/main/res/color/switch_thumb_tint.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/color/switch_track_tint.xml b/app/src/main/res/color/switch_track_tint.xml deleted file mode 100644 index a0899857..00000000 --- a/app/src/main/res/color/switch_track_tint.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_donate_flattr.xml b/app/src/main/res/drawable/ic_donate_flattr.xml deleted file mode 100644 index 62de4a6a..00000000 --- a/app/src/main/res/drawable/ic_donate_flattr.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/ic_gitlab.xml b/app/src/main/res/drawable/ic_gitlab.xml deleted file mode 100644 index a4089ea0..00000000 --- a/app/src/main/res/drawable/ic_gitlab.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_kde.xml b/app/src/main/res/drawable/ic_kde.xml deleted file mode 100644 index ffd419f3..00000000 --- a/app/src/main/res/drawable/ic_kde.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_video.xml b/app/src/main/res/drawable/ic_video.xml new file mode 100644 index 00000000..9959ba37 --- /dev/null +++ b/app/src/main/res/drawable/ic_video.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/app_detail_header.xml b/app/src/main/res/layout/app_detail_header.xml index 957530c3..1a957be6 100644 --- a/app/src/main/res/layout/app_detail_header.xml +++ b/app/src/main/res/layout/app_detail_header.xml @@ -90,7 +90,6 @@ @@ -120,7 +119,6 @@ diff --git a/app/src/main/res/layout/edit_repository.xml b/app/src/main/res/layout/edit_repository.xml index bd6468fb..fed51e2d 100644 --- a/app/src/main/res/layout/edit_repository.xml +++ b/app/src/main/res/layout/edit_repository.xml @@ -10,77 +10,68 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - + android:orientation="vertical" + android:paddingHorizontal="12dp" + android:paddingTop="4dp" + android:paddingBottom="12dp"> - + android:hint="@string/address" + android:paddingVertical="12dp"> - + android:layout_height="match_parent" + android:inputType="textNoSuggestions|textVisiblePassword" /> + - - + - + android:layout_height="match_parent" /> + - - + - + android:layout_height="match_parent" + android:autofillHints="username" /> + - - + - - - - - - - + android:layout_height="match_parent" + android:autofillHints="password" + android:inputType="textNoSuggestions|textVisiblePassword" /> + + diff --git a/app/src/main/res/layout/enum_type.xml b/app/src/main/res/layout/enum_type.xml index f7b2651a..ee6dc1c9 100644 --- a/app/src/main/res/layout/enum_type.xml +++ b/app/src/main/res/layout/enum_type.xml @@ -19,4 +19,4 @@ android:layout_height="wrap_content" android:textAppearance="?textAppearanceBodyMedium" android:textColor="?colorOutline" /> - \ No newline at end of file + diff --git a/app/src/main/res/layout/recycler_view_with_fab.xml b/app/src/main/res/layout/recycler_view_with_fab.xml index 16596c09..c4d7d91d 100644 --- a/app/src/main/res/layout/recycler_view_with_fab.xml +++ b/app/src/main/res/layout/recycler_view_with_fab.xml @@ -19,4 +19,4 @@ app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" tools:ignore="ContentDescription" /> - \ No newline at end of file + diff --git a/app/src/main/res/layout/release_item.xml b/app/src/main/res/layout/release_item.xml index d9356563..79180b28 100644 --- a/app/src/main/res/layout/release_item.xml +++ b/app/src/main/res/layout/release_item.xml @@ -20,7 +20,7 @@ android:orientation="horizontal"> + - \ No newline at end of file + diff --git a/app/src/main/res/layout/repository_page.xml b/app/src/main/res/layout/repository_page.xml index 64e31e2a..8e668a83 100644 --- a/app/src/main/res/layout/repository_page.xml +++ b/app/src/main/res/layout/repository_page.xml @@ -2,7 +2,8 @@ + android:orientation="vertical" + android:paddingBottom="20dp"> - - - diff --git a/app/src/main/res/layout/settings_page.xml b/app/src/main/res/layout/settings_page.xml index 7005048c..8be43ed1 100644 --- a/app/src/main/res/layout/settings_page.xml +++ b/app/src/main/res/layout/settings_page.xml @@ -145,6 +145,9 @@ + - - - \ No newline at end of file + + + diff --git a/app/src/main/res/layout/video_button.xml b/app/src/main/res/layout/video_button.xml new file mode 100644 index 00000000..190da49c --- /dev/null +++ b/app/src/main/res/layout/video_button.xml @@ -0,0 +1,14 @@ + +