v0.6.4
This commit is contained in:
@@ -6,9 +6,8 @@ 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
|
||||
ij_kotlin_name_count_to_use_star_import = 999
|
||||
ij_kotlin_name_count_to_use_star_import_for_members = 999
|
||||
|
||||
|
||||
77
.github/workflows/build_debug.yml
vendored
77
.github/workflows/build_debug.yml
vendored
@@ -27,48 +27,51 @@ 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: Build Debug APK
|
||||
run: ./gradlew assembleDebug
|
||||
- name: Format Code
|
||||
run: ./gradlew ktlintFormat
|
||||
|
||||
- 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: Build Debug APK
|
||||
run: ./gradlew assembleDebug
|
||||
|
||||
- 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: 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: Upload the APK
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: debug
|
||||
path: app/build/outputs/apk/debug/app-debug*.apk
|
||||
- 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
|
||||
|
||||
24
.github/workflows/release_build.yml
vendored
24
.github/workflows/release_build.yml
vendored
@@ -56,34 +56,12 @@ jobs:
|
||||
keyStorePassword: ${{ secrets.KEYSTORE_PASS }}
|
||||
keyPassword: ${{ secrets.KEYSTORE_PASS }}
|
||||
env:
|
||||
BUILD_TOOLS_VERSION: "35.0.0"
|
||||
|
||||
- name: Extract Version Code
|
||||
id: extract_version
|
||||
run: |
|
||||
VERSION_CODE=$(grep -oP '(?<=versionCode=)\d+' app/build.gradle) # 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
|
||||
BUILD_TOOLS_VERSION: "34.0.0"
|
||||
|
||||
- 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
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
[](https://github.com/Iamlooker/Droid-ify/releases/)
|
||||
[](https://github.com/Iamlooker/Droid-ify/releases/latest)
|
||||
[](https://f-droid.org/packages/com.looker.droidify)
|
||||
</div>
|
||||
|
||||
<div align="left">
|
||||
|
||||
## Features
|
||||
@@ -22,10 +22,8 @@
|
||||
<img src="metadata/en-US/images/phoneScreenshots/1.png" width="25%" /><img src="metadata/en-US/images/phoneScreenshots/2.png" width="25%" /><img src="metadata/en-US/images/phoneScreenshots/3.png" width="25%" /><img src="metadata/en-US/images/phoneScreenshots/4.png" width="25%" />
|
||||
|
||||
## 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."
|
||||
@@ -50,7 +48,6 @@
|
||||
- Your PR will undergo review
|
||||
|
||||
## Translations
|
||||
|
||||
[](https://hosted.weblate.org/engage/droidify/?utm_source=widget)
|
||||
|
||||
## License
|
||||
@@ -70,5 +67,3 @@ 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 <http://www.gnu.org/licenses/>.
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
@@ -1,45 +1,14 @@
|
||||
import com.android.build.gradle.internal.tasks.factory.dependsOn
|
||||
import org.jlleitschuh.gradle.ktlint.reporter.ReporterType
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.android.application)
|
||||
alias(libs.plugins.kotlin.android)
|
||||
alias(libs.plugins.ktlint)
|
||||
alias(libs.plugins.ksp)
|
||||
alias(libs.plugins.hilt)
|
||||
alias(libs.plugins.kotlin.serialization)
|
||||
alias(libs.plugins.looker.android.application)
|
||||
alias(libs.plugins.looker.hilt.work)
|
||||
alias(libs.plugins.looker.lint)
|
||||
alias(libs.plugins.kotlin.parcelize)
|
||||
}
|
||||
|
||||
android {
|
||||
val latestVersionName = "0.6.5"
|
||||
namespace = "com.looker.droidify"
|
||||
buildToolsVersion = "35.0.0"
|
||||
compileSdk = 35
|
||||
defaultConfig {
|
||||
minSdk = 23
|
||||
targetSdk = 35
|
||||
applicationId = "com.looker.droidify"
|
||||
versionCode = 650
|
||||
versionName = latestVersionName
|
||||
vectorDrawables.useSupportLibrary = false
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
isCoreLibraryDesugaringEnabled = true
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "17"
|
||||
freeCompilerArgs = listOf(
|
||||
"-opt-in=kotlin.RequiresOptIn",
|
||||
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
||||
"-opt-in=kotlinx.coroutines.FlowPreview",
|
||||
"-Xcontext-receivers"
|
||||
)
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
}
|
||||
|
||||
androidResources {
|
||||
@@ -54,11 +23,11 @@ android {
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
debug {
|
||||
getByName("debug") {
|
||||
applicationIdSuffix = ".debug"
|
||||
resValue("string", "application_name", "Droid-ify-Debug")
|
||||
}
|
||||
release {
|
||||
getByName("release") {
|
||||
isMinifyEnabled = true
|
||||
isShrinkResources = true
|
||||
resValue("string", "application_name", "Droid-ify")
|
||||
@@ -82,7 +51,7 @@ android {
|
||||
buildConfigField(
|
||||
type = "String",
|
||||
name = "VERSION_NAME",
|
||||
value = "\"v$latestVersionName\""
|
||||
value = "\"v${DefaultConfig.versionName}\""
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -95,8 +64,7 @@ android {
|
||||
"/META-INF/**.kotlin_module",
|
||||
"/META-INF/**.pro",
|
||||
"/META-INF/**.version",
|
||||
"/META-INF/{AL2.0,LGPL2.1,LICENSE*}",
|
||||
"/META-INF/versions/9/previous-**.bin",
|
||||
"/META-INF/versions/9/previous-**.bin"
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -105,91 +73,33 @@ android {
|
||||
viewBinding = true
|
||||
buildConfig = true
|
||||
}
|
||||
dependenciesInfo {
|
||||
includeInApk = false
|
||||
includeInBundle = false
|
||||
}
|
||||
}
|
||||
|
||||
ktlint {
|
||||
android.set(true)
|
||||
ignoreFailures.set(true)
|
||||
debug.set(true)
|
||||
reporters {
|
||||
reporter(ReporterType.HTML)
|
||||
}
|
||||
filter {
|
||||
exclude("**/generated/**")
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
coreLibraryDesugaring(libs.desugaring)
|
||||
|
||||
implementation(libs.material)
|
||||
implementation(libs.core.ktx)
|
||||
implementation(libs.activity)
|
||||
implementation(libs.appcompat)
|
||||
implementation(libs.fragment.ktx)
|
||||
implementation(libs.lifecycle.viewModel)
|
||||
implementation(libs.recyclerview)
|
||||
implementation(libs.sqlite.ktx)
|
||||
|
||||
implementation(libs.image.viewer)
|
||||
implementation(libs.bundles.coil)
|
||||
|
||||
implementation(libs.datastore.core)
|
||||
implementation(libs.datastore.proto)
|
||||
|
||||
implementation(libs.kotlin.stdlib)
|
||||
implementation(libs.datetime)
|
||||
|
||||
implementation(libs.coroutines.core)
|
||||
implementation(libs.coroutines.android)
|
||||
implementation(libs.coroutines.guava)
|
||||
|
||||
implementation(libs.libsu.core)
|
||||
implementation(libs.shizuku.api)
|
||||
api(libs.shizuku.provider)
|
||||
modules(
|
||||
Modules.coreDomain,
|
||||
// Modules.coreData,
|
||||
Modules.coreCommon,
|
||||
Modules.coreNetwork,
|
||||
Modules.coreDatastore,
|
||||
Modules.coreDI,
|
||||
Modules.installer,
|
||||
)
|
||||
|
||||
implementation(libs.android.material)
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.activity)
|
||||
implementation(libs.androidx.appcompat)
|
||||
implementation(libs.androidx.fragment.ktx)
|
||||
implementation(libs.androidx.lifecycle.viewModel)
|
||||
implementation(libs.androidx.recyclerview)
|
||||
implementation(libs.androidx.sqlite.ktx)
|
||||
implementation(libs.coil.kt)
|
||||
implementation(libs.kotlinx.datetime)
|
||||
implementation(libs.kotlinx.coroutines.android)
|
||||
implementation(libs.jackson.core)
|
||||
implementation(libs.serialization)
|
||||
|
||||
implementation(libs.ktor.core)
|
||||
implementation(libs.ktor.okhttp)
|
||||
|
||||
implementation(libs.work.ktx)
|
||||
|
||||
implementation(libs.hilt.core)
|
||||
implementation(libs.hilt.android)
|
||||
implementation(libs.hilt.ext.work)
|
||||
ksp(libs.hilt.compiler)
|
||||
ksp(libs.hilt.ext.compiler)
|
||||
|
||||
testImplementation(platform(libs.junit.bom))
|
||||
testImplementation(libs.bundles.test.unit)
|
||||
testRuntimeOnly(libs.junit.platform)
|
||||
androidTestImplementation(platform(libs.junit.bom))
|
||||
androidTestImplementation(libs.bundles.test.android)
|
||||
implementation(libs.image.viewer)
|
||||
|
||||
// debugImplementation(libs.leakcanary)
|
||||
}
|
||||
|
||||
// using a task as a preBuild dependency instead of a function that takes some time insures that it runs
|
||||
task("detectAndroidLocals") {
|
||||
val langsList: MutableSet<String> = HashSet()
|
||||
|
||||
// in /res are (almost) all languages that have a translated string is saved. this is safer and saves some time
|
||||
fileTree("src/main/res").visit {
|
||||
if (this.file.path.endsWith("strings.xml") &&
|
||||
this.file.canonicalFile.readText().contains("<string")
|
||||
) {
|
||||
var languageCode = this.file.parentFile.name.replace("values-", "")
|
||||
languageCode = if (languageCode == "values") "en" else languageCode
|
||||
langsList.add(languageCode)
|
||||
}
|
||||
}
|
||||
val langsListString = "{${langsList.sorted().joinToString(",") { "\"${it}\"" }}}"
|
||||
android.defaultConfig.buildConfigField("String[]", "DETECTED_LOCALES", langsListString)
|
||||
}
|
||||
tasks.preBuild.dependsOn("detectAndroidLocals")
|
||||
|
||||
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -1,92 +0,0 @@
|
||||
package com.looker.droidify.index
|
||||
|
||||
import android.content.Context
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.filters.SmallTest
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import com.looker.droidify.model.Repository
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import java.io.File
|
||||
import kotlin.math.sqrt
|
||||
import kotlin.system.measureTimeMillis
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@SmallTest
|
||||
class RepositoryUpdaterTest {
|
||||
|
||||
private lateinit var context: Context
|
||||
private lateinit var repository: Repository
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
context = InstrumentationRegistry.getInstrumentation().context
|
||||
repository = Repository(
|
||||
id = 15,
|
||||
address = "https://apt.izzysoft.de/fdroid/repo",
|
||||
mirrors = emptyList(),
|
||||
name = "IzzyOnDroid F-Droid Repo",
|
||||
description = "",
|
||||
version = 20002,
|
||||
enabled = true,
|
||||
fingerprint = "3BF0D6ABFEAE2F401707B6D966BE743BF0EEE49C2561B9BA39073711F628937A",
|
||||
lastModified = "",
|
||||
entityTag = "",
|
||||
updated = 1735315749835,
|
||||
timestamp = 1725352450000,
|
||||
authentication = "",
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun processFile() {
|
||||
testRepetition(1) {
|
||||
val createFile = File.createTempFile("index", "entry")
|
||||
val mergerFile = File.createTempFile("index", "merger")
|
||||
val jarStream = context.resources.assets.open("index-v1.jar")
|
||||
jarStream.copyTo(createFile.outputStream())
|
||||
process(createFile, mergerFile)
|
||||
}
|
||||
}
|
||||
|
||||
private fun process(file: File, merger: File) = measureTimeMillis {
|
||||
RepositoryUpdater.processFile(
|
||||
context = context,
|
||||
repository = repository,
|
||||
indexType = RepositoryUpdater.IndexType.INDEX_V1,
|
||||
unstable = false,
|
||||
file = file,
|
||||
mergerFile = merger,
|
||||
lastModified = "",
|
||||
entityTag = "",
|
||||
callback = { stage, current, total ->
|
||||
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private inline fun testRepetition(repetition: Int, block: () -> Long) {
|
||||
val times = (1..repetition).map {
|
||||
System.gc()
|
||||
System.runFinalization()
|
||||
block().toDouble()
|
||||
}
|
||||
val meanAndDeviation = times.culledMeanAndDeviation()
|
||||
println(times)
|
||||
println("${meanAndDeviation.first} ± ${meanAndDeviation.second}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<Double>.culledMeanAndDeviation(): Pair<Double, Double> = when {
|
||||
isEmpty() -> Double.NaN to Double.NaN
|
||||
size == 1 || size == 2 -> this.meanAndDeviation()
|
||||
else -> sorted().subList(1, size - 1).meanAndDeviation()
|
||||
}
|
||||
|
||||
private fun List<Double>.meanAndDeviation(): Pair<Double, Double> {
|
||||
val mean = average()
|
||||
return mean to sqrt(fold(0.0) { acc, value -> acc + (value - mean).squared() } / size)
|
||||
}
|
||||
|
||||
private fun Double.squared() = this * this
|
||||
@@ -1,308 +0,0 @@
|
||||
package com.looker.droidify.sync
|
||||
|
||||
import android.content.Context
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import com.looker.droidify.domain.model.Repo
|
||||
import com.looker.droidify.sync.common.IndexJarValidator
|
||||
import com.looker.droidify.sync.common.Izzy
|
||||
import com.looker.droidify.sync.common.JsonParser
|
||||
import com.looker.droidify.sync.common.downloadIndex
|
||||
import com.looker.droidify.sync.common.benchmark
|
||||
import com.looker.droidify.sync.common.toV2
|
||||
import com.looker.droidify.sync.v1.V1Parser
|
||||
import com.looker.droidify.sync.v1.V1Syncable
|
||||
import com.looker.droidify.sync.v1.model.IndexV1
|
||||
import com.looker.droidify.sync.v2.V2Parser
|
||||
import com.looker.droidify.sync.v2.model.FileV2
|
||||
import com.looker.droidify.sync.v2.model.IndexV2
|
||||
import com.looker.droidify.sync.v2.model.MetadataV2
|
||||
import com.looker.droidify.sync.v2.model.VersionV2
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Before
|
||||
import org.junit.runner.RunWith
|
||||
import kotlin.system.measureTimeMillis
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertContentEquals
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotNull
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class V1SyncableTest {
|
||||
|
||||
private lateinit var dispatcher: CoroutineDispatcher
|
||||
private lateinit var context: Context
|
||||
private lateinit var syncable: Syncable<IndexV1>
|
||||
private lateinit var parser: Parser<IndexV1>
|
||||
private lateinit var v2Parser: Parser<IndexV2>
|
||||
private lateinit var validator: IndexValidator
|
||||
private lateinit var repo: Repo
|
||||
|
||||
@Before
|
||||
fun before() {
|
||||
context = InstrumentationRegistry.getInstrumentation().context
|
||||
dispatcher = StandardTestDispatcher()
|
||||
validator = IndexJarValidator(dispatcher)
|
||||
parser = V1Parser(dispatcher, JsonParser, validator)
|
||||
v2Parser = V2Parser(dispatcher, JsonParser)
|
||||
syncable = V1Syncable(context, FakeDownloader, dispatcher)
|
||||
repo = Izzy
|
||||
}
|
||||
|
||||
@Test
|
||||
fun benchmark_sync_v1() = runTest(dispatcher) {
|
||||
val output = benchmark(10) {
|
||||
measureTimeMillis { syncable.sync(repo) }
|
||||
}
|
||||
println(output)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun benchmark_v1_parser() = runTest(dispatcher) {
|
||||
val file = FakeDownloader.downloadIndex(context, repo, "izzy", "index-v1.jar")
|
||||
val output = benchmark(10) {
|
||||
measureTimeMillis {
|
||||
parser.parse(
|
||||
file = file,
|
||||
repo = repo
|
||||
)
|
||||
}
|
||||
}
|
||||
println(output)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun benchmark_v1_vs_v2_parser() = runTest(dispatcher) {
|
||||
val v1File = FakeDownloader.downloadIndex(context, repo, "izzy-v1", "index-v1.jar")
|
||||
val v2File = FakeDownloader.downloadIndex(context, repo, "izzy-v2", "index-v2.json")
|
||||
val output1 = benchmark(10) {
|
||||
measureTimeMillis {
|
||||
parser.parse(
|
||||
file = v1File,
|
||||
repo = repo
|
||||
)
|
||||
}
|
||||
}
|
||||
val output2 = benchmark(10) {
|
||||
measureTimeMillis {
|
||||
parser.parse(
|
||||
file = v2File,
|
||||
repo = repo,
|
||||
)
|
||||
}
|
||||
}
|
||||
println(output1)
|
||||
println(output2)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun v1tov2() = runTest(dispatcher) {
|
||||
testIndexConversion("index-v1.jar", "index-v2-updated.json")
|
||||
}
|
||||
|
||||
// @Test
|
||||
fun v1tov2FDroidRepo() = runTest(dispatcher) {
|
||||
testIndexConversion("fdroid-index-v1.jar", "fdroid-index-v2.json")
|
||||
}
|
||||
|
||||
private suspend fun testIndexConversion(
|
||||
v1: String,
|
||||
v2: String,
|
||||
targeted: String? = null,
|
||||
) {
|
||||
val fileV1 = FakeDownloader.downloadIndex(context, repo, "data-v1", v1)
|
||||
val fileV2 = FakeDownloader.downloadIndex(context, repo, "data-v2", v2)
|
||||
val (fingerV1, foundIndexV1) = parser.parse(fileV1, repo)
|
||||
val (fingerV2, expectedIndex) = v2Parser.parse(fileV2, repo)
|
||||
val foundIndex = foundIndexV1.toV2()
|
||||
assertEquals(fingerV2, fingerV1)
|
||||
assertNotNull(foundIndex)
|
||||
assertNotNull(expectedIndex)
|
||||
assertEquals(expectedIndex.repo.timestamp, foundIndex.repo.timestamp)
|
||||
assertEquals(expectedIndex.packages.size, foundIndex.packages.size)
|
||||
assertContentEquals(
|
||||
expectedIndex.packages.keys.sorted(),
|
||||
foundIndex.packages.keys.sorted(),
|
||||
)
|
||||
if (targeted == null) {
|
||||
expectedIndex.packages.keys.forEach { key ->
|
||||
val expectedPackage = expectedIndex.packages[key]
|
||||
val foundPackage = foundIndex.packages[key]
|
||||
|
||||
println("**".repeat(25))
|
||||
println("Testing: ${expectedPackage?.metadata?.name?.get("en-US")} <$key>")
|
||||
|
||||
assertNotNull(expectedPackage)
|
||||
assertNotNull(foundPackage)
|
||||
assertMetadata(expectedPackage.metadata, foundPackage.metadata)
|
||||
assertVersion(expectedPackage.versions, foundPackage.versions)
|
||||
}
|
||||
} else {
|
||||
val expectedPackage = expectedIndex.packages[targeted]
|
||||
val foundPackage = foundIndex.packages[targeted]
|
||||
|
||||
println("**".repeat(25))
|
||||
println("Testing: ${expectedPackage?.metadata?.name?.get("en-US")} <$targeted>")
|
||||
|
||||
assertNotNull(expectedPackage)
|
||||
assertNotNull(foundPackage)
|
||||
assertMetadata(expectedPackage.metadata, foundPackage.metadata)
|
||||
assertVersion(expectedPackage.versions, foundPackage.versions)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Cannot assert following:
|
||||
* - `name` => because fdroidserver behaves weirdly
|
||||
* */
|
||||
private fun assertMetadata(expectedMetaData: MetadataV2, foundMetadata: MetadataV2) {
|
||||
assertEquals(expectedMetaData.preferredSigner, foundMetadata.preferredSigner)
|
||||
// assertLocalizedString(expectedMetaData.name, foundMetadata.name)
|
||||
assertLocalizedString(expectedMetaData.summary, foundMetadata.summary)
|
||||
assertLocalizedString(expectedMetaData.description, foundMetadata.description)
|
||||
assertContentEquals(expectedMetaData.categories, foundMetadata.categories)
|
||||
// Update
|
||||
assertEquals(expectedMetaData.changelog, foundMetadata.changelog)
|
||||
assertEquals(expectedMetaData.added, foundMetadata.added)
|
||||
assertEquals(expectedMetaData.lastUpdated, foundMetadata.lastUpdated)
|
||||
// Author
|
||||
assertEquals(expectedMetaData.authorEmail, foundMetadata.authorEmail)
|
||||
assertEquals(expectedMetaData.authorName, foundMetadata.authorName)
|
||||
assertEquals(expectedMetaData.authorPhone, foundMetadata.authorPhone)
|
||||
assertEquals(expectedMetaData.authorWebSite, foundMetadata.authorWebSite)
|
||||
// Donate
|
||||
assertEquals(expectedMetaData.bitcoin, foundMetadata.bitcoin)
|
||||
assertEquals(expectedMetaData.liberapay, foundMetadata.liberapay)
|
||||
assertEquals(expectedMetaData.flattrID, foundMetadata.flattrID)
|
||||
assertEquals(expectedMetaData.openCollective, foundMetadata.openCollective)
|
||||
assertEquals(expectedMetaData.litecoin, foundMetadata.litecoin)
|
||||
assertContentEquals(expectedMetaData.donate, foundMetadata.donate)
|
||||
// Source
|
||||
assertEquals(expectedMetaData.translation, foundMetadata.translation)
|
||||
assertEquals(expectedMetaData.issueTracker, foundMetadata.issueTracker)
|
||||
assertEquals(expectedMetaData.license, foundMetadata.license)
|
||||
assertEquals(expectedMetaData.sourceCode, foundMetadata.sourceCode)
|
||||
// Graphics
|
||||
assertLocalizedString(expectedMetaData.video, foundMetadata.video)
|
||||
assertLocalized(expectedMetaData.icon, foundMetadata.icon) { expected, found ->
|
||||
assertEquals(expected.name, found.name)
|
||||
}
|
||||
assertLocalized(expectedMetaData.promoGraphic, foundMetadata.promoGraphic) { expected, found ->
|
||||
assertEquals(expected.name, found.name)
|
||||
}
|
||||
assertLocalized(expectedMetaData.tvBanner, foundMetadata.tvBanner) { expected, found ->
|
||||
assertEquals(expected.name, found.name)
|
||||
}
|
||||
assertLocalized(
|
||||
expectedMetaData.featureGraphic,
|
||||
foundMetadata.featureGraphic
|
||||
) { expected, found ->
|
||||
assertEquals(expected.name, found.name)
|
||||
}
|
||||
assertLocalized(
|
||||
expectedMetaData.screenshots?.phone,
|
||||
foundMetadata.screenshots?.phone
|
||||
) { expected, found ->
|
||||
assertFiles(expected, found)
|
||||
}
|
||||
assertLocalized(
|
||||
expectedMetaData.screenshots?.sevenInch,
|
||||
foundMetadata.screenshots?.sevenInch
|
||||
) { expected, found ->
|
||||
assertFiles(expected, found)
|
||||
}
|
||||
assertLocalized(
|
||||
expectedMetaData.screenshots?.tenInch,
|
||||
foundMetadata.screenshots?.tenInch
|
||||
) { expected, found ->
|
||||
assertFiles(expected, found)
|
||||
}
|
||||
assertLocalized(
|
||||
expectedMetaData.screenshots?.tv,
|
||||
foundMetadata.screenshots?.tv
|
||||
) { expected, found ->
|
||||
assertFiles(expected, found)
|
||||
}
|
||||
assertLocalized(
|
||||
expectedMetaData.screenshots?.wear,
|
||||
foundMetadata.screenshots?.wear
|
||||
) { expected, found ->
|
||||
assertFiles(expected, found)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Cannot assert following:
|
||||
* - `whatsNew` => we added same changelog to all versions
|
||||
* - `antiFeatures` => anti features are now version specific
|
||||
* */
|
||||
private fun assertVersion(
|
||||
expected: Map<String, VersionV2>,
|
||||
found: Map<String, VersionV2>,
|
||||
) {
|
||||
assertEquals(expected.keys.size, found.keys.size)
|
||||
assertContentEquals(expected.keys.sorted(), found.keys.sorted().asIterable())
|
||||
expected.keys.forEach { versionHash ->
|
||||
val expectedVersion = expected[versionHash]
|
||||
val foundVersion = found[versionHash]
|
||||
assertNotNull(expectedVersion)
|
||||
assertNotNull(foundVersion)
|
||||
|
||||
assertEquals(expectedVersion.added, foundVersion.added)
|
||||
assertEquals(expectedVersion.file.name, foundVersion.file.name)
|
||||
assertEquals(expectedVersion.src?.name, foundVersion.src?.name)
|
||||
|
||||
val expectedMan = expectedVersion.manifest
|
||||
val foundMan = foundVersion.manifest
|
||||
|
||||
assertEquals(expectedMan.versionCode, foundMan.versionCode)
|
||||
assertEquals(expectedMan.versionName, foundMan.versionName)
|
||||
assertEquals(expectedMan.maxSdkVersion, foundMan.maxSdkVersion)
|
||||
assertEquals(expectedMan.usesSdk, foundMan.usesSdk)
|
||||
|
||||
assertContentEquals(
|
||||
expectedMan.features.sortedBy { it.name },
|
||||
foundMan.features.sortedBy { it.name },
|
||||
)
|
||||
assertContentEquals(expectedMan.usesPermission, foundMan.usesPermission)
|
||||
assertContentEquals(expectedMan.usesPermissionSdk23, foundMan.usesPermissionSdk23)
|
||||
assertContentEquals(expectedMan.signer?.sha256?.sorted(), foundMan.signer?.sha256?.sorted())
|
||||
assertContentEquals(expectedMan.nativecode.sorted(), foundMan.nativecode.sorted())
|
||||
}
|
||||
}
|
||||
|
||||
private fun assertLocalizedString(
|
||||
expected: Map<String, String>?,
|
||||
found: Map<String, String>?,
|
||||
message: String? = null,
|
||||
) {
|
||||
assertLocalized(expected, found) { one, two ->
|
||||
assertEquals(one, two, message)
|
||||
}
|
||||
}
|
||||
|
||||
private fun <T> assertLocalized(
|
||||
expected: Map<String, T>?,
|
||||
found: Map<String, T>?,
|
||||
block: (expected: T, found: T) -> Unit,
|
||||
) {
|
||||
if (expected == null || found == null) {
|
||||
assertEquals(expected, found)
|
||||
return
|
||||
}
|
||||
assertNotNull(expected)
|
||||
assertNotNull(found)
|
||||
assertEquals(expected.size, found.size)
|
||||
assertContentEquals(expected.keys.sorted(), found.keys.sorted().asIterable())
|
||||
expected.keys.forEach {
|
||||
if (expected[it] != null && found[it] != null) block(expected[it]!!, found[it]!!)
|
||||
}
|
||||
}
|
||||
|
||||
private fun assertFiles(expected: List<FileV2>, found: List<FileV2>, message: String? = null) {
|
||||
// Only check name, because we cannot add sha to old index
|
||||
assertContentEquals(expected.map { it.name }, found.map { it.name }.asIterable(), message)
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
package com.looker.droidify.sync.common
|
||||
|
||||
import kotlin.math.pow
|
||||
import kotlin.math.sqrt
|
||||
|
||||
internal inline fun benchmark(
|
||||
repetition: Int,
|
||||
extraMessage: String? = null,
|
||||
block: () -> Long,
|
||||
): String {
|
||||
if (extraMessage != null) {
|
||||
println("=".repeat(50))
|
||||
println(extraMessage)
|
||||
println("=".repeat(50))
|
||||
}
|
||||
val times = DoubleArray(repetition)
|
||||
repeat(repetition) { iteration ->
|
||||
System.gc()
|
||||
System.runFinalization()
|
||||
times[iteration] = block().toDouble()
|
||||
}
|
||||
val meanAndDeviation = times.culledMeanAndDeviation()
|
||||
return buildString {
|
||||
append("=".repeat(50))
|
||||
append("\n")
|
||||
append(times.joinToString(" | "))
|
||||
append("\n")
|
||||
append("${meanAndDeviation.first} ms ± ${meanAndDeviation.second.toFloat()} ms")
|
||||
append("\n")
|
||||
append("=".repeat(50))
|
||||
append("\n")
|
||||
}
|
||||
}
|
||||
|
||||
private fun DoubleArray.culledMeanAndDeviation(): Pair<Double, Double> {
|
||||
sort()
|
||||
return meanAndDeviation()
|
||||
}
|
||||
|
||||
private fun DoubleArray.meanAndDeviation(): Pair<Double, Double> {
|
||||
val mean = average()
|
||||
return mean to sqrt(fold(0.0) { acc, value -> acc + (value - mean).pow(2) } / size)
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
package com.looker.droidify.sync.common
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import java.io.InputStream
|
||||
|
||||
fun assets(name: String): InputStream {
|
||||
return InstrumentationRegistry.getInstrumentation().context.assets.open(name)
|
||||
}
|
||||
@@ -26,7 +26,7 @@
|
||||
android:required="false" />
|
||||
|
||||
<application
|
||||
android:name=".Droidify"
|
||||
android:name=".MainApplication"
|
||||
android:allowBackup="true"
|
||||
android:banner="@drawable/tv_banner"
|
||||
android:enableOnBackInvokedCallback="true"
|
||||
@@ -38,7 +38,7 @@
|
||||
tools:ignore="UnusedAttribute">
|
||||
|
||||
<receiver
|
||||
android:name=".Droidify$BootReceiver"
|
||||
android:name=".MainApplication$BootReceiver"
|
||||
android:exported="true">
|
||||
|
||||
<intent-filter>
|
||||
@@ -50,6 +50,7 @@
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTask"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SHOW_APP_INFO" />
|
||||
@@ -147,7 +148,7 @@
|
||||
android:foregroundServiceType="dataSync" />
|
||||
|
||||
<receiver
|
||||
android:name=".installer.installers.session.SessionInstallerReceiver"
|
||||
android:name="com.looker.installer.installers.session.SessionInstallerReceiver"
|
||||
android:exported="false" />
|
||||
|
||||
<service
|
||||
@@ -173,7 +174,7 @@
|
||||
android:permission="android.permission.INTERACT_ACROSS_USERS_FULL" />
|
||||
|
||||
<provider
|
||||
android:name=".utility.common.cache.Cache$Provider"
|
||||
android:name="com.looker.core.common.cache.Cache$Provider"
|
||||
android:authorities="${applicationId}.provider.cache"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true" />
|
||||
|
||||
@@ -1,307 +1,29 @@
|
||||
package com.looker.droidify
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.commit
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.looker.droidify.utility.common.DeeplinkType
|
||||
import com.looker.droidify.utility.common.SdkCheck
|
||||
import com.looker.droidify.utility.common.deeplinkType
|
||||
import com.looker.droidify.utility.common.extension.homeAsUp
|
||||
import com.looker.droidify.utility.common.extension.inputManager
|
||||
import com.looker.droidify.utility.common.getInstallPackageName
|
||||
import com.looker.droidify.utility.common.requestNotificationPermission
|
||||
import com.looker.droidify.database.CursorOwner
|
||||
import com.looker.droidify.datastore.SettingsRepository
|
||||
import com.looker.droidify.datastore.extension.getThemeRes
|
||||
import com.looker.droidify.datastore.get
|
||||
import com.looker.droidify.installer.InstallManager
|
||||
import com.looker.droidify.installer.model.installFrom
|
||||
import com.looker.droidify.ui.appDetail.AppDetailFragment
|
||||
import com.looker.droidify.ui.favourites.FavouritesFragment
|
||||
import com.looker.droidify.ui.repository.EditRepositoryFragment
|
||||
import com.looker.droidify.ui.repository.RepositoriesFragment
|
||||
import com.looker.droidify.ui.repository.RepositoryFragment
|
||||
import com.looker.droidify.ui.settings.SettingsFragment
|
||||
import com.looker.droidify.ui.tabsFragment.TabsFragment
|
||||
import dagger.hilt.EntryPoint
|
||||
import dagger.hilt.InstallIn
|
||||
import com.looker.core.common.getInstallPackageName
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import dagger.hilt.android.EntryPointAccessors
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import kotlinx.coroutines.flow.drop
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class MainActivity : AppCompatActivity() {
|
||||
class MainActivity : ScreenActivity() {
|
||||
companion object {
|
||||
private const val STATE_FRAGMENT_STACK = "fragmentStack"
|
||||
const val ACTION_UPDATES = "${BuildConfig.APPLICATION_ID}.intent.action.UPDATES"
|
||||
const val ACTION_INSTALL = "${BuildConfig.APPLICATION_ID}.intent.action.INSTALL"
|
||||
const val EXTRA_CACHE_FILE_NAME =
|
||||
"${BuildConfig.APPLICATION_ID}.intent.extra.CACHE_FILE_NAME"
|
||||
}
|
||||
|
||||
private val notificationPermission =
|
||||
registerForActivityResult(ActivityResultContracts.RequestPermission()) { }
|
||||
|
||||
@Inject
|
||||
lateinit var installer: InstallManager
|
||||
|
||||
@Parcelize
|
||||
private class FragmentStackItem(
|
||||
val className: String, val arguments: Bundle?, val savedState: Fragment.SavedState?
|
||||
) : Parcelable
|
||||
|
||||
lateinit var cursorOwner: CursorOwner
|
||||
private set
|
||||
|
||||
private var onBackPressedCallback: OnBackPressedCallback? = null
|
||||
|
||||
private val fragmentStack = mutableListOf<FragmentStackItem>()
|
||||
|
||||
private val currentFragment: Fragment?
|
||||
get() {
|
||||
supportFragmentManager.executePendingTransactions()
|
||||
return supportFragmentManager.findFragmentById(R.id.main_content)
|
||||
}
|
||||
|
||||
@EntryPoint
|
||||
@InstallIn(SingletonComponent::class)
|
||||
interface CustomUserRepositoryInjector {
|
||||
fun settingsRepository(): SettingsRepository
|
||||
}
|
||||
|
||||
private fun collectChange() {
|
||||
val hiltEntryPoint = EntryPointAccessors.fromApplication(
|
||||
this, CustomUserRepositoryInjector::class.java
|
||||
)
|
||||
val newSettings = hiltEntryPoint.settingsRepository().get { theme to dynamicTheme }
|
||||
runBlocking {
|
||||
val theme = newSettings.first()
|
||||
setTheme(
|
||||
resources.configuration.getThemeRes(
|
||||
theme = theme.first, dynamicTheme = theme.second
|
||||
)
|
||||
)
|
||||
}
|
||||
lifecycleScope.launch {
|
||||
newSettings.drop(1).collect { themeAndDynamic ->
|
||||
setTheme(
|
||||
resources.configuration.getThemeRes(
|
||||
theme = themeAndDynamic.first, dynamicTheme = themeAndDynamic.second
|
||||
)
|
||||
)
|
||||
recreate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
collectChange()
|
||||
super.onCreate(savedInstanceState)
|
||||
val rootView = FrameLayout(this).apply { id = R.id.main_content }
|
||||
addContentView(
|
||||
rootView, ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT
|
||||
)
|
||||
)
|
||||
|
||||
requestNotificationPermission(request = notificationPermission::launch)
|
||||
|
||||
supportFragmentManager.addFragmentOnAttachListener { _, _ ->
|
||||
hideKeyboard()
|
||||
}
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
cursorOwner = CursorOwner()
|
||||
supportFragmentManager.commit {
|
||||
add(cursorOwner, CursorOwner::class.java.name)
|
||||
}
|
||||
} else {
|
||||
cursorOwner =
|
||||
supportFragmentManager.findFragmentByTag(CursorOwner::class.java.name) as CursorOwner
|
||||
}
|
||||
|
||||
savedInstanceState?.getParcelableArrayList<FragmentStackItem>(STATE_FRAGMENT_STACK)
|
||||
?.let { fragmentStack += it }
|
||||
if (savedInstanceState == null) {
|
||||
replaceFragment(TabsFragment(), null)
|
||||
if ((intent.flags and Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY) == 0) {
|
||||
handleIntent(intent)
|
||||
}
|
||||
}
|
||||
if (SdkCheck.isR) {
|
||||
window.statusBarColor = resources.getColor(android.R.color.transparent, theme)
|
||||
window.navigationBarColor = resources.getColor(android.R.color.transparent, theme)
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
}
|
||||
backHandler()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
onBackPressedCallback = null
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
outState.putParcelableArrayList(STATE_FRAGMENT_STACK, ArrayList(fragmentStack))
|
||||
}
|
||||
|
||||
private fun backHandler() {
|
||||
if (onBackPressedCallback == null) {
|
||||
onBackPressedCallback = object : OnBackPressedCallback(enabled = false) {
|
||||
override fun handleOnBackPressed() {
|
||||
hideKeyboard()
|
||||
popFragment()
|
||||
}
|
||||
}
|
||||
onBackPressedDispatcher.addCallback(
|
||||
this,
|
||||
onBackPressedCallback!!,
|
||||
)
|
||||
}
|
||||
onBackPressedCallback?.isEnabled = fragmentStack.isNotEmpty()
|
||||
}
|
||||
|
||||
private fun replaceFragment(fragment: Fragment, open: Boolean?) {
|
||||
if (open != null) {
|
||||
currentFragment?.view?.translationZ =
|
||||
(if (open) Int.MIN_VALUE else Int.MAX_VALUE).toFloat()
|
||||
}
|
||||
supportFragmentManager.commit {
|
||||
if (open != null) {
|
||||
setCustomAnimations(
|
||||
if (open) R.animator.slide_in else 0,
|
||||
if (open) R.animator.slide_in_keep else R.animator.slide_out
|
||||
)
|
||||
}
|
||||
setReorderingAllowed(true)
|
||||
replace(R.id.main_content, fragment)
|
||||
}
|
||||
}
|
||||
|
||||
private fun pushFragment(fragment: Fragment) {
|
||||
currentFragment?.let {
|
||||
fragmentStack.add(
|
||||
FragmentStackItem(
|
||||
it::class.java.name,
|
||||
it.arguments,
|
||||
supportFragmentManager.saveFragmentInstanceState(it)
|
||||
)
|
||||
)
|
||||
}
|
||||
replaceFragment(fragment, true)
|
||||
backHandler()
|
||||
}
|
||||
|
||||
private fun popFragment(): Boolean {
|
||||
return fragmentStack.isNotEmpty() && run {
|
||||
val stackItem = fragmentStack.removeAt(fragmentStack.size - 1)
|
||||
val fragment = Class.forName(stackItem.className).newInstance() as Fragment
|
||||
stackItem.arguments?.let(fragment::setArguments)
|
||||
stackItem.savedState?.let(fragment::setInitialSavedState)
|
||||
replaceFragment(fragment, false)
|
||||
backHandler()
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
private fun hideKeyboard() {
|
||||
inputManager?.hideSoftInputFromWindow((currentFocus ?: window.decorView).windowToken, 0)
|
||||
}
|
||||
|
||||
internal fun onToolbarCreated(toolbar: Toolbar) {
|
||||
if (fragmentStack.isNotEmpty()) {
|
||||
toolbar.navigationIcon = toolbar.context.homeAsUp
|
||||
toolbar.setNavigationOnClickListener { onBackPressedDispatcher.onBackPressed() }
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent)
|
||||
handleIntent(intent)
|
||||
}
|
||||
|
||||
private fun handleIntent(intent: Intent?) {
|
||||
override fun handleIntent(intent: Intent?) {
|
||||
when (intent?.action) {
|
||||
ACTION_UPDATES -> {
|
||||
if (currentFragment !is TabsFragment) {
|
||||
fragmentStack.clear()
|
||||
replaceFragment(TabsFragment(), true)
|
||||
}
|
||||
val tabsFragment = currentFragment as TabsFragment
|
||||
tabsFragment.selectUpdates()
|
||||
backHandler()
|
||||
}
|
||||
ACTION_UPDATES -> handleSpecialIntent(SpecialIntent.Updates)
|
||||
ACTION_INSTALL -> handleSpecialIntent(
|
||||
SpecialIntent.Install(
|
||||
intent.getInstallPackageName,
|
||||
intent.getStringExtra(EXTRA_CACHE_FILE_NAME)
|
||||
)
|
||||
)
|
||||
|
||||
ACTION_INSTALL -> {
|
||||
val packageName = intent.getInstallPackageName
|
||||
if (!packageName.isNullOrEmpty()) {
|
||||
navigateProduct(packageName)
|
||||
val cacheFile = intent.getStringExtra(EXTRA_CACHE_FILE_NAME) ?: return
|
||||
val installItem = packageName installFrom cacheFile
|
||||
lifecycleScope.launch { installer install installItem }
|
||||
}
|
||||
}
|
||||
|
||||
Intent.ACTION_VIEW -> {
|
||||
when (val deeplink = intent.deeplinkType) {
|
||||
is DeeplinkType.AppDetail -> {
|
||||
val fragment = currentFragment
|
||||
if (fragment !is AppDetailFragment) {
|
||||
navigateProduct(deeplink.packageName, deeplink.repoAddress)
|
||||
}
|
||||
}
|
||||
|
||||
is DeeplinkType.AddRepository -> {
|
||||
navigateAddRepository(repoAddress = deeplink.address)
|
||||
}
|
||||
|
||||
null -> {}
|
||||
}
|
||||
}
|
||||
|
||||
Intent.ACTION_SHOW_APP_INFO -> {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
val packageName = intent.getStringExtra(Intent.EXTRA_PACKAGE_NAME)
|
||||
|
||||
if (packageName != null && currentFragment !is AppDetailFragment) {
|
||||
navigateProduct(packageName)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
else -> super.handleIntent(intent)
|
||||
}
|
||||
}
|
||||
|
||||
fun navigateFavourites() = pushFragment(FavouritesFragment())
|
||||
fun navigateProduct(packageName: String, repoAddress: String? = null) =
|
||||
pushFragment(AppDetailFragment(packageName, repoAddress))
|
||||
|
||||
fun navigateRepositories() = pushFragment(RepositoriesFragment())
|
||||
fun navigatePreferences() = pushFragment(SettingsFragment.newInstance())
|
||||
fun navigateAddRepository(repoAddress: String? = null) =
|
||||
pushFragment(EditRepositoryFragment(null, repoAddress))
|
||||
|
||||
fun navigateRepository(repositoryId: Long) =
|
||||
pushFragment(RepositoryFragment(repositoryId))
|
||||
|
||||
fun navigateEditRepository(repositoryId: Long) =
|
||||
pushFragment(EditRepositoryFragment(repositoryId, null))
|
||||
}
|
||||
|
||||
@@ -6,45 +6,39 @@ import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.os.Build
|
||||
import android.os.StrictMode
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.hilt.work.HiltWorkerFactory
|
||||
import androidx.work.Configuration
|
||||
import androidx.work.NetworkType
|
||||
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 coil.ImageLoader
|
||||
import coil.ImageLoaderFactory
|
||||
import coil.disk.DiskCache
|
||||
import coil.memory.MemoryCache
|
||||
import com.looker.core.common.Constants
|
||||
import com.looker.core.common.cache.Cache
|
||||
import com.looker.core.common.extension.getInstalledPackagesCompat
|
||||
import com.looker.core.common.extension.jobScheduler
|
||||
import com.looker.core.common.log
|
||||
import com.looker.core.datastore.SettingsRepository
|
||||
import com.looker.core.datastore.get
|
||||
import com.looker.core.datastore.model.AutoSync
|
||||
import com.looker.core.datastore.model.InstallerType
|
||||
import com.looker.core.datastore.model.ProxyPreference
|
||||
import com.looker.core.datastore.model.ProxyType
|
||||
import com.looker.droidify.content.ProductPreferences
|
||||
import com.looker.droidify.database.Database
|
||||
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.ProxyPreference
|
||||
import com.looker.droidify.datastore.model.ProxyType
|
||||
import com.looker.droidify.index.RepositoryUpdater
|
||||
import com.looker.droidify.installer.InstallManager
|
||||
import com.looker.droidify.network.Downloader
|
||||
import com.looker.droidify.receivers.InstalledAppReceiver
|
||||
import com.looker.droidify.service.Connection
|
||||
import com.looker.droidify.service.SyncService
|
||||
import com.looker.droidify.sync.SyncPreference
|
||||
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
|
||||
import com.looker.droidify.utility.extension.toInstalledItem
|
||||
import com.looker.droidify.work.CleanUpWorker
|
||||
import com.looker.installer.InstallManager
|
||||
import com.looker.installer.installers.root.RootPermissionHandler
|
||||
import com.looker.installer.installers.shizuku.ShizukuPermissionHandler
|
||||
import com.looker.network.Downloader
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -58,9 +52,10 @@ import java.net.Proxy
|
||||
import javax.inject.Inject
|
||||
import kotlin.time.Duration.Companion.INFINITE
|
||||
import kotlin.time.Duration.Companion.hours
|
||||
import com.looker.core.common.R as CommonR
|
||||
|
||||
@HiltAndroidApp
|
||||
class Droidify : Application(), SingletonImageLoader.Factory, Configuration.Provider {
|
||||
class MainApplication : Application(), ImageLoaderFactory, Configuration.Provider {
|
||||
|
||||
private val parentJob = SupervisorJob()
|
||||
private val appScope = CoroutineScope(Dispatchers.Default + parentJob)
|
||||
@@ -74,21 +69,25 @@ class Droidify : Application(), SingletonImageLoader.Factory, Configuration.Prov
|
||||
@Inject
|
||||
lateinit var downloader: Downloader
|
||||
|
||||
@Inject
|
||||
lateinit var shizukuPermissionHandler: ShizukuPermissionHandler
|
||||
|
||||
@Inject
|
||||
lateinit var rootPermissionHandler: RootPermissionHandler
|
||||
|
||||
@Inject
|
||||
lateinit var workerFactory: HiltWorkerFactory
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
if (BuildConfig.DEBUG && SdkCheck.isOreo) strictThreadPolicy()
|
||||
|
||||
val databaseUpdated = Database.init(this)
|
||||
ProductPreferences.init(this, appScope)
|
||||
RepositoryUpdater.init(appScope, downloader)
|
||||
listenApplications()
|
||||
checkLanguage()
|
||||
updatePreference()
|
||||
appScope.launch { installer() }
|
||||
setupInstaller()
|
||||
|
||||
if (databaseUpdated) forceSyncAll()
|
||||
}
|
||||
@@ -99,24 +98,53 @@ class Droidify : Application(), SingletonImageLoader.Factory, Configuration.Prov
|
||||
installer.close()
|
||||
}
|
||||
|
||||
private fun listenApplications() {
|
||||
appScope.launch(Dispatchers.Default) {
|
||||
registerReceiver(
|
||||
InstalledAppReceiver(packageManager),
|
||||
IntentFilter().apply {
|
||||
addAction(Intent.ACTION_PACKAGE_ADDED)
|
||||
addAction(Intent.ACTION_PACKAGE_REMOVED)
|
||||
addDataScheme("package")
|
||||
private fun setupInstaller() {
|
||||
appScope.launch {
|
||||
launch {
|
||||
settingsRepository.get { installerType }.collect {
|
||||
if (it == InstallerType.SHIZUKU) handleShizukuInstaller()
|
||||
if (it == InstallerType.ROOT) {
|
||||
if (!rootPermissionHandler.isGranted) {
|
||||
settingsRepository.setInstallerType(InstallerType.Default)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
val installedItems =
|
||||
packageManager.getInstalledPackagesCompat()
|
||||
?.map { it.toInstalledItem() }
|
||||
?: return@launch
|
||||
Database.InstalledAdapter.putAll(installedItems)
|
||||
}
|
||||
installer()
|
||||
}
|
||||
}
|
||||
|
||||
private fun CoroutineScope.handleShizukuInstaller() = launch {
|
||||
shizukuPermissionHandler.state.collect { (isGranted, isAlive, _) ->
|
||||
if (isAlive && isGranted) {
|
||||
settingsRepository.setInstallerType(InstallerType.SHIZUKU)
|
||||
return@collect
|
||||
}
|
||||
if (isAlive) {
|
||||
settingsRepository.setInstallerType(InstallerType.Default)
|
||||
shizukuPermissionHandler.requestPermission()
|
||||
return@collect
|
||||
}
|
||||
settingsRepository.setInstallerType(InstallerType.Default)
|
||||
}
|
||||
}
|
||||
|
||||
private fun listenApplications() {
|
||||
registerReceiver(
|
||||
InstalledAppReceiver(packageManager),
|
||||
IntentFilter().apply {
|
||||
addAction(Intent.ACTION_PACKAGE_ADDED)
|
||||
addAction(Intent.ACTION_PACKAGE_REMOVED)
|
||||
addDataScheme("package")
|
||||
}
|
||||
)
|
||||
val installedItems =
|
||||
packageManager.getInstalledPackagesCompat()
|
||||
?.map { it.toInstalledItem() }
|
||||
?: return
|
||||
Database.InstalledAdapter.putAll(installedItems)
|
||||
}
|
||||
|
||||
private fun checkLanguage() {
|
||||
appScope.launch {
|
||||
val lastSetLanguage = settingsRepository.getInitial().language
|
||||
@@ -223,14 +251,9 @@ class Droidify : Application(), SingletonImageLoader.Factory, Configuration.Prov
|
||||
override fun onReceive(context: Context, intent: Intent) = Unit
|
||||
}
|
||||
|
||||
override val workManagerConfiguration: Configuration
|
||||
get() = Configuration.Builder()
|
||||
.setWorkerFactory(workerFactory)
|
||||
.build()
|
||||
|
||||
override fun newImageLoader(context: PlatformContext): ImageLoader {
|
||||
val memoryCache = MemoryCache.Builder()
|
||||
.maxSizePercent(context, 0.25)
|
||||
override fun newImageLoader(): ImageLoader {
|
||||
val memoryCache = MemoryCache.Builder(this)
|
||||
.maxSizePercent(0.25)
|
||||
.build()
|
||||
|
||||
val diskCache = DiskCache.Builder()
|
||||
@@ -241,27 +264,13 @@ class Droidify : Application(), SingletonImageLoader.Factory, Configuration.Prov
|
||||
return ImageLoader.Builder(this)
|
||||
.memoryCache(memoryCache)
|
||||
.diskCache(diskCache)
|
||||
.error(getDrawableCompat(R.drawable.ic_cannot_load).asImage())
|
||||
.error(CommonR.drawable.ic_cannot_load)
|
||||
.crossfade(350)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
fun strictThreadPolicy() {
|
||||
StrictMode.setThreadPolicy(
|
||||
StrictMode.ThreadPolicy.Builder()
|
||||
.detectDiskReads()
|
||||
.detectDiskWrites()
|
||||
.detectNetwork()
|
||||
.detectUnbufferedIo()
|
||||
.penaltyLog()
|
||||
override val workManagerConfiguration: Configuration
|
||||
get() = Configuration.Builder()
|
||||
.setWorkerFactory(workerFactory)
|
||||
.build()
|
||||
)
|
||||
StrictMode.setVmPolicy(
|
||||
StrictMode.VmPolicy.Builder()
|
||||
.detectAll()
|
||||
.penaltyLog()
|
||||
.build()
|
||||
)
|
||||
}
|
||||
308
app/src/main/kotlin/com/looker/droidify/ScreenActivity.kt
Normal file
308
app/src/main/kotlin/com/looker/droidify/ScreenActivity.kt
Normal file
@@ -0,0 +1,308 @@
|
||||
package com.looker.droidify
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.commit
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.looker.core.common.DeeplinkType
|
||||
import com.looker.core.common.SdkCheck
|
||||
import com.looker.core.common.deeplinkType
|
||||
import com.looker.core.common.extension.homeAsUp
|
||||
import com.looker.core.common.extension.inputManager
|
||||
import com.looker.core.common.requestNotificationPermission
|
||||
import com.looker.core.datastore.SettingsRepository
|
||||
import com.looker.core.datastore.extension.getThemeRes
|
||||
import com.looker.core.datastore.get
|
||||
import com.looker.droidify.database.CursorOwner
|
||||
import com.looker.droidify.ui.appDetail.AppDetailFragment
|
||||
import com.looker.droidify.ui.favourites.FavouritesFragment
|
||||
import com.looker.droidify.ui.repository.EditRepositoryFragment
|
||||
import com.looker.droidify.ui.repository.RepositoriesFragment
|
||||
import com.looker.droidify.ui.repository.RepositoryFragment
|
||||
import com.looker.droidify.ui.settings.SettingsFragment
|
||||
import com.looker.droidify.ui.tabsFragment.TabsFragment
|
||||
import com.looker.installer.InstallManager
|
||||
import com.looker.installer.model.installFrom
|
||||
import dagger.hilt.EntryPoint
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import dagger.hilt.android.EntryPointAccessors
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import kotlinx.coroutines.flow.drop
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
abstract class ScreenActivity : AppCompatActivity() {
|
||||
companion object {
|
||||
private const val STATE_FRAGMENT_STACK = "fragmentStack"
|
||||
}
|
||||
|
||||
sealed interface SpecialIntent {
|
||||
data object Updates : SpecialIntent
|
||||
class Install(val packageName: String?, val cacheFileName: String?) : SpecialIntent
|
||||
}
|
||||
|
||||
private val notificationPermission =
|
||||
registerForActivityResult(ActivityResultContracts.RequestPermission()) { }
|
||||
|
||||
@Inject
|
||||
lateinit var installer: InstallManager
|
||||
|
||||
@Parcelize
|
||||
private class FragmentStackItem(
|
||||
val className: String, val arguments: Bundle?, val savedState: Fragment.SavedState?
|
||||
) : Parcelable
|
||||
|
||||
lateinit var cursorOwner: CursorOwner
|
||||
private set
|
||||
|
||||
private var onBackPressedCallback: OnBackPressedCallback? = null
|
||||
|
||||
private val fragmentStack = mutableListOf<FragmentStackItem>()
|
||||
|
||||
private val currentFragment: Fragment?
|
||||
get() {
|
||||
supportFragmentManager.executePendingTransactions()
|
||||
return supportFragmentManager.findFragmentById(R.id.main_content)
|
||||
}
|
||||
|
||||
@EntryPoint
|
||||
@InstallIn(SingletonComponent::class)
|
||||
interface CustomUserRepositoryInjector {
|
||||
fun settingsRepository(): SettingsRepository
|
||||
}
|
||||
|
||||
private fun collectChange() {
|
||||
val hiltEntryPoint = EntryPointAccessors.fromApplication(
|
||||
this, CustomUserRepositoryInjector::class.java
|
||||
)
|
||||
val newSettings = hiltEntryPoint.settingsRepository().get { theme to dynamicTheme }
|
||||
runBlocking {
|
||||
val theme = newSettings.first()
|
||||
setTheme(
|
||||
resources.configuration.getThemeRes(
|
||||
theme = theme.first, dynamicTheme = theme.second
|
||||
)
|
||||
)
|
||||
}
|
||||
lifecycleScope.launch {
|
||||
newSettings.drop(1).collect { themeAndDynamic ->
|
||||
setTheme(
|
||||
resources.configuration.getThemeRes(
|
||||
theme = themeAndDynamic.first, dynamicTheme = themeAndDynamic.second
|
||||
)
|
||||
)
|
||||
recreate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
collectChange()
|
||||
super.onCreate(savedInstanceState)
|
||||
val rootView = FrameLayout(this).apply { id = R.id.main_content }
|
||||
addContentView(
|
||||
rootView, ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT
|
||||
)
|
||||
)
|
||||
|
||||
requestNotificationPermission(request = notificationPermission::launch)
|
||||
|
||||
supportFragmentManager.addFragmentOnAttachListener { _, _ ->
|
||||
hideKeyboard()
|
||||
}
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
cursorOwner = CursorOwner()
|
||||
supportFragmentManager.commit {
|
||||
add(cursorOwner, CursorOwner::class.java.name)
|
||||
}
|
||||
} else {
|
||||
cursorOwner =
|
||||
supportFragmentManager.findFragmentByTag(CursorOwner::class.java.name) as CursorOwner
|
||||
}
|
||||
|
||||
savedInstanceState?.getParcelableArrayList<FragmentStackItem>(STATE_FRAGMENT_STACK)
|
||||
?.let { fragmentStack += it }
|
||||
if (savedInstanceState == null) {
|
||||
replaceFragment(TabsFragment(), null)
|
||||
if ((intent.flags and Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY) == 0) {
|
||||
handleIntent(intent)
|
||||
}
|
||||
}
|
||||
if (SdkCheck.isR) {
|
||||
window.statusBarColor = resources.getColor(android.R.color.transparent, theme)
|
||||
window.navigationBarColor = resources.getColor(android.R.color.transparent, theme)
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
}
|
||||
backHandler()
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
outState.putParcelableArrayList(STATE_FRAGMENT_STACK, ArrayList(fragmentStack))
|
||||
}
|
||||
|
||||
private fun backHandler() {
|
||||
if (onBackPressedCallback == null) {
|
||||
onBackPressedCallback = object : OnBackPressedCallback(enabled = false) {
|
||||
override fun handleOnBackPressed() {
|
||||
hideKeyboard()
|
||||
popFragment()
|
||||
}
|
||||
}
|
||||
onBackPressedDispatcher.addCallback(
|
||||
this,
|
||||
onBackPressedCallback!!,
|
||||
)
|
||||
}
|
||||
onBackPressedCallback?.isEnabled = fragmentStack.isNotEmpty()
|
||||
}
|
||||
|
||||
private fun replaceFragment(fragment: Fragment, open: Boolean?) {
|
||||
if (open != null) {
|
||||
currentFragment?.view?.translationZ =
|
||||
(if (open) Int.MIN_VALUE else Int.MAX_VALUE).toFloat()
|
||||
}
|
||||
supportFragmentManager.commit {
|
||||
if (open != null) {
|
||||
setCustomAnimations(
|
||||
if (open) R.animator.slide_in else 0,
|
||||
if (open) R.animator.slide_in_keep else R.animator.slide_out
|
||||
)
|
||||
}
|
||||
setReorderingAllowed(true)
|
||||
replace(R.id.main_content, fragment)
|
||||
}
|
||||
}
|
||||
|
||||
private fun pushFragment(fragment: Fragment) {
|
||||
currentFragment?.let {
|
||||
fragmentStack.add(
|
||||
FragmentStackItem(
|
||||
it::class.java.name,
|
||||
it.arguments,
|
||||
supportFragmentManager.saveFragmentInstanceState(it)
|
||||
)
|
||||
)
|
||||
}
|
||||
replaceFragment(fragment, true)
|
||||
backHandler()
|
||||
}
|
||||
|
||||
private fun popFragment(): Boolean {
|
||||
return fragmentStack.isNotEmpty() && run {
|
||||
val stackItem = fragmentStack.removeAt(fragmentStack.size - 1)
|
||||
val fragment = Class.forName(stackItem.className).newInstance() as Fragment
|
||||
stackItem.arguments?.let(fragment::setArguments)
|
||||
stackItem.savedState?.let(fragment::setInitialSavedState)
|
||||
replaceFragment(fragment, false)
|
||||
backHandler()
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
private fun hideKeyboard() {
|
||||
inputManager?.hideSoftInputFromWindow((currentFocus ?: window.decorView).windowToken, 0)
|
||||
}
|
||||
|
||||
internal fun onToolbarCreated(toolbar: Toolbar) {
|
||||
if (fragmentStack.isNotEmpty()) {
|
||||
toolbar.navigationIcon = toolbar.context.homeAsUp
|
||||
toolbar.setNavigationOnClickListener { onBackPressedDispatcher.onBackPressed() }
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent)
|
||||
handleIntent(intent)
|
||||
}
|
||||
|
||||
protected fun handleSpecialIntent(specialIntent: SpecialIntent) {
|
||||
when (specialIntent) {
|
||||
is SpecialIntent.Updates -> {
|
||||
if (currentFragment !is TabsFragment) {
|
||||
fragmentStack.clear()
|
||||
replaceFragment(TabsFragment(), true)
|
||||
}
|
||||
val tabsFragment = currentFragment as TabsFragment
|
||||
tabsFragment.selectUpdates()
|
||||
backHandler()
|
||||
}
|
||||
|
||||
is SpecialIntent.Install -> {
|
||||
val packageName = specialIntent.packageName
|
||||
if (!packageName.isNullOrEmpty()) {
|
||||
navigateProduct(packageName)
|
||||
specialIntent.cacheFileName?.also { cacheFile ->
|
||||
val installItem = packageName installFrom cacheFile
|
||||
lifecycleScope.launch { installer install installItem }
|
||||
}
|
||||
}
|
||||
Unit
|
||||
}
|
||||
}::class
|
||||
}
|
||||
|
||||
open fun handleIntent(intent: Intent?) {
|
||||
when (intent?.action) {
|
||||
Intent.ACTION_VIEW -> {
|
||||
when (val deeplink = intent.deeplinkType) {
|
||||
is DeeplinkType.AppDetail -> {
|
||||
val fragment = currentFragment
|
||||
if (fragment !is AppDetailFragment) {
|
||||
navigateProduct(deeplink.packageName, deeplink.repoAddress)
|
||||
}
|
||||
}
|
||||
|
||||
is DeeplinkType.AddRepository -> {
|
||||
navigateAddRepository(repoAddress = deeplink.address)
|
||||
}
|
||||
|
||||
null -> {}
|
||||
}
|
||||
}
|
||||
|
||||
Intent.ACTION_SHOW_APP_INFO -> {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
val packageName = intent.getStringExtra(Intent.EXTRA_PACKAGE_NAME)
|
||||
|
||||
if (packageName != null && currentFragment !is AppDetailFragment) {
|
||||
navigateProduct(packageName)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun navigateFavourites() = pushFragment(FavouritesFragment())
|
||||
internal fun navigateProduct(packageName: String, repoAddress: String? = null) =
|
||||
pushFragment(AppDetailFragment(packageName, repoAddress))
|
||||
|
||||
internal fun navigateRepositories() = pushFragment(RepositoriesFragment())
|
||||
internal fun navigatePreferences() = pushFragment(SettingsFragment.newInstance())
|
||||
internal fun navigateAddRepository(repoAddress: String? = null) =
|
||||
pushFragment(EditRepositoryFragment(null, repoAddress))
|
||||
|
||||
internal fun navigateRepository(repositoryId: Long) =
|
||||
pushFragment(RepositoryFragment(repositoryId))
|
||||
|
||||
internal fun navigateEditRepository(repositoryId: Long) =
|
||||
pushFragment(EditRepositoryFragment(repositoryId, null))
|
||||
}
|
||||
@@ -2,9 +2,9 @@ package com.looker.droidify.content
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
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.core.common.extension.Json
|
||||
import com.looker.core.common.extension.parseDictionary
|
||||
import com.looker.core.common.extension.writeDictionary
|
||||
import com.looker.droidify.model.ProductPreference
|
||||
import com.looker.droidify.database.Database
|
||||
import com.looker.droidify.utility.serialization.productPreference
|
||||
|
||||
@@ -5,36 +5,35 @@ import android.os.Bundle
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.loader.app.LoaderManager
|
||||
import androidx.loader.content.Loader
|
||||
import com.looker.droidify.datastore.model.SortOrder
|
||||
import com.looker.core.datastore.model.SortOrder
|
||||
import com.looker.droidify.model.ProductItem
|
||||
|
||||
class CursorOwner : Fragment(), LoaderManager.LoaderCallbacks<Cursor> {
|
||||
sealed class Request {
|
||||
internal abstract val id: Int
|
||||
|
||||
class Available(
|
||||
data class ProductsAvailable(
|
||||
val searchQuery: String,
|
||||
val section: ProductItem.Section,
|
||||
val order: SortOrder,
|
||||
val order: SortOrder
|
||||
) : Request() {
|
||||
override val id: Int
|
||||
get() = 1
|
||||
}
|
||||
|
||||
class Installed(
|
||||
data class ProductsInstalled(
|
||||
val searchQuery: String,
|
||||
val section: ProductItem.Section,
|
||||
val order: SortOrder,
|
||||
val order: SortOrder
|
||||
) : Request() {
|
||||
override val id: Int
|
||||
get() = 2
|
||||
}
|
||||
|
||||
class Updates(
|
||||
data class ProductsUpdates(
|
||||
val searchQuery: String,
|
||||
val section: ProductItem.Section,
|
||||
val order: SortOrder,
|
||||
val skipSignatureCheck: Boolean,
|
||||
val order: SortOrder
|
||||
) : Request() {
|
||||
override val id: Int
|
||||
get() = 3
|
||||
@@ -53,7 +52,7 @@ class CursorOwner : Fragment(), LoaderManager.LoaderCallbacks<Cursor> {
|
||||
private data class ActiveRequest(
|
||||
val request: Request,
|
||||
val callback: Callback?,
|
||||
val cursor: Cursor?,
|
||||
val cursor: Cursor?
|
||||
)
|
||||
|
||||
init {
|
||||
@@ -94,7 +93,7 @@ class CursorOwner : Fragment(), LoaderManager.LoaderCallbacks<Cursor> {
|
||||
val request = activeRequests[id]!!.request
|
||||
return QueryLoader(requireContext()) {
|
||||
when (request) {
|
||||
is Request.Available ->
|
||||
is Request.ProductsAvailable ->
|
||||
Database.ProductAdapter
|
||||
.query(
|
||||
installed = false,
|
||||
@@ -102,10 +101,10 @@ class CursorOwner : Fragment(), LoaderManager.LoaderCallbacks<Cursor> {
|
||||
searchQuery = request.searchQuery,
|
||||
section = request.section,
|
||||
order = request.order,
|
||||
signal = it,
|
||||
signal = it
|
||||
)
|
||||
|
||||
is Request.Installed ->
|
||||
is Request.ProductsInstalled ->
|
||||
Database.ProductAdapter
|
||||
.query(
|
||||
installed = true,
|
||||
@@ -113,10 +112,10 @@ class CursorOwner : Fragment(), LoaderManager.LoaderCallbacks<Cursor> {
|
||||
searchQuery = request.searchQuery,
|
||||
section = request.section,
|
||||
order = request.order,
|
||||
signal = it,
|
||||
signal = it
|
||||
)
|
||||
|
||||
is Request.Updates ->
|
||||
is Request.ProductsUpdates ->
|
||||
Database.ProductAdapter
|
||||
.query(
|
||||
installed = true,
|
||||
@@ -124,8 +123,7 @@ class CursorOwner : Fragment(), LoaderManager.LoaderCallbacks<Cursor> {
|
||||
searchQuery = request.searchQuery,
|
||||
section = request.section,
|
||||
order = request.order,
|
||||
signal = it,
|
||||
skipSignatureCheck = request.skipSignatureCheck,
|
||||
signal = it
|
||||
)
|
||||
|
||||
is Request.Repositories -> Database.RepositoryAdapter.query(it)
|
||||
|
||||
@@ -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.droidify.BuildConfig
|
||||
import com.looker.droidify.datastore.model.SortOrder
|
||||
import com.looker.core.common.extension.Json
|
||||
import com.looker.core.common.extension.asSequence
|
||||
import com.looker.core.common.extension.firstOrNull
|
||||
import com.looker.core.common.extension.parseDictionary
|
||||
import com.looker.core.common.extension.writeDictionary
|
||||
import com.looker.core.common.log
|
||||
import com.looker.core.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.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.BuildConfig
|
||||
import com.looker.droidify.utility.serialization.product
|
||||
import com.looker.droidify.utility.serialization.productItem
|
||||
import com.looker.droidify.utility.serialization.repository
|
||||
@@ -71,20 +71,14 @@ object Database {
|
||||
get() = "$databasePrefix$innerName"
|
||||
|
||||
fun formatCreateTable(name: String): String {
|
||||
return buildString(128) {
|
||||
append("CREATE TABLE ")
|
||||
append(name)
|
||||
append(" (")
|
||||
trimAndJoin(createTable)
|
||||
append(")")
|
||||
}
|
||||
return "CREATE TABLE $name (${QueryBuilder.trimQuery(createTable)})"
|
||||
}
|
||||
|
||||
val createIndexPairFormatted: Pair<String, String>?
|
||||
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)"
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -190,7 +184,7 @@ object Database {
|
||||
}
|
||||
}
|
||||
|
||||
private class Helper(context: Context) : SQLiteOpenHelper(context, "droidify", null, 5) {
|
||||
private class Helper(context: Context) : SQLiteOpenHelper(context, "droidify", null, 4) {
|
||||
var created = false
|
||||
private set
|
||||
var updated = false
|
||||
@@ -220,7 +214,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
|
||||
@@ -233,7 +227,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
|
||||
}
|
||||
@@ -267,7 +261,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()
|
||||
@@ -295,7 +289,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_") }
|
||||
@@ -351,7 +345,7 @@ object Database {
|
||||
private fun SQLiteDatabase.insertOrReplace(
|
||||
replace: Boolean,
|
||||
table: String,
|
||||
contentValues: ContentValues,
|
||||
contentValues: ContentValues
|
||||
): Long {
|
||||
return if (replace) {
|
||||
replace(table, null, contentValues)
|
||||
@@ -359,7 +353,7 @@ object Database {
|
||||
insert(
|
||||
table,
|
||||
null,
|
||||
contentValues,
|
||||
contentValues
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -369,7 +363,7 @@ object Database {
|
||||
columns: Array<String>? = null,
|
||||
selection: Pair<String, Array<String>>? = null,
|
||||
orderBy: String? = null,
|
||||
signal: CancellationSignal? = null,
|
||||
signal: CancellationSignal? = null
|
||||
): Cursor {
|
||||
return query(
|
||||
false,
|
||||
@@ -381,7 +375,7 @@ object Database {
|
||||
null,
|
||||
orderBy,
|
||||
null,
|
||||
signal,
|
||||
signal
|
||||
)
|
||||
}
|
||||
|
||||
@@ -403,7 +397,7 @@ object Database {
|
||||
internal fun putWithoutNotification(
|
||||
repository: Repository,
|
||||
shouldReplace: Boolean,
|
||||
database: SQLiteDatabase,
|
||||
database: SQLiteDatabase
|
||||
): Long {
|
||||
return database.insertOrReplace(
|
||||
shouldReplace,
|
||||
@@ -415,7 +409,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))
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -448,8 +442,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) }
|
||||
}
|
||||
|
||||
@@ -469,9 +463,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() }
|
||||
}
|
||||
|
||||
@@ -479,7 +473,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() }
|
||||
}
|
||||
|
||||
@@ -495,9 +489,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)
|
||||
@@ -514,7 +508,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)
|
||||
}
|
||||
@@ -525,18 +519,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
|
||||
@@ -561,7 +555,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)
|
||||
}
|
||||
|
||||
@@ -583,28 +577,26 @@ object Database {
|
||||
.map { get(packageName, null) }
|
||||
.flowOn(Dispatchers.IO)
|
||||
|
||||
suspend fun getUpdates(skipSignatureCheck: Boolean): List<ProductItem> =
|
||||
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()
|
||||
}
|
||||
suspend fun getUpdates(): List<ProductItem> = 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()
|
||||
}
|
||||
}
|
||||
|
||||
fun getUpdatesStream(skipSignatureCheck: Boolean): Flow<List<ProductItem>> = flowOf(Unit)
|
||||
fun getUpdatesStream(): Flow<List<ProductItem>> = flowOf(Unit)
|
||||
.onCompletion { if (it == null) emitAll(flowCollection(Subject.Products)) }
|
||||
// Crashes due to immediate retrieval of data?
|
||||
.onEach { delay(50) }
|
||||
.map { getUpdates(skipSignatureCheck) }
|
||||
.map { getUpdates() }
|
||||
.flowOn(Dispatchers.IO)
|
||||
|
||||
fun get(packageName: String, signal: CancellationSignal?): List<Product> {
|
||||
@@ -613,10 +605,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() }
|
||||
}
|
||||
|
||||
@@ -631,26 +623,24 @@ 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 = 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} != ''"""
|
||||
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} != ''"""
|
||||
|
||||
builder += """SELECT product.rowid AS _id, product.${Schema.Product.ROW_REPOSITORY_ID},
|
||||
product.${Schema.Product.ROW_PACKAGE_NAME}, product.${Schema.Product.ROW_NAME},
|
||||
@@ -738,10 +728,6 @@ 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 {
|
||||
@@ -807,10 +793,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) }
|
||||
}
|
||||
|
||||
@@ -823,7 +809,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)
|
||||
@@ -843,7 +829,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)
|
||||
@@ -855,7 +841,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))
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -868,7 +854,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)
|
||||
@@ -924,9 +910,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(
|
||||
@@ -936,7 +922,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)
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -949,20 +935,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}")
|
||||
@@ -971,7 +957,7 @@ object Database {
|
||||
notifyChanged(
|
||||
Subject.Repositories,
|
||||
Subject.Repository(repository.id),
|
||||
Subject.Products,
|
||||
Subject.Products
|
||||
)
|
||||
} else {
|
||||
db.execSQL("DROP TABLE IF EXISTS ${Schema.Product.temporaryName}")
|
||||
|
||||
@@ -3,20 +3,26 @@ package com.looker.droidify.database
|
||||
import android.database.Cursor
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import android.os.CancellationSignal
|
||||
import com.looker.core.common.extension.asSequence
|
||||
import com.looker.core.common.log
|
||||
import com.looker.droidify.BuildConfig
|
||||
import com.looker.droidify.utility.common.extension.asSequence
|
||||
import com.looker.droidify.utility.common.log
|
||||
|
||||
class QueryBuilder {
|
||||
companion object {
|
||||
fun trimQuery(query: String): String {
|
||||
return query.lines().map { it.trim() }.filter { it.isNotEmpty() }
|
||||
.joinToString(separator = " ")
|
||||
}
|
||||
}
|
||||
|
||||
private val builder = StringBuilder(256)
|
||||
private val builder = StringBuilder()
|
||||
private val arguments = mutableListOf<String>()
|
||||
|
||||
operator fun plusAssign(query: String) {
|
||||
if (builder.isNotEmpty()) {
|
||||
builder.append(" ")
|
||||
}
|
||||
builder.trimAndJoin(query)
|
||||
builder.append(trimQuery(query))
|
||||
}
|
||||
|
||||
operator fun remAssign(argument: String) {
|
||||
@@ -42,53 +48,3 @@ 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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,15 +3,15 @@ package com.looker.droidify.database
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import com.fasterxml.jackson.core.JsonToken
|
||||
import com.looker.droidify.utility.common.Exporter
|
||||
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.core.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.core.di.ApplicationScope
|
||||
import com.looker.core.di.IoDispatcher
|
||||
import com.looker.droidify.model.Repository
|
||||
import com.looker.droidify.utility.serialization.repository
|
||||
import com.looker.droidify.utility.serialization.serialize
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
package com.looker.droidify.datastore.migration
|
||||
|
||||
import com.looker.droidify.datastore.PreferenceSettingsRepository.PreferencesKeys.setting
|
||||
import com.looker.droidify.datastore.Settings
|
||||
import kotlinx.coroutines.flow.first
|
||||
|
||||
class ProtoToPreferenceMigration(
|
||||
private val oldDataStore: androidx.datastore.core.DataStore<Settings>
|
||||
) : androidx.datastore.core.DataMigration<androidx.datastore.preferences.core.Preferences> {
|
||||
override suspend fun cleanUp() {
|
||||
}
|
||||
|
||||
override suspend fun shouldMigrate(currentData: androidx.datastore.preferences.core.Preferences): Boolean {
|
||||
return currentData.asMap().isEmpty()
|
||||
}
|
||||
|
||||
override suspend fun migrate(currentData: androidx.datastore.preferences.core.Preferences): androidx.datastore.preferences.core.Preferences {
|
||||
val settings = oldDataStore.data.first()
|
||||
val preferences = currentData.toMutablePreferences()
|
||||
preferences.setting(settings)
|
||||
return preferences
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
package com.looker.droidify.domain.model
|
||||
|
||||
import java.security.MessageDigest
|
||||
import java.security.cert.Certificate
|
||||
import java.util.Locale
|
||||
|
||||
@JvmInline
|
||||
value class Fingerprint(val value: String) {
|
||||
init {
|
||||
require(value.isNotBlank() && value.length == FINGERPRINT_LENGTH) { "Invalid Fingerprint: $value" }
|
||||
}
|
||||
|
||||
override fun toString(): String = value
|
||||
}
|
||||
|
||||
@Suppress("NOTHING_TO_INLINE")
|
||||
inline fun Fingerprint.check(found: Fingerprint): Boolean {
|
||||
return found.value.equals(value, ignoreCase = true)
|
||||
}
|
||||
|
||||
private const val FINGERPRINT_LENGTH = 64
|
||||
|
||||
fun ByteArray.hex(): String = joinToString(separator = "") { byte ->
|
||||
"%02x".format(Locale.US, byte.toInt() and 0xff)
|
||||
}
|
||||
|
||||
fun Fingerprint.formattedString(): String = value.windowed(2, 2, false)
|
||||
.take(FINGERPRINT_LENGTH / 2).joinToString(separator = " ") { it.uppercase(Locale.US) }
|
||||
|
||||
fun Certificate.fingerprint(): Fingerprint {
|
||||
val bytes = encoded
|
||||
return if (bytes.size >= 256) {
|
||||
try {
|
||||
val fingerprint = MessageDigest.getInstance("sha256").digest(bytes)
|
||||
Fingerprint(fingerprint.hex().uppercase())
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
Fingerprint("")
|
||||
}
|
||||
} else {
|
||||
Fingerprint("")
|
||||
}
|
||||
}
|
||||
@@ -51,6 +51,7 @@ open class DrawableWrapper(val drawable: Drawable) : Drawable() {
|
||||
drawable.colorFilter = colorFilter
|
||||
}
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
@Suppress("DEPRECATION")
|
||||
override fun getOpacity(): Int = drawable.opacity
|
||||
}
|
||||
|
||||
@@ -3,10 +3,11 @@ package com.looker.droidify.index
|
||||
import android.content.ContentValues
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import com.fasterxml.jackson.core.JsonToken
|
||||
import com.looker.droidify.utility.common.extension.Json
|
||||
import com.looker.droidify.utility.common.extension.asSequence
|
||||
import com.looker.droidify.utility.common.extension.collectNotNull
|
||||
import com.looker.droidify.utility.common.extension.writeDictionary
|
||||
import com.looker.core.common.extension.Json
|
||||
import com.looker.core.common.extension.asSequence
|
||||
import com.looker.core.common.extension.collectNotNull
|
||||
import com.looker.core.common.extension.execWithResult
|
||||
import com.looker.core.common.extension.writeDictionary
|
||||
import com.looker.droidify.model.Product
|
||||
import com.looker.droidify.model.Release
|
||||
import com.looker.droidify.utility.serialization.product
|
||||
@@ -82,9 +83,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 {
|
||||
@@ -111,8 +112,4 @@ class IndexMerger(file: File) : Closeable {
|
||||
override fun close() {
|
||||
db.use { closeTransaction() }
|
||||
}
|
||||
|
||||
private inline fun SQLiteDatabase.execWithResult(sql: String) {
|
||||
rawQuery(sql, null).use { it.count }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,27 +5,16 @@ 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.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.core.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.core.common.nullIfEmpty
|
||||
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 {
|
||||
@@ -43,12 +32,9 @@ object IndexV1Parser {
|
||||
}
|
||||
|
||||
private class Screenshots(
|
||||
val video: List<String>,
|
||||
val phone: List<String>,
|
||||
val smallTablet: List<String>,
|
||||
val largeTablet: List<String>,
|
||||
val wear: List<String>,
|
||||
val tv: List<String>,
|
||||
val largeTablet: List<String>
|
||||
)
|
||||
|
||||
private class Localized(
|
||||
@@ -104,9 +90,10 @@ object IndexV1Parser {
|
||||
}
|
||||
|
||||
private fun <T> Map<String, Localized>.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 <T> Map<String, Localized>.findLocalized(callback: (Localized) -> T?): T? {
|
||||
@@ -135,11 +122,12 @@ object IndexV1Parser {
|
||||
|
||||
internal object DonateComparator : Comparator<Product.Donate> {
|
||||
private val classes = listOf(
|
||||
Regular::class,
|
||||
Bitcoin::class,
|
||||
Litecoin::class,
|
||||
Liberapay::class,
|
||||
OpenCollective::class
|
||||
Product.Donate.Regular::class,
|
||||
Product.Donate.Bitcoin::class,
|
||||
Product.Donate.Litecoin::class,
|
||||
Product.Donate.Flattr::class,
|
||||
Product.Donate.Liberapay::class,
|
||||
Product.Donate.OpenCollective::class
|
||||
)
|
||||
|
||||
override fun compare(donate1: Product.Donate, donate2: Product.Donate): Int {
|
||||
@@ -153,25 +141,14 @@ object IndexV1Parser {
|
||||
}
|
||||
}
|
||||
|
||||
private const val DICT_REPO = "repo"
|
||||
private const val DICT_PRODUCT = "apps"
|
||||
private const val DICT_RELEASE = "packages"
|
||||
|
||||
private const val KEY_REPO_ADDRESS = "address"
|
||||
private const val KEY_REPO_MIRRORS = "mirrors"
|
||||
private const val KEY_REPO_NAME = "name"
|
||||
private const val KEY_REPO_DESC = "description"
|
||||
private const val KEY_REPO_VER = "version"
|
||||
private const val KEY_REPO_TIME = "timestamp"
|
||||
|
||||
fun parse(repositoryId: Long, inputStream: InputStream, callback: Callback) {
|
||||
val jsonParser = Json.factory.createParser(inputStream)
|
||||
if (jsonParser.nextToken() != JsonToken.START_OBJECT) {
|
||||
jsonParser.illegal()
|
||||
} else {
|
||||
jsonParser.forEachKey { key ->
|
||||
jsonParser.forEachKey { it ->
|
||||
when {
|
||||
key.dictionary(DICT_REPO) -> {
|
||||
it.dictionary("repo") -> {
|
||||
var address = ""
|
||||
var mirrors = emptyList<String>()
|
||||
var name = ""
|
||||
@@ -180,14 +157,12 @@ object IndexV1Parser {
|
||||
var timestamp = 0L
|
||||
forEachKey {
|
||||
when {
|
||||
it.string(KEY_REPO_ADDRESS) -> address = valueAsString
|
||||
it.array(KEY_REPO_MIRRORS) -> mirrors =
|
||||
collectDistinctNotEmptyStrings()
|
||||
|
||||
it.string(KEY_REPO_NAME) -> name = valueAsString
|
||||
it.string(KEY_REPO_DESC) -> description = valueAsString
|
||||
it.number(KEY_REPO_VER) -> version = valueAsInt
|
||||
it.number(KEY_REPO_TIME) -> timestamp = valueAsLong
|
||||
it.string("address") -> address = valueAsString
|
||||
it.array("mirrors") -> mirrors = collectDistinctNotEmptyStrings()
|
||||
it.string("name") -> name = valueAsString
|
||||
it.string("description") -> description = valueAsString
|
||||
it.number("version") -> version = valueAsInt
|
||||
it.number("timestamp") -> timestamp = valueAsLong
|
||||
else -> skipChildren()
|
||||
}
|
||||
}
|
||||
@@ -207,12 +182,12 @@ object IndexV1Parser {
|
||||
)
|
||||
}
|
||||
|
||||
key.array(DICT_PRODUCT) -> forEach(JsonToken.START_OBJECT) {
|
||||
it.array("apps") -> forEach(JsonToken.START_OBJECT) {
|
||||
val product = parseProduct(repositoryId)
|
||||
callback.onProduct(product)
|
||||
}
|
||||
|
||||
key.dictionary(DICT_RELEASE) -> forEachKey {
|
||||
it.dictionary("packages") -> forEachKey {
|
||||
if (it.token == JsonToken.START_ARRAY) {
|
||||
val packageName = it.key
|
||||
val releases = collectNotNull(JsonToken.START_OBJECT) { parseRelease() }
|
||||
@@ -228,38 +203,6 @@ object IndexV1Parser {
|
||||
}
|
||||
}
|
||||
|
||||
private const val KEY_PRODUCT_PACKAGENAME = "packageName"
|
||||
private const val KEY_PRODUCT_NAME = "name"
|
||||
private const val KEY_PRODUCT_SUMMARY = "summary"
|
||||
private const val KEY_PRODUCT_DESCRIPTION = "description"
|
||||
private const val KEY_PRODUCT_ICON = "icon"
|
||||
private const val KEY_PRODUCT_AUTHORNAME = "authorName"
|
||||
private const val KEY_PRODUCT_AUTHOREMAIL = "authorEmail"
|
||||
private const val KEY_PRODUCT_AUTHORWEBSITE = "authorWebSite"
|
||||
private const val KEY_PRODUCT_SOURCECODE = "sourceCode"
|
||||
private const val KEY_PRODUCT_CHANGELOG = "changelog"
|
||||
private const val KEY_PRODUCT_WEBSITE = "webSite"
|
||||
private const val KEY_PRODUCT_ISSUETRACKER = "issueTracker"
|
||||
private const val KEY_PRODUCT_ADDED = "added"
|
||||
private const val KEY_PRODUCT_LASTUPDATED = "lastUpdated"
|
||||
private const val KEY_PRODUCT_SUGGESTEDVERSIONCODE = "suggestedVersionCode"
|
||||
private const val KEY_PRODUCT_CATEGORIES = "categories"
|
||||
private const val KEY_PRODUCT_ANTIFEATURES = "antiFeatures"
|
||||
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_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_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 = ""
|
||||
var nameFallback = ""
|
||||
@@ -281,42 +224,42 @@ object IndexV1Parser {
|
||||
val licenses = mutableListOf<String>()
|
||||
val donates = mutableListOf<Product.Donate>()
|
||||
val localizedMap = mutableMapOf<String, Localized>()
|
||||
forEachKey { key ->
|
||||
forEachKey { it ->
|
||||
when {
|
||||
key.string(KEY_PRODUCT_PACKAGENAME) -> packageName = valueAsString
|
||||
key.string(KEY_PRODUCT_NAME) -> nameFallback = valueAsString
|
||||
key.string(KEY_PRODUCT_SUMMARY) -> summaryFallback = valueAsString
|
||||
key.string(KEY_PRODUCT_DESCRIPTION) -> descriptionFallback = valueAsString
|
||||
key.string(KEY_PRODUCT_ICON) -> icon = validateIcon(valueAsString)
|
||||
key.string(KEY_PRODUCT_AUTHORNAME) -> authorName = valueAsString
|
||||
key.string(KEY_PRODUCT_AUTHOREMAIL) -> authorEmail = valueAsString
|
||||
key.string(KEY_PRODUCT_AUTHORWEBSITE) -> authorWeb = valueAsString
|
||||
key.string(KEY_PRODUCT_SOURCECODE) -> source = valueAsString
|
||||
key.string(KEY_PRODUCT_CHANGELOG) -> changelog = valueAsString
|
||||
key.string(KEY_PRODUCT_WEBSITE) -> web = valueAsString
|
||||
key.string(KEY_PRODUCT_ISSUETRACKER) -> tracker = valueAsString
|
||||
key.number(KEY_PRODUCT_ADDED) -> added = valueAsLong
|
||||
key.number(KEY_PRODUCT_LASTUPDATED) -> updated = valueAsLong
|
||||
key.string(KEY_PRODUCT_SUGGESTEDVERSIONCODE) ->
|
||||
it.string("packageName") -> packageName = valueAsString
|
||||
it.string("name") -> nameFallback = valueAsString
|
||||
it.string("summary") -> summaryFallback = valueAsString
|
||||
it.string("description") -> descriptionFallback = valueAsString
|
||||
it.string("icon") -> icon = validateIcon(valueAsString)
|
||||
it.string("authorName") -> authorName = valueAsString
|
||||
it.string("authorEmail") -> authorEmail = valueAsString
|
||||
it.string("authorWebSite") -> authorWeb = valueAsString
|
||||
it.string("sourceCode") -> source = valueAsString
|
||||
it.string("changelog") -> changelog = valueAsString
|
||||
it.string("webSite") -> web = valueAsString
|
||||
it.string("issueTracker") -> tracker = valueAsString
|
||||
it.number("added") -> added = valueAsLong
|
||||
it.number("lastUpdated") -> updated = valueAsLong
|
||||
it.string("suggestedVersionCode") ->
|
||||
suggestedVersionCode =
|
||||
valueAsString.toLongOrNull() ?: 0L
|
||||
|
||||
key.array(KEY_PRODUCT_CATEGORIES) -> categories = collectDistinctNotEmptyStrings()
|
||||
key.array(KEY_PRODUCT_ANTIFEATURES) -> antiFeatures =
|
||||
collectDistinctNotEmptyStrings()
|
||||
|
||||
key.string(KEY_PRODUCT_LICENSE) -> licenses += valueAsString.split(',')
|
||||
it.array("categories") -> categories = collectDistinctNotEmptyStrings()
|
||||
it.array("antiFeatures") -> antiFeatures = collectDistinctNotEmptyStrings()
|
||||
it.string("license") -> licenses += valueAsString.split(',')
|
||||
.filter { it.isNotEmpty() }
|
||||
|
||||
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)
|
||||
it.string("donate") -> donates += Product.Donate.Regular(valueAsString)
|
||||
it.string("bitcoin") -> donates += Product.Donate.Bitcoin(valueAsString)
|
||||
it.string("flattrID") -> donates += Product.Donate.Flattr(valueAsString)
|
||||
it.string("liberapayID") -> donates += Product.Donate.Liberapay(valueAsString)
|
||||
it.string("openCollective") -> donates += Product.Donate.OpenCollective(
|
||||
valueAsString
|
||||
)
|
||||
|
||||
key.dictionary(KEY_PRODUCT_LOCALIZED) -> forEachKey { localizedKey ->
|
||||
if (localizedKey.token == JsonToken.START_OBJECT) {
|
||||
val locale = localizedKey.key
|
||||
it.dictionary("localized") -> forEachKey { it ->
|
||||
if (it.token == JsonToken.START_OBJECT) {
|
||||
val locale = it.key
|
||||
var name = ""
|
||||
var summary = ""
|
||||
var description = ""
|
||||
@@ -325,52 +268,46 @@ object IndexV1Parser {
|
||||
var phone = emptyList<String>()
|
||||
var smallTablet = emptyList<String>()
|
||||
var largeTablet = emptyList<String>()
|
||||
var wear = emptyList<String>()
|
||||
var tv = emptyList<String>()
|
||||
var video = emptyList<String>()
|
||||
forEachKey {
|
||||
when {
|
||||
it.string(KEY_PRODUCT_NAME) -> name = valueAsString
|
||||
it.string(KEY_PRODUCT_SUMMARY) -> summary = valueAsString
|
||||
it.string(KEY_PRODUCT_DESCRIPTION) -> description = valueAsString
|
||||
it.string(KEY_PRODUCT_WHATSNEW) -> whatsNew = valueAsString
|
||||
it.string(KEY_PRODUCT_ICON) -> metadataIcon = valueAsString
|
||||
it.string(KEY_PRODUCT_VIDEO) -> video = listOf(valueAsString)
|
||||
it.array(KEY_PRODUCT_PHONE_SCREENSHOTS) ->
|
||||
phone = collectDistinctNotEmptyStrings()
|
||||
it.string("name") -> name = valueAsString
|
||||
it.string("summary") -> summary = valueAsString
|
||||
it.string("description") -> description = valueAsString
|
||||
it.string("whatsNew") -> whatsNew = valueAsString
|
||||
it.string("icon") -> metadataIcon = valueAsString
|
||||
it.array("phoneScreenshots") ->
|
||||
phone =
|
||||
collectDistinctNotEmptyStrings()
|
||||
|
||||
it.array(KEY_PRODUCT_SEVEN_INCH_SCREENSHOTS) ->
|
||||
smallTablet = collectDistinctNotEmptyStrings()
|
||||
it.array("sevenInchScreenshots") ->
|
||||
smallTablet =
|
||||
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()
|
||||
it.array("tenInchScreenshots") ->
|
||||
largeTablet =
|
||||
collectDistinctNotEmptyStrings()
|
||||
|
||||
else -> skipChildren()
|
||||
}
|
||||
}
|
||||
val isScreenshotEmpty =
|
||||
arrayOf(video, phone, smallTablet, largeTablet, wear, tv)
|
||||
.any { it.isNotEmpty() }
|
||||
val screenshots =
|
||||
if (isScreenshotEmpty) {
|
||||
Screenshots(video, phone, smallTablet, largeTablet, wear, tv)
|
||||
if (sequenceOf(
|
||||
phone,
|
||||
smallTablet,
|
||||
largeTablet
|
||||
).any { it.isNotEmpty() }
|
||||
) {
|
||||
Screenshots(phone, smallTablet, largeTablet)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
localizedMap[locale] = Localized(
|
||||
name = name,
|
||||
summary = summary,
|
||||
description = description,
|
||||
whatsNew = whatsNew,
|
||||
metadataIcon = metadataIcon.nullIfEmpty()?.let { "$locale/$it" }
|
||||
.orEmpty(),
|
||||
screenshots = screenshots,
|
||||
name,
|
||||
summary,
|
||||
description,
|
||||
whatsNew,
|
||||
metadataIcon.nullIfEmpty()?.let { "$locale/$it" }.orEmpty(),
|
||||
screenshots
|
||||
)
|
||||
} else {
|
||||
skipChildren()
|
||||
@@ -393,61 +330,54 @@ object IndexV1Parser {
|
||||
}
|
||||
val screenshotPairs =
|
||||
localizedMap.find { key, localized -> localized.screenshots?.let { Pair(key, it) } }
|
||||
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()
|
||||
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()
|
||||
return Product(
|
||||
repositoryId = repositoryId,
|
||||
packageName = packageName,
|
||||
name = name,
|
||||
summary = summary,
|
||||
description = description,
|
||||
whatsNew = whatsNew,
|
||||
icon = icon,
|
||||
metadataIcon = metadataIcon,
|
||||
author = Product.Author(authorName, authorEmail, authorWeb),
|
||||
source = source,
|
||||
changelog = changelog,
|
||||
web = web,
|
||||
tracker = tracker,
|
||||
added = added,
|
||||
updated = updated,
|
||||
suggestedVersionCode = suggestedVersionCode,
|
||||
categories = categories,
|
||||
antiFeatures = antiFeatures,
|
||||
licenses = licenses,
|
||||
donates = donates.sortedWith(DonateComparator),
|
||||
screenshots = screenshots,
|
||||
releases = emptyList()
|
||||
repositoryId,
|
||||
packageName,
|
||||
name,
|
||||
summary,
|
||||
description,
|
||||
whatsNew,
|
||||
icon,
|
||||
metadataIcon,
|
||||
Product.Author(authorName, authorEmail, authorWeb),
|
||||
source,
|
||||
changelog,
|
||||
web,
|
||||
tracker,
|
||||
added,
|
||||
updated,
|
||||
suggestedVersionCode,
|
||||
categories,
|
||||
antiFeatures,
|
||||
licenses,
|
||||
donates.sortedWith(DonateComparator),
|
||||
screenshots,
|
||||
emptyList()
|
||||
)
|
||||
}
|
||||
|
||||
private const val KEY_RELEASE_VERSIONNAME = "versionName"
|
||||
private const val KEY_RELEASE_VERSIONCODE = "versionCode"
|
||||
private const val KEY_RELEASE_ADDED = "added"
|
||||
private const val KEY_RELEASE_SIZE = "size"
|
||||
private const val KEY_RELEASE_MINSDKVERSION = "minSdkVersion"
|
||||
private const val KEY_RELEASE_TARGETSDKVERSION = "targetSdkVersion"
|
||||
private const val KEY_RELEASE_MAXSDKVERSION = "maxSdkVersion"
|
||||
private const val KEY_RELEASE_SRCNAME = "srcname"
|
||||
private const val KEY_RELEASE_APKNAME = "apkName"
|
||||
private const val KEY_RELEASE_HASH = "hash"
|
||||
private const val KEY_RELEASE_HASHTYPE = "hashType"
|
||||
private const val KEY_RELEASE_SIG = "sig"
|
||||
private const val KEY_RELEASE_OBBMAINFILE = "obbMainFile"
|
||||
private const val KEY_RELEASE_OBBMAINFILESHA256 = "obbMainFileSha256"
|
||||
private const val KEY_RELEASE_OBBPATCHFILE = "obbPatchFile"
|
||||
private const val KEY_RELEASE_OBBPATCHFILESHA256 = "obbPatchFileSha256"
|
||||
private const val KEY_RELEASE_USESPERMISSION = "uses-permission"
|
||||
private const val KEY_RELEASE_USESPERMISSIONSDK23 = "uses-permission-sdk-23"
|
||||
private const val KEY_RELEASE_FEATURES = "features"
|
||||
private const val KEY_RELEASE_NATIVECODE = "nativecode"
|
||||
|
||||
private fun JsonParser.parseRelease(): Release {
|
||||
var version = ""
|
||||
var versionCode = 0L
|
||||
@@ -468,28 +398,28 @@ object IndexV1Parser {
|
||||
val permissions = linkedSetOf<String>()
|
||||
var features = emptyList<String>()
|
||||
var platforms = emptyList<String>()
|
||||
forEachKey { key ->
|
||||
forEachKey {
|
||||
when {
|
||||
key.string(KEY_RELEASE_VERSIONNAME) -> version = valueAsString
|
||||
key.number(KEY_RELEASE_VERSIONCODE) -> versionCode = valueAsLong
|
||||
key.number(KEY_RELEASE_ADDED) -> added = valueAsLong
|
||||
key.number(KEY_RELEASE_SIZE) -> size = valueAsLong
|
||||
key.number(KEY_RELEASE_MINSDKVERSION) -> minSdkVersion = valueAsInt
|
||||
key.number(KEY_RELEASE_TARGETSDKVERSION) -> targetSdkVersion = valueAsInt
|
||||
key.number(KEY_RELEASE_MAXSDKVERSION) -> maxSdkVersion = valueAsInt
|
||||
key.string(KEY_RELEASE_SRCNAME) -> source = valueAsString
|
||||
key.string(KEY_RELEASE_APKNAME) -> release = valueAsString
|
||||
key.string(KEY_RELEASE_HASH) -> hash = valueAsString
|
||||
key.string(KEY_RELEASE_HASHTYPE) -> hashTypeCandidate = valueAsString
|
||||
key.string(KEY_RELEASE_SIG) -> signature = valueAsString
|
||||
key.string(KEY_RELEASE_OBBMAINFILE) -> obbMain = valueAsString
|
||||
key.string(KEY_RELEASE_OBBMAINFILESHA256) -> obbMainHash = valueAsString
|
||||
key.string(KEY_RELEASE_OBBPATCHFILE) -> obbPatch = valueAsString
|
||||
key.string(KEY_RELEASE_OBBPATCHFILESHA256) -> obbPatchHash = valueAsString
|
||||
key.array(KEY_RELEASE_USESPERMISSION) -> collectPermissions(permissions, 0)
|
||||
key.array(KEY_RELEASE_USESPERMISSIONSDK23) -> collectPermissions(permissions, 23)
|
||||
key.array(KEY_RELEASE_FEATURES) -> features = collectDistinctNotEmptyStrings()
|
||||
key.array(KEY_RELEASE_NATIVECODE) -> platforms = collectDistinctNotEmptyStrings()
|
||||
it.string("versionName") -> version = valueAsString
|
||||
it.number("versionCode") -> versionCode = valueAsLong
|
||||
it.number("added") -> added = valueAsLong
|
||||
it.number("size") -> size = valueAsLong
|
||||
it.number("minSdkVersion") -> minSdkVersion = valueAsInt
|
||||
it.number("targetSdkVersion") -> targetSdkVersion = valueAsInt
|
||||
it.number("maxSdkVersion") -> maxSdkVersion = valueAsInt
|
||||
it.string("srcname") -> source = valueAsString
|
||||
it.string("apkName") -> release = valueAsString
|
||||
it.string("hash") -> hash = valueAsString
|
||||
it.string("hashType") -> hashTypeCandidate = valueAsString
|
||||
it.string("sig") -> signature = valueAsString
|
||||
it.string("obbMainFile") -> obbMain = valueAsString
|
||||
it.string("obbMainFileSha256") -> obbMainHash = valueAsString
|
||||
it.string("obbPatchFile") -> obbPatch = valueAsString
|
||||
it.string("obbPatchFileSha256") -> obbPatchHash = valueAsString
|
||||
it.array("uses-permission") -> collectPermissions(permissions, 0)
|
||||
it.array("uses-permission-sdk-23") -> collectPermissions(permissions, 23)
|
||||
it.array("features") -> features = collectDistinctNotEmptyStrings()
|
||||
it.array("nativecode") -> platforms = collectDistinctNotEmptyStrings()
|
||||
else -> skipChildren()
|
||||
}
|
||||
}
|
||||
@@ -498,29 +428,29 @@ object IndexV1Parser {
|
||||
val obbMainHashType = if (obbMainHash.isNotEmpty()) "sha256" else ""
|
||||
val obbPatchHashType = if (obbPatchHash.isNotEmpty()) "sha256" else ""
|
||||
return Release(
|
||||
selected = false,
|
||||
version = version,
|
||||
versionCode = versionCode,
|
||||
added = added,
|
||||
size = size,
|
||||
minSdkVersion = minSdkVersion,
|
||||
targetSdkVersion = targetSdkVersion,
|
||||
maxSdkVersion = maxSdkVersion,
|
||||
source = source,
|
||||
release = release,
|
||||
hash = hash,
|
||||
hashType = hashType,
|
||||
signature = signature,
|
||||
obbMain = obbMain,
|
||||
obbMainHash = obbMainHash,
|
||||
obbMainHashType = obbMainHashType,
|
||||
obbPatch = obbPatch,
|
||||
obbPatchHash = obbPatchHash,
|
||||
obbPatchHashType = obbPatchHashType,
|
||||
permissions = permissions.toList(),
|
||||
features = features,
|
||||
platforms = platforms,
|
||||
incompatibilities = emptyList()
|
||||
false,
|
||||
version,
|
||||
versionCode,
|
||||
added,
|
||||
size,
|
||||
minSdkVersion,
|
||||
targetSdkVersion,
|
||||
maxSdkVersion,
|
||||
source,
|
||||
release,
|
||||
hash,
|
||||
hashType,
|
||||
signature,
|
||||
obbMain,
|
||||
obbMainHash,
|
||||
obbMainHashType,
|
||||
obbPatch,
|
||||
obbPatchHash,
|
||||
obbPatchHashType,
|
||||
permissions.toList(),
|
||||
features,
|
||||
platforms,
|
||||
emptyList()
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,28 +2,28 @@ package com.looker.droidify.index
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import com.looker.droidify.database.Database
|
||||
import com.looker.droidify.domain.model.fingerprint
|
||||
import com.looker.core.common.SdkCheck
|
||||
import com.looker.core.common.cache.Cache
|
||||
import com.looker.core.common.extension.fingerprint
|
||||
import com.looker.core.common.extension.toFormattedString
|
||||
import com.looker.core.common.result.Result
|
||||
import com.looker.droidify.model.Product
|
||||
import com.looker.droidify.model.Release
|
||||
import com.looker.droidify.model.Repository
|
||||
import com.looker.droidify.network.Downloader
|
||||
import com.looker.droidify.network.NetworkResponse
|
||||
import com.looker.droidify.utility.common.SdkCheck
|
||||
import com.looker.droidify.utility.common.cache.Cache
|
||||
import com.looker.droidify.utility.common.extension.toFormattedString
|
||||
import com.looker.droidify.utility.common.result.Result
|
||||
import com.looker.droidify.database.Database
|
||||
import com.looker.droidify.utility.extension.android.Android
|
||||
import com.looker.droidify.utility.getProgress
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.drop
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.map
|
||||
import com.looker.network.Downloader
|
||||
import com.looker.network.NetworkResponse
|
||||
import java.io.File
|
||||
import java.security.CodeSigner
|
||||
import java.security.cert.Certificate
|
||||
import java.util.jar.JarEntry
|
||||
import java.util.jar.JarFile
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.drop
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
object RepositoryUpdater {
|
||||
enum class Stage {
|
||||
@@ -31,7 +31,7 @@ object RepositoryUpdater {
|
||||
}
|
||||
|
||||
// TODO Add support for Index-V2 and also cleanup everything here
|
||||
enum class IndexType(
|
||||
private enum class IndexType(
|
||||
val jarName: String,
|
||||
val contentName: String
|
||||
) {
|
||||
@@ -219,13 +219,12 @@ object RepositoryUpdater {
|
||||
}
|
||||
}
|
||||
|
||||
fun processFile(
|
||||
private fun processFile(
|
||||
context: Context,
|
||||
repository: Repository,
|
||||
indexType: IndexType,
|
||||
unstable: Boolean,
|
||||
file: File,
|
||||
mergerFile: File = Cache.getTemporaryFile(context),
|
||||
lastModified: String,
|
||||
entityTag: String,
|
||||
callback: (Stage, Long, Long?) -> Unit
|
||||
@@ -242,6 +241,7 @@ object RepositoryUpdater {
|
||||
|
||||
var changedRepository: Repository? = null
|
||||
|
||||
val mergerFile = Cache.getTemporaryFile(context)
|
||||
try {
|
||||
val unmergedProducts = mutableListOf<Product>()
|
||||
val unmergedReleases = mutableListOf<Pair<String, List<Release>>>()
|
||||
@@ -344,7 +344,6 @@ object RepositoryUpdater {
|
||||
.codeSigner
|
||||
.certificate
|
||||
.fingerprint()
|
||||
.toString()
|
||||
.uppercase()
|
||||
|
||||
val commitRepository = if (!workRepository.fingerprint.equals(
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
package com.looker.droidify.installer.installers
|
||||
|
||||
import com.looker.droidify.domain.model.PackageName
|
||||
import com.looker.droidify.installer.model.InstallItem
|
||||
import com.looker.droidify.installer.model.InstallState
|
||||
|
||||
interface Installer : AutoCloseable {
|
||||
|
||||
suspend fun install(installItem: InstallItem): InstallState
|
||||
|
||||
suspend fun uninstall(packageName: PackageName)
|
||||
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
package com.looker.droidify.installer.installers
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import com.looker.droidify.utility.common.extension.getLauncherActivities
|
||||
import com.looker.droidify.utility.common.extension.getPackageInfoCompat
|
||||
import com.looker.droidify.utility.common.extension.intent
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import rikka.shizuku.ShizukuProvider
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
private const val SHIZUKU_PERMISSION_REQUEST_CODE = 87263
|
||||
|
||||
fun launchShizuku(context: Context) {
|
||||
val activities =
|
||||
context.packageManager.getLauncherActivities(ShizukuProvider.MANAGER_APPLICATION_ID)
|
||||
val intent = intent(Intent.ACTION_MAIN) {
|
||||
addCategory(Intent.CATEGORY_LAUNCHER)
|
||||
setComponent(
|
||||
ComponentName(
|
||||
ShizukuProvider.MANAGER_APPLICATION_ID,
|
||||
activities.first().first
|
||||
)
|
||||
)
|
||||
setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
context.startActivity(intent)
|
||||
}
|
||||
|
||||
fun isShizukuInstalled(context: Context) =
|
||||
context.packageManager.getPackageInfoCompat(ShizukuProvider.MANAGER_APPLICATION_ID) != null
|
||||
|
||||
fun isShizukuAlive() = rikka.shizuku.Shizuku.pingBinder()
|
||||
|
||||
fun isShizukuGranted() = rikka.shizuku.Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED
|
||||
|
||||
suspend fun requestPermissionListener() = suspendCancellableCoroutine {
|
||||
val listener = rikka.shizuku.Shizuku.OnRequestPermissionResultListener { requestCode, grantResult ->
|
||||
if (requestCode == SHIZUKU_PERMISSION_REQUEST_CODE) {
|
||||
it.resume(grantResult == PackageManager.PERMISSION_GRANTED)
|
||||
}
|
||||
}
|
||||
rikka.shizuku.Shizuku.addRequestPermissionResultListener(listener)
|
||||
rikka.shizuku.Shizuku.requestPermission(SHIZUKU_PERMISSION_REQUEST_CODE)
|
||||
it.invokeOnCancellation {
|
||||
rikka.shizuku.Shizuku.removeRequestPermissionResultListener(listener)
|
||||
}
|
||||
}
|
||||
|
||||
fun requestShizuku() {
|
||||
rikka.shizuku.Shizuku.shouldShowRequestPermissionRationale()
|
||||
rikka.shizuku.Shizuku.requestPermission(SHIZUKU_PERMISSION_REQUEST_CODE)
|
||||
}
|
||||
|
||||
fun isMagiskGranted(): Boolean {
|
||||
com.topjohnwu.superuser.Shell.getCachedShell() ?: com.topjohnwu.superuser.Shell.getShell()
|
||||
return com.topjohnwu.superuser.Shell.isAppGrantedRoot() == true
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
package com.looker.droidify.installer.installers
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.util.AndroidRuntimeException
|
||||
import androidx.core.net.toUri
|
||||
import com.looker.droidify.domain.model.PackageName
|
||||
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.looker.droidify.utility.common.extension.intent
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
class LegacyInstaller(private val context: Context) : Installer {
|
||||
|
||||
companion object {
|
||||
private const val APK_MIME = "application/vnd.android.package-archive"
|
||||
}
|
||||
|
||||
override suspend fun install(
|
||||
installItem: InstallItem,
|
||||
): InstallState = suspendCancellableCoroutine { cont ->
|
||||
val installFlag = if (SdkCheck.isNougat) Intent.FLAG_GRANT_READ_URI_PERMISSION else 0
|
||||
val fileUri = if (SdkCheck.isNougat) {
|
||||
Cache.getReleaseUri(
|
||||
context,
|
||||
installItem.installFileName
|
||||
)
|
||||
} else {
|
||||
Cache.getReleaseFile(context, installItem.installFileName).toUri()
|
||||
}
|
||||
val installIntent = intent(Intent.ACTION_INSTALL_PACKAGE) {
|
||||
setDataAndType(fileUri, APK_MIME)
|
||||
flags = installFlag
|
||||
}
|
||||
try {
|
||||
context.startActivity(installIntent)
|
||||
cont.resume(InstallState.Installed)
|
||||
} catch (e: AndroidRuntimeException) {
|
||||
installIntent.flags = installFlag or Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
context.startActivity(installIntent)
|
||||
cont.resume(InstallState.Installed)
|
||||
} catch (e: Exception) {
|
||||
cont.resume(InstallState.Failed)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun uninstall(packageName: PackageName) =
|
||||
context.uninstallPackage(packageName)
|
||||
|
||||
override fun close() {}
|
||||
}
|
||||
|
||||
suspend fun Context.uninstallPackage(packageName: PackageName) =
|
||||
suspendCancellableCoroutine { cont ->
|
||||
try {
|
||||
startActivity(
|
||||
intent(Intent.ACTION_UNINSTALL_PACKAGE) {
|
||||
data = "package:${packageName.name}".toUri()
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
}
|
||||
)
|
||||
cont.resume(Unit)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
cont.resume(Unit)
|
||||
}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
package com.looker.droidify.installer.installers.root
|
||||
|
||||
import android.content.Context
|
||||
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 kotlin.coroutines.resume
|
||||
|
||||
class RootInstaller(private val context: Context) : Installer {
|
||||
|
||||
override suspend fun install(
|
||||
installItem: InstallItem,
|
||||
): InstallState = suspendCancellableCoroutine { cont ->
|
||||
val releaseFile = Cache.getReleaseFile(context, installItem.installFileName)
|
||||
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)
|
||||
val deleteCommand = DELETE_COMMAND.format(utilBox(), releaseFile.absolutePath)
|
||||
Shell.cmd(deleteCommand).submit()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun uninstall(packageName: PackageName) =
|
||||
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("]")
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package com.looker.droidify.installer.model
|
||||
|
||||
import com.looker.droidify.domain.model.PackageName
|
||||
import com.looker.droidify.domain.model.toPackageName
|
||||
|
||||
class InstallItem(
|
||||
val packageName: PackageName,
|
||||
val installFileName: String
|
||||
)
|
||||
|
||||
infix fun String.installFrom(fileName: String) = InstallItem(this.toPackageName(), fileName)
|
||||
@@ -1,10 +1,5 @@
|
||||
package com.looker.droidify.model
|
||||
|
||||
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,
|
||||
val packageName: String,
|
||||
@@ -35,41 +30,20 @@ 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"),
|
||||
WEAR("wear"),
|
||||
TV("tv")
|
||||
LARGE_TABLET("largeTablet")
|
||||
}
|
||||
|
||||
val identifier: String
|
||||
get() = "$locale.${type.name}.$path"
|
||||
|
||||
fun url(
|
||||
context: Context,
|
||||
repository: Repository,
|
||||
packageName: 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"
|
||||
}
|
||||
}
|
||||
|
||||
// Same releases with different signatures
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package com.looker.droidify.model
|
||||
|
||||
import android.os.Parcelable
|
||||
import android.view.View
|
||||
import com.looker.droidify.utility.common.extension.dpi
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
data class ProductItem(
|
||||
@@ -18,39 +16,15 @@ data class ProductItem(
|
||||
var canUpdate: Boolean,
|
||||
var matchRank: Int
|
||||
) {
|
||||
sealed interface Section : Parcelable {
|
||||
sealed class Section : Parcelable {
|
||||
|
||||
@Parcelize
|
||||
object All : Section
|
||||
data object All : Section()
|
||||
|
||||
@Parcelize
|
||||
class Category(val name: String) : Section
|
||||
data class Category(val name: String) : Section()
|
||||
|
||||
@Parcelize
|
||||
class Repository(val id: Long, val name: String) : Section
|
||||
}
|
||||
|
||||
private val supportedDpi = intArrayOf(120, 160, 240, 320, 480, 640)
|
||||
private var deviceDpi: Int = -1
|
||||
|
||||
fun icon(
|
||||
view: View,
|
||||
repository: Repository
|
||||
): String? {
|
||||
if (packageName.isBlank()) return null
|
||||
if (icon.isBlank() && metadataIcon.isBlank()) return null
|
||||
if (repository.version < 11 && icon.isNotBlank()) {
|
||||
return "${repository.address}/icons/$icon"
|
||||
}
|
||||
if (icon.isNotBlank()) {
|
||||
if (deviceDpi == -1) {
|
||||
deviceDpi = supportedDpi.find { it >= view.dpi } ?: supportedDpi.last()
|
||||
}
|
||||
return "${repository.address}/icons-$deviceDpi/$icon"
|
||||
}
|
||||
if (metadataIcon.isNotBlank()) {
|
||||
return "${repository.address}/$packageName/$metadataIcon"
|
||||
}
|
||||
return null
|
||||
data class Repository(val id: Long, val name: String) : Section()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ data class Release(
|
||||
object MinSdk : Incompatibility()
|
||||
object MaxSdk : Incompatibility()
|
||||
object Platform : Incompatibility()
|
||||
class Feature(val feature: String) : Incompatibility()
|
||||
data class Feature(val feature: String) : Incompatibility()
|
||||
}
|
||||
|
||||
val identifier: String
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.looker.droidify.model
|
||||
|
||||
import com.looker.core.common.extension.isOnion
|
||||
import java.net.URL
|
||||
|
||||
data class Repository(
|
||||
@@ -15,9 +16,19 @@ data class Repository(
|
||||
val entityTag: String,
|
||||
val updated: Long,
|
||||
val timestamp: Long,
|
||||
val authentication: String,
|
||||
val authentication: String
|
||||
) {
|
||||
|
||||
/**
|
||||
* Remove all onion addresses and supply it as random address
|
||||
*
|
||||
* If the list only contains onion urls we will provide the default address
|
||||
*/
|
||||
val randomAddress: String
|
||||
get() = (mirrors + address)
|
||||
.filter { !it.isOnion }
|
||||
.randomOrNull() ?: address
|
||||
|
||||
fun edit(address: String, fingerprint: String, authentication: String): Repository {
|
||||
val isAddressChanged = this.address != address
|
||||
val isFingerprintChanged = this.fingerprint != fingerprint
|
||||
@@ -38,7 +49,7 @@ data class Repository(
|
||||
version: Int,
|
||||
lastModified: String,
|
||||
entityTag: String,
|
||||
timestamp: Long,
|
||||
timestamp: Long
|
||||
): Repository {
|
||||
return copy(
|
||||
mirrors = mirrors,
|
||||
@@ -62,7 +73,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 +90,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,6 +161,14 @@ 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",
|
||||
@@ -339,7 +358,10 @@ data class Repository(
|
||||
name = "SimpleX Chat F-Droid",
|
||||
description = "SimpleX Chat official F-Droid repository.",
|
||||
fingerprint = "9F358FF284D1F71656A2BFAF0E005DEAE6AA14143720E089F11FF2DDCFEB01BA"
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
val newlyAdded = listOf<Repository>(
|
||||
defaultRepository(
|
||||
address = "https://f-droid.monerujo.io/fdroid/repo",
|
||||
name = "Monerujo Wallet",
|
||||
@@ -389,29 +411,5 @@ data class Repository(
|
||||
fingerprint = "5D87CE1FAD3772425C2A7ED987A57595A20B07543B9595A7FD2CED25DFF3CF12"
|
||||
),
|
||||
)
|
||||
|
||||
val newlyAdded: List<Repository> = listOf(
|
||||
defaultRepository(
|
||||
address = "https://fdroid.ironfoxoss.org/fdroid/repo",
|
||||
name = "IronFox",
|
||||
description = "The official repository for IronFox:" +
|
||||
" A privacy and security-oriented Firefox-based browser for Android.",
|
||||
fingerprint = "C5E291B5A571F9C8CD9A9799C2C94E02EC9703948893F2CA756D67B94204F904"
|
||||
),
|
||||
defaultRepository(
|
||||
address = "https://raw.githubusercontent.com/chrisgch/tca/master/fdroid/repo",
|
||||
name = "Total Commander",
|
||||
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"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
package com.looker.droidify.network.validation
|
||||
|
||||
class ValidationException(override val message: String) : Exception(message)
|
||||
|
||||
@Suppress("NOTHING_TO_INLINE")
|
||||
inline fun invalid(message: String): Nothing = throw ValidationException(message)
|
||||
@@ -4,7 +4,7 @@ import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import com.looker.droidify.utility.common.extension.getPackageInfoCompat
|
||||
import com.looker.core.common.extension.getPackageInfoCompat
|
||||
import com.looker.droidify.database.Database
|
||||
import com.looker.droidify.utility.extension.toInstalledItem
|
||||
|
||||
|
||||
@@ -6,34 +6,35 @@ import android.graphics.Color
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import androidx.core.app.NotificationCompat
|
||||
import com.looker.core.common.Constants
|
||||
import com.looker.core.common.Constants.NOTIFICATION_CHANNEL_INSTALL
|
||||
import com.looker.core.common.R
|
||||
import com.looker.core.common.SdkCheck
|
||||
import com.looker.core.common.cache.Cache
|
||||
import com.looker.core.common.createNotificationChannel
|
||||
import com.looker.core.common.extension.notificationManager
|
||||
import com.looker.core.common.extension.percentBy
|
||||
import com.looker.core.common.extension.startSelf
|
||||
import com.looker.core.common.extension.stopForegroundCompat
|
||||
import com.looker.core.common.extension.toPendingIntent
|
||||
import com.looker.core.common.extension.updateAsMutable
|
||||
import com.looker.core.common.log
|
||||
import com.looker.core.datastore.SettingsRepository
|
||||
import com.looker.core.datastore.get
|
||||
import com.looker.core.datastore.model.InstallerType
|
||||
import com.looker.droidify.BuildConfig
|
||||
import com.looker.droidify.MainActivity
|
||||
import com.looker.droidify.datastore.SettingsRepository
|
||||
import com.looker.droidify.datastore.get
|
||||
import com.looker.droidify.datastore.model.InstallerType
|
||||
import com.looker.droidify.installer.InstallManager
|
||||
import com.looker.droidify.installer.model.InstallState
|
||||
import com.looker.droidify.installer.model.installFrom
|
||||
import com.looker.droidify.installer.notification.createInstallNotification
|
||||
import com.looker.droidify.installer.notification.installNotification
|
||||
import com.looker.droidify.model.Release
|
||||
import com.looker.droidify.model.Repository
|
||||
import com.looker.droidify.network.DataSize
|
||||
import com.looker.droidify.network.Downloader
|
||||
import com.looker.droidify.network.NetworkResponse
|
||||
import com.looker.droidify.network.percentBy
|
||||
import com.looker.droidify.network.validation.ValidationException
|
||||
import com.looker.droidify.utility.common.Constants
|
||||
import com.looker.droidify.utility.common.Constants.NOTIFICATION_CHANNEL_INSTALL
|
||||
import com.looker.droidify.utility.common.SdkCheck
|
||||
import com.looker.droidify.utility.common.cache.Cache
|
||||
import com.looker.droidify.utility.common.createNotificationChannel
|
||||
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.extension.toPendingIntent
|
||||
import com.looker.droidify.utility.common.extension.updateAsMutable
|
||||
import com.looker.droidify.utility.common.log
|
||||
import com.looker.installer.InstallManager
|
||||
import com.looker.installer.model.InstallState
|
||||
import com.looker.installer.model.installFrom
|
||||
import com.looker.installer.notification.createInstallNotification
|
||||
import com.looker.installer.notification.installNotification
|
||||
import com.looker.network.DataSize
|
||||
import com.looker.network.Downloader
|
||||
import com.looker.network.NetworkResponse
|
||||
import com.looker.network.validation.ValidationException
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
@@ -50,7 +51,7 @@ import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.yield
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
import com.looker.droidify.R.string as stringRes
|
||||
import com.looker.core.common.R.string as stringRes
|
||||
|
||||
@AndroidEntryPoint
|
||||
class DownloadService : ConnectionService<DownloadService.Binder>() {
|
||||
@@ -174,7 +175,7 @@ class DownloadService : ConnectionService<DownloadService.Binder>() {
|
||||
)
|
||||
createNotificationChannel(
|
||||
id = NOTIFICATION_CHANNEL_INSTALL,
|
||||
name = getString(stringRes.install)
|
||||
name = getString(R.string.install)
|
||||
)
|
||||
|
||||
lifecycleScope.launch {
|
||||
@@ -378,7 +379,7 @@ class DownloadService : ConnectionService<DownloadService.Binder>() {
|
||||
}
|
||||
if (!started) {
|
||||
started = true
|
||||
startServiceCompat()
|
||||
startSelf()
|
||||
}
|
||||
val task = tasks.removeFirstOrNull() ?: return
|
||||
with(stateNotificationBuilder) {
|
||||
|
||||
@@ -2,17 +2,17 @@ package com.looker.droidify.service
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.StringRes
|
||||
import com.looker.droidify.utility.common.extension.calculateHash
|
||||
import com.looker.droidify.utility.common.extension.getPackageArchiveInfoCompat
|
||||
import com.looker.droidify.utility.common.extension.singleSignature
|
||||
import com.looker.droidify.utility.common.extension.versionCodeCompat
|
||||
import com.looker.droidify.network.validation.FileValidator
|
||||
import com.looker.droidify.utility.common.signature.Hash
|
||||
import com.looker.droidify.network.validation.invalid
|
||||
import com.looker.droidify.utility.common.signature.verifyHash
|
||||
import com.looker.core.common.extension.calculateHash
|
||||
import com.looker.core.common.extension.getPackageArchiveInfoCompat
|
||||
import com.looker.core.common.extension.singleSignature
|
||||
import com.looker.core.common.extension.versionCodeCompat
|
||||
import com.looker.network.validation.FileValidator
|
||||
import com.looker.core.common.signature.Hash
|
||||
import com.looker.network.validation.invalid
|
||||
import com.looker.core.common.signature.verifyHash
|
||||
import com.looker.droidify.model.Release
|
||||
import java.io.File
|
||||
import com.looker.droidify.R.string as strings
|
||||
import com.looker.core.common.R.string as strings
|
||||
|
||||
class ReleaseFileValidator(
|
||||
private val context: Context,
|
||||
|
||||
@@ -15,16 +15,17 @@ import android.text.style.ForegroundColorSpan
|
||||
import android.view.ContextThemeWrapper
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.looker.droidify.utility.common.Constants
|
||||
import com.looker.droidify.utility.common.SdkCheck
|
||||
import com.looker.droidify.utility.common.createNotificationChannel
|
||||
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.result.Result
|
||||
import com.looker.droidify.utility.common.sdkAbove
|
||||
import com.looker.droidify.datastore.SettingsRepository
|
||||
import com.looker.core.common.Constants
|
||||
import com.looker.core.common.SdkCheck
|
||||
import com.looker.core.common.createNotificationChannel
|
||||
import com.looker.core.common.extension.getColorFromAttr
|
||||
import com.looker.core.common.extension.notificationManager
|
||||
import com.looker.core.common.extension.startSelf
|
||||
import com.looker.core.common.extension.stopForegroundCompat
|
||||
import com.looker.core.common.log
|
||||
import com.looker.core.common.result.Result
|
||||
import com.looker.core.common.sdkAbove
|
||||
import com.looker.core.datastore.SettingsRepository
|
||||
import com.looker.droidify.BuildConfig
|
||||
import com.looker.droidify.MainActivity
|
||||
import com.looker.droidify.database.Database
|
||||
@@ -32,8 +33,8 @@ import com.looker.droidify.index.RepositoryUpdater
|
||||
import com.looker.droidify.model.ProductItem
|
||||
import com.looker.droidify.model.Repository
|
||||
import com.looker.droidify.utility.extension.startUpdate
|
||||
import com.looker.droidify.network.DataSize
|
||||
import com.looker.droidify.network.percentBy
|
||||
import com.looker.network.DataSize
|
||||
import com.looker.network.percentBy
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -51,12 +52,9 @@ import kotlinx.coroutines.sync.withLock
|
||||
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
|
||||
import com.looker.core.common.R as CommonR
|
||||
import com.looker.core.common.R.string as stringRes
|
||||
import com.looker.core.common.R.style as styleRes
|
||||
import kotlinx.coroutines.Job as CoroutinesJob
|
||||
|
||||
@AndroidEntryPoint
|
||||
@@ -70,16 +68,15 @@ class SyncService : ConnectionService<SyncService.Binder>() {
|
||||
private const val MAX_UPDATE_NOTIFICATION = 5
|
||||
private const val ACTION_CANCEL = "${BuildConfig.APPLICATION_ID}.intent.action.CANCEL"
|
||||
|
||||
val syncState = MutableSharedFlow<State>()
|
||||
private val syncState = MutableSharedFlow<State>()
|
||||
}
|
||||
|
||||
@Inject
|
||||
lateinit var settingsRepository: SettingsRepository
|
||||
|
||||
sealed class State(val name: String) {
|
||||
class Connecting(appName: String) : State(appName)
|
||||
|
||||
class Syncing(
|
||||
data class Connecting(val appName: String) : State(appName)
|
||||
data class Syncing(
|
||||
val appName: String,
|
||||
val stage: RepositoryUpdater.Stage,
|
||||
val read: DataSize,
|
||||
@@ -87,18 +84,6 @@ class SyncService : ConnectionService<SyncService.Binder>() {
|
||||
) : 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)
|
||||
@@ -140,7 +125,7 @@ class SyncService : ConnectionService<SyncService.Binder>() {
|
||||
handleNextTask(cancelledTask?.hasUpdates == true)
|
||||
if (request != SyncRequest.AUTO && started == Started.AUTO) {
|
||||
started = Started.MANUAL
|
||||
startServiceCompat()
|
||||
startSelf()
|
||||
handleSetStarted()
|
||||
currentTask?.lastState?.let { publishForegroundState(true, it) }
|
||||
}
|
||||
@@ -159,8 +144,7 @@ class SyncService : ConnectionService<SyncService.Binder>() {
|
||||
}
|
||||
|
||||
suspend fun updateAllApps() {
|
||||
val skipSignature = settingsRepository.getInitial().ignoreSignature
|
||||
updateAllAppsInternal(skipSignature)
|
||||
updateAllAppsInternal()
|
||||
}
|
||||
|
||||
fun setUpdateNotificationBlocker(fragment: Fragment?) {
|
||||
@@ -213,7 +197,6 @@ class SyncService : ConnectionService<SyncService.Binder>() {
|
||||
private val binder = Binder()
|
||||
override fun onBind(intent: Intent): Binder = binder
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
@@ -293,10 +276,10 @@ class SyncService : ConnectionService<SyncService.Binder>() {
|
||||
Constants.NOTIFICATION_ID_SYNCING,
|
||||
NotificationCompat
|
||||
.Builder(this, Constants.NOTIFICATION_CHANNEL_SYNCING)
|
||||
.setSmallIcon(AndroidR.drawable.stat_sys_warning)
|
||||
.setSmallIcon(android.R.drawable.stat_sys_warning)
|
||||
.setColor(
|
||||
ContextThemeWrapper(this, styleRes.Theme_Main_Light)
|
||||
.getColorFromAttr(AndroidR.attr.colorPrimary).defaultColor
|
||||
.getColorFromAttr(android.R.attr.colorPrimary).defaultColor
|
||||
)
|
||||
.setContentTitle(getString(stringRes.could_not_sync_FORMAT, repository.name))
|
||||
.setContentText(description)
|
||||
@@ -307,10 +290,10 @@ class SyncService : ConnectionService<SyncService.Binder>() {
|
||||
private val stateNotificationBuilder by lazy {
|
||||
NotificationCompat
|
||||
.Builder(this, Constants.NOTIFICATION_CHANNEL_SYNCING)
|
||||
.setSmallIcon(R.drawable.ic_sync)
|
||||
.setSmallIcon(CommonR.drawable.ic_sync)
|
||||
.setColor(
|
||||
ContextThemeWrapper(this, styleRes.Theme_Main_Light)
|
||||
.getColorFromAttr(AndroidR.attr.colorPrimary).defaultColor
|
||||
.getColorFromAttr(android.R.attr.colorPrimary).defaultColor
|
||||
)
|
||||
.addAction(
|
||||
0,
|
||||
@@ -385,7 +368,7 @@ class SyncService : ConnectionService<SyncService.Binder>() {
|
||||
}
|
||||
|
||||
is State.Finish -> {}
|
||||
}
|
||||
}::class
|
||||
}.build()
|
||||
)
|
||||
}
|
||||
@@ -405,8 +388,7 @@ class SyncService : ConnectionService<SyncService.Binder>() {
|
||||
handleUpdates(
|
||||
hasUpdates = hasUpdates,
|
||||
notifyUpdates = setting.notifyUpdate,
|
||||
autoUpdate = setting.autoUpdate,
|
||||
skipSignature = setting.ignoreSignature,
|
||||
autoUpdate = setting.autoUpdate
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -423,7 +405,7 @@ class SyncService : ConnectionService<SyncService.Binder>() {
|
||||
}
|
||||
started = newStarted
|
||||
if (newStarted == Started.MANUAL && lastStarted != Started.MANUAL) {
|
||||
startServiceCompat()
|
||||
startSelf()
|
||||
handleSetStarted()
|
||||
}
|
||||
val initialState = State.Connecting(repository!!.name)
|
||||
@@ -487,8 +469,7 @@ class SyncService : ConnectionService<SyncService.Binder>() {
|
||||
private suspend fun handleUpdates(
|
||||
hasUpdates: Boolean,
|
||||
notifyUpdates: Boolean,
|
||||
autoUpdate: Boolean,
|
||||
skipSignature: Boolean,
|
||||
autoUpdate: Boolean
|
||||
) {
|
||||
try {
|
||||
if (!hasUpdates) {
|
||||
@@ -499,16 +480,15 @@ class SyncService : ConnectionService<SyncService.Binder>() {
|
||||
return
|
||||
}
|
||||
val blocked = updateNotificationBlockerFragment?.get()?.isAdded == true
|
||||
val updates = Database.ProductAdapter.getUpdates(skipSignature)
|
||||
val updates = Database.ProductAdapter.getUpdates()
|
||||
if (!blocked && updates.isNotEmpty()) {
|
||||
if (notifyUpdates) displayUpdatesNotification(updates)
|
||||
if (autoUpdate) updateAllAppsInternal(skipSignature)
|
||||
if (autoUpdate) updateAllAppsInternal()
|
||||
}
|
||||
handleUpdates(
|
||||
hasUpdates = false,
|
||||
notifyUpdates = notifyUpdates,
|
||||
autoUpdate = autoUpdate,
|
||||
skipSignature = skipSignature,
|
||||
autoUpdate = autoUpdate
|
||||
)
|
||||
} finally {
|
||||
withContext(NonCancellable) {
|
||||
@@ -518,9 +498,10 @@ class SyncService : ConnectionService<SyncService.Binder>() {
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun updateAllAppsInternal(skipSignature: Boolean) {
|
||||
private suspend fun updateAllAppsInternal() {
|
||||
log("Check Running", "Syncing")
|
||||
Database.ProductAdapter
|
||||
.getUpdates(skipSignature)
|
||||
.getUpdates()
|
||||
// Update Droid-ify the last
|
||||
.sortedBy { if (it.packageName == packageName) 1 else -1 }
|
||||
.map {
|
||||
@@ -541,22 +522,23 @@ class SyncService : ConnectionService<SyncService.Binder>() {
|
||||
}
|
||||
|
||||
private fun displayUpdatesNotification(productItems: List<ProductItem>) {
|
||||
fun <T> T.applyHack(callback: T.() -> Unit): T = apply(callback)
|
||||
notificationManager?.notify(
|
||||
Constants.NOTIFICATION_ID_UPDATES,
|
||||
NotificationCompat
|
||||
.Builder(this, Constants.NOTIFICATION_CHANNEL_UPDATES)
|
||||
.setSmallIcon(R.drawable.ic_new_releases)
|
||||
.setSmallIcon(CommonR.drawable.ic_new_releases)
|
||||
.setContentTitle(getString(stringRes.new_updates_available))
|
||||
.setContentText(
|
||||
resources.getQuantityString(
|
||||
R.plurals.new_updates_DESC_FORMAT,
|
||||
CommonR.plurals.new_updates_DESC_FORMAT,
|
||||
productItems.size,
|
||||
productItems.size
|
||||
)
|
||||
)
|
||||
.setColor(
|
||||
ContextThemeWrapper(this, styleRes.Theme_Main_Light)
|
||||
.getColorFromAttr(AndroidR.attr.colorPrimary).defaultColor
|
||||
.getColorFromAttr(android.R.attr.colorPrimary).defaultColor
|
||||
)
|
||||
.setContentIntent(
|
||||
PendingIntent.getActivity(
|
||||
@@ -568,7 +550,7 @@ class SyncService : ConnectionService<SyncService.Binder>() {
|
||||
)
|
||||
)
|
||||
.setStyle(
|
||||
NotificationCompat.InboxStyle().also {
|
||||
NotificationCompat.InboxStyle().applyHack {
|
||||
for (productItem in productItems.take(MAX_UPDATE_NOTIFICATION)) {
|
||||
val builder = SpannableStringBuilder(productItem.name)
|
||||
builder.setSpan(
|
||||
@@ -578,7 +560,7 @@ class SyncService : ConnectionService<SyncService.Binder>() {
|
||||
SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
builder.append(' ').append(productItem.version)
|
||||
it.addLine(builder)
|
||||
addLine(builder)
|
||||
}
|
||||
if (productItems.size > MAX_UPDATE_NOTIFICATION) {
|
||||
val summary =
|
||||
@@ -586,11 +568,7 @@ class SyncService : ConnectionService<SyncService.Binder>() {
|
||||
stringRes.plus_more_FORMAT,
|
||||
productItems.size - MAX_UPDATE_NOTIFICATION
|
||||
)
|
||||
if (SdkCheck.isNougat) {
|
||||
it.addLine(summary)
|
||||
} else {
|
||||
it.setSummaryText(summary)
|
||||
}
|
||||
if (SdkCheck.isNougat) addLine(summary) else setSummaryText(summary)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
package com.looker.droidify.sync.common
|
||||
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
val JsonParser = Json {
|
||||
ignoreUnknownKeys = true
|
||||
coerceInputValues = true
|
||||
isLenient = true
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
package com.looker.droidify.sync.v1
|
||||
|
||||
import com.looker.droidify.domain.model.Fingerprint
|
||||
import com.looker.droidify.domain.model.Repo
|
||||
import com.looker.droidify.sync.IndexValidator
|
||||
import com.looker.droidify.sync.Parser
|
||||
import com.looker.droidify.sync.utils.toJarFile
|
||||
import com.looker.droidify.sync.v1.model.IndexV1
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.io.File
|
||||
|
||||
class V1Parser(
|
||||
private val dispatcher: CoroutineDispatcher,
|
||||
private val json: Json,
|
||||
private val validator: IndexValidator,
|
||||
) : Parser<IndexV1> {
|
||||
|
||||
override suspend fun parse(
|
||||
file: File,
|
||||
repo: Repo,
|
||||
): Pair<Fingerprint, IndexV1> = withContext(dispatcher) {
|
||||
val jar = file.toJarFile()
|
||||
val entry = jar.getJarEntry("index-v1.json")
|
||||
val indexString = jar.getInputStream(entry).use {
|
||||
it.readBytes().decodeToString()
|
||||
}
|
||||
validator.validate(entry, repo.fingerprint) to json.decodeFromString(indexString)
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
package com.looker.droidify.sync.v1.model
|
||||
|
||||
/*
|
||||
* PackageV1, PermissionV1 are licensed under the GPL 3.0 to FDroid Organization.
|
||||
* */
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class PackageV1(
|
||||
val added: Long? = null,
|
||||
val apkName: String,
|
||||
val hash: String,
|
||||
val hashType: String,
|
||||
val minSdkVersion: Int? = null,
|
||||
val maxSdkVersion: Int? = null,
|
||||
val targetSdkVersion: Int? = minSdkVersion,
|
||||
val packageName: String,
|
||||
val sig: String? = null,
|
||||
val signer: String? = null,
|
||||
val size: Long,
|
||||
@SerialName("srcname")
|
||||
val srcName: String? = null,
|
||||
@SerialName("uses-permission")
|
||||
val usesPermission: List<PermissionV1> = emptyList(),
|
||||
@SerialName("uses-permission-sdk-23")
|
||||
val usesPermission23: List<PermissionV1> = emptyList(),
|
||||
val versionCode: Long? = null,
|
||||
val versionName: String,
|
||||
@SerialName("nativecode")
|
||||
val nativeCode: List<String>? = null,
|
||||
val features: List<String>? = null,
|
||||
val antiFeatures: List<String>? = null,
|
||||
)
|
||||
|
||||
typealias PermissionV1 = Array<String?>
|
||||
|
||||
val PermissionV1.name: String get() = first()!!
|
||||
val PermissionV1.maxSdk: Int? get() = getOrNull(1)?.toInt()
|
||||
@@ -1,25 +0,0 @@
|
||||
package com.looker.droidify.sync.v2
|
||||
|
||||
import com.looker.droidify.domain.model.Fingerprint
|
||||
import com.looker.droidify.domain.model.Repo
|
||||
import com.looker.droidify.sync.Parser
|
||||
import com.looker.droidify.sync.v2.model.IndexV2Diff
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.io.File
|
||||
|
||||
class DiffParser(
|
||||
private val dispatcher: CoroutineDispatcher,
|
||||
private val json: Json,
|
||||
) : Parser<IndexV2Diff> {
|
||||
|
||||
override suspend fun parse(
|
||||
file: File,
|
||||
repo: Repo
|
||||
): Pair<Fingerprint, IndexV2Diff> = withContext(dispatcher) {
|
||||
requireNotNull(repo.fingerprint) {
|
||||
"Fingerprint should not be null when parsing diff"
|
||||
} to json.decodeFromString(file.readBytes().decodeToString())
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
package com.looker.droidify.sync.v2
|
||||
|
||||
import com.looker.droidify.domain.model.Fingerprint
|
||||
import com.looker.droidify.domain.model.Repo
|
||||
import com.looker.droidify.sync.IndexValidator
|
||||
import com.looker.droidify.sync.Parser
|
||||
import com.looker.droidify.sync.utils.toJarFile
|
||||
import com.looker.droidify.sync.v2.model.Entry
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.io.File
|
||||
|
||||
class EntryParser(
|
||||
private val dispatcher: CoroutineDispatcher,
|
||||
private val json: Json,
|
||||
private val validator: IndexValidator,
|
||||
) : Parser<Entry> {
|
||||
|
||||
override suspend fun parse(
|
||||
file: File,
|
||||
repo: Repo,
|
||||
): Pair<Fingerprint, Entry> = withContext(dispatcher) {
|
||||
val jar = file.toJarFile()
|
||||
val entry = jar.getJarEntry("entry.json")
|
||||
val entryString = jar.getInputStream(entry).use {
|
||||
it.readBytes().decodeToString()
|
||||
}
|
||||
validator.validate(entry, repo.fingerprint) to json.decodeFromString(entryString)
|
||||
}
|
||||
}
|
||||
@@ -11,8 +11,8 @@ import androidx.core.os.bundleOf
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.looker.droidify.utility.common.SdkCheck
|
||||
import com.looker.droidify.utility.common.nullIfEmpty
|
||||
import com.looker.core.common.SdkCheck
|
||||
import com.looker.core.common.nullIfEmpty
|
||||
import com.looker.droidify.model.Release
|
||||
import com.looker.droidify.ui.repository.RepositoryFragment
|
||||
import com.looker.droidify.utility.PackageItemResolver
|
||||
@@ -20,7 +20,7 @@ import com.looker.droidify.utility.extension.android.Android
|
||||
import kotlinx.parcelize.Parceler
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import kotlinx.parcelize.TypeParceler
|
||||
import com.looker.droidify.R.string as stringRes
|
||||
import com.looker.core.common.R.string as stringRes
|
||||
|
||||
class MessageDialog() : DialogFragment() {
|
||||
companion object {
|
||||
|
||||
@@ -1,35 +1,20 @@
|
||||
package com.looker.droidify.ui.appDetail
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.*
|
||||
import android.content.pm.PermissionGroupInfo
|
||||
import android.content.pm.PermissionInfo
|
||||
import android.content.res.Resources
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Paint
|
||||
import android.graphics.Typeface
|
||||
import android.graphics.*
|
||||
import android.net.Uri
|
||||
import android.os.Parcelable
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.format.DateFormat
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.text.style.BulletSpan
|
||||
import android.text.style.ClickableSpan
|
||||
import android.text.style.RelativeSizeSpan
|
||||
import android.text.style.ReplacementSpan
|
||||
import android.text.style.TypefaceSpan
|
||||
import android.text.style.URLSpan
|
||||
import android.text.style.*
|
||||
import android.text.util.Linkify
|
||||
import android.view.Gravity
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextSwitcher
|
||||
import android.widget.TextView
|
||||
import android.view.*
|
||||
import android.widget.*
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.core.net.toUri
|
||||
@@ -40,13 +25,17 @@ import androidx.core.text.util.LinkifyCompat
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import coil3.load
|
||||
import coil.load
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import com.google.android.material.card.MaterialCardView
|
||||
import com.google.android.material.imageview.ShapeableImageView
|
||||
import com.google.android.material.materialswitch.MaterialSwitch
|
||||
import com.google.android.material.progressindicator.LinearProgressIndicator
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.looker.network.DataSize
|
||||
import com.looker.core.common.extension.*
|
||||
import com.looker.core.common.formatSize
|
||||
import com.looker.core.common.nullIfEmpty
|
||||
import com.looker.droidify.R
|
||||
import com.looker.droidify.content.ProductPreferences
|
||||
import com.looker.droidify.model.InstalledItem
|
||||
@@ -55,22 +44,8 @@ import com.looker.droidify.model.ProductPreference
|
||||
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
|
||||
import com.looker.droidify.utility.common.extension.corneredBackground
|
||||
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.getDrawableCompat
|
||||
import com.looker.droidify.utility.common.extension.getMutatedIcon
|
||||
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.ImageUtils.icon
|
||||
import com.looker.droidify.utility.extension.android.Android
|
||||
import com.looker.droidify.utility.extension.resources.TypefaceExtra
|
||||
import com.looker.droidify.utility.extension.resources.sizeScaled
|
||||
@@ -88,8 +63,8 @@ import kotlin.math.PI
|
||||
import kotlin.math.roundToInt
|
||||
import kotlin.math.sin
|
||||
import com.google.android.material.R as MaterialR
|
||||
import com.looker.droidify.R.drawable as drawableRes
|
||||
import com.looker.droidify.R.string as stringRes
|
||||
import com.looker.core.common.R.drawable as drawableRes
|
||||
import com.looker.core.common.R.string as stringRes
|
||||
|
||||
class AppDetailAdapter(private val callbacks: Callbacks) :
|
||||
StableRecyclerAdapter<AppDetailAdapter.ViewType, RecyclerView.ViewHolder>() {
|
||||
@@ -103,7 +78,7 @@ class AppDetailAdapter(private val callbacks: Callbacks) :
|
||||
fun onFavouriteClicked()
|
||||
fun onPreferenceChanged(preference: ProductPreference)
|
||||
fun onPermissionsClick(group: String?, permissions: List<String>)
|
||||
fun onScreenshotClick(position: Int)
|
||||
fun onScreenshotClick(screenshot: Product.Screenshot, parentView: ImageView)
|
||||
fun onReleaseClick(release: Release)
|
||||
fun onRequestAddRepository(address: String)
|
||||
fun onUriClick(uri: Uri, shouldConfirm: Boolean): Boolean
|
||||
@@ -315,6 +290,7 @@ 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
|
||||
}
|
||||
@@ -323,6 +299,7 @@ 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,8 +308,12 @@ 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(
|
||||
@@ -557,7 +538,6 @@ class AppDetailAdapter(private val callbacks: Callbacks) :
|
||||
val size = itemView.findViewById<TextView>(R.id.size)!!
|
||||
val signature = itemView.findViewById<TextView>(R.id.signature)!!
|
||||
val compatibility = itemView.findViewById<TextView>(R.id.compatibility)!!
|
||||
val targetSdk = itemView.findViewById<TextView>(R.id.target_sdk)!!
|
||||
|
||||
val statefulViews: Sequence<View>
|
||||
get() = sequenceOf(
|
||||
@@ -568,8 +548,7 @@ class AppDetailAdapter(private val callbacks: Callbacks) :
|
||||
added,
|
||||
size,
|
||||
signature,
|
||||
compatibility,
|
||||
targetSdk,
|
||||
compatibility
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1315,7 +1294,7 @@ class AppDetailAdapter(private val callbacks: Callbacks) :
|
||||
}
|
||||
}
|
||||
}
|
||||
holder.size.text = DataSize(product?.displayRelease?.size ?: 0).toString()
|
||||
holder.size.text = product?.displayRelease?.size?.formatSize()
|
||||
|
||||
holder.dev.setOnClickListener {
|
||||
product?.source?.let { link ->
|
||||
@@ -1363,10 +1342,12 @@ class AppDetailAdapter(private val callbacks: Callbacks) :
|
||||
)
|
||||
holder.progress.isIndeterminate = status.total == null
|
||||
if (status.total != null) {
|
||||
holder.progress.setProgressCompat(
|
||||
status.read.value percentBy status.total.value,
|
||||
true
|
||||
)
|
||||
holder.progress.progress =
|
||||
(
|
||||
holder.progress.max.toFloat() *
|
||||
status.read.value /
|
||||
status.total.value
|
||||
).roundToInt()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1426,15 +1407,17 @@ 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(callbacks::onScreenshotClick).apply {
|
||||
setScreenshots(item.repository, item.packageName, item.screenshots)
|
||||
}
|
||||
adapter =
|
||||
ScreenshotsAdapter { screenshot, view ->
|
||||
callbacks.onScreenshotClick(screenshot, view)
|
||||
}.apply {
|
||||
setScreenshots(item.repository, item.packageName, item.screenshots)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1620,7 +1603,7 @@ class AppDetailAdapter(private val callbacks: Callbacks) :
|
||||
holder.version.text =
|
||||
context.getString(stringRes.version_FORMAT, item.release.version)
|
||||
|
||||
with(holder.status) {
|
||||
holder.status.apply {
|
||||
isVisible = installed || suggested
|
||||
setText(
|
||||
when {
|
||||
@@ -1631,15 +1614,14 @@ class AppDetailAdapter(private val callbacks: Callbacks) :
|
||||
)
|
||||
background = context.corneredBackground
|
||||
setPadding(15, 15, 15, 15)
|
||||
if (installed) {
|
||||
backgroundTintList =
|
||||
context.getColorFromAttr(MaterialR.attr.colorSecondaryContainer)
|
||||
setTextColor(context.getColorFromAttr(MaterialR.attr.colorOnSecondaryContainer))
|
||||
val (background, foreground) = if (installed) {
|
||||
MaterialR.attr.colorSecondaryContainer to MaterialR.attr.colorOnSecondaryContainer
|
||||
} else {
|
||||
backgroundTintList =
|
||||
context.getColorFromAttr(MaterialR.attr.colorPrimaryContainer)
|
||||
setTextColor(context.getColorFromAttr(MaterialR.attr.colorOnPrimaryContainer))
|
||||
MaterialR.attr.colorPrimaryContainer to MaterialR.attr.colorOnPrimaryContainer
|
||||
}
|
||||
backgroundTintList =
|
||||
context.getColorFromAttr(background)
|
||||
setTextColor(context.getColorFromAttr(foreground))
|
||||
}
|
||||
holder.source.text =
|
||||
context.getString(stringRes.provided_by_FORMAT, item.repository.name)
|
||||
@@ -1654,7 +1636,7 @@ class AppDetailAdapter(private val callbacks: Callbacks) :
|
||||
holder.dateFormat.format(item.release.added)
|
||||
}
|
||||
holder.added.text = dateFormat
|
||||
holder.size.text = DataSize(item.release.size).toString()
|
||||
holder.size.text = item.release.size.formatSize()
|
||||
holder.signature.isVisible =
|
||||
item.showSignature && item.release.signature.isNotEmpty()
|
||||
if (item.showSignature && item.release.signature.isNotEmpty()) {
|
||||
@@ -1683,44 +1665,35 @@ class AppDetailAdapter(private val callbacks: Callbacks) :
|
||||
}
|
||||
holder.signature.text = builder
|
||||
}
|
||||
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
|
||||
)
|
||||
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
|
||||
)
|
||||
|
||||
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
|
||||
)
|
||||
}
|
||||
} else if (singlePlatform != null) {
|
||||
setTextColor(context.getColorFromAttr(android.R.attr.textColorSecondary))
|
||||
text = context.getString(
|
||||
stringRes.only_compatible_with_FORMAT,
|
||||
singlePlatform,
|
||||
is Release.Incompatibility.Feature -> context.getString(
|
||||
stringRes.requires_FORMAT,
|
||||
incompatibility.feature
|
||||
)
|
||||
}
|
||||
}
|
||||
with(holder.targetSdk) {
|
||||
val sdkVersion = sdkName.getOrDefault(
|
||||
item.release.targetSdkVersion,
|
||||
context.getString(
|
||||
stringRes.label_unknown_sdk,
|
||||
item.release.targetSdkVersion,
|
||||
),
|
||||
} else if (singlePlatform != null) {
|
||||
holder.compatibility.setTextColor(
|
||||
context.getColorFromAttr(android.R.attr.textColorSecondary)
|
||||
)
|
||||
text = context.getString(stringRes.label_targets_sdk, sdkVersion)
|
||||
holder.compatibility.text =
|
||||
context.getString(stringRes.only_compatible_with_FORMAT, singlePlatform)
|
||||
}
|
||||
val enabled = status == Status.Idle
|
||||
holder.statefulViews.forEach { it.isEnabled = enabled }
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.looker.droidify.ui.appDetail
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.ComponentName
|
||||
import android.content.Intent
|
||||
@@ -9,6 +8,7 @@ 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,13 +20,16 @@ import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||
import coil3.load
|
||||
import coil3.request.allowHardware
|
||||
import coil.load
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.looker.core.common.cache.Cache
|
||||
import com.looker.core.common.extension.getLauncherActivities
|
||||
import com.looker.core.common.extension.getMutatedIcon
|
||||
import com.looker.core.common.extension.isFirstItemVisible
|
||||
import com.looker.core.common.extension.isSystemApplication
|
||||
import com.looker.core.common.extension.systemBarsPadding
|
||||
import com.looker.core.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
|
||||
@@ -40,21 +43,17 @@ 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.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.ImageUtils.url
|
||||
import com.looker.droidify.utility.extension.screenActivity
|
||||
import com.looker.droidify.utility.extension.startUpdate
|
||||
import com.looker.installer.model.InstallState
|
||||
import com.looker.installer.model.isCancellable
|
||||
import com.stfalcon.imageviewer.StfalconImageViewer
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import com.looker.droidify.R.string as stringRes
|
||||
import com.looker.core.common.R.string as stringRes
|
||||
|
||||
@AndroidEntryPoint
|
||||
class AppDetailFragment() : ScreenFragment(), AppDetailAdapter.Callbacks {
|
||||
@@ -90,7 +89,6 @@ class AppDetailFragment() : ScreenFragment(), AppDetailAdapter.Callbacks {
|
||||
|
||||
private val viewModel: AppDetailViewModel by viewModels()
|
||||
|
||||
@SuppressLint("RestrictedApi")
|
||||
private var layoutManagerState: LinearLayoutManager.SavedState? = null
|
||||
|
||||
private var actions = Pair(emptySet<Action>(), null as Action?)
|
||||
@@ -101,7 +99,6 @@ class AppDetailFragment() : ScreenFragment(), AppDetailAdapter.Callbacks {
|
||||
|
||||
private var recyclerView: RecyclerView? = null
|
||||
private var detailAdapter: AppDetailAdapter? = null
|
||||
private var imageViewer: StfalconImageViewer.Builder<Product.Screenshot>? = null
|
||||
|
||||
private val downloadConnection = Connection(
|
||||
serviceClass = DownloadService::class.java,
|
||||
@@ -112,12 +109,11 @@ class AppDetailFragment() : ScreenFragment(), AppDetailAdapter.Callbacks {
|
||||
}
|
||||
)
|
||||
|
||||
@SuppressLint("RestrictedApi")
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
detailAdapter = AppDetailAdapter(this@AppDetailFragment)
|
||||
mainActivity.onToolbarCreated(toolbar)
|
||||
screenActivity.onToolbarCreated(toolbar)
|
||||
toolbar.menu.apply {
|
||||
Action.entries.forEach { action ->
|
||||
add(0, action.id, 0, action.adapterAction.titleResId)
|
||||
@@ -209,12 +205,10 @@ 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)
|
||||
|
||||
@@ -353,20 +347,10 @@ class AppDetailFragment() : ScreenFragment(), AppDetailAdapter.Callbacks {
|
||||
MessageDialog(Message.InsufficientStorage).show(childFragmentManager)
|
||||
return
|
||||
}
|
||||
val shizukuState = viewModel.shizukuState(requireContext())
|
||||
if (shizukuState != null && shizukuState.check) {
|
||||
shizukuDialog(
|
||||
context = requireContext(),
|
||||
shizukuState = shizukuState,
|
||||
openShizuku = { launchShizuku(requireContext()) },
|
||||
switchInstaller = { viewModel.setDefaultInstaller() },
|
||||
).show()
|
||||
return
|
||||
}
|
||||
downloadConnection.startUpdate(
|
||||
packageName = viewModel.packageName,
|
||||
installedItem = installed?.installedItem,
|
||||
products = products,
|
||||
viewModel.packageName,
|
||||
installed?.installedItem,
|
||||
products
|
||||
)
|
||||
}
|
||||
|
||||
@@ -452,27 +436,20 @@ class AppDetailFragment() : ScreenFragment(), AppDetailAdapter.Callbacks {
|
||||
.show(childFragmentManager)
|
||||
}
|
||||
|
||||
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
|
||||
override fun onScreenshotClick(screenshot: Product.Screenshot, parentView: ImageView) {
|
||||
val product = products
|
||||
.firstOrNull { (product, _) ->
|
||||
product.screenshots.find { it === screenshot }?.identifier != null
|
||||
}
|
||||
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()
|
||||
?: 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()
|
||||
}
|
||||
|
||||
override fun onReleaseClick(release: Release) {
|
||||
@@ -530,7 +507,7 @@ class AppDetailFragment() : ScreenFragment(), AppDetailAdapter.Callbacks {
|
||||
}
|
||||
|
||||
override fun onRequestAddRepository(address: String) {
|
||||
mainActivity.navigateAddRepository(address)
|
||||
screenActivity.navigateAddRepository(address)
|
||||
}
|
||||
|
||||
override fun onUriClick(uri: Uri, shouldConfirm: Boolean): Boolean {
|
||||
|
||||
@@ -1,32 +1,25 @@
|
||||
package com.looker.droidify.ui.appDetail
|
||||
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.looker.droidify.utility.common.extension.asStateFlow
|
||||
import com.looker.droidify.datastore.SettingsRepository
|
||||
import com.looker.droidify.datastore.model.InstallerType
|
||||
import com.looker.droidify.domain.model.toPackageName
|
||||
import com.looker.core.common.extension.asStateFlow
|
||||
import com.looker.core.datastore.SettingsRepository
|
||||
import com.looker.core.domain.model.toPackageName
|
||||
import com.looker.droidify.BuildConfig
|
||||
import com.looker.droidify.database.Database
|
||||
import com.looker.droidify.model.InstalledItem
|
||||
import com.looker.droidify.model.Product
|
||||
import com.looker.droidify.model.Repository
|
||||
import com.looker.droidify.installer.InstallManager
|
||||
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.installer.model.InstallState
|
||||
import com.looker.droidify.installer.model.installFrom
|
||||
import com.looker.installer.InstallManager
|
||||
import com.looker.installer.model.InstallState
|
||||
import com.looker.installer.model.installFrom
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.mapNotNull
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
@@ -69,31 +62,6 @@ class AppDetailViewModel @Inject constructor(
|
||||
)
|
||||
}.asStateFlow(AppDetailUiState())
|
||||
|
||||
fun shizukuState(context: Context): ShizukuState? {
|
||||
val isSelected =
|
||||
runBlocking { settingsRepository.getInitial().installerType == InstallerType.SHIZUKU }
|
||||
if (!isSelected) return null
|
||||
val isAlive = isShizukuAlive()
|
||||
val isGranted = if (isAlive) {
|
||||
if (isShizukuGranted()) {
|
||||
true
|
||||
} else {
|
||||
runBlocking { requestPermissionListener() }
|
||||
}
|
||||
} else false
|
||||
return ShizukuState(
|
||||
isNotInstalled = !isShizukuInstalled(context),
|
||||
isNotGranted = !isGranted,
|
||||
isNotAlive = !isAlive,
|
||||
)
|
||||
}
|
||||
|
||||
fun setDefaultInstaller() {
|
||||
viewModelScope.launch {
|
||||
settingsRepository.setInstallerType(InstallerType.Default)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun shouldIgnoreSignature(): Boolean {
|
||||
return settingsRepository.getInitial().ignoreSignature
|
||||
}
|
||||
@@ -128,15 +96,6 @@ class AppDetailViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
data class ShizukuState(
|
||||
val isNotInstalled: Boolean,
|
||||
val isNotGranted: Boolean,
|
||||
val isNotAlive: Boolean,
|
||||
) {
|
||||
val check: Boolean
|
||||
get() = isNotInstalled || isNotAlive || isNotGranted
|
||||
}
|
||||
|
||||
data class AppDetailUiState(
|
||||
val products: List<Product> = emptyList(),
|
||||
val repos: List<Repository> = emptyList(),
|
||||
|
||||
@@ -5,71 +5,56 @@ 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 coil3.asImage
|
||||
import coil3.dispose
|
||||
import coil3.load
|
||||
import coil3.request.placeholder
|
||||
import coil3.size.Scale
|
||||
import coil.dispose
|
||||
import coil.load
|
||||
import coil.size.Dimension
|
||||
import coil.size.Scale
|
||||
import com.google.android.material.imageview.ShapeableImageView
|
||||
import com.looker.droidify.databinding.VideoButtonBinding
|
||||
import com.looker.droidify.graphics.PaddingDrawable
|
||||
import com.looker.core.common.extension.aspectRatio
|
||||
import com.looker.core.common.extension.authentication
|
||||
import com.looker.core.common.extension.camera
|
||||
import com.looker.core.common.extension.dp
|
||||
import com.looker.core.common.extension.dpToPx
|
||||
import com.looker.core.common.extension.getColorFromAttr
|
||||
import com.looker.core.common.extension.selectableBackground
|
||||
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.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.utility.extension.ImageUtils.url
|
||||
import com.looker.droidify.widget.StableRecyclerAdapter
|
||||
import com.google.android.material.R as MaterialR
|
||||
import com.looker.droidify.R.dimen as dimenRes
|
||||
import com.looker.core.common.R.dimen as dimenRes
|
||||
|
||||
class ScreenshotsAdapter(private val onClick: (position: Int) -> Unit) :
|
||||
class ScreenshotsAdapter(private val onClick: (Product.Screenshot, ImageView) -> Unit) :
|
||||
StableRecyclerAdapter<ScreenshotsAdapter.ViewType, RecyclerView.ViewHolder>() {
|
||||
enum class ViewType { SCREENSHOT, VIDEO }
|
||||
enum class ViewType { SCREENSHOT }
|
||||
|
||||
private val items = mutableListOf<Item>()
|
||||
private val items = mutableListOf<Item.ScreenshotItem>()
|
||||
|
||||
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 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 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
|
||||
@@ -84,14 +69,6 @@ class ScreenshotsAdapter(private val onClick: (position: Int) -> Unit) :
|
||||
marginEnd = radius.toInt()
|
||||
}
|
||||
foregroundGravity = Gravity.CENTER
|
||||
setOnClickListener {
|
||||
val position = if (items.any { it.viewType == ViewType.VIDEO }) {
|
||||
absoluteAdapterPosition - 1
|
||||
} else {
|
||||
absoluteAdapterPosition
|
||||
}
|
||||
onClick(position)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -99,73 +76,67 @@ class ScreenshotsAdapter(private val onClick: (position: Int) -> Unit) :
|
||||
fun setScreenshots(
|
||||
repository: Repository,
|
||||
packageName: String,
|
||||
screenshots: List<Product.Screenshot>,
|
||||
screenshots: List<Product.Screenshot>
|
||||
) {
|
||||
items.clear()
|
||||
items += screenshots.map {
|
||||
if (it.type == Product.Screenshot.Type.VIDEO) Item.VideoItem(it.path)
|
||||
else Item.ScreenshotItem(repository, packageName, it)
|
||||
}
|
||||
items += screenshots.map { Item.ScreenshotItem(repository, packageName, it) }
|
||||
notifyItemRangeInserted(0, screenshots.size)
|
||||
}
|
||||
|
||||
override val viewTypeClass: Class<ViewType> 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 val viewTypeClass: Class<ViewType>
|
||||
get() = ViewType::class.java
|
||||
|
||||
override fun getItemEnumViewType(position: Int): ViewType {
|
||||
return ViewType.SCREENSHOT
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
viewType: ViewType,
|
||||
viewType: ViewType
|
||||
): RecyclerView.ViewHolder {
|
||||
return when (viewType) {
|
||||
ViewType.VIDEO -> VideoViewHolder(VideoButtonBinding.inflate(parent.context.layoutInflater))
|
||||
ViewType.SCREENSHOT -> ScreenshotViewHolder(parent.context)
|
||||
return ViewHolder(parent.context).apply {
|
||||
image.setOnClickListener {
|
||||
onClick(
|
||||
items[absoluteAdapterPosition].screenshot,
|
||||
it as ImageView
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
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())
|
||||
}
|
||||
}
|
||||
}
|
||||
override fun getItemDescriptor(position: Int): String = items[position].descriptor
|
||||
override fun getItemCount(): Int = items.size
|
||||
|
||||
ViewType.VIDEO -> {}
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewRecycled(holder: RecyclerView.ViewHolder) {
|
||||
super.onViewRecycled(holder)
|
||||
if (holder is ScreenshotViewHolder) holder.image.dispose()
|
||||
holder as ViewHolder
|
||||
holder.image.dispose()
|
||||
}
|
||||
|
||||
private sealed interface Item {
|
||||
|
||||
val descriptor: String
|
||||
val viewType: ViewType
|
||||
private sealed class Item {
|
||||
abstract val descriptor: String
|
||||
|
||||
class ScreenshotItem(
|
||||
val repository: Repository,
|
||||
val packageName: String,
|
||||
val screenshot: Product.Screenshot,
|
||||
) : Item {
|
||||
override val viewType: ViewType get() = ViewType.SCREENSHOT
|
||||
val screenshot: Product.Screenshot
|
||||
) : Item() {
|
||||
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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
package com.looker.droidify.ui.appDetail
|
||||
|
||||
import android.content.Context
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.looker.droidify.R.string as stringRes
|
||||
|
||||
fun shizukuDialog(
|
||||
context: Context,
|
||||
shizukuState: ShizukuState,
|
||||
openShizuku: () -> Unit,
|
||||
switchInstaller: () -> Unit
|
||||
) = with(MaterialAlertDialogBuilder(context)) {
|
||||
when {
|
||||
shizukuState.isNotAlive -> {
|
||||
setTitle(stringRes.error_shizuku_service_unavailable)
|
||||
setMessage(stringRes.error_shizuku_not_running_DESC)
|
||||
}
|
||||
|
||||
shizukuState.isNotGranted -> {
|
||||
setTitle(stringRes.error_shizuku_not_granted)
|
||||
setMessage(stringRes.error_shizuku_not_granted_DESC)
|
||||
}
|
||||
|
||||
shizukuState.isNotInstalled -> {
|
||||
setTitle(stringRes.error_shizuku_not_installed)
|
||||
setMessage(stringRes.error_shizuku_not_installed_DESC)
|
||||
}
|
||||
}
|
||||
setPositiveButton(stringRes.switch_to_default_installer) { _, _ ->
|
||||
switchInstaller()
|
||||
}
|
||||
setNeutralButton(stringRes.open_shizuku) { _, _ ->
|
||||
openShizuku()
|
||||
}
|
||||
setNegativeButton(stringRes.cancel, null)
|
||||
}
|
||||
@@ -2,7 +2,6 @@ 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
|
||||
@@ -10,44 +9,37 @@ import android.widget.FrameLayout
|
||||
import android.widget.TextView
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import coil3.load
|
||||
import coil.load
|
||||
import com.google.android.material.R as MaterialR
|
||||
import com.google.android.material.imageview.ShapeableImageView
|
||||
import com.google.android.material.progressindicator.CircularProgressIndicator
|
||||
import com.looker.droidify.R
|
||||
import com.looker.droidify.database.Database
|
||||
import com.looker.core.common.extension.authentication
|
||||
import com.looker.core.common.extension.corneredBackground
|
||||
import com.looker.core.common.extension.dp
|
||||
import com.looker.core.common.extension.getColorFromAttr
|
||||
import com.looker.core.common.extension.inflate
|
||||
import com.looker.core.common.extension.setTextSizeScaled
|
||||
import com.looker.core.common.nullIfEmpty
|
||||
import com.looker.droidify.model.ProductItem
|
||||
import com.looker.droidify.model.Repository
|
||||
import com.looker.droidify.utility.common.extension.authentication
|
||||
import com.looker.droidify.utility.common.extension.corneredBackground
|
||||
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.R
|
||||
import com.looker.droidify.database.Database
|
||||
import com.looker.droidify.utility.extension.ImageUtils.icon
|
||||
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: (packageName: String) -> Unit,
|
||||
private val onClick: (ProductItem) -> Unit
|
||||
) : CursorRecyclerAdapter<AppListAdapter.ViewType, RecyclerView.ViewHolder>() {
|
||||
|
||||
enum class ViewType { PRODUCT, LOADING, EMPTY }
|
||||
|
||||
private inner class ProductViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
private class ProductViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
val name = itemView.findViewById<TextView>(R.id.name)!!
|
||||
val status = itemView.findViewById<TextView>(R.id.status)!!
|
||||
val summary = itemView.findViewById<TextView>(R.id.summary)!!
|
||||
val icon = itemView.findViewById<ShapeableImageView>(R.id.icon)!!
|
||||
|
||||
init {
|
||||
itemView.setOnClickListener {
|
||||
log(measureTimeMillis { onClick(getPackageName(absoluteAdapterPosition)) }, "Bench")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class LoadingViewHolder(context: Context) :
|
||||
@@ -55,14 +47,7 @@ class AppListAdapter(
|
||||
init {
|
||||
with(itemView as FrameLayout) {
|
||||
val progressBar = CircularProgressIndicator(context)
|
||||
progressBar.isIndeterminate = true
|
||||
addView(
|
||||
progressBar,
|
||||
FrameLayout.LayoutParams(
|
||||
FrameLayout.LayoutParams.WRAP_CONTENT,
|
||||
FrameLayout.LayoutParams.WRAP_CONTENT,
|
||||
).apply { gravity = Gravity.CENTER }
|
||||
)
|
||||
addView(progressBar)
|
||||
layoutParams = RecyclerView.LayoutParams(
|
||||
RecyclerView.LayoutParams.MATCH_PARENT,
|
||||
RecyclerView.LayoutParams.MATCH_PARENT
|
||||
@@ -91,14 +76,12 @@ class AppListAdapter(
|
||||
}
|
||||
}
|
||||
|
||||
private val repositories: HashMap<Long, Repository> = HashMap()
|
||||
|
||||
fun updateRepos(repos: List<Repository>) {
|
||||
repos.forEach {
|
||||
repositories[it.id] = it
|
||||
var repositories: Map<Long, Repository> = emptyMap()
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
set(value) {
|
||||
field = value
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
var emptyText: String = ""
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
@@ -128,31 +111,24 @@ 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))
|
||||
ViewType.PRODUCT -> ProductViewHolder(parent.inflate(R.layout.product_item)).apply {
|
||||
itemView.setOnClickListener { onClick(getProductItem(absoluteAdapterPosition)) }
|
||||
}
|
||||
|
||||
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 -> {
|
||||
@@ -160,9 +136,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 = repositories[productItem.repositoryId]
|
||||
holder.summary.isVisible =
|
||||
productItem.summary.isNotEmpty() && productItem.name != productItem.summary
|
||||
val repository: Repository? = repositories[productItem.repositoryId]
|
||||
if (repository != null) {
|
||||
val iconUrl = productItem.icon(view = holder.icon, repository = repository)
|
||||
holder.icon.load(iconUrl) {
|
||||
@@ -179,38 +155,28 @@ class AppListAdapter(
|
||||
val isInstalled = productItem.installedVersion.nullIfEmpty() != null
|
||||
when {
|
||||
productItem.canUpdate -> {
|
||||
if (updateBackground == null) {
|
||||
updateBackground =
|
||||
context.getColorFromAttr(MaterialR.attr.colorTertiaryContainer)
|
||||
}
|
||||
if (updateForeground == null) {
|
||||
updateForeground =
|
||||
context.getColorFromAttr(MaterialR.attr.colorOnTertiaryContainer)
|
||||
}
|
||||
backgroundTintList = updateBackground
|
||||
setTextColor(updateForeground)
|
||||
backgroundTintList =
|
||||
context.getColorFromAttr(MaterialR.attr.colorTertiaryContainer)
|
||||
setTextColor(
|
||||
context.getColorFromAttr(MaterialR.attr.colorOnTertiaryContainer)
|
||||
)
|
||||
}
|
||||
|
||||
isInstalled -> {
|
||||
if (installedBackground == null) {
|
||||
installedBackground =
|
||||
context.getColorFromAttr(MaterialR.attr.colorSecondaryContainer)
|
||||
}
|
||||
if (installedForeground == null) {
|
||||
installedForeground =
|
||||
context.getColorFromAttr(MaterialR.attr.colorOnSecondaryContainer)
|
||||
}
|
||||
backgroundTintList = installedBackground
|
||||
setTextColor(installedForeground)
|
||||
backgroundTintList =
|
||||
context.getColorFromAttr(MaterialR.attr.colorSecondaryContainer)
|
||||
setTextColor(
|
||||
context.getColorFromAttr(MaterialR.attr.colorOnSecondaryContainer)
|
||||
)
|
||||
}
|
||||
|
||||
else -> {
|
||||
setPadding(0, 0, 0, 0)
|
||||
if (defaultForeground == null) {
|
||||
defaultForeground =
|
||||
context.getColorFromAttr(MaterialR.attr.colorOnBackground)
|
||||
}
|
||||
setTextColor(defaultForeground)
|
||||
setTextColor(
|
||||
holder.status.context.getColorFromAttr(
|
||||
MaterialR.attr.colorOnBackground
|
||||
)
|
||||
)
|
||||
background = null
|
||||
return@with
|
||||
}
|
||||
@@ -219,9 +185,9 @@ class AppListAdapter(
|
||||
6.dp.let { setPadding(it, it, it, it) }
|
||||
}
|
||||
val enabled = productItem.compatible || productItem.installedVersion.isNotEmpty()
|
||||
holder.name.isEnabled = enabled
|
||||
holder.status.isEnabled = enabled
|
||||
holder.summary.isEnabled = enabled
|
||||
sequenceOf(holder.name, holder.status, holder.summary).forEach {
|
||||
it.isEnabled = enabled
|
||||
}
|
||||
}
|
||||
|
||||
ViewType.LOADING -> {
|
||||
@@ -232,6 +198,6 @@ class AppListAdapter(
|
||||
holder as EmptyViewHolder
|
||||
holder.text.text = emptyText
|
||||
}
|
||||
}
|
||||
}::class
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.R
|
||||
import com.looker.core.common.Scroller
|
||||
import com.looker.core.common.R as CommonR
|
||||
import com.looker.core.common.R.string as stringRes
|
||||
import com.looker.core.common.extension.dp
|
||||
import com.looker.core.common.extension.isFirstItemVisible
|
||||
import com.looker.core.common.extension.systemBarsMargin
|
||||
import com.looker.core.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.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.utility.extension.mainActivity
|
||||
import com.looker.droidify.utility.extension.screenActivity
|
||||
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,15 +63,14 @@ class AppListFragment() : Fragment(), CursorOwner.Callback {
|
||||
get() = requireArguments().getString(EXTRA_SOURCE)!!.let(Source::valueOf)
|
||||
|
||||
private lateinit var recyclerView: RecyclerView
|
||||
private lateinit var appListAdapter: AppListAdapter
|
||||
private var scroller: Scroller? = null
|
||||
private lateinit var recyclerViewAdapter: AppListAdapter
|
||||
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)
|
||||
|
||||
@@ -84,16 +83,18 @@ class AppListFragment() : Fragment(), CursorOwner.Callback {
|
||||
isMotionEventSplittingEnabled = false
|
||||
setHasFixedSize(true)
|
||||
recycledViewPool.setMaxRecycledViews(AppListAdapter.ViewType.PRODUCT.ordinal, 30)
|
||||
appListAdapter = AppListAdapter(source, mainActivity::navigateProduct)
|
||||
adapter = appListAdapter
|
||||
recyclerViewAdapter = AppListAdapter(source) {
|
||||
screenActivity.navigateProduct(it.packageName)
|
||||
}
|
||||
adapter = recyclerViewAdapter
|
||||
systemBarsPadding()
|
||||
}
|
||||
val fab = binding.scrollUp
|
||||
with(fab) {
|
||||
if (source.updateAll) {
|
||||
text = getString(R.string.update_all)
|
||||
text = getString(CommonR.string.update_all)
|
||||
setOnClickListener { viewModel.updateAll() }
|
||||
setIconResource(R.drawable.ic_download)
|
||||
setIconResource(CommonR.drawable.ic_download)
|
||||
alpha = 1f
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
viewModel.showUpdateAllButton.collect {
|
||||
@@ -102,13 +103,11 @@ class AppListFragment() : Fragment(), CursorOwner.Callback {
|
||||
}
|
||||
systemBarsMargin(16.dp)
|
||||
} else {
|
||||
text = null
|
||||
setIconResource(R.drawable.arrow_up)
|
||||
text = ""
|
||||
setIconResource(CommonR.drawable.arrow_up)
|
||||
setOnClickListener {
|
||||
if (scroller == null) {
|
||||
scroller = Scroller(requireContext())
|
||||
}
|
||||
scroller!!.targetPosition = 0
|
||||
val scroller = Scroller(requireContext())
|
||||
scroller.targetPosition = 0
|
||||
recyclerView.layoutManager?.startSmoothScroll(scroller)
|
||||
}
|
||||
alpha = 0f
|
||||
@@ -139,7 +138,7 @@ class AppListFragment() : Fragment(), CursorOwner.Callback {
|
||||
repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
||||
launch {
|
||||
viewModel.reposStream.collect { repos ->
|
||||
appListAdapter.updateRepos(repos)
|
||||
recyclerViewAdapter.repositories = repos.associateBy { it.id }
|
||||
}
|
||||
}
|
||||
launch {
|
||||
@@ -161,13 +160,12 @@ class AppListFragment() : Fragment(), CursorOwner.Callback {
|
||||
super.onDestroyView()
|
||||
viewModel.syncConnection.unbind(requireContext())
|
||||
_binding = null
|
||||
scroller = null
|
||||
mainActivity.cursorOwner.detach(this)
|
||||
screenActivity.cursorOwner.detach(this)
|
||||
}
|
||||
|
||||
override fun onCursorData(request: CursorOwner.Request, cursor: Cursor?) {
|
||||
appListAdapter.cursor = cursor
|
||||
appListAdapter.emptyText = when {
|
||||
recyclerViewAdapter.cursor = cursor
|
||||
recyclerViewAdapter.emptyText = when {
|
||||
cursor == null -> ""
|
||||
viewModel.searchQuery.value.isNotEmpty() -> {
|
||||
getString(stringRes.no_matching_applications_found)
|
||||
@@ -199,7 +197,7 @@ class AppListFragment() : Fragment(), CursorOwner.Callback {
|
||||
|
||||
private fun updateRequest() {
|
||||
if (view != null) {
|
||||
mainActivity.cursorOwner.attach(this, viewModel.request(source))
|
||||
screenActivity.cursorOwner.attach(this, viewModel.request(source))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,52 +2,40 @@ package com.looker.droidify.ui.appList
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
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.core.common.extension.asStateFlow
|
||||
import com.looker.core.datastore.SettingsRepository
|
||||
import com.looker.core.datastore.get
|
||||
import com.looker.core.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())
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
val showUpdateAllButton = skipSignatureStream.flatMapConcat { skip ->
|
||||
Database.ProductAdapter
|
||||
.getUpdatesStream(skip)
|
||||
.map { it.isNotEmpty() }
|
||||
}.asStateFlow(false)
|
||||
val showUpdateAllButton = Database.ProductAdapter
|
||||
.getUpdatesStream()
|
||||
.map { it.isNotEmpty() }
|
||||
.asStateFlow(false)
|
||||
|
||||
val sortOrderFlow = settingsRepository.get { sortOrder }
|
||||
.asStateFlow(SortOrder.UPDATED)
|
||||
|
||||
private val sections = MutableStateFlow<ProductItem.Section>(All)
|
||||
|
||||
@@ -63,23 +51,22 @@ class AppListViewModel
|
||||
|
||||
fun request(source: AppListFragment.Source): CursorOwner.Request {
|
||||
return when (source) {
|
||||
AppListFragment.Source.AVAILABLE -> Available(
|
||||
searchQuery = searchQuery.value,
|
||||
section = sections.value,
|
||||
order = sortOrderFlow.value,
|
||||
AppListFragment.Source.AVAILABLE -> CursorOwner.Request.ProductsAvailable(
|
||||
searchQuery.value,
|
||||
sections.value,
|
||||
sortOrderFlow.value
|
||||
)
|
||||
|
||||
AppListFragment.Source.INSTALLED -> Installed(
|
||||
searchQuery = searchQuery.value,
|
||||
section = sections.value,
|
||||
order = sortOrderFlow.value,
|
||||
AppListFragment.Source.INSTALLED -> CursorOwner.Request.ProductsInstalled(
|
||||
searchQuery.value,
|
||||
sections.value,
|
||||
sortOrderFlow.value
|
||||
)
|
||||
|
||||
AppListFragment.Source.UPDATES -> Updates(
|
||||
searchQuery = searchQuery.value,
|
||||
section = sections.value,
|
||||
order = sortOrderFlow.value,
|
||||
skipSignatureCheck = skipSignatureStream.value,
|
||||
AppListFragment.Source.UPDATES -> CursorOwner.Request.ProductsUpdates(
|
||||
searchQuery.value,
|
||||
sections.value,
|
||||
sortOrderFlow.value
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,16 +4,17 @@ import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import coil3.load
|
||||
import com.looker.droidify.databinding.ProductItemBinding
|
||||
import coil.load
|
||||
import com.google.android.material.R as MaterialR
|
||||
import com.looker.core.common.extension.authentication
|
||||
import com.looker.core.common.extension.corneredBackground
|
||||
import com.looker.core.common.extension.dp
|
||||
import com.looker.core.common.extension.getColorFromAttr
|
||||
import com.looker.core.common.nullIfEmpty
|
||||
import com.looker.droidify.model.Product
|
||||
import com.looker.droidify.model.Repository
|
||||
import com.looker.droidify.utility.common.extension.authentication
|
||||
import com.looker.droidify.utility.common.extension.corneredBackground
|
||||
import com.looker.droidify.utility.common.extension.dp
|
||||
import com.looker.droidify.utility.common.extension.getColorFromAttr
|
||||
import com.looker.droidify.utility.common.nullIfEmpty
|
||||
import com.google.android.material.R as MaterialR
|
||||
import com.looker.droidify.databinding.ProductItemBinding
|
||||
import com.looker.droidify.utility.extension.ImageUtils.icon
|
||||
|
||||
class FavouriteFragmentAdapter(
|
||||
private val onProductClick: (String) -> Unit
|
||||
|
||||
@@ -11,11 +11,11 @@ import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.looker.droidify.R
|
||||
import com.looker.droidify.utility.common.extension.systemBarsPadding
|
||||
import com.looker.core.common.R as CommonR
|
||||
import com.looker.core.common.extension.systemBarsPadding
|
||||
import com.looker.droidify.database.Database
|
||||
import com.looker.droidify.ui.ScreenFragment
|
||||
import com.looker.droidify.utility.extension.mainActivity
|
||||
import com.looker.droidify.utility.extension.screenActivity
|
||||
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 { mainActivity.navigateProduct(it) }
|
||||
FavouriteFragmentAdapter { screenActivity.navigateProduct(it) }
|
||||
this.adapter = recyclerViewAdapter
|
||||
systemBarsPadding(includeFab = false)
|
||||
recyclerView = this
|
||||
@@ -68,12 +68,12 @@ class FavouritesFragment : ScreenFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
toolbar.title = getString(R.string.favourites)
|
||||
toolbar.title = getString(CommonR.string.favourites)
|
||||
return view
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
mainActivity.onToolbarCreated(toolbar)
|
||||
screenActivity.onToolbarCreated(toolbar)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
package com.looker.droidify.ui.favourites
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.looker.droidify.database.Database
|
||||
import com.looker.droidify.datastore.SettingsRepository
|
||||
import com.looker.droidify.datastore.get
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.looker.core.common.extension.asStateFlow
|
||||
import com.looker.core.datastore.SettingsRepository
|
||||
import com.looker.core.datastore.get
|
||||
import com.looker.droidify.model.Product
|
||||
import com.looker.droidify.utility.common.extension.asStateFlow
|
||||
import com.looker.droidify.database.Database
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@HiltViewModel
|
||||
class FavouritesViewModel @Inject constructor(
|
||||
settingsRepository: SettingsRepository,
|
||||
private val settingsRepository: SettingsRepository
|
||||
) : ViewModel() {
|
||||
|
||||
val favouriteApps: StateFlow<List<List<Product>>> =
|
||||
@@ -25,4 +27,9 @@ class FavouritesViewModel @Inject constructor(
|
||||
}
|
||||
}.asStateFlow(emptyList())
|
||||
|
||||
fun updateFavourites(packageName: String) {
|
||||
viewModelScope.launch {
|
||||
settingsRepository.toggleFavourites(packageName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,25 +15,26 @@ 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.R
|
||||
import com.looker.core.common.extension.clipboardManager
|
||||
import com.looker.core.common.extension.get
|
||||
import com.looker.core.common.extension.getMutatedIcon
|
||||
import com.looker.core.common.nullIfEmpty
|
||||
import com.looker.droidify.model.Repository
|
||||
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.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 com.looker.droidify.utility.extension.screenActivity
|
||||
import com.looker.network.Downloader
|
||||
import com.looker.network.NetworkResponse
|
||||
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
|
||||
@@ -43,7 +44,8 @@ import java.nio.charset.Charset
|
||||
import java.util.Locale
|
||||
import javax.inject.Inject
|
||||
import kotlin.math.min
|
||||
import com.looker.droidify.R.string as stringRes
|
||||
import com.looker.core.common.R as CommonR
|
||||
import com.looker.core.common.R.string as stringRes
|
||||
|
||||
@AndroidEntryPoint
|
||||
class EditRepositoryFragment() : ScreenFragment() {
|
||||
@@ -80,12 +82,14 @@ class EditRepositoryFragment() : ScreenFragment() {
|
||||
|
||||
syncConnection.bind(requireContext())
|
||||
|
||||
mainActivity.onToolbarCreated(toolbar)
|
||||
screenActivity.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))
|
||||
.setIcon(toolbar.context.getMutatedIcon(CommonR.drawable.ic_save))
|
||||
.setEnabled(false)
|
||||
.setShowAsActionFlags(MenuItem.SHOW_AS_ACTION_ALWAYS).setOnMenuItemClickListener {
|
||||
onSaveRepositoryClick(true)
|
||||
@@ -167,7 +171,7 @@ class EditRepositoryFragment() : ScreenFragment() {
|
||||
val mirrors = repository.mirrors.map { it.withoutKnownPath }
|
||||
binding.addressContainer.apply {
|
||||
isEndIconVisible = mirrors.isNotEmpty()
|
||||
setEndIconDrawable(R.drawable.ic_arrow_down)
|
||||
setEndIconDrawable(CommonR.drawable.ic_arrow_down)
|
||||
setEndIconOnClickListener {
|
||||
SelectMirrorDialog(mirrors).show(
|
||||
childFragmentManager,
|
||||
@@ -236,8 +240,6 @@ class EditRepositoryFragment() : ScreenFragment() {
|
||||
|
||||
saveMenuItem = null
|
||||
syncConnection.unbind(requireContext())
|
||||
checkJob?.cancel()
|
||||
checkJob = null
|
||||
_binding = null
|
||||
}
|
||||
|
||||
@@ -392,22 +394,22 @@ class EditRepositoryFragment() : ScreenFragment() {
|
||||
}
|
||||
|
||||
private suspend fun checkAddress(
|
||||
rawAddress: String,
|
||||
address: String,
|
||||
authentication: String
|
||||
): String? = coroutineScope {
|
||||
checkInProgress = true
|
||||
invalidateState()
|
||||
val allAddresses = addressSuffixes.map { "$rawAddress/$it" } + rawAddress
|
||||
allAddresses
|
||||
.sortedBy { it.length }
|
||||
.forEach { address ->
|
||||
val response = downloader.headCall(
|
||||
url = "$address/index-v1.jar",
|
||||
val allAddresses = addressSuffixes.map { "$address/$it" } + address
|
||||
val pathCheck = allAddresses.map {
|
||||
async {
|
||||
downloader.headCall(
|
||||
url = "$it/index-v1.jar",
|
||||
headers = { authentication(authentication) }
|
||||
)
|
||||
if (response is NetworkResponse.Success) return@coroutineScope address
|
||||
) is NetworkResponse.Success
|
||||
}
|
||||
null
|
||||
}
|
||||
val indexOfValidAddress = pathCheck.awaitAll().indexOf(true)
|
||||
allAddresses[indexOfValidAddress].nullIfEmpty()
|
||||
}
|
||||
|
||||
private fun onSaveRepositoryProceedInvalidate(
|
||||
@@ -429,7 +431,7 @@ class EditRepositoryFragment() : ScreenFragment() {
|
||||
if (repositoryId == null && changedRepository.enabled) {
|
||||
binder.sync(changedRepository)
|
||||
}
|
||||
mainActivity.onBackPressedDispatcher.onBackPressed()
|
||||
screenActivity.onBackPressedDispatcher.onBackPressed()
|
||||
}
|
||||
} else {
|
||||
invalidateState()
|
||||
@@ -441,7 +443,7 @@ class EditRepositoryFragment() : ScreenFragment() {
|
||||
invalidateState()
|
||||
Snackbar.make(
|
||||
requireView(),
|
||||
R.string.repository_unreachable,
|
||||
CommonR.string.repository_unreachable,
|
||||
Snackbar.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
@@ -468,6 +470,6 @@ class EditRepositoryFragment() : ScreenFragment() {
|
||||
const val EXTRA_REPOSITORY_ID = "repositoryId"
|
||||
const val EXTRA_REPOSITORY_ADDRESS = "repositoryAddress"
|
||||
|
||||
val addressSuffixes = arrayOf("fdroid/repo", "repo")
|
||||
val addressSuffixes = listOf("fdroid/repo", "repo")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,16 +6,16 @@ import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.looker.droidify.R
|
||||
import com.looker.droidify.utility.common.extension.dp
|
||||
import com.looker.droidify.utility.common.extension.systemBarsMargin
|
||||
import com.looker.droidify.utility.common.extension.systemBarsPadding
|
||||
import com.looker.core.common.R as CommonR
|
||||
import com.looker.core.common.extension.dp
|
||||
import com.looker.core.common.extension.systemBarsMargin
|
||||
import com.looker.core.common.extension.systemBarsPadding
|
||||
import com.looker.droidify.database.CursorOwner
|
||||
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.mainActivity
|
||||
import com.looker.droidify.utility.extension.screenActivity
|
||||
import com.looker.droidify.widget.addDivider
|
||||
|
||||
class RepositoriesFragment : ScreenFragment(), CursorOwner.Callback {
|
||||
@@ -34,9 +34,9 @@ class RepositoriesFragment : ScreenFragment(), CursorOwner.Callback {
|
||||
_binding = RecyclerViewWithFabBinding.inflate(inflater, container, false)
|
||||
val view = fragmentBinding.root.apply {
|
||||
binding.scrollUp.apply {
|
||||
setIconResource(R.drawable.ic_add)
|
||||
setText(R.string.add_repository)
|
||||
setOnClickListener { mainActivity.navigateAddRepository() }
|
||||
setIconResource(CommonR.drawable.ic_add)
|
||||
setText(CommonR.string.add_repository)
|
||||
setOnClickListener { screenActivity.navigateAddRepository() }
|
||||
systemBarsMargin(16.dp)
|
||||
}
|
||||
binding.recyclerView.apply {
|
||||
@@ -44,7 +44,7 @@ class RepositoriesFragment : ScreenFragment(), CursorOwner.Callback {
|
||||
isMotionEventSplittingEnabled = false
|
||||
setHasFixedSize(true)
|
||||
adapter = RepositoriesAdapter(
|
||||
navigate = { mainActivity.navigateRepository(it.id) }
|
||||
navigate = { screenActivity.navigateRepository(it.id) }
|
||||
) { repository, isEnabled ->
|
||||
repository.enabled != isEnabled &&
|
||||
syncConnection.binder?.setEnabled(repository, isEnabled) == true
|
||||
@@ -79,9 +79,9 @@ class RepositoriesFragment : ScreenFragment(), CursorOwner.Callback {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
syncConnection.bind(requireContext())
|
||||
mainActivity.cursorOwner.attach(this, CursorOwner.Request.Repositories)
|
||||
mainActivity.onToolbarCreated(toolbar)
|
||||
toolbar.title = getString(R.string.repositories)
|
||||
screenActivity.cursorOwner.attach(this, CursorOwner.Request.Repositories)
|
||||
screenActivity.onToolbarCreated(toolbar)
|
||||
toolbar.title = getString(CommonR.string.repositories)
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
@@ -89,7 +89,7 @@ class RepositoriesFragment : ScreenFragment(), CursorOwner.Callback {
|
||||
|
||||
_binding = null
|
||||
syncConnection.unbind(requireContext())
|
||||
mainActivity.cursorOwner.detach(this)
|
||||
screenActivity.cursorOwner.detach(this)
|
||||
}
|
||||
|
||||
override fun onCursorData(request: CursorOwner.Request, cursor: Cursor?) {
|
||||
|
||||
@@ -15,21 +15,21 @@ import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import com.looker.droidify.utility.common.extension.getColorFromAttr
|
||||
import com.looker.droidify.utility.common.extension.systemBarsPadding
|
||||
import com.looker.core.common.extension.getColorFromAttr
|
||||
import com.looker.core.common.extension.systemBarsPadding
|
||||
import com.looker.droidify.model.Repository
|
||||
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.mainActivity
|
||||
import com.looker.droidify.utility.extension.screenActivity
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import com.google.android.material.R as MaterialR
|
||||
import com.looker.droidify.R.string as stringRes
|
||||
import com.looker.core.common.R.string as stringRes
|
||||
|
||||
@AndroidEntryPoint
|
||||
class RepositoryFragment() : ScreenFragment() {
|
||||
@@ -53,7 +53,7 @@ class RepositoryFragment() : ScreenFragment() {
|
||||
super.onCreateView(inflater, container, savedInstanceState)
|
||||
_binding = RepositoryPageBinding.inflate(inflater, container, false)
|
||||
viewModel.bindService(requireContext())
|
||||
mainActivity.onToolbarCreated(toolbar)
|
||||
screenActivity.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 {
|
||||
mainActivity.navigateEditRepository(viewModel.id)
|
||||
screenActivity.navigateEditRepository(viewModel.id)
|
||||
}
|
||||
|
||||
deleteRepoButton.setOnClickListener {
|
||||
|
||||
@@ -4,7 +4,7 @@ import android.content.Context
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.looker.droidify.utility.common.extension.asStateFlow
|
||||
import com.looker.core.common.extension.asStateFlow
|
||||
import com.looker.droidify.model.Repository
|
||||
import com.looker.droidify.database.Database
|
||||
import com.looker.droidify.service.Connection
|
||||
|
||||
@@ -23,36 +23,38 @@ 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.core.common.SdkCheck
|
||||
import com.looker.core.common.extension.getColorFromAttr
|
||||
import com.looker.core.common.extension.homeAsUp
|
||||
import com.looker.core.common.extension.systemBarsPadding
|
||||
import com.looker.core.common.extension.updateAsMutable
|
||||
import com.looker.core.common.isIgnoreBatteryEnabled
|
||||
import com.looker.core.common.requestBatteryFreedom
|
||||
import com.looker.core.datastore.Settings
|
||||
import com.looker.core.datastore.extension.autoSyncName
|
||||
import com.looker.core.datastore.extension.installerName
|
||||
import com.looker.core.datastore.extension.proxyName
|
||||
import com.looker.core.datastore.extension.themeName
|
||||
import com.looker.core.datastore.extension.toTime
|
||||
import com.looker.core.datastore.model.AutoSync
|
||||
import com.looker.core.datastore.model.InstallerType
|
||||
import com.looker.core.datastore.model.ProxyType
|
||||
import com.looker.core.datastore.model.Theme
|
||||
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
|
||||
import com.looker.droidify.datastore.extension.proxyName
|
||||
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.ProxyType
|
||||
import com.looker.droidify.datastore.model.Theme
|
||||
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.core.common.BuildConfig as CommonBuildConfig
|
||||
import com.looker.core.common.R as CommonR
|
||||
|
||||
@AndroidEntryPoint
|
||||
class SettingsFragment : Fragment() {
|
||||
@@ -64,7 +66,7 @@ class SettingsFragment : Fragment() {
|
||||
private const val REPO_BACKUP_NAME = "droidify_repos"
|
||||
private const val SETTINGS_BACKUP_NAME = "droidify_settings"
|
||||
|
||||
private val localeCodesList: List<String> = BuildConfig.DETECTED_LOCALES
|
||||
private val localeCodesList: List<String> = CommonBuildConfig.DETECTED_LOCALES
|
||||
.toList()
|
||||
.updateAsMutable { add(0, "system") }
|
||||
|
||||
@@ -91,7 +93,7 @@ class SettingsFragment : Fragment() {
|
||||
if (fileUri != null) {
|
||||
viewModel.importSettings(fileUri)
|
||||
} else {
|
||||
viewModel.createSnackbar(R.string.file_format_error_DESC)
|
||||
viewModel.createSnackbar(CommonR.string.file_format_error_DESC)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,7 +109,7 @@ class SettingsFragment : Fragment() {
|
||||
if (fileUri != null) {
|
||||
viewModel.importRepos(fileUri)
|
||||
} else {
|
||||
viewModel.createSnackbar(R.string.file_format_error_DESC)
|
||||
viewModel.createSnackbar(CommonR.string.file_format_error_DESC)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,172 +120,173 @@ class SettingsFragment : Fragment() {
|
||||
): View {
|
||||
_binding = SettingsPageBinding.inflate(inflater, container, false)
|
||||
binding.nestedScrollView.systemBarsPadding()
|
||||
viewModel.toggleBackgroundAccess(requireContext().isIgnoreBatteryEnabled())
|
||||
if (requireContext().isIgnoreBatteryEnabled()) {
|
||||
viewModel.allowBackground()
|
||||
}
|
||||
val toolbar = binding.toolbar
|
||||
toolbar.navigationIcon = toolbar.context.homeAsUp
|
||||
toolbar.setNavigationOnClickListener { activity?.onBackPressedDispatcher?.onBackPressed() }
|
||||
toolbar.title = getString(R.string.settings)
|
||||
toolbar.title = getString(CommonR.string.settings)
|
||||
with(binding) {
|
||||
dynamicTheme.root.isVisible = SdkCheck.isSnowCake
|
||||
dynamicTheme.connect(
|
||||
titleText = getString(R.string.material_you),
|
||||
contentText = getString(R.string.material_you_desc),
|
||||
titleText = getString(CommonR.string.material_you),
|
||||
contentText = getString(CommonR.string.material_you_desc),
|
||||
setting = viewModel.getInitialSetting { dynamicTheme }
|
||||
)
|
||||
homeScreenSwiping.connect(
|
||||
titleText = getString(R.string.home_screen_swiping),
|
||||
contentText = getString(R.string.home_screen_swiping_DESC),
|
||||
titleText = getString(CommonR.string.home_screen_swiping),
|
||||
contentText = getString(CommonR.string.home_screen_swiping_DESC),
|
||||
setting = viewModel.getInitialSetting { homeScreenSwiping }
|
||||
)
|
||||
autoUpdate.connect(
|
||||
titleText = getString(R.string.auto_update),
|
||||
contentText = getString(R.string.auto_update_apps),
|
||||
titleText = getString(CommonR.string.auto_update),
|
||||
contentText = getString(CommonR.string.auto_update_apps),
|
||||
setting = viewModel.getInitialSetting { autoUpdate }
|
||||
)
|
||||
notifyUpdates.connect(
|
||||
titleText = getString(R.string.notify_about_updates),
|
||||
contentText = getString(R.string.notify_about_updates_summary),
|
||||
titleText = getString(CommonR.string.notify_about_updates),
|
||||
contentText = getString(CommonR.string.notify_about_updates_summary),
|
||||
setting = viewModel.getInitialSetting { notifyUpdate }
|
||||
)
|
||||
unstableUpdates.connect(
|
||||
titleText = getString(R.string.unstable_updates),
|
||||
contentText = getString(R.string.unstable_updates_summary),
|
||||
titleText = getString(CommonR.string.unstable_updates),
|
||||
contentText = getString(CommonR.string.unstable_updates_summary),
|
||||
setting = viewModel.getInitialSetting { unstableUpdate }
|
||||
)
|
||||
ignoreSignature.connect(
|
||||
titleText = getString(R.string.ignore_signature),
|
||||
contentText = getString(R.string.ignore_signature_summary),
|
||||
titleText = getString(CommonR.string.ignore_signature),
|
||||
contentText = getString(CommonR.string.ignore_signature_summary),
|
||||
setting = viewModel.getInitialSetting { ignoreSignature }
|
||||
)
|
||||
incompatibleUpdates.connect(
|
||||
titleText = getString(R.string.incompatible_versions),
|
||||
contentText = getString(R.string.incompatible_versions_summary),
|
||||
titleText = getString(CommonR.string.incompatible_versions),
|
||||
contentText = getString(CommonR.string.incompatible_versions_summary),
|
||||
setting = viewModel.getInitialSetting { incompatibleVersions }
|
||||
)
|
||||
language.connect(
|
||||
titleText = getString(R.string.prefs_language_title),
|
||||
titleText = getString(CommonR.string.prefs_language_title),
|
||||
map = { translateLocale(getLocaleOfCode(it)) },
|
||||
setting = viewModel.getSetting { language }
|
||||
) { selectedLocale, valueToString ->
|
||||
addSingleCorrectDialog(
|
||||
initialValue = selectedLocale,
|
||||
values = localeCodesList,
|
||||
title = R.string.prefs_language_title,
|
||||
iconRes = R.drawable.ic_language,
|
||||
title = CommonR.string.prefs_language_title,
|
||||
iconRes = CommonR.drawable.ic_language,
|
||||
valueToString = valueToString,
|
||||
onClick = viewModel::setLanguage
|
||||
)
|
||||
}
|
||||
theme.connect(
|
||||
titleText = getString(R.string.theme),
|
||||
titleText = getString(CommonR.string.theme),
|
||||
setting = viewModel.getSetting { theme },
|
||||
map = { themeName(it) }
|
||||
) { theme, valueToString ->
|
||||
addSingleCorrectDialog(
|
||||
initialValue = theme,
|
||||
values = Theme.entries,
|
||||
title = R.string.themes,
|
||||
iconRes = R.drawable.ic_themes,
|
||||
title = CommonR.string.themes,
|
||||
iconRes = CommonR.drawable.ic_themes,
|
||||
valueToString = valueToString,
|
||||
onClick = viewModel::setTheme
|
||||
)
|
||||
}
|
||||
cleanUp.connect(
|
||||
titleText = getString(R.string.cleanup_title),
|
||||
titleText = getString(CommonR.string.cleanup_title),
|
||||
setting = viewModel.getSetting { cleanUpInterval },
|
||||
map = { toTime(it) }
|
||||
) { duration, valueToString ->
|
||||
addSingleCorrectDialog(
|
||||
initialValue = duration,
|
||||
values = cleanUpIntervals,
|
||||
title = R.string.cleanup_title,
|
||||
iconRes = R.drawable.ic_time,
|
||||
title = CommonR.string.cleanup_title,
|
||||
iconRes = CommonR.drawable.ic_time,
|
||||
valueToString = valueToString,
|
||||
onClick = viewModel::setCleanUpInterval
|
||||
)
|
||||
}
|
||||
autoSync.connect(
|
||||
titleText = getString(R.string.sync_repositories_automatically),
|
||||
titleText = getString(CommonR.string.sync_repositories_automatically),
|
||||
setting = viewModel.getSetting { autoSync },
|
||||
map = { autoSyncName(it) }
|
||||
) { autoSync, valueToString ->
|
||||
addSingleCorrectDialog(
|
||||
initialValue = autoSync,
|
||||
values = AutoSync.entries,
|
||||
title = R.string.sync_repositories_automatically,
|
||||
iconRes = R.drawable.ic_sync_type,
|
||||
title = CommonR.string.sync_repositories_automatically,
|
||||
iconRes = CommonR.drawable.ic_sync_type,
|
||||
valueToString = valueToString,
|
||||
onClick = viewModel::setAutoSync
|
||||
)
|
||||
}
|
||||
installer.connect(
|
||||
titleText = getString(R.string.installer),
|
||||
titleText = getString(CommonR.string.installer),
|
||||
setting = viewModel.getSetting { installerType },
|
||||
map = { installerName(it) }
|
||||
) { installerType, valueToString ->
|
||||
addSingleCorrectDialog(
|
||||
initialValue = installerType,
|
||||
values = InstallerType.entries,
|
||||
title = R.string.installer,
|
||||
iconRes = R.drawable.ic_apk_install,
|
||||
title = CommonR.string.installer,
|
||||
iconRes = CommonR.drawable.ic_apk_install,
|
||||
valueToString = valueToString,
|
||||
onClick = { viewModel.setInstaller(requireContext(), it) }
|
||||
onClick = viewModel::setInstaller
|
||||
)
|
||||
}
|
||||
proxyType.connect(
|
||||
titleText = getString(R.string.proxy_type),
|
||||
titleText = getString(CommonR.string.proxy_type),
|
||||
setting = viewModel.getSetting { proxy.type },
|
||||
map = { proxyName(it) }
|
||||
) { proxyType, valueToString ->
|
||||
addSingleCorrectDialog(
|
||||
initialValue = proxyType,
|
||||
values = ProxyType.entries,
|
||||
title = R.string.proxy_type,
|
||||
iconRes = R.drawable.ic_proxy,
|
||||
title = CommonR.string.proxy_type,
|
||||
iconRes = CommonR.drawable.ic_proxy,
|
||||
valueToString = valueToString,
|
||||
onClick = viewModel::setProxyType
|
||||
)
|
||||
}
|
||||
proxyHost.connect(
|
||||
titleText = getString(R.string.proxy_host),
|
||||
titleText = getString(CommonR.string.proxy_host),
|
||||
setting = viewModel.getSetting { proxy.host },
|
||||
map = { it }
|
||||
) { host, _ ->
|
||||
addEditTextDialog(
|
||||
initialValue = host,
|
||||
title = R.string.proxy_host,
|
||||
title = CommonR.string.proxy_host,
|
||||
onFinish = viewModel::setProxyHost
|
||||
)
|
||||
}
|
||||
proxyPort.connect(
|
||||
titleText = getString(R.string.proxy_port),
|
||||
titleText = getString(CommonR.string.proxy_port),
|
||||
setting = viewModel.getSetting { proxy.port },
|
||||
map = { it.toString() }
|
||||
) { port, _ ->
|
||||
addEditTextDialog(
|
||||
initialValue = port.toString(),
|
||||
title = R.string.proxy_port,
|
||||
title = CommonR.string.proxy_port,
|
||||
onFinish = viewModel::setProxyPort
|
||||
)
|
||||
}
|
||||
|
||||
forceCleanUp.title.text = getString(R.string.force_clean_up)
|
||||
forceCleanUp.content.text = getString(R.string.force_clean_up_DESC)
|
||||
forceCleanUp.title.text = getString(CommonR.string.force_clean_up)
|
||||
forceCleanUp.content.text = getString(CommonR.string.force_clean_up_DESC)
|
||||
|
||||
importSettings.title.text = getString(R.string.import_settings_title)
|
||||
importSettings.content.text = getString(R.string.import_settings_DESC)
|
||||
exportSettings.title.text = getString(R.string.export_settings_title)
|
||||
exportSettings.content.text = getString(R.string.export_settings_DESC)
|
||||
importSettings.title.text = getString(CommonR.string.import_settings_title)
|
||||
importSettings.content.text = getString(CommonR.string.import_settings_DESC)
|
||||
exportSettings.title.text = getString(CommonR.string.export_settings_title)
|
||||
exportSettings.content.text = getString(CommonR.string.export_settings_DESC)
|
||||
|
||||
importRepos.title.text = getString(R.string.import_repos_title)
|
||||
importRepos.content.text = getString(R.string.import_repos_DESC)
|
||||
exportRepos.title.text = getString(R.string.export_repos_title)
|
||||
exportRepos.content.text = getString(R.string.export_repos_DESC)
|
||||
importRepos.title.text = getString(CommonR.string.import_repos_title)
|
||||
importRepos.content.text = getString(CommonR.string.import_repos_DESC)
|
||||
exportRepos.title.text = getString(CommonR.string.export_repos_title)
|
||||
exportRepos.content.text = getString(CommonR.string.export_repos_DESC)
|
||||
|
||||
allowBackgroundWork.root.isVisible = false
|
||||
allowBackgroundWork.title.text = getString(R.string.require_background_access)
|
||||
allowBackgroundWork.title.text = getString(CommonR.string.require_background_access)
|
||||
allowBackgroundWork.content.text =
|
||||
getString(R.string.require_background_access_DESC)
|
||||
getString(CommonR.string.require_background_access_DESC)
|
||||
allowBackgroundWork.root.setBackgroundColor(
|
||||
requireContext()
|
||||
.getColorFromAttr(MaterialR.attr.colorErrorContainer)
|
||||
@@ -297,7 +300,7 @@ class SettingsFragment : Fragment() {
|
||||
requireContext()
|
||||
.getColorFromAttr(MaterialR.attr.colorOnErrorContainer)
|
||||
)
|
||||
creditFoxy.title.text = getString(R.string.special_credits)
|
||||
creditFoxy.title.text = getString(CommonR.string.special_credits)
|
||||
creditFoxy.content.text = FOXY_DROID_TITLE
|
||||
droidify.title.text = DROID_IFY_TITLE
|
||||
droidify.content.text = BuildConfig.VERSION_NAME
|
||||
@@ -313,8 +316,8 @@ class SettingsFragment : Fragment() {
|
||||
launch {
|
||||
viewModel.settingsFlow.collect { setting ->
|
||||
updateSettings(setting)
|
||||
binding.allowBackgroundWork.root.isVisible =
|
||||
!viewModel.isBackgroundAllowed && setting.autoSync != AutoSync.NEVER
|
||||
binding.allowBackgroundWork.root.isVisible = !viewModel.backgroundTask.first()
|
||||
&& setting.autoSync != AutoSync.NEVER
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -324,7 +327,9 @@ class SettingsFragment : Fragment() {
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
viewModel.toggleBackgroundAccess(requireContext().isIgnoreBatteryEnabled())
|
||||
if (requireContext().isIgnoreBatteryEnabled()) {
|
||||
viewModel.allowBackground()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
@@ -372,7 +377,9 @@ class SettingsFragment : Fragment() {
|
||||
}
|
||||
allowBackgroundWork.root.setOnClickListener {
|
||||
requireContext().requestBatteryFreedom()
|
||||
viewModel.toggleBackgroundAccess(requireContext().isIgnoreBatteryEnabled())
|
||||
if (requireContext().isIgnoreBatteryEnabled()) {
|
||||
viewModel.allowBackground()
|
||||
}
|
||||
}
|
||||
creditFoxy.root.setOnClickListener {
|
||||
openLink(FOXY_DROID_URL)
|
||||
@@ -414,7 +421,7 @@ class SettingsFragment : Fragment() {
|
||||
)
|
||||
)
|
||||
} else {
|
||||
getString(R.string.system)
|
||||
getString(CommonR.string.system)
|
||||
}
|
||||
return languageDisplay
|
||||
}
|
||||
@@ -423,7 +430,7 @@ class SettingsFragment : Fragment() {
|
||||
try {
|
||||
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(link)))
|
||||
} catch (e: IllegalStateException) {
|
||||
viewModel.createSnackbar(R.string.cannot_open_link)
|
||||
viewModel.createSnackbar(CommonR.string.cannot_open_link)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -508,7 +515,7 @@ class SettingsFragment : Fragment() {
|
||||
onClick(values.elementAt(newValue))
|
||||
}
|
||||
}
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.setNegativeButton(CommonR.string.cancel, null)
|
||||
.create()
|
||||
|
||||
private fun View.addEditTextDialog(
|
||||
@@ -519,7 +526,7 @@ class SettingsFragment : Fragment() {
|
||||
val scroll = NestedScrollView(context)
|
||||
val customEditText = TextInputEditText(context)
|
||||
customEditText.id = android.R.id.edit
|
||||
val paddingValue = context.resources.getDimension(R.dimen.shape_margin_large).toInt()
|
||||
val paddingValue = context.resources.getDimension(CommonR.dimen.shape_margin_large).toInt()
|
||||
scroll.setPadding(paddingValue, 0, paddingValue, 0)
|
||||
customEditText.setText(initialValue)
|
||||
customEditText.hint = customEditText.text.toString()
|
||||
@@ -533,10 +540,10 @@ class SettingsFragment : Fragment() {
|
||||
return MaterialAlertDialogBuilder(context)
|
||||
.setTitle(title)
|
||||
.setView(scroll)
|
||||
.setPositiveButton(R.string.ok) { _, _ ->
|
||||
.setPositiveButton(CommonR.string.ok) { _, _ ->
|
||||
post { onFinish(customEditText.text.toString()) }
|
||||
}
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.setNegativeButton(CommonR.string.cancel, null)
|
||||
.create()
|
||||
.apply {
|
||||
window!!.setSoftInputMode(
|
||||
|
||||
@@ -7,39 +7,38 @@ 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.core.common.extension.toLocale
|
||||
import com.looker.core.datastore.Settings
|
||||
import com.looker.core.datastore.SettingsRepository
|
||||
import com.looker.core.datastore.get
|
||||
import com.looker.core.datastore.model.AutoSync
|
||||
import com.looker.core.datastore.model.InstallerType
|
||||
import com.looker.core.datastore.model.ProxyType
|
||||
import com.looker.core.datastore.model.Theme
|
||||
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.ROOT
|
||||
import com.looker.droidify.datastore.model.InstallerType.SHIZUKU
|
||||
import com.looker.droidify.datastore.model.ProxyType
|
||||
import com.looker.droidify.datastore.model.Theme
|
||||
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 com.looker.installer.installers.shizuku.ShizukuPermissionHandler
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.cancel
|
||||
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.first
|
||||
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.core.common.R as CommonR
|
||||
|
||||
@HiltViewModel
|
||||
class SettingsViewModel
|
||||
@Inject constructor(
|
||||
private val settingsRepository: SettingsRepository,
|
||||
private val shizukuPermissionHandler: ShizukuPermissionHandler,
|
||||
private val repositoryExporter: RepositoryExporter
|
||||
) : ViewModel() {
|
||||
|
||||
@@ -48,8 +47,8 @@ class SettingsViewModel
|
||||
}
|
||||
val settingsFlow get() = settingsRepository.data
|
||||
|
||||
var isBackgroundAllowed = true
|
||||
private set
|
||||
private val _backgroundTask = MutableStateFlow(false)
|
||||
val backgroundTask = _backgroundTask.asStateFlow()
|
||||
|
||||
private val _snackbarStringId = MutableSharedFlow<Int>()
|
||||
val snackbarStringId = _snackbarStringId.asSharedFlow()
|
||||
@@ -58,8 +57,10 @@ class SettingsViewModel
|
||||
|
||||
fun <T> getInitialSetting(block: Settings.() -> T): Flow<T> = initialSetting.map { it.block() }
|
||||
|
||||
fun toggleBackgroundAccess(enable: Boolean) {
|
||||
isBackgroundAllowed = enable
|
||||
fun allowBackground() {
|
||||
viewModelScope.launch {
|
||||
_backgroundTask.emit(true)
|
||||
}
|
||||
}
|
||||
|
||||
fun setLanguage(language: String) {
|
||||
@@ -152,42 +153,16 @@ class SettingsViewModel
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
settingsRepository.setProxyPort(proxyPort.toInt())
|
||||
} catch (_: NumberFormatException) {
|
||||
createSnackbar(R.string.proxy_port_error_not_int)
|
||||
} catch (e: NumberFormatException) {
|
||||
createSnackbar(CommonR.string.proxy_port_error_not_int)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setInstaller(context: Context, installerType: InstallerType) {
|
||||
fun setInstaller(installerType: InstallerType) {
|
||||
viewModelScope.launch {
|
||||
when (installerType) {
|
||||
SHIZUKU -> {
|
||||
if (isShizukuInstalled(context)) {
|
||||
if (!isShizukuAlive()) {
|
||||
createSnackbar(R.string.shizuku_not_alive)
|
||||
return@launch
|
||||
} else if (isShizukuGranted()) {
|
||||
settingsRepository.setInstallerType(installerType)
|
||||
} else if (!isShizukuGranted()) {
|
||||
if (requestPermissionListener()) {
|
||||
settingsRepository.setInstallerType(installerType)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
createSnackbar(R.string.shizuku_not_installed)
|
||||
}
|
||||
}
|
||||
|
||||
ROOT -> {
|
||||
if (isMagiskGranted()) {
|
||||
settingsRepository.setInstallerType(installerType)
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
settingsRepository.setInstallerType(installerType)
|
||||
}
|
||||
}
|
||||
settingsRepository.setInstallerType(installerType)
|
||||
if (installerType == InstallerType.SHIZUKU) handleShizuku()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,18 +197,18 @@ class SettingsViewModel
|
||||
_snackbarStringId.emit(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun String.toLocale(): Locale = when {
|
||||
contains("-r") -> Locale(
|
||||
substring(0, 2),
|
||||
substring(4)
|
||||
)
|
||||
|
||||
contains("_") -> Locale(
|
||||
substring(0, 2),
|
||||
substring(3)
|
||||
)
|
||||
|
||||
else -> Locale(this)
|
||||
|
||||
private fun handleShizuku() {
|
||||
viewModelScope.launch {
|
||||
val state = shizukuPermissionHandler.state.first()
|
||||
if (state.isAlive && state.isPermissionGranted) cancel()
|
||||
if (state.isInstalled) {
|
||||
if (!state.isAlive) {
|
||||
createSnackbar(CommonR.string.shizuku_not_alive)
|
||||
}
|
||||
} else {
|
||||
createSnackbar(CommonR.string.shizuku_not_installed)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ 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
|
||||
@@ -29,23 +28,23 @@ 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.core.common.device.Huawei
|
||||
import com.looker.core.common.extension.dp
|
||||
import com.looker.core.common.extension.getMutatedIcon
|
||||
import com.looker.core.common.extension.selectableBackground
|
||||
import com.looker.core.common.extension.systemBarsPadding
|
||||
import com.looker.core.common.sdkAbove
|
||||
import com.looker.core.datastore.extension.sortOrderName
|
||||
import com.looker.core.datastore.model.SortOrder
|
||||
import com.looker.droidify.R
|
||||
import com.looker.droidify.databinding.TabsToolbarBinding
|
||||
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.utility.extension.resources.sizeScaled
|
||||
import com.looker.droidify.utility.extension.mainActivity
|
||||
import com.looker.droidify.utility.extension.screenActivity
|
||||
import com.looker.droidify.widget.DividerConfiguration
|
||||
import com.looker.droidify.widget.FocusSearchView
|
||||
import com.looker.droidify.widget.StableRecyclerAdapter
|
||||
@@ -54,7 +53,8 @@ import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.roundToInt
|
||||
import com.looker.droidify.R.string as stringRes
|
||||
import com.looker.core.common.R as CommonR
|
||||
import com.looker.core.common.R.string as stringRes
|
||||
|
||||
@AndroidEntryPoint
|
||||
class TabsFragment : ScreenFragment() {
|
||||
@@ -152,7 +152,7 @@ class TabsFragment : ScreenFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
mainActivity.onToolbarCreated(toolbar)
|
||||
screenActivity.onToolbarCreated(toolbar)
|
||||
toolbar.title = getString(R.string.application_name)
|
||||
// Move focus from SearchView to Toolbar
|
||||
toolbar.isFocusable = true
|
||||
@@ -184,7 +184,7 @@ class TabsFragment : ScreenFragment() {
|
||||
}
|
||||
|
||||
searchMenuItem = add(0, R.id.toolbar_search, 0, stringRes.search)
|
||||
.setIcon(toolbar.context.getMutatedIcon(R.drawable.ic_search))
|
||||
.setIcon(toolbar.context.getMutatedIcon(CommonR.drawable.ic_search))
|
||||
.setActionView(searchView)
|
||||
.setShowAsActionFlags(
|
||||
MenuItem.SHOW_AS_ACTION_ALWAYS or MenuItem.SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW
|
||||
@@ -202,14 +202,15 @@ class TabsFragment : ScreenFragment() {
|
||||
})
|
||||
|
||||
syncRepositoriesMenuItem = add(0, 0, 0, stringRes.sync_repositories)
|
||||
.setIcon(toolbar.context.getMutatedIcon(R.drawable.ic_sync))
|
||||
.setIcon(toolbar.context.getMutatedIcon(CommonR.drawable.ic_sync))
|
||||
.setOnMenuItemClickListener {
|
||||
// SyncWorker.startSyncWork(requireContext())
|
||||
syncConnection.binder?.sync(SyncService.SyncRequest.MANUAL)
|
||||
true
|
||||
}
|
||||
|
||||
sortOrderMenu = addSubMenu(0, 0, 0, stringRes.sorting_order)
|
||||
.setIcon(toolbar.context.getMutatedIcon(R.drawable.ic_sort))
|
||||
.setIcon(toolbar.context.getMutatedIcon(CommonR.drawable.ic_sort))
|
||||
.let { menu ->
|
||||
menu.item.setShowAsActionFlags(MenuItem.SHOW_AS_ACTION_ALWAYS)
|
||||
val menuItems = SortOrder.entries.map { sortOrder ->
|
||||
@@ -225,22 +226,22 @@ class TabsFragment : ScreenFragment() {
|
||||
|
||||
favouritesItem = add(1, 0, 0, stringRes.favourites)
|
||||
.setIcon(
|
||||
toolbar.context.getMutatedIcon(R.drawable.ic_favourite_checked)
|
||||
toolbar.context.getMutatedIcon(CommonR.drawable.ic_favourite_checked)
|
||||
)
|
||||
.setOnMenuItemClickListener {
|
||||
view.post { mainActivity.navigateFavourites() }
|
||||
view.post { screenActivity.navigateFavourites() }
|
||||
true
|
||||
}
|
||||
|
||||
add(1, 0, 0, stringRes.repositories)
|
||||
.setOnMenuItemClickListener {
|
||||
view.post { mainActivity.navigateRepositories() }
|
||||
view.post { screenActivity.navigateRepositories() }
|
||||
true
|
||||
}
|
||||
|
||||
add(1, 0, 0, stringRes.settings)
|
||||
.setOnMenuItemClickListener {
|
||||
view.post { mainActivity.navigatePreferences() }
|
||||
view.post { screenActivity.navigatePreferences() }
|
||||
true
|
||||
}
|
||||
}
|
||||
@@ -297,31 +298,12 @@ 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val backgroundPath = ShapeAppearanceModel.builder()
|
||||
.setAllCornerSizes(
|
||||
context?.resources?.getDimension(R.dimen.shape_large_corner) ?: 0F
|
||||
context?.resources?.getDimension(CommonR.dimen.shape_large_corner) ?: 0F
|
||||
)
|
||||
.build()
|
||||
val sectionBackground = MaterialShapeDrawable(backgroundPath)
|
||||
|
||||
@@ -3,19 +3,19 @@ package com.looker.droidify.ui.tabsFragment
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
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.core.common.extension.asStateFlow
|
||||
import com.looker.core.datastore.SettingsRepository
|
||||
import com.looker.core.datastore.get
|
||||
import com.looker.core.datastore.model.SortOrder
|
||||
import com.looker.droidify.model.ProductItem
|
||||
import com.looker.droidify.database.Database
|
||||
import com.looker.droidify.ui.tabsFragment.TabsFragment.BackAction
|
||||
import com.looker.droidify.utility.common.extension.asStateFlow
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class TabsViewModel @Inject constructor(
|
||||
@@ -57,18 +57,7 @@ class TabsViewModel @Inject constructor(
|
||||
|
||||
val showSections = MutableStateFlow(false)
|
||||
|
||||
val backAction = combine(
|
||||
currentSection,
|
||||
isSearchActionItemExpanded,
|
||||
showSections
|
||||
) { currentSection, isSearchActionItemExpanded, showSections ->
|
||||
when {
|
||||
currentSection != ProductItem.Section.All -> BackAction.ProductAll
|
||||
isSearchActionItemExpanded -> BackAction.CollapseSearchView
|
||||
showSections -> BackAction.HideSections
|
||||
else -> BackAction.None
|
||||
}
|
||||
}.asStateFlow(BackAction.None)
|
||||
val backAction = combine(currentSection, isSearchActionItemExpanded, showSections, ::calcBackAction).asStateFlow(BackAction.None)
|
||||
|
||||
fun setSection(section: ProductItem.Section) {
|
||||
savedStateHandle[STATE_SECTION] = section
|
||||
@@ -80,6 +69,30 @@ class TabsViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun calcBackAction(
|
||||
currentSection: ProductItem.Section,
|
||||
isSearchActionItemExpanded: Boolean,
|
||||
showSections: Boolean,
|
||||
): BackAction {
|
||||
return when {
|
||||
currentSection != ProductItem.Section.All -> {
|
||||
BackAction.ProductAll
|
||||
}
|
||||
|
||||
isSearchActionItemExpanded -> {
|
||||
BackAction.CollapseSearchView
|
||||
}
|
||||
|
||||
showSections -> {
|
||||
BackAction.HideSections
|
||||
}
|
||||
|
||||
else -> {
|
||||
BackAction.None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val STATE_SECTION = "section"
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import android.content.pm.PackageItemInfo
|
||||
import android.content.pm.PermissionInfo
|
||||
import android.content.res.Resources
|
||||
import android.os.Build
|
||||
import com.looker.droidify.utility.common.SdkCheck
|
||||
import com.looker.core.common.SdkCheck
|
||||
import java.util.Locale
|
||||
|
||||
object PackageItemResolver {
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
package com.looker.droidify.utility.common
|
||||
|
||||
import android.os.Build
|
||||
import androidx.annotation.ChecksSdkIntAtLeast
|
||||
|
||||
@ChecksSdkIntAtLeast(parameter = 0, lambda = 1)
|
||||
inline fun sdkAbove(sdk: Int, onSuccessful: () -> Unit) {
|
||||
if (Build.VERSION.SDK_INT >= sdk) onSuccessful()
|
||||
}
|
||||
|
||||
object SdkCheck {
|
||||
|
||||
val sdk: Int = Build.VERSION.SDK_INT
|
||||
|
||||
// Allows auto install if target sdk of apk is one less then current sdk
|
||||
fun canAutoInstall(targetSdk: Int) = targetSdk >= sdk - 1 && isSnowCake
|
||||
|
||||
@get:ChecksSdkIntAtLeast(api = Build.VERSION_CODES.TIRAMISU)
|
||||
val isTiramisu: Boolean get() = sdk >= Build.VERSION_CODES.TIRAMISU
|
||||
|
||||
@get:ChecksSdkIntAtLeast(api = Build.VERSION_CODES.R)
|
||||
val isR: Boolean get() = sdk >= Build.VERSION_CODES.R
|
||||
|
||||
@get:ChecksSdkIntAtLeast(api = Build.VERSION_CODES.P)
|
||||
val isPie: Boolean get() = sdk >= Build.VERSION_CODES.P
|
||||
|
||||
@get:ChecksSdkIntAtLeast(api = Build.VERSION_CODES.O)
|
||||
val isOreo: Boolean get() = sdk >= Build.VERSION_CODES.O
|
||||
|
||||
@get:ChecksSdkIntAtLeast(api = Build.VERSION_CODES.S)
|
||||
val isSnowCake: Boolean get() = sdk >= Build.VERSION_CODES.S
|
||||
|
||||
@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",
|
||||
)
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
package com.looker.droidify.utility.common
|
||||
|
||||
import android.util.Log
|
||||
|
||||
fun <T : CharSequence> T.nullIfEmpty(): T? {
|
||||
return if (isNullOrBlank()) null else this
|
||||
}
|
||||
|
||||
fun Any.log(
|
||||
message: Any?,
|
||||
tag: String = this::class.java.simpleName + ".DEBUG",
|
||||
type: Int = Log.DEBUG
|
||||
) {
|
||||
Log.println(type, tag, message.toString())
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
package com.looker.droidify.utility.common.extension
|
||||
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import java.util.TimeZone
|
||||
|
||||
private val HTTP_DATE_FORMAT: SimpleDateFormat
|
||||
get() = SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US).apply {
|
||||
timeZone = TimeZone.getTimeZone("GMT")
|
||||
}
|
||||
|
||||
fun Date.toFormattedString(): String = HTTP_DATE_FORMAT.format(this)
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.looker.droidify.utility.extension
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.looker.droidify.MainActivity
|
||||
import com.looker.droidify.ScreenActivity
|
||||
|
||||
inline val Fragment.mainActivity: MainActivity
|
||||
get() = requireActivity() as MainActivity
|
||||
inline val Fragment.screenActivity: ScreenActivity
|
||||
get() = requireActivity() as ScreenActivity
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.looker.droidify.utility.extension
|
||||
|
||||
import android.view.View
|
||||
import com.looker.core.common.Singleton
|
||||
import com.looker.core.common.extension.dpi
|
||||
import com.looker.droidify.model.Product
|
||||
import com.looker.droidify.model.ProductItem
|
||||
import com.looker.droidify.model.Repository
|
||||
|
||||
object ImageUtils {
|
||||
private val SUPPORTED_DPI = listOf(120, 160, 240, 320, 480, 640)
|
||||
private var DeviceDpi = Singleton<String>()
|
||||
|
||||
fun Product.Screenshot.url(
|
||||
repository: Repository,
|
||||
packageName: String
|
||||
): String {
|
||||
val phoneType = when (type) {
|
||||
Product.Screenshot.Type.PHONE -> "phoneScreenshots"
|
||||
Product.Screenshot.Type.SMALL_TABLET -> "sevenInchScreenshots"
|
||||
Product.Screenshot.Type.LARGE_TABLET -> "tenInchScreenshots"
|
||||
}
|
||||
return "${repository.address}/$packageName/$locale/$phoneType/$path"
|
||||
}
|
||||
|
||||
fun ProductItem.icon(
|
||||
view: View,
|
||||
repository: Repository
|
||||
): String? {
|
||||
if (packageName.isBlank()) return null
|
||||
if (icon.isBlank() && metadataIcon.isBlank()) return null
|
||||
if (repository.version < 11 && icon.isNotBlank()) {
|
||||
return "${repository.address}/icons/$icon"
|
||||
}
|
||||
if (icon.isNotBlank()) {
|
||||
val deviceDpi = DeviceDpi.getOrUpdate {
|
||||
(SUPPORTED_DPI.find { it >= view.dpi } ?: SUPPORTED_DPI.last()).toString()
|
||||
}
|
||||
return "${repository.address}/icons-$deviceDpi/$icon"
|
||||
}
|
||||
if (metadataIcon.isNotBlank()) {
|
||||
return "${repository.address}/$packageName/$metadataIcon"
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
package com.looker.droidify.utility.extension
|
||||
|
||||
import android.content.pm.PackageInfo
|
||||
import com.looker.droidify.utility.common.extension.calculateHash
|
||||
import com.looker.droidify.utility.common.extension.singleSignature
|
||||
import com.looker.droidify.utility.common.extension.versionCodeCompat
|
||||
import com.looker.core.common.extension.calculateHash
|
||||
import com.looker.core.common.extension.singleSignature
|
||||
import com.looker.core.common.extension.versionCodeCompat
|
||||
import com.looker.droidify.model.InstalledItem
|
||||
|
||||
fun PackageInfo.toInstalledItem(): InstalledItem {
|
||||
|
||||
@@ -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.droidify.utility.common.extension.forEachKey
|
||||
import com.looker.core.common.extension.forEachKey
|
||||
import com.looker.droidify.model.ProductItem
|
||||
|
||||
fun ProductItem.serialize(generator: JsonGenerator) {
|
||||
|
||||
@@ -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.droidify.utility.common.extension.forEachKey
|
||||
import com.looker.core.common.extension.forEachKey
|
||||
import com.looker.droidify.model.ProductPreference
|
||||
|
||||
fun ProductPreference.serialize(generator: JsonGenerator) {
|
||||
|
||||
@@ -3,97 +3,102 @@ 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.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.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.model.Product
|
||||
import com.looker.droidify.model.Release
|
||||
|
||||
fun Product.serialize(generator: JsonGenerator) {
|
||||
generator.writeNumberField(REPOSITORYID, repositoryId)
|
||||
generator.writeNumberField(SERIALVERSION, 1)
|
||||
generator.writeStringField(PACKAGENAME, packageName)
|
||||
generator.writeStringField(NAME, name)
|
||||
generator.writeStringField(SUMMARY, summary)
|
||||
generator.writeStringField(DESCRIPTION, description)
|
||||
generator.writeStringField(WHATSNEW, whatsNew)
|
||||
generator.writeStringField(ICON, icon)
|
||||
generator.writeStringField(METADATAICON, metadataIcon)
|
||||
generator.writeStringField(AUTHORNAME, author.name)
|
||||
generator.writeStringField(AUTHOREMAIL, author.email)
|
||||
generator.writeStringField(AUTHORWEB, author.web)
|
||||
generator.writeStringField(SOURCE, source)
|
||||
generator.writeStringField(CHANGELOG, changelog)
|
||||
generator.writeStringField(WEB, web)
|
||||
generator.writeStringField(TRACKER, tracker)
|
||||
generator.writeNumberField(ADDED, added)
|
||||
generator.writeNumberField(UPDATED, updated)
|
||||
generator.writeNumberField(SUGGESTEDVERSIONCODE, suggestedVersionCode)
|
||||
generator.writeArray(CATEGORIES) { categories.forEach(::writeString) }
|
||||
generator.writeArray(ANTIFEATURES) { antiFeatures.forEach(::writeString) }
|
||||
generator.writeArray(LICENSES) { licenses.forEach(::writeString) }
|
||||
generator.writeArray(DONATES) {
|
||||
generator.writeNumberField("repositoryId", repositoryId)
|
||||
generator.writeNumberField("serialVersion", 1)
|
||||
generator.writeStringField("packageName", packageName)
|
||||
generator.writeStringField("name", name)
|
||||
generator.writeStringField("summary", summary)
|
||||
generator.writeStringField("description", description)
|
||||
generator.writeStringField("whatsNew", whatsNew)
|
||||
generator.writeStringField("icon", icon)
|
||||
generator.writeStringField("metadataIcon", metadataIcon)
|
||||
generator.writeStringField("authorName", author.name)
|
||||
generator.writeStringField("authorEmail", author.email)
|
||||
generator.writeStringField("authorWeb", author.web)
|
||||
generator.writeStringField("source", source)
|
||||
generator.writeStringField("changelog", changelog)
|
||||
generator.writeStringField("web", web)
|
||||
generator.writeStringField("tracker", tracker)
|
||||
generator.writeNumberField("added", added)
|
||||
generator.writeNumberField("updated", updated)
|
||||
generator.writeNumberField("suggestedVersionCode", suggestedVersionCode)
|
||||
generator.writeArray("categories") { categories.forEach(::writeString) }
|
||||
generator.writeArray("antiFeatures") { antiFeatures.forEach(::writeString) }
|
||||
generator.writeArray("licenses") { licenses.forEach(::writeString) }
|
||||
generator.writeArray("donates") {
|
||||
donates.forEach {
|
||||
writeDictionary {
|
||||
when (it) {
|
||||
is Product.Donate.Regular -> {
|
||||
writeStringField(TYPE, DONATION_EMPTY)
|
||||
writeStringField(URL, it.url)
|
||||
writeStringField("type", "")
|
||||
writeStringField("url", it.url)
|
||||
}
|
||||
|
||||
is Product.Donate.Bitcoin -> {
|
||||
writeStringField(TYPE, DONATION_BITCOIN)
|
||||
writeStringField(ADDRESS, it.address)
|
||||
writeStringField("type", "bitcoin")
|
||||
writeStringField("address", it.address)
|
||||
}
|
||||
|
||||
is Product.Donate.Litecoin -> {
|
||||
writeStringField(TYPE, DONATION_LITECOIN)
|
||||
writeStringField(ADDRESS, it.address)
|
||||
writeStringField("type", "litecoin")
|
||||
writeStringField("address", it.address)
|
||||
}
|
||||
|
||||
is Product.Donate.Flattr -> {
|
||||
writeStringField("type", "flattr")
|
||||
writeStringField("id", it.id)
|
||||
}
|
||||
|
||||
is Product.Donate.Liberapay -> {
|
||||
writeStringField(TYPE, DONATION_LIBERAPAY)
|
||||
writeStringField(ID, it.id)
|
||||
writeStringField("type", "liberapay")
|
||||
writeStringField("id", it.id)
|
||||
}
|
||||
|
||||
is Product.Donate.OpenCollective -> {
|
||||
writeStringField(TYPE, DONATION_OPENCOLLECTIVE)
|
||||
writeStringField(ID, it.id)
|
||||
writeStringField("type", "openCollective")
|
||||
writeStringField("id", it.id)
|
||||
}
|
||||
}::class
|
||||
}
|
||||
}
|
||||
}
|
||||
generator.writeArray(SCREENSHOTS) {
|
||||
generator.writeArray("screenshots") {
|
||||
screenshots.forEach {
|
||||
writeDictionary {
|
||||
writeStringField(LOCALE, it.locale)
|
||||
writeStringField(TYPE, it.type.jsonName)
|
||||
writeStringField(PATH, it.path)
|
||||
writeStringField("locale", it.locale)
|
||||
writeStringField("type", it.type.jsonName)
|
||||
writeStringField("path", it.path)
|
||||
}
|
||||
}
|
||||
}
|
||||
generator.writeArray(RELEASES) { releases.forEach { writeDictionary { it.serialize(this) } } }
|
||||
generator.writeArray("releases") { releases.forEach { writeDictionary { it.serialize(this) } } }
|
||||
}
|
||||
|
||||
fun JsonParser.product(): Product {
|
||||
var repositoryId = 0L
|
||||
var packageName = KEY_EMPTY
|
||||
var name = KEY_EMPTY
|
||||
var summary = KEY_EMPTY
|
||||
var description = KEY_EMPTY
|
||||
var whatsNew = KEY_EMPTY
|
||||
var icon = KEY_EMPTY
|
||||
var metadataIcon = KEY_EMPTY
|
||||
var authorName = KEY_EMPTY
|
||||
var authorEmail = KEY_EMPTY
|
||||
var authorWeb = KEY_EMPTY
|
||||
var source = KEY_EMPTY
|
||||
var changelog = KEY_EMPTY
|
||||
var web = KEY_EMPTY
|
||||
var tracker = KEY_EMPTY
|
||||
var packageName = ""
|
||||
var name = ""
|
||||
var summary = ""
|
||||
var description = ""
|
||||
var whatsNew = ""
|
||||
var icon = ""
|
||||
var metadataIcon = ""
|
||||
var authorName = ""
|
||||
var authorEmail = ""
|
||||
var authorWeb = ""
|
||||
var source = ""
|
||||
var changelog = ""
|
||||
var web = ""
|
||||
var tracker = ""
|
||||
var added = 0L
|
||||
var updated = 0L
|
||||
var suggestedVersionCode = 0L
|
||||
@@ -103,64 +108,65 @@ fun JsonParser.product(): Product {
|
||||
var donates = emptyList<Product.Donate>()
|
||||
var screenshots = emptyList<Product.Screenshot>()
|
||||
var releases = emptyList<Release>()
|
||||
forEachKey { key ->
|
||||
forEachKey { it ->
|
||||
when {
|
||||
key.string(REPOSITORYID) -> repositoryId = valueAsLong
|
||||
key.string(PACKAGENAME) -> packageName = valueAsString
|
||||
key.string(NAME) -> name = valueAsString
|
||||
key.string(SUMMARY) -> summary = valueAsString
|
||||
key.string(DESCRIPTION) -> description = valueAsString
|
||||
key.string(WHATSNEW) -> whatsNew = valueAsString
|
||||
key.string(ICON) -> icon = valueAsString
|
||||
key.string(METADATAICON) -> metadataIcon = valueAsString
|
||||
key.string(AUTHORNAME) -> authorName = valueAsString
|
||||
key.string(AUTHOREMAIL) -> authorEmail = valueAsString
|
||||
key.string(AUTHORWEB) -> authorWeb = valueAsString
|
||||
key.string(SOURCE) -> source = valueAsString
|
||||
key.string(CHANGELOG) -> changelog = valueAsString
|
||||
key.string(WEB) -> web = valueAsString
|
||||
key.string(TRACKER) -> tracker = valueAsString
|
||||
key.number(ADDED) -> added = valueAsLong
|
||||
key.number(UPDATED) -> updated = valueAsLong
|
||||
key.number(SUGGESTEDVERSIONCODE) -> suggestedVersionCode = valueAsLong
|
||||
key.array(CATEGORIES) -> categories = collectNotNullStrings()
|
||||
key.array(ANTIFEATURES) -> antiFeatures = collectNotNullStrings()
|
||||
key.array(LICENSES) -> licenses = collectNotNullStrings()
|
||||
key.array(DONATES) -> donates = collectNotNull(JsonToken.START_OBJECT) {
|
||||
var type = KEY_EMPTY
|
||||
var url = KEY_EMPTY
|
||||
var address = KEY_EMPTY
|
||||
var id = KEY_EMPTY
|
||||
it.string("repositoryId") -> repositoryId = valueAsLong
|
||||
it.string("packageName") -> packageName = valueAsString
|
||||
it.string("name") -> name = valueAsString
|
||||
it.string("summary") -> summary = valueAsString
|
||||
it.string("description") -> description = valueAsString
|
||||
it.string("whatsNew") -> whatsNew = valueAsString
|
||||
it.string("icon") -> icon = valueAsString
|
||||
it.string("metadataIcon") -> metadataIcon = valueAsString
|
||||
it.string("authorName") -> authorName = valueAsString
|
||||
it.string("authorEmail") -> authorEmail = valueAsString
|
||||
it.string("authorWeb") -> authorWeb = valueAsString
|
||||
it.string("source") -> source = valueAsString
|
||||
it.string("changelog") -> changelog = valueAsString
|
||||
it.string("web") -> web = valueAsString
|
||||
it.string("tracker") -> tracker = valueAsString
|
||||
it.number("added") -> added = valueAsLong
|
||||
it.number("updated") -> updated = valueAsLong
|
||||
it.number("suggestedVersionCode") -> suggestedVersionCode = valueAsLong
|
||||
it.array("categories") -> categories = collectNotNullStrings()
|
||||
it.array("antiFeatures") -> antiFeatures = collectNotNullStrings()
|
||||
it.array("licenses") -> licenses = collectNotNullStrings()
|
||||
it.array("donates") -> donates = collectNotNull(JsonToken.START_OBJECT) {
|
||||
var type = ""
|
||||
var url = ""
|
||||
var address = ""
|
||||
var id = ""
|
||||
forEachKey {
|
||||
when {
|
||||
it.string(TYPE) -> type = valueAsString
|
||||
it.string(URL) -> url = valueAsString
|
||||
it.string(ADDRESS) -> address = valueAsString
|
||||
it.string(ID) -> id = valueAsString
|
||||
it.string("type") -> type = valueAsString
|
||||
it.string("url") -> url = valueAsString
|
||||
it.string("address") -> address = valueAsString
|
||||
it.string("id") -> id = valueAsString
|
||||
else -> skipChildren()
|
||||
}
|
||||
}
|
||||
when (type) {
|
||||
DONATION_EMPTY -> Product.Donate.Regular(url)
|
||||
DONATION_BITCOIN -> Product.Donate.Bitcoin(address)
|
||||
DONATION_LITECOIN -> Product.Donate.Litecoin(address)
|
||||
DONATION_LIBERAPAY -> Product.Donate.Liberapay(id)
|
||||
DONATION_OPENCOLLECTIVE -> Product.Donate.OpenCollective(id)
|
||||
"" -> Product.Donate.Regular(url)
|
||||
"bitcoin" -> Product.Donate.Bitcoin(address)
|
||||
"litecoin" -> Product.Donate.Litecoin(address)
|
||||
"flattr" -> Product.Donate.Flattr(id)
|
||||
"liberapay" -> Product.Donate.Liberapay(id)
|
||||
"openCollective" -> Product.Donate.OpenCollective(id)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
key.array(SCREENSHOTS) ->
|
||||
it.array("screenshots") ->
|
||||
screenshots =
|
||||
collectNotNull(JsonToken.START_OBJECT) {
|
||||
var locale = KEY_EMPTY
|
||||
var type = KEY_EMPTY
|
||||
var path = KEY_EMPTY
|
||||
var locale = ""
|
||||
var type = ""
|
||||
var path = ""
|
||||
forEachKey {
|
||||
when {
|
||||
it.string(LOCALE) -> locale = valueAsString
|
||||
it.string(TYPE) -> type = valueAsString
|
||||
it.string(PATH) -> path = valueAsString
|
||||
it.string("locale") -> locale = valueAsString
|
||||
it.string("type") -> type = valueAsString
|
||||
it.string("path") -> path = valueAsString
|
||||
else -> skipChildren()
|
||||
}
|
||||
}
|
||||
@@ -168,7 +174,7 @@ fun JsonParser.product(): Product {
|
||||
?.let { Product.Screenshot(locale, it, path) }
|
||||
}
|
||||
|
||||
key.array(RELEASES) ->
|
||||
it.array("releases") ->
|
||||
releases =
|
||||
collectNotNull(JsonToken.START_OBJECT) { release() }
|
||||
|
||||
@@ -176,66 +182,27 @@ fun JsonParser.product(): Product {
|
||||
}
|
||||
}
|
||||
return Product(
|
||||
repositoryId = repositoryId,
|
||||
packageName = packageName,
|
||||
name = name,
|
||||
summary = summary,
|
||||
description = description,
|
||||
whatsNew = whatsNew,
|
||||
icon = icon,
|
||||
metadataIcon = metadataIcon,
|
||||
author = Product.Author(authorName, authorEmail, authorWeb),
|
||||
source = source,
|
||||
changelog = changelog,
|
||||
web = web,
|
||||
tracker = tracker,
|
||||
added = added,
|
||||
updated = updated,
|
||||
suggestedVersionCode = suggestedVersionCode,
|
||||
categories = categories,
|
||||
antiFeatures = antiFeatures,
|
||||
licenses = licenses,
|
||||
donates = donates,
|
||||
screenshots = screenshots,
|
||||
releases = releases
|
||||
repositoryId,
|
||||
packageName,
|
||||
name,
|
||||
summary,
|
||||
description,
|
||||
whatsNew,
|
||||
icon,
|
||||
metadataIcon,
|
||||
Product.Author(authorName, authorEmail, authorWeb),
|
||||
source,
|
||||
changelog,
|
||||
web,
|
||||
tracker,
|
||||
added,
|
||||
updated,
|
||||
suggestedVersionCode,
|
||||
categories,
|
||||
antiFeatures,
|
||||
licenses,
|
||||
donates,
|
||||
screenshots,
|
||||
releases
|
||||
)
|
||||
}
|
||||
|
||||
private const val REPOSITORYID = "repositoryId"
|
||||
private const val SERIALVERSION = "serialVersion"
|
||||
private const val PACKAGENAME = "packageName"
|
||||
private const val NAME = "name"
|
||||
private const val SUMMARY = "summary"
|
||||
private const val DESCRIPTION = "description"
|
||||
private const val WHATSNEW = "whatsNew"
|
||||
private const val ICON = "icon"
|
||||
private const val METADATAICON = "metadataIcon"
|
||||
private const val AUTHORNAME = "authorName"
|
||||
private const val AUTHOREMAIL = "authorEmail"
|
||||
private const val AUTHORWEB = "authorWeb"
|
||||
private const val SOURCE = "source"
|
||||
private const val CHANGELOG = "changelog"
|
||||
private const val WEB = "web"
|
||||
private const val TRACKER = "tracker"
|
||||
private const val ADDED = "added"
|
||||
private const val UPDATED = "updated"
|
||||
private const val SUGGESTEDVERSIONCODE = "suggestedVersionCode"
|
||||
private const val CATEGORIES = "categories"
|
||||
private const val ANTIFEATURES = "antiFeatures"
|
||||
private const val LICENSES = "licenses"
|
||||
private const val DONATES = "donates"
|
||||
private const val ADDRESS = "address"
|
||||
private const val URL = "url"
|
||||
private const val TYPE = "type"
|
||||
private const val ID = "id"
|
||||
private const val SCREENSHOTS = "screenshots"
|
||||
private const val RELEASES = "releases"
|
||||
private const val PATH = "path"
|
||||
private const val LOCALE = "locale"
|
||||
|
||||
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_LIBERAPAY = "liberapay"
|
||||
private const val DONATION_OPENCOLLECTIVE = "openCollective"
|
||||
|
||||
@@ -3,56 +3,56 @@ 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.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.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.model.Release
|
||||
|
||||
fun Release.serialize(generator: JsonGenerator) {
|
||||
generator.writeNumberField(SERIALVERSION, 1)
|
||||
generator.writeBooleanField(SELECTED, selected)
|
||||
generator.writeStringField(VERSION, version)
|
||||
generator.writeNumberField(VERSIONCODE, versionCode)
|
||||
generator.writeNumberField(ADDED, added)
|
||||
generator.writeNumberField(SIZE, size)
|
||||
generator.writeNumberField(MINSDKVERSION, minSdkVersion)
|
||||
generator.writeNumberField(TARGETSDKVERSION, targetSdkVersion)
|
||||
generator.writeNumberField(MAXSDKVERSION, maxSdkVersion)
|
||||
generator.writeStringField(SOURCE, source)
|
||||
generator.writeStringField(RELEASE, release)
|
||||
generator.writeStringField(HASH, hash)
|
||||
generator.writeStringField(HASHTYPE, hashType)
|
||||
generator.writeStringField(SIGNATURE, signature)
|
||||
generator.writeStringField(OBBMAIN, obbMain)
|
||||
generator.writeStringField(OBBMAINHASH, obbMainHash)
|
||||
generator.writeStringField(OBBMAINHASHTYPE, obbMainHashType)
|
||||
generator.writeStringField(OBBPATCH, obbPatch)
|
||||
generator.writeStringField(OBBPATCHHASH, obbPatchHash)
|
||||
generator.writeStringField(OBBPATCHHASHTYPE, obbPatchHashType)
|
||||
generator.writeArray(PERMISSIONS) { permissions.forEach { writeString(it) } }
|
||||
generator.writeArray(FEATURES) { features.forEach { writeString(it) } }
|
||||
generator.writeArray(PLATFORMS) { platforms.forEach { writeString(it) } }
|
||||
generator.writeArray(INCOMPATIBILITIES) {
|
||||
generator.writeNumberField("serialVersion", 1)
|
||||
generator.writeBooleanField("selected", selected)
|
||||
generator.writeStringField("version", version)
|
||||
generator.writeNumberField("versionCode", versionCode)
|
||||
generator.writeNumberField("added", added)
|
||||
generator.writeNumberField("size", size)
|
||||
generator.writeNumberField("minSdkVersion", minSdkVersion)
|
||||
generator.writeNumberField("targetSdkVersion", targetSdkVersion)
|
||||
generator.writeNumberField("maxSdkVersion", maxSdkVersion)
|
||||
generator.writeStringField("source", source)
|
||||
generator.writeStringField("release", release)
|
||||
generator.writeStringField("hash", hash)
|
||||
generator.writeStringField("hashType", hashType)
|
||||
generator.writeStringField("signature", signature)
|
||||
generator.writeStringField("obbMain", obbMain)
|
||||
generator.writeStringField("obbMainHash", obbMainHash)
|
||||
generator.writeStringField("obbMainHashType", obbMainHashType)
|
||||
generator.writeStringField("obbPatch", obbPatch)
|
||||
generator.writeStringField("obbPatchHash", obbPatchHash)
|
||||
generator.writeStringField("obbPatchHashType", obbPatchHashType)
|
||||
generator.writeArray("permissions") { permissions.forEach { writeString(it) } }
|
||||
generator.writeArray("features") { features.forEach { writeString(it) } }
|
||||
generator.writeArray("platforms") { platforms.forEach { writeString(it) } }
|
||||
generator.writeArray("incompatibilities") {
|
||||
incompatibilities.forEach {
|
||||
writeDictionary {
|
||||
when (it) {
|
||||
is Release.Incompatibility.MinSdk -> {
|
||||
writeStringField(INCOMPATIBILITY_TYPE, MIN_SDK)
|
||||
writeStringField("type", "minSdk")
|
||||
}
|
||||
|
||||
is Release.Incompatibility.MaxSdk -> {
|
||||
writeStringField(INCOMPATIBILITY_TYPE, MAX_SDK)
|
||||
writeStringField("type", "maxSdk")
|
||||
}
|
||||
|
||||
is Release.Incompatibility.Platform -> {
|
||||
writeStringField(INCOMPATIBILITY_TYPE, PLATFORM)
|
||||
writeStringField("type", "platform")
|
||||
}
|
||||
|
||||
is Release.Incompatibility.Feature -> {
|
||||
writeStringField(INCOMPATIBILITY_TYPE, INCOMPATIBILITY_FEATURE)
|
||||
writeStringField(INCOMPATIBILITY_FEATURE, it.feature)
|
||||
writeStringField("type", "feature")
|
||||
writeStringField("feature", it.feature)
|
||||
}
|
||||
}::class
|
||||
}
|
||||
@@ -60,72 +60,71 @@ fun Release.serialize(generator: JsonGenerator) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun JsonParser.release(): Release {
|
||||
var selected = false
|
||||
var version = KEY_EMPTY
|
||||
var version = ""
|
||||
var versionCode = 0L
|
||||
var added = 0L
|
||||
var size = 0L
|
||||
var minSdkVersion = 0
|
||||
var targetSdkVersion = 0
|
||||
var maxSdkVersion = 0
|
||||
var source = KEY_EMPTY
|
||||
var release = KEY_EMPTY
|
||||
var hash = KEY_EMPTY
|
||||
var hashType = KEY_EMPTY
|
||||
var signature = KEY_EMPTY
|
||||
var obbMain = KEY_EMPTY
|
||||
var obbMainHash = KEY_EMPTY
|
||||
var obbMainHashType = KEY_EMPTY
|
||||
var obbPatch = KEY_EMPTY
|
||||
var obbPatchHash = KEY_EMPTY
|
||||
var obbPatchHashType = KEY_EMPTY
|
||||
var source = ""
|
||||
var release = ""
|
||||
var hash = ""
|
||||
var hashType = ""
|
||||
var signature = ""
|
||||
var obbMain = ""
|
||||
var obbMainHash = ""
|
||||
var obbMainHashType = ""
|
||||
var obbPatch = ""
|
||||
var obbPatchHash = ""
|
||||
var obbPatchHashType = ""
|
||||
var permissions = emptyList<String>()
|
||||
var features = emptyList<String>()
|
||||
var platforms = emptyList<String>()
|
||||
var incompatibilities = emptyList<Release.Incompatibility>()
|
||||
forEachKey { key ->
|
||||
forEachKey { it ->
|
||||
when {
|
||||
key.boolean(SELECTED) -> selected = valueAsBoolean
|
||||
key.string(VERSION) -> version = valueAsString
|
||||
key.number(VERSIONCODE) -> versionCode = valueAsLong
|
||||
key.number(ADDED) -> added = valueAsLong
|
||||
key.number(SIZE) -> size = valueAsLong
|
||||
key.number(MINSDKVERSION) -> minSdkVersion = valueAsInt
|
||||
key.number(TARGETSDKVERSION) -> targetSdkVersion = valueAsInt
|
||||
key.number(MAXSDKVERSION) -> maxSdkVersion = valueAsInt
|
||||
key.string(SOURCE) -> source = valueAsString
|
||||
key.string(RELEASE) -> release = valueAsString
|
||||
key.string(HASH) -> hash = valueAsString
|
||||
key.string(HASHTYPE) -> hashType = valueAsString
|
||||
key.string(SIGNATURE) -> signature = valueAsString
|
||||
key.string(OBBMAIN) -> obbMain = valueAsString
|
||||
key.string(OBBMAINHASH) -> obbMainHash = valueAsString
|
||||
key.string(OBBMAINHASHTYPE) -> obbMainHashType = valueAsString
|
||||
key.string(OBBPATCH) -> obbPatch = valueAsString
|
||||
key.string(OBBPATCHHASH) -> obbPatchHash = valueAsString
|
||||
key.string(OBBPATCHHASHTYPE) -> obbPatchHashType = valueAsString
|
||||
key.array(PERMISSIONS) -> permissions = collectNotNullStrings()
|
||||
key.array(FEATURES) -> features = collectNotNullStrings()
|
||||
key.array(PLATFORMS) -> platforms = collectNotNullStrings()
|
||||
key.array(INCOMPATIBILITIES) ->
|
||||
it.boolean("selected") -> selected = valueAsBoolean
|
||||
it.string("version") -> version = valueAsString
|
||||
it.number("versionCode") -> versionCode = valueAsLong
|
||||
it.number("added") -> added = valueAsLong
|
||||
it.number("size") -> size = valueAsLong
|
||||
it.number("minSdkVersion") -> minSdkVersion = valueAsInt
|
||||
it.number("targetSdkVersion") -> targetSdkVersion = valueAsInt
|
||||
it.number("maxSdkVersion") -> maxSdkVersion = valueAsInt
|
||||
it.string("source") -> source = valueAsString
|
||||
it.string("release") -> release = valueAsString
|
||||
it.string("hash") -> hash = valueAsString
|
||||
it.string("hashType") -> hashType = valueAsString
|
||||
it.string("signature") -> signature = valueAsString
|
||||
it.string("obbMain") -> obbMain = valueAsString
|
||||
it.string("obbMainHash") -> obbMainHash = valueAsString
|
||||
it.string("obbMainHashType") -> obbMainHashType = valueAsString
|
||||
it.string("obbPatch") -> obbPatch = valueAsString
|
||||
it.string("obbPatchHash") -> obbPatchHash = valueAsString
|
||||
it.string("obbPatchHashType") -> obbPatchHashType = valueAsString
|
||||
it.array("permissions") -> permissions = collectNotNullStrings()
|
||||
it.array("features") -> features = collectNotNullStrings()
|
||||
it.array("platforms") -> platforms = collectNotNullStrings()
|
||||
it.array("incompatibilities") ->
|
||||
incompatibilities =
|
||||
collectNotNull(JsonToken.START_OBJECT) {
|
||||
var type = KEY_EMPTY
|
||||
var feature = KEY_EMPTY
|
||||
var type = ""
|
||||
var feature = ""
|
||||
forEachKey {
|
||||
when {
|
||||
it.string(INCOMPATIBILITY_TYPE) -> type = valueAsString
|
||||
it.string(INCOMPATIBILITY_FEATURE) -> feature = valueAsString
|
||||
it.string("type") -> type = valueAsString
|
||||
it.string("feature") -> feature = valueAsString
|
||||
else -> skipChildren()
|
||||
}
|
||||
}
|
||||
when (type) {
|
||||
MIN_SDK -> Release.Incompatibility.MinSdk
|
||||
MAX_SDK -> Release.Incompatibility.MaxSdk
|
||||
PLATFORM -> Release.Incompatibility.Platform
|
||||
INCOMPATIBILITY_FEATURE -> Release.Incompatibility.Feature(feature)
|
||||
"minSdk" -> Release.Incompatibility.MinSdk
|
||||
"maxSdk" -> Release.Incompatibility.MaxSdk
|
||||
"platform" -> Release.Incompatibility.Platform
|
||||
"feature" -> Release.Incompatibility.Feature(feature)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
@@ -134,59 +133,28 @@ fun JsonParser.release(): Release {
|
||||
}
|
||||
}
|
||||
return Release(
|
||||
selected = selected,
|
||||
version = version,
|
||||
versionCode = versionCode,
|
||||
added = added,
|
||||
size = size,
|
||||
minSdkVersion = minSdkVersion,
|
||||
targetSdkVersion = targetSdkVersion,
|
||||
maxSdkVersion = maxSdkVersion,
|
||||
source = source,
|
||||
release = release,
|
||||
hash = hash,
|
||||
hashType = hashType,
|
||||
signature = signature,
|
||||
obbMain = obbMain,
|
||||
obbMainHash = obbMainHash,
|
||||
obbMainHashType = obbMainHashType,
|
||||
obbPatch = obbPatch,
|
||||
obbPatchHash = obbPatchHash,
|
||||
obbPatchHashType = obbPatchHashType,
|
||||
permissions = permissions,
|
||||
features = features,
|
||||
platforms = platforms,
|
||||
incompatibilities = incompatibilities
|
||||
selected,
|
||||
version,
|
||||
versionCode,
|
||||
added,
|
||||
size,
|
||||
minSdkVersion,
|
||||
targetSdkVersion,
|
||||
maxSdkVersion,
|
||||
source,
|
||||
release,
|
||||
hash,
|
||||
hashType,
|
||||
signature,
|
||||
obbMain,
|
||||
obbMainHash,
|
||||
obbMainHashType,
|
||||
obbPatch,
|
||||
obbPatchHash,
|
||||
obbPatchHashType,
|
||||
permissions,
|
||||
features,
|
||||
platforms,
|
||||
incompatibilities
|
||||
)
|
||||
}
|
||||
|
||||
private const val KEY_EMPTY = ""
|
||||
private const val SERIALVERSION = "serialVersion"
|
||||
private const val SELECTED = "selected"
|
||||
private const val VERSION = "version"
|
||||
private const val VERSIONCODE = "versionCode"
|
||||
private const val ADDED = "added"
|
||||
private const val SIZE = "size"
|
||||
private const val MINSDKVERSION = "minSdkVersion"
|
||||
private const val TARGETSDKVERSION = "targetSdkVersion"
|
||||
private const val MAXSDKVERSION = "maxSdkVersion"
|
||||
private const val SOURCE = "source"
|
||||
private const val RELEASE = "release"
|
||||
private const val HASH = "hash"
|
||||
private const val HASHTYPE = "hashType"
|
||||
private const val SIGNATURE = "signature"
|
||||
private const val OBBMAIN = "obbMain"
|
||||
private const val OBBMAINHASH = "obbMainHash"
|
||||
private const val OBBMAINHASHTYPE = "obbMainHashType"
|
||||
private const val OBBPATCH = "obbPatch"
|
||||
private const val OBBPATCHHASH = "obbPatchHash"
|
||||
private const val OBBPATCHHASHTYPE = "obbPatchHashType"
|
||||
private const val PERMISSIONS = "permissions"
|
||||
private const val FEATURES = "features"
|
||||
private const val PLATFORMS = "platforms"
|
||||
private const val INCOMPATIBILITIES = "incompatibilities"
|
||||
private const val INCOMPATIBILITY_TYPE = "type"
|
||||
private const val INCOMPATIBILITY_FEATURE = "feature"
|
||||
private const val MIN_SDK = "minSdk"
|
||||
private const val MAX_SDK = "maxSdk"
|
||||
private const val PLATFORM = "platform"
|
||||
|
||||
@@ -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.droidify.utility.common.extension.collectNotNullStrings
|
||||
import com.looker.droidify.utility.common.extension.forEachKey
|
||||
import com.looker.droidify.utility.common.extension.writeArray
|
||||
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.model.Repository
|
||||
|
||||
fun Repository.serialize(generator: JsonGenerator) {
|
||||
|
||||
@@ -5,7 +5,7 @@ import android.graphics.Canvas
|
||||
import android.graphics.Rect
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.looker.droidify.utility.common.extension.divider
|
||||
import com.looker.core.common.extension.divider
|
||||
import com.looker.droidify.R
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
|
||||
@@ -10,8 +10,8 @@ import androidx.work.OneTimeWorkRequestBuilder
|
||||
import androidx.work.PeriodicWorkRequestBuilder
|
||||
import androidx.work.WorkManager
|
||||
import androidx.work.WorkerParameters
|
||||
import com.looker.droidify.utility.common.cache.Cache
|
||||
import com.looker.droidify.datastore.SettingsRepository
|
||||
import com.looker.core.common.cache.Cache
|
||||
import com.looker.core.datastore.SettingsRepository
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlin.time.Duration
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M380,660L660,480L380,300L380,660ZM160,800Q127,800 103.5,776.5Q80,753 80,720L80,240Q80,207 103.5,183.5Q127,160 160,160L800,160Q833,160 856.5,183.5Q880,207 880,240L880,720Q880,753 856.5,776.5Q833,800 800,800L160,800ZM160,720L800,720Q800,720 800,720Q800,720 800,720L800,240Q800,240 800,240Q800,240 800,240L160,240Q160,240 160,240Q160,240 160,240L160,720Q160,720 160,720Q160,720 160,720ZM160,720Q160,720 160,720Q160,720 160,720L160,240Q160,240 160,240Q160,240 160,240L160,240Q160,240 160,240Q160,240 160,240L160,720Q160,720 160,720Q160,720 160,720Z"/>
|
||||
</vector>
|
||||
@@ -90,6 +90,7 @@
|
||||
</LinearLayout>
|
||||
|
||||
<com.google.android.material.divider.MaterialDivider
|
||||
android:id="@+id/divider1"
|
||||
android:layout_width="2dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginVertical="12dp" />
|
||||
@@ -119,6 +120,7 @@
|
||||
</LinearLayout>
|
||||
|
||||
<com.google.android.material.divider.MaterialDivider
|
||||
android:id="@+id/divider2"
|
||||
android:layout_width="2dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginVertical="12dp" />
|
||||
|
||||
@@ -10,68 +10,77 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
style="?materialCardViewElevatedStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingHorizontal="12dp"
|
||||
android:paddingTop="4dp"
|
||||
android:paddingBottom="12dp">
|
||||
android:layout_marginHorizontal="12dp"
|
||||
android:layout_marginTop="12dp">
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/address_container"
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/address"
|
||||
android:paddingVertical="12dp">
|
||||
android:orientation="vertical"
|
||||
android:paddingHorizontal="12dp"
|
||||
android:paddingTop="4dp"
|
||||
android:paddingBottom="12dp">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/address"
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/address_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:inputType="textNoSuggestions|textVisiblePassword" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/address"
|
||||
android:paddingVertical="12dp">
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/fingerprint"
|
||||
android:paddingVertical="12dp">
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/address"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:inputType="textNoSuggestions|textVisiblePassword" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/fingerprint"
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/fingerprint"
|
||||
android:paddingVertical="12dp">
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/username"
|
||||
android:paddingVertical="12dp">
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/fingerprint"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/username"
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:autofillHints="username" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/username"
|
||||
android:paddingVertical="12dp">
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/password"
|
||||
android:paddingVertical="12dp">
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/username"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:autofillHints="username" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/password"
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:autofillHints="password"
|
||||
android:inputType="textNoSuggestions|textVisiblePassword" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/password"
|
||||
android:paddingVertical="12dp">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/password"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:autofillHints="password"
|
||||
android:inputType="textNoSuggestions|textVisiblePassword" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
</LinearLayout>
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
|
||||
<LinearLayout
|
||||
@@ -81,7 +90,6 @@
|
||||
android:background="?android:attr/colorBackground"
|
||||
android:clickable="true"
|
||||
android:gravity="center"
|
||||
android:visibility="gone"
|
||||
android:orientation="vertical"
|
||||
tools:ignore="KeyboardInaccessibleWidget">
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
android:orientation="horizontal">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center_vertical"
|
||||
@@ -96,13 +96,6 @@
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/target_sdk"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:singleLine="true"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:textSize="14sp" />
|
||||
</LinearLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
@@ -2,8 +2,7 @@
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:paddingBottom="20dp">
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.google.android.material.materialswitch.MaterialSwitch
|
||||
android:id="@+id/repo_switch"
|
||||
@@ -66,4 +65,9 @@
|
||||
android:text="@string/delete"
|
||||
android:textColor="?colorOnError" />
|
||||
</LinearLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="20dp"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
@@ -44,12 +44,8 @@
|
||||
android:layout_marginEnd="20dp"
|
||||
android:src="@drawable/ic_arrow_down"
|
||||
tools:ignore="ContentDescription" />
|
||||
</LinearLayout>
|
||||
</FrameLayout>
|
||||
|
||||
<com.google.android.material.progressindicator.LinearProgressIndicator
|
||||
android:id="@+id/sync_state"
|
||||
android:visibility="gone"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
</LinearLayout>
|
||||
|
||||
</FrameLayout>
|
||||
</LinearLayout>
|
||||
@@ -1,14 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Button xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/video_button"
|
||||
style="?materialIconButtonFilledTonalStyle"
|
||||
android:text="@string/label_open_video"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="150dp"
|
||||
android:paddingHorizontal="24dp"
|
||||
app:icon="@drawable/ic_video"
|
||||
app:iconGravity="textTop"
|
||||
app:iconPadding="6dp"
|
||||
app:iconSize="24dp" />
|
||||
|
||||
15
app/src/main/res/menu/navigation_menu_main.xml
Normal file
15
app/src/main/res/menu/navigation_menu_main.xml
Normal file
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item
|
||||
android:id="@+id/exploreTab"
|
||||
android:icon="@drawable/ic_public"
|
||||
android:title="@string/explore" />
|
||||
<item
|
||||
android:id="@+id/latestTab"
|
||||
android:icon="@drawable/ic_new_releases"
|
||||
android:title="@string/latest" />
|
||||
<item
|
||||
android:id="@+id/installedTab"
|
||||
android:icon="@drawable/ic_launch"
|
||||
android:title="@string/installed" />
|
||||
</menu>
|
||||
BIN
app/src/main/res/mipmap-hdpi/ic_launcher_round.png
Normal file
BIN
app/src/main/res/mipmap-hdpi/ic_launcher_round.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.5 KiB |
3
build-logic/gradle.properties
Normal file
3
build-logic/gradle.properties
Normal file
@@ -0,0 +1,3 @@
|
||||
org.gradle.parallel=true
|
||||
org.gradle.caching=true
|
||||
org.gradle.configureondemand=true
|
||||
15
build-logic/settings.gradle.kts
Normal file
15
build-logic/settings.gradle.kts
Normal file
@@ -0,0 +1,15 @@
|
||||
dependencyResolutionManagement {
|
||||
repositories {
|
||||
gradlePluginPortal()
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
versionCatalogs {
|
||||
create("libs") {
|
||||
from(files("../gradle/libs.versions.toml"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.name = "build-logic"
|
||||
include(":structure")
|
||||
62
build-logic/structure/build.gradle.kts
Normal file
62
build-logic/structure/build.gradle.kts
Normal file
@@ -0,0 +1,62 @@
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
|
||||
plugins {
|
||||
`kotlin-dsl`
|
||||
}
|
||||
|
||||
group = "buildlogic"
|
||||
|
||||
java {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
jvmTarget = JvmTarget.JVM_17
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly(libs.android.gradlePlugin)
|
||||
compileOnly(libs.kotlin.gradlePlugin)
|
||||
compileOnly(libs.kotlin.ktlint)
|
||||
compileOnly(libs.ksp.gradlePlugin)
|
||||
}
|
||||
|
||||
gradlePlugin {
|
||||
plugins {
|
||||
register("lintPlugin") {
|
||||
id = "looker.lint"
|
||||
implementationClass = "AndroidLintPlugin"
|
||||
}
|
||||
register("serializationPlugin") {
|
||||
id = "looker.serialization"
|
||||
implementationClass = "AndroidSerializationPlugin"
|
||||
}
|
||||
register("hiltPlugin") {
|
||||
id = "looker.hilt"
|
||||
implementationClass = "AndroidHiltPlugin"
|
||||
}
|
||||
register("hiltWorkPlugin") {
|
||||
id = "looker.hilt.work"
|
||||
implementationClass = "AndroidHiltWorkerPlugin"
|
||||
}
|
||||
register("roomPlugin") {
|
||||
id = "looker.room"
|
||||
implementationClass = "AndroidRoomPlugin"
|
||||
}
|
||||
register("androidApplicationPlugin") {
|
||||
id = "looker.android.application"
|
||||
implementationClass = "AndroidApplicationPlugin"
|
||||
}
|
||||
register("androidLibraryPlugin") {
|
||||
id = "looker.android.library"
|
||||
implementationClass = "AndroidLibraryPlugin"
|
||||
}
|
||||
register("jvmLibraryPlugin") {
|
||||
id = "looker.jvm.library"
|
||||
implementationClass = "JvmLibraryPlugin"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import com.android.build.api.dsl.ApplicationExtension
|
||||
import com.looker.droidify.configureKotlinAndroid
|
||||
import com.looker.droidify.kotlin2
|
||||
import com.looker.droidify.libs
|
||||
import org.gradle.api.Plugin
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.kotlin.dsl.configure
|
||||
import org.gradle.kotlin.dsl.dependencies
|
||||
|
||||
class AndroidApplicationPlugin : Plugin<Project> {
|
||||
override fun apply(target: Project) {
|
||||
with(target) {
|
||||
with(pluginManager) {
|
||||
apply("com.android.application")
|
||||
apply("org.jetbrains.kotlin.android")
|
||||
}
|
||||
|
||||
extensions.configure<ApplicationExtension> {
|
||||
configureKotlinAndroid(this)
|
||||
buildToolsVersion = DefaultConfig.buildTools
|
||||
defaultConfig {
|
||||
targetSdk = DefaultConfig.compileSdk
|
||||
applicationId = DefaultConfig.appId
|
||||
versionCode = DefaultConfig.versionCode
|
||||
versionName = DefaultConfig.versionName
|
||||
}
|
||||
}
|
||||
dependencies {
|
||||
add("implementation", kotlin2("stdlib", libs))
|
||||
add("implementation", kotlin2("reflect", libs))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
26
build-logic/structure/src/main/kotlin/AndroidHiltPlugin.kt
Normal file
26
build-logic/structure/src/main/kotlin/AndroidHiltPlugin.kt
Normal file
@@ -0,0 +1,26 @@
|
||||
import com.android.build.gradle.api.AndroidBasePlugin
|
||||
import com.looker.droidify.getLibrary
|
||||
import com.looker.droidify.libs
|
||||
import org.gradle.api.Plugin
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.kotlin.dsl.dependencies
|
||||
|
||||
class AndroidHiltPlugin : Plugin<Project> {
|
||||
override fun apply(target: Project) {
|
||||
with(target) {
|
||||
pluginManager.apply("com.google.devtools.ksp")
|
||||
dependencies {
|
||||
add("ksp", libs.getLibrary("hilt.compiler"))
|
||||
add("implementation", libs.getLibrary("hilt.core"))
|
||||
}
|
||||
|
||||
/** Add support for Android modules, based on [AndroidBasePlugin] */
|
||||
pluginManager.withPlugin("com.android.base") {
|
||||
pluginManager.apply("dagger.hilt.android.plugin")
|
||||
dependencies {
|
||||
add("implementation", libs.getLibrary("hilt.android"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import com.looker.droidify.getLibrary
|
||||
import com.looker.droidify.libs
|
||||
import org.gradle.api.Plugin
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.kotlin.dsl.dependencies
|
||||
|
||||
class AndroidHiltWorkerPlugin : Plugin<Project> {
|
||||
override fun apply(target: Project) {
|
||||
with(target) {
|
||||
with(pluginManager) {
|
||||
apply("looker.hilt")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
add("implementation", libs.getLibrary("androidx.work.ktx"))
|
||||
add("implementation", libs.getLibrary("hilt.ext.work"))
|
||||
add("ksp", libs.getLibrary("hilt.ext.compiler"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import com.android.build.api.variant.LibraryAndroidComponentsExtension
|
||||
import com.android.build.gradle.LibraryExtension
|
||||
import com.looker.droidify.configureKotlinAndroid
|
||||
import com.looker.droidify.kotlin2
|
||||
import com.looker.droidify.libs
|
||||
import org.gradle.api.Plugin
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.kotlin.dsl.configure
|
||||
import org.gradle.kotlin.dsl.dependencies
|
||||
|
||||
class AndroidLibraryPlugin : Plugin<Project> {
|
||||
override fun apply(target: Project) {
|
||||
with(target) {
|
||||
with(pluginManager) {
|
||||
apply("com.android.library")
|
||||
apply("org.jetbrains.kotlin.android")
|
||||
}
|
||||
|
||||
extensions.configure<LibraryExtension> {
|
||||
configureKotlinAndroid(this)
|
||||
defaultConfig.targetSdk = DefaultConfig.compileSdk
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = true
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"${rootDir.path}/app/proguard.pro"
|
||||
)
|
||||
}
|
||||
create("alpha") {
|
||||
initWith(getByName("debug"))
|
||||
isMinifyEnabled = false
|
||||
}
|
||||
}
|
||||
}
|
||||
extensions.configure<LibraryAndroidComponentsExtension> {
|
||||
beforeVariants {
|
||||
it.enableAndroidTest = it.enableAndroidTest
|
||||
&& project.projectDir.resolve("src/androidTest").exists()
|
||||
}
|
||||
}
|
||||
dependencies {
|
||||
add("implementation", kotlin2("stdlib", libs))
|
||||
add("implementation", kotlin2("reflect", libs))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
27
build-logic/structure/src/main/kotlin/AndroidLintPlugin.kt
Normal file
27
build-logic/structure/src/main/kotlin/AndroidLintPlugin.kt
Normal file
@@ -0,0 +1,27 @@
|
||||
import org.gradle.api.Plugin
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.kotlin.dsl.configure
|
||||
import org.jlleitschuh.gradle.ktlint.KtlintExtension
|
||||
import org.jlleitschuh.gradle.ktlint.reporter.ReporterType
|
||||
|
||||
class AndroidLintPlugin : Plugin<Project> {
|
||||
override fun apply(target: Project) {
|
||||
with(target) {
|
||||
with(pluginManager) {
|
||||
apply("org.jlleitschuh.gradle.ktlint")
|
||||
}
|
||||
|
||||
extensions.configure<KtlintExtension> {
|
||||
android.set(true)
|
||||
ignoreFailures.set(true)
|
||||
debug.set(true)
|
||||
reporters {
|
||||
reporter(ReporterType.HTML)
|
||||
}
|
||||
filter {
|
||||
exclude("**/generated/**")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
46
build-logic/structure/src/main/kotlin/AndroidRoomPlugin.kt
Normal file
46
build-logic/structure/src/main/kotlin/AndroidRoomPlugin.kt
Normal file
@@ -0,0 +1,46 @@
|
||||
import com.google.devtools.ksp.gradle.KspExtension
|
||||
import com.looker.droidify.getLibrary
|
||||
import com.looker.droidify.libs
|
||||
import org.gradle.api.Plugin
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.api.tasks.InputDirectory
|
||||
import org.gradle.api.tasks.PathSensitive
|
||||
import org.gradle.api.tasks.PathSensitivity
|
||||
import org.gradle.kotlin.dsl.configure
|
||||
import org.gradle.kotlin.dsl.dependencies
|
||||
import org.gradle.process.CommandLineArgumentProvider
|
||||
import java.io.File
|
||||
|
||||
class AndroidRoomPlugin : Plugin<Project> {
|
||||
|
||||
override fun apply(target: Project) {
|
||||
with(target) {
|
||||
pluginManager.apply("com.google.devtools.ksp")
|
||||
|
||||
extensions.configure<KspExtension> {
|
||||
// The schemas directory contains a schema file for each version of the Room database.
|
||||
// This is required to enable Room auto migrations.
|
||||
// See https://developer.android.com/reference/kotlin/androidx/room/AutoMigration.
|
||||
arg(RoomSchemaArgProvider(File(projectDir, "schemas")))
|
||||
}
|
||||
|
||||
dependencies {
|
||||
add("implementation", libs.getLibrary("room.ktx"))
|
||||
add("implementation", libs.getLibrary("room.runtime"))
|
||||
add("ksp", libs.getLibrary("room.compiler"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* https://issuetracker.google.com/issues/132245929
|
||||
* [Export schemas](https://developer.android.com/training/data-storage/room/migrating-db-versions#export-schemas)
|
||||
*/
|
||||
class RoomSchemaArgProvider(
|
||||
@get:InputDirectory
|
||||
@get:PathSensitive(PathSensitivity.RELATIVE)
|
||||
val schemaDir: File,
|
||||
) : CommandLineArgumentProvider {
|
||||
override fun asArguments() = listOf("room.schemaLocation=${schemaDir.path}")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import com.looker.droidify.getLibrary
|
||||
import com.looker.droidify.libs
|
||||
import org.gradle.api.Plugin
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.kotlin.dsl.dependencies
|
||||
|
||||
class AndroidSerializationPlugin : Plugin<Project> {
|
||||
override fun apply(target: Project) {
|
||||
with(target) {
|
||||
with(pluginManager) {
|
||||
apply("org.jetbrains.kotlin.plugin.serialization")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
add("implementation", libs.getLibrary("kotlinx.serialization.json"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user