v0.6.4
This commit is contained in:
@@ -6,9 +6,8 @@ insert_final_newline = true
|
|||||||
trim_trailing_whitespace = true
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
[*.{kt,kts}]
|
[*.{kt,kts}]
|
||||||
|
ktlint_code_style = android_studio
|
||||||
indent_size = 4
|
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 = 999
|
||||||
ij_kotlin_name_count_to_use_star_import_for_members = 999
|
ij_kotlin_name_count_to_use_star_import_for_members = 999
|
||||||
|
|
||||||
|
|||||||
3
.github/workflows/build_debug.yml
vendored
3
.github/workflows/build_debug.yml
vendored
@@ -48,6 +48,9 @@ jobs:
|
|||||||
- name: Grant execution permission to Gradle Wrapper
|
- name: Grant execution permission to Gradle Wrapper
|
||||||
run: chmod +x gradlew
|
run: chmod +x gradlew
|
||||||
|
|
||||||
|
- name: Format Code
|
||||||
|
run: ./gradlew ktlintFormat
|
||||||
|
|
||||||
- name: Build Debug APK
|
- name: Build Debug APK
|
||||||
run: ./gradlew assembleDebug
|
run: ./gradlew assembleDebug
|
||||||
|
|
||||||
|
|||||||
24
.github/workflows/release_build.yml
vendored
24
.github/workflows/release_build.yml
vendored
@@ -56,34 +56,12 @@ jobs:
|
|||||||
keyStorePassword: ${{ secrets.KEYSTORE_PASS }}
|
keyStorePassword: ${{ secrets.KEYSTORE_PASS }}
|
||||||
keyPassword: ${{ secrets.KEYSTORE_PASS }}
|
keyPassword: ${{ secrets.KEYSTORE_PASS }}
|
||||||
env:
|
env:
|
||||||
BUILD_TOOLS_VERSION: "35.0.0"
|
BUILD_TOOLS_VERSION: "34.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
|
|
||||||
|
|
||||||
- uses: softprops/action-gh-release@v2
|
- uses: softprops/action-gh-release@v2
|
||||||
name: Create Release
|
name: Create Release
|
||||||
id: publish_release
|
id: publish_release
|
||||||
with:
|
with:
|
||||||
body: ${{ steps.read_changelog.outputs.changelog }}
|
|
||||||
tag_name: ${{ github.ref }}
|
|
||||||
name: Release ${{ github.ref }}
|
|
||||||
files: ${{steps.sign_app.outputs.signedReleaseFile}}
|
files: ${{steps.sign_app.outputs.signedReleaseFile}}
|
||||||
draft: true
|
draft: true
|
||||||
prerelease: false
|
prerelease: false
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
[](https://github.com/Iamlooker/Droid-ify/releases/)
|
[](https://github.com/Iamlooker/Droid-ify/releases/)
|
||||||
[](https://github.com/Iamlooker/Droid-ify/releases/latest)
|
[](https://github.com/Iamlooker/Droid-ify/releases/latest)
|
||||||
[](https://f-droid.org/packages/com.looker.droidify)
|
[](https://f-droid.org/packages/com.looker.droidify)
|
||||||
</div>
|
|
||||||
<div align="left">
|
<div align="left">
|
||||||
|
|
||||||
## Features
|
## 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%" />
|
<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
|
## Building and Installing
|
||||||
|
|
||||||
1. **Install Android Studio**:
|
1. **Install Android Studio**:
|
||||||
- Download and install [Android Studio](https://developer.android.com/studio) on your computer
|
- Download and install [Android Studio](https://developer.android.com/studio) on your computer if you haven't already.
|
||||||
if you haven't already.
|
|
||||||
|
|
||||||
2. **Clone the Repository**:
|
2. **Clone the Repository**:
|
||||||
- Open Android Studio and select "Project from Version Control."
|
- Open Android Studio and select "Project from Version Control."
|
||||||
@@ -50,7 +48,6 @@
|
|||||||
- Your PR will undergo review
|
- Your PR will undergo review
|
||||||
|
|
||||||
## Translations
|
## Translations
|
||||||
|
|
||||||
[](https://hosted.weblate.org/engage/droidify/?utm_source=widget)
|
[](https://hosted.weblate.org/engage/droidify/?utm_source=widget)
|
||||||
|
|
||||||
## License
|
## License
|
||||||
@@ -70,5 +67,3 @@ GNU General Public License for more details.
|
|||||||
You should have received a copy of the GNU General Public License
|
You should have received a copy of the GNU General Public License
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
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 {
|
plugins {
|
||||||
alias(libs.plugins.android.application)
|
alias(libs.plugins.looker.android.application)
|
||||||
alias(libs.plugins.kotlin.android)
|
alias(libs.plugins.looker.hilt.work)
|
||||||
alias(libs.plugins.ktlint)
|
alias(libs.plugins.looker.lint)
|
||||||
alias(libs.plugins.ksp)
|
|
||||||
alias(libs.plugins.hilt)
|
|
||||||
alias(libs.plugins.kotlin.serialization)
|
|
||||||
alias(libs.plugins.kotlin.parcelize)
|
alias(libs.plugins.kotlin.parcelize)
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
val latestVersionName = "0.6.5"
|
|
||||||
namespace = "com.looker.droidify"
|
namespace = "com.looker.droidify"
|
||||||
buildToolsVersion = "35.0.0"
|
|
||||||
compileSdk = 35
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdk = 23
|
vectorDrawables.useSupportLibrary = true
|
||||||
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"
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
androidResources {
|
androidResources {
|
||||||
@@ -54,11 +23,11 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
debug {
|
getByName("debug") {
|
||||||
applicationIdSuffix = ".debug"
|
applicationIdSuffix = ".debug"
|
||||||
resValue("string", "application_name", "Droid-ify-Debug")
|
resValue("string", "application_name", "Droid-ify-Debug")
|
||||||
}
|
}
|
||||||
release {
|
getByName("release") {
|
||||||
isMinifyEnabled = true
|
isMinifyEnabled = true
|
||||||
isShrinkResources = true
|
isShrinkResources = true
|
||||||
resValue("string", "application_name", "Droid-ify")
|
resValue("string", "application_name", "Droid-ify")
|
||||||
@@ -82,7 +51,7 @@ android {
|
|||||||
buildConfigField(
|
buildConfigField(
|
||||||
type = "String",
|
type = "String",
|
||||||
name = "VERSION_NAME",
|
name = "VERSION_NAME",
|
||||||
value = "\"v$latestVersionName\""
|
value = "\"v${DefaultConfig.versionName}\""
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -95,8 +64,7 @@ android {
|
|||||||
"/META-INF/**.kotlin_module",
|
"/META-INF/**.kotlin_module",
|
||||||
"/META-INF/**.pro",
|
"/META-INF/**.pro",
|
||||||
"/META-INF/**.version",
|
"/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
|
viewBinding = true
|
||||||
buildConfig = 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 {
|
dependencies {
|
||||||
coreLibraryDesugaring(libs.desugaring)
|
|
||||||
|
|
||||||
implementation(libs.material)
|
modules(
|
||||||
implementation(libs.core.ktx)
|
Modules.coreDomain,
|
||||||
implementation(libs.activity)
|
// Modules.coreData,
|
||||||
implementation(libs.appcompat)
|
Modules.coreCommon,
|
||||||
implementation(libs.fragment.ktx)
|
Modules.coreNetwork,
|
||||||
implementation(libs.lifecycle.viewModel)
|
Modules.coreDatastore,
|
||||||
implementation(libs.recyclerview)
|
Modules.coreDI,
|
||||||
implementation(libs.sqlite.ktx)
|
Modules.installer,
|
||||||
|
)
|
||||||
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)
|
|
||||||
|
|
||||||
|
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.jackson.core)
|
||||||
implementation(libs.serialization)
|
implementation(libs.image.viewer)
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
// debugImplementation(libs.leakcanary)
|
// 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" />
|
android:required="false" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".Droidify"
|
android:name=".MainApplication"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:banner="@drawable/tv_banner"
|
android:banner="@drawable/tv_banner"
|
||||||
android:enableOnBackInvokedCallback="true"
|
android:enableOnBackInvokedCallback="true"
|
||||||
@@ -38,7 +38,7 @@
|
|||||||
tools:ignore="UnusedAttribute">
|
tools:ignore="UnusedAttribute">
|
||||||
|
|
||||||
<receiver
|
<receiver
|
||||||
android:name=".Droidify$BootReceiver"
|
android:name=".MainApplication$BootReceiver"
|
||||||
android:exported="true">
|
android:exported="true">
|
||||||
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
@@ -50,6 +50,7 @@
|
|||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
|
android:launchMode="singleTask"
|
||||||
android:windowSoftInputMode="adjustResize">
|
android:windowSoftInputMode="adjustResize">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.SHOW_APP_INFO" />
|
<action android:name="android.intent.action.SHOW_APP_INFO" />
|
||||||
@@ -147,7 +148,7 @@
|
|||||||
android:foregroundServiceType="dataSync" />
|
android:foregroundServiceType="dataSync" />
|
||||||
|
|
||||||
<receiver
|
<receiver
|
||||||
android:name=".installer.installers.session.SessionInstallerReceiver"
|
android:name="com.looker.installer.installers.session.SessionInstallerReceiver"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
<service
|
<service
|
||||||
@@ -173,7 +174,7 @@
|
|||||||
android:permission="android.permission.INTERACT_ACROSS_USERS_FULL" />
|
android:permission="android.permission.INTERACT_ACROSS_USERS_FULL" />
|
||||||
|
|
||||||
<provider
|
<provider
|
||||||
android:name=".utility.common.cache.Cache$Provider"
|
android:name="com.looker.core.common.cache.Cache$Provider"
|
||||||
android:authorities="${applicationId}.provider.cache"
|
android:authorities="${applicationId}.provider.cache"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:grantUriPermissions="true" />
|
android:grantUriPermissions="true" />
|
||||||
|
|||||||
@@ -1,307 +1,29 @@
|
|||||||
package com.looker.droidify
|
package com.looker.droidify
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Build
|
import com.looker.core.common.getInstallPackageName
|
||||||
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 dagger.hilt.android.AndroidEntryPoint
|
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
|
@AndroidEntryPoint
|
||||||
class MainActivity : AppCompatActivity() {
|
class MainActivity : ScreenActivity() {
|
||||||
companion object {
|
companion object {
|
||||||
private const val STATE_FRAGMENT_STACK = "fragmentStack"
|
|
||||||
const val ACTION_UPDATES = "${BuildConfig.APPLICATION_ID}.intent.action.UPDATES"
|
const val ACTION_UPDATES = "${BuildConfig.APPLICATION_ID}.intent.action.UPDATES"
|
||||||
const val ACTION_INSTALL = "${BuildConfig.APPLICATION_ID}.intent.action.INSTALL"
|
const val ACTION_INSTALL = "${BuildConfig.APPLICATION_ID}.intent.action.INSTALL"
|
||||||
const val EXTRA_CACHE_FILE_NAME =
|
const val EXTRA_CACHE_FILE_NAME =
|
||||||
"${BuildConfig.APPLICATION_ID}.intent.extra.CACHE_FILE_NAME"
|
"${BuildConfig.APPLICATION_ID}.intent.extra.CACHE_FILE_NAME"
|
||||||
}
|
}
|
||||||
|
|
||||||
private val notificationPermission =
|
override fun handleIntent(intent: Intent?) {
|
||||||
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?) {
|
|
||||||
when (intent?.action) {
|
when (intent?.action) {
|
||||||
ACTION_UPDATES -> {
|
ACTION_UPDATES -> handleSpecialIntent(SpecialIntent.Updates)
|
||||||
if (currentFragment !is TabsFragment) {
|
ACTION_INSTALL -> handleSpecialIntent(
|
||||||
fragmentStack.clear()
|
SpecialIntent.Install(
|
||||||
replaceFragment(TabsFragment(), true)
|
intent.getInstallPackageName,
|
||||||
}
|
intent.getStringExtra(EXTRA_CACHE_FILE_NAME)
|
||||||
val tabsFragment = currentFragment as TabsFragment
|
)
|
||||||
tabsFragment.selectUpdates()
|
)
|
||||||
backHandler()
|
|
||||||
}
|
|
||||||
|
|
||||||
ACTION_INSTALL -> {
|
else -> super.handleIntent(intent)
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.IntentFilter
|
import android.content.IntentFilter
|
||||||
import android.os.Build
|
|
||||||
import android.os.StrictMode
|
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
import androidx.hilt.work.HiltWorkerFactory
|
import androidx.hilt.work.HiltWorkerFactory
|
||||||
import androidx.work.Configuration
|
import androidx.work.Configuration
|
||||||
import androidx.work.NetworkType
|
import androidx.work.NetworkType
|
||||||
import coil3.ImageLoader
|
import coil.ImageLoader
|
||||||
import coil3.PlatformContext
|
import coil.ImageLoaderFactory
|
||||||
import coil3.SingletonImageLoader
|
import coil.disk.DiskCache
|
||||||
import coil3.asImage
|
import coil.memory.MemoryCache
|
||||||
import coil3.disk.DiskCache
|
import com.looker.core.common.Constants
|
||||||
import coil3.disk.directory
|
import com.looker.core.common.cache.Cache
|
||||||
import coil3.memory.MemoryCache
|
import com.looker.core.common.extension.getInstalledPackagesCompat
|
||||||
import coil3.request.crossfade
|
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.content.ProductPreferences
|
||||||
import com.looker.droidify.database.Database
|
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.index.RepositoryUpdater
|
||||||
import com.looker.droidify.installer.InstallManager
|
|
||||||
import com.looker.droidify.network.Downloader
|
|
||||||
import com.looker.droidify.receivers.InstalledAppReceiver
|
import com.looker.droidify.receivers.InstalledAppReceiver
|
||||||
import com.looker.droidify.service.Connection
|
import com.looker.droidify.service.Connection
|
||||||
import com.looker.droidify.service.SyncService
|
import com.looker.droidify.service.SyncService
|
||||||
import com.looker.droidify.sync.SyncPreference
|
import com.looker.droidify.sync.SyncPreference
|
||||||
import com.looker.droidify.sync.toJobNetworkType
|
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.utility.extension.toInstalledItem
|
||||||
import com.looker.droidify.work.CleanUpWorker
|
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 dagger.hilt.android.HiltAndroidApp
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -58,9 +52,10 @@ import java.net.Proxy
|
|||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlin.time.Duration.Companion.INFINITE
|
import kotlin.time.Duration.Companion.INFINITE
|
||||||
import kotlin.time.Duration.Companion.hours
|
import kotlin.time.Duration.Companion.hours
|
||||||
|
import com.looker.core.common.R as CommonR
|
||||||
|
|
||||||
@HiltAndroidApp
|
@HiltAndroidApp
|
||||||
class Droidify : Application(), SingletonImageLoader.Factory, Configuration.Provider {
|
class MainApplication : Application(), ImageLoaderFactory, Configuration.Provider {
|
||||||
|
|
||||||
private val parentJob = SupervisorJob()
|
private val parentJob = SupervisorJob()
|
||||||
private val appScope = CoroutineScope(Dispatchers.Default + parentJob)
|
private val appScope = CoroutineScope(Dispatchers.Default + parentJob)
|
||||||
@@ -74,21 +69,25 @@ class Droidify : Application(), SingletonImageLoader.Factory, Configuration.Prov
|
|||||||
@Inject
|
@Inject
|
||||||
lateinit var downloader: Downloader
|
lateinit var downloader: Downloader
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var shizukuPermissionHandler: ShizukuPermissionHandler
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var rootPermissionHandler: RootPermissionHandler
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var workerFactory: HiltWorkerFactory
|
lateinit var workerFactory: HiltWorkerFactory
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
|
||||||
if (BuildConfig.DEBUG && SdkCheck.isOreo) strictThreadPolicy()
|
|
||||||
|
|
||||||
val databaseUpdated = Database.init(this)
|
val databaseUpdated = Database.init(this)
|
||||||
ProductPreferences.init(this, appScope)
|
ProductPreferences.init(this, appScope)
|
||||||
RepositoryUpdater.init(appScope, downloader)
|
RepositoryUpdater.init(appScope, downloader)
|
||||||
listenApplications()
|
listenApplications()
|
||||||
checkLanguage()
|
checkLanguage()
|
||||||
updatePreference()
|
updatePreference()
|
||||||
appScope.launch { installer() }
|
setupInstaller()
|
||||||
|
|
||||||
if (databaseUpdated) forceSyncAll()
|
if (databaseUpdated) forceSyncAll()
|
||||||
}
|
}
|
||||||
@@ -99,8 +98,38 @@ class Droidify : Application(), SingletonImageLoader.Factory, Configuration.Prov
|
|||||||
installer.close()
|
installer.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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() {
|
private fun listenApplications() {
|
||||||
appScope.launch(Dispatchers.Default) {
|
|
||||||
registerReceiver(
|
registerReceiver(
|
||||||
InstalledAppReceiver(packageManager),
|
InstalledAppReceiver(packageManager),
|
||||||
IntentFilter().apply {
|
IntentFilter().apply {
|
||||||
@@ -112,10 +141,9 @@ class Droidify : Application(), SingletonImageLoader.Factory, Configuration.Prov
|
|||||||
val installedItems =
|
val installedItems =
|
||||||
packageManager.getInstalledPackagesCompat()
|
packageManager.getInstalledPackagesCompat()
|
||||||
?.map { it.toInstalledItem() }
|
?.map { it.toInstalledItem() }
|
||||||
?: return@launch
|
?: return
|
||||||
Database.InstalledAdapter.putAll(installedItems)
|
Database.InstalledAdapter.putAll(installedItems)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private fun checkLanguage() {
|
private fun checkLanguage() {
|
||||||
appScope.launch {
|
appScope.launch {
|
||||||
@@ -223,14 +251,9 @@ class Droidify : Application(), SingletonImageLoader.Factory, Configuration.Prov
|
|||||||
override fun onReceive(context: Context, intent: Intent) = Unit
|
override fun onReceive(context: Context, intent: Intent) = Unit
|
||||||
}
|
}
|
||||||
|
|
||||||
override val workManagerConfiguration: Configuration
|
override fun newImageLoader(): ImageLoader {
|
||||||
get() = Configuration.Builder()
|
val memoryCache = MemoryCache.Builder(this)
|
||||||
.setWorkerFactory(workerFactory)
|
.maxSizePercent(0.25)
|
||||||
.build()
|
|
||||||
|
|
||||||
override fun newImageLoader(context: PlatformContext): ImageLoader {
|
|
||||||
val memoryCache = MemoryCache.Builder()
|
|
||||||
.maxSizePercent(context, 0.25)
|
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
val diskCache = DiskCache.Builder()
|
val diskCache = DiskCache.Builder()
|
||||||
@@ -241,27 +264,13 @@ class Droidify : Application(), SingletonImageLoader.Factory, Configuration.Prov
|
|||||||
return ImageLoader.Builder(this)
|
return ImageLoader.Builder(this)
|
||||||
.memoryCache(memoryCache)
|
.memoryCache(memoryCache)
|
||||||
.diskCache(diskCache)
|
.diskCache(diskCache)
|
||||||
.error(getDrawableCompat(R.drawable.ic_cannot_load).asImage())
|
.error(CommonR.drawable.ic_cannot_load)
|
||||||
.crossfade(350)
|
.crossfade(350)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.O)
|
override val workManagerConfiguration: Configuration
|
||||||
fun strictThreadPolicy() {
|
get() = Configuration.Builder()
|
||||||
StrictMode.setThreadPolicy(
|
.setWorkerFactory(workerFactory)
|
||||||
StrictMode.ThreadPolicy.Builder()
|
|
||||||
.detectDiskReads()
|
|
||||||
.detectDiskWrites()
|
|
||||||
.detectNetwork()
|
|
||||||
.detectUnbufferedIo()
|
|
||||||
.penaltyLog()
|
|
||||||
.build()
|
.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.Context
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import com.looker.droidify.utility.common.extension.Json
|
import com.looker.core.common.extension.Json
|
||||||
import com.looker.droidify.utility.common.extension.parseDictionary
|
import com.looker.core.common.extension.parseDictionary
|
||||||
import com.looker.droidify.utility.common.extension.writeDictionary
|
import com.looker.core.common.extension.writeDictionary
|
||||||
import com.looker.droidify.model.ProductPreference
|
import com.looker.droidify.model.ProductPreference
|
||||||
import com.looker.droidify.database.Database
|
import com.looker.droidify.database.Database
|
||||||
import com.looker.droidify.utility.serialization.productPreference
|
import com.looker.droidify.utility.serialization.productPreference
|
||||||
|
|||||||
@@ -5,36 +5,35 @@ import android.os.Bundle
|
|||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.loader.app.LoaderManager
|
import androidx.loader.app.LoaderManager
|
||||||
import androidx.loader.content.Loader
|
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
|
import com.looker.droidify.model.ProductItem
|
||||||
|
|
||||||
class CursorOwner : Fragment(), LoaderManager.LoaderCallbacks<Cursor> {
|
class CursorOwner : Fragment(), LoaderManager.LoaderCallbacks<Cursor> {
|
||||||
sealed class Request {
|
sealed class Request {
|
||||||
internal abstract val id: Int
|
internal abstract val id: Int
|
||||||
|
|
||||||
class Available(
|
data class ProductsAvailable(
|
||||||
val searchQuery: String,
|
val searchQuery: String,
|
||||||
val section: ProductItem.Section,
|
val section: ProductItem.Section,
|
||||||
val order: SortOrder,
|
val order: SortOrder
|
||||||
) : Request() {
|
) : Request() {
|
||||||
override val id: Int
|
override val id: Int
|
||||||
get() = 1
|
get() = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
class Installed(
|
data class ProductsInstalled(
|
||||||
val searchQuery: String,
|
val searchQuery: String,
|
||||||
val section: ProductItem.Section,
|
val section: ProductItem.Section,
|
||||||
val order: SortOrder,
|
val order: SortOrder
|
||||||
) : Request() {
|
) : Request() {
|
||||||
override val id: Int
|
override val id: Int
|
||||||
get() = 2
|
get() = 2
|
||||||
}
|
}
|
||||||
|
|
||||||
class Updates(
|
data class ProductsUpdates(
|
||||||
val searchQuery: String,
|
val searchQuery: String,
|
||||||
val section: ProductItem.Section,
|
val section: ProductItem.Section,
|
||||||
val order: SortOrder,
|
val order: SortOrder
|
||||||
val skipSignatureCheck: Boolean,
|
|
||||||
) : Request() {
|
) : Request() {
|
||||||
override val id: Int
|
override val id: Int
|
||||||
get() = 3
|
get() = 3
|
||||||
@@ -53,7 +52,7 @@ class CursorOwner : Fragment(), LoaderManager.LoaderCallbacks<Cursor> {
|
|||||||
private data class ActiveRequest(
|
private data class ActiveRequest(
|
||||||
val request: Request,
|
val request: Request,
|
||||||
val callback: Callback?,
|
val callback: Callback?,
|
||||||
val cursor: Cursor?,
|
val cursor: Cursor?
|
||||||
)
|
)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@@ -94,7 +93,7 @@ class CursorOwner : Fragment(), LoaderManager.LoaderCallbacks<Cursor> {
|
|||||||
val request = activeRequests[id]!!.request
|
val request = activeRequests[id]!!.request
|
||||||
return QueryLoader(requireContext()) {
|
return QueryLoader(requireContext()) {
|
||||||
when (request) {
|
when (request) {
|
||||||
is Request.Available ->
|
is Request.ProductsAvailable ->
|
||||||
Database.ProductAdapter
|
Database.ProductAdapter
|
||||||
.query(
|
.query(
|
||||||
installed = false,
|
installed = false,
|
||||||
@@ -102,10 +101,10 @@ class CursorOwner : Fragment(), LoaderManager.LoaderCallbacks<Cursor> {
|
|||||||
searchQuery = request.searchQuery,
|
searchQuery = request.searchQuery,
|
||||||
section = request.section,
|
section = request.section,
|
||||||
order = request.order,
|
order = request.order,
|
||||||
signal = it,
|
signal = it
|
||||||
)
|
)
|
||||||
|
|
||||||
is Request.Installed ->
|
is Request.ProductsInstalled ->
|
||||||
Database.ProductAdapter
|
Database.ProductAdapter
|
||||||
.query(
|
.query(
|
||||||
installed = true,
|
installed = true,
|
||||||
@@ -113,10 +112,10 @@ class CursorOwner : Fragment(), LoaderManager.LoaderCallbacks<Cursor> {
|
|||||||
searchQuery = request.searchQuery,
|
searchQuery = request.searchQuery,
|
||||||
section = request.section,
|
section = request.section,
|
||||||
order = request.order,
|
order = request.order,
|
||||||
signal = it,
|
signal = it
|
||||||
)
|
)
|
||||||
|
|
||||||
is Request.Updates ->
|
is Request.ProductsUpdates ->
|
||||||
Database.ProductAdapter
|
Database.ProductAdapter
|
||||||
.query(
|
.query(
|
||||||
installed = true,
|
installed = true,
|
||||||
@@ -124,8 +123,7 @@ class CursorOwner : Fragment(), LoaderManager.LoaderCallbacks<Cursor> {
|
|||||||
searchQuery = request.searchQuery,
|
searchQuery = request.searchQuery,
|
||||||
section = request.section,
|
section = request.section,
|
||||||
order = request.order,
|
order = request.order,
|
||||||
signal = it,
|
signal = it
|
||||||
skipSignatureCheck = request.skipSignatureCheck,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
is Request.Repositories -> Database.RepositoryAdapter.query(it)
|
is Request.Repositories -> Database.RepositoryAdapter.query(it)
|
||||||
|
|||||||
@@ -9,18 +9,18 @@ import android.os.CancellationSignal
|
|||||||
import androidx.core.database.sqlite.transaction
|
import androidx.core.database.sqlite.transaction
|
||||||
import com.fasterxml.jackson.core.JsonGenerator
|
import com.fasterxml.jackson.core.JsonGenerator
|
||||||
import com.fasterxml.jackson.core.JsonParser
|
import com.fasterxml.jackson.core.JsonParser
|
||||||
import com.looker.droidify.BuildConfig
|
import com.looker.core.common.extension.Json
|
||||||
import com.looker.droidify.datastore.model.SortOrder
|
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.InstalledItem
|
||||||
import com.looker.droidify.model.Product
|
import com.looker.droidify.model.Product
|
||||||
import com.looker.droidify.model.ProductItem
|
import com.looker.droidify.model.ProductItem
|
||||||
import com.looker.droidify.model.Repository
|
import com.looker.droidify.model.Repository
|
||||||
import com.looker.droidify.utility.common.extension.Json
|
import com.looker.droidify.BuildConfig
|
||||||
import com.looker.droidify.utility.common.extension.asSequence
|
|
||||||
import com.looker.droidify.utility.common.extension.firstOrNull
|
|
||||||
import com.looker.droidify.utility.common.extension.parseDictionary
|
|
||||||
import com.looker.droidify.utility.common.extension.writeDictionary
|
|
||||||
import com.looker.droidify.utility.common.log
|
|
||||||
import com.looker.droidify.utility.serialization.product
|
import com.looker.droidify.utility.serialization.product
|
||||||
import com.looker.droidify.utility.serialization.productItem
|
import com.looker.droidify.utility.serialization.productItem
|
||||||
import com.looker.droidify.utility.serialization.repository
|
import com.looker.droidify.utility.serialization.repository
|
||||||
@@ -71,20 +71,14 @@ object Database {
|
|||||||
get() = "$databasePrefix$innerName"
|
get() = "$databasePrefix$innerName"
|
||||||
|
|
||||||
fun formatCreateTable(name: String): String {
|
fun formatCreateTable(name: String): String {
|
||||||
return buildString(128) {
|
return "CREATE TABLE $name (${QueryBuilder.trimQuery(createTable)})"
|
||||||
append("CREATE TABLE ")
|
|
||||||
append(name)
|
|
||||||
append(" (")
|
|
||||||
trimAndJoin(createTable)
|
|
||||||
append(")")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val createIndexPairFormatted: Pair<String, String>?
|
val createIndexPairFormatted: Pair<String, String>?
|
||||||
get() = createIndex?.let {
|
get() = createIndex?.let {
|
||||||
Pair(
|
Pair(
|
||||||
"CREATE INDEX ${innerName}_index ON $innerName ($it)",
|
"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
|
var created = false
|
||||||
private set
|
private set
|
||||||
var updated = false
|
var updated = false
|
||||||
@@ -220,7 +214,7 @@ object Database {
|
|||||||
Schema.Product,
|
Schema.Product,
|
||||||
Schema.Category,
|
Schema.Category,
|
||||||
Schema.Installed,
|
Schema.Installed,
|
||||||
Schema.Lock,
|
Schema.Lock
|
||||||
)
|
)
|
||||||
dropOldTables(db, Schema.Repository, Schema.Product, Schema.Category)
|
dropOldTables(db, Schema.Repository, Schema.Product, Schema.Category)
|
||||||
this.created = this.created || create
|
this.created = this.created || create
|
||||||
@@ -233,7 +227,7 @@ object Database {
|
|||||||
val sql = db.query(
|
val sql = db.query(
|
||||||
"${table.databasePrefix}sqlite_master",
|
"${table.databasePrefix}sqlite_master",
|
||||||
columns = arrayOf("sql"),
|
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()
|
).use { it.firstOrNull()?.getString(0) }.orEmpty()
|
||||||
table.formatCreateTable(table.innerName) != sql
|
table.formatCreateTable(table.innerName) != sql
|
||||||
}
|
}
|
||||||
@@ -267,7 +261,7 @@ object Database {
|
|||||||
val sqls = db.query(
|
val sqls = db.query(
|
||||||
"${table.databasePrefix}sqlite_master",
|
"${table.databasePrefix}sqlite_master",
|
||||||
columns = arrayOf("name", "sql"),
|
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 ->
|
.use { cursor ->
|
||||||
cursor.asSequence()
|
cursor.asSequence()
|
||||||
@@ -295,7 +289,7 @@ object Database {
|
|||||||
val tables = db.query(
|
val tables = db.query(
|
||||||
"sqlite_master",
|
"sqlite_master",
|
||||||
columns = arrayOf("name"),
|
columns = arrayOf("name"),
|
||||||
selection = Pair("type = ?", arrayOf("table")),
|
selection = Pair("type = ?", arrayOf("table"))
|
||||||
)
|
)
|
||||||
.use { cursor -> cursor.asSequence().mapNotNull { it.getString(0) }.toList() }
|
.use { cursor -> cursor.asSequence().mapNotNull { it.getString(0) }.toList() }
|
||||||
.filter { !it.startsWith("sqlite_") && !it.startsWith("android_") }
|
.filter { !it.startsWith("sqlite_") && !it.startsWith("android_") }
|
||||||
@@ -351,7 +345,7 @@ object Database {
|
|||||||
private fun SQLiteDatabase.insertOrReplace(
|
private fun SQLiteDatabase.insertOrReplace(
|
||||||
replace: Boolean,
|
replace: Boolean,
|
||||||
table: String,
|
table: String,
|
||||||
contentValues: ContentValues,
|
contentValues: ContentValues
|
||||||
): Long {
|
): Long {
|
||||||
return if (replace) {
|
return if (replace) {
|
||||||
replace(table, null, contentValues)
|
replace(table, null, contentValues)
|
||||||
@@ -359,7 +353,7 @@ object Database {
|
|||||||
insert(
|
insert(
|
||||||
table,
|
table,
|
||||||
null,
|
null,
|
||||||
contentValues,
|
contentValues
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -369,7 +363,7 @@ object Database {
|
|||||||
columns: Array<String>? = null,
|
columns: Array<String>? = null,
|
||||||
selection: Pair<String, Array<String>>? = null,
|
selection: Pair<String, Array<String>>? = null,
|
||||||
orderBy: String? = null,
|
orderBy: String? = null,
|
||||||
signal: CancellationSignal? = null,
|
signal: CancellationSignal? = null
|
||||||
): Cursor {
|
): Cursor {
|
||||||
return query(
|
return query(
|
||||||
false,
|
false,
|
||||||
@@ -381,7 +375,7 @@ object Database {
|
|||||||
null,
|
null,
|
||||||
orderBy,
|
orderBy,
|
||||||
null,
|
null,
|
||||||
signal,
|
signal
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -403,7 +397,7 @@ object Database {
|
|||||||
internal fun putWithoutNotification(
|
internal fun putWithoutNotification(
|
||||||
repository: Repository,
|
repository: Repository,
|
||||||
shouldReplace: Boolean,
|
shouldReplace: Boolean,
|
||||||
database: SQLiteDatabase,
|
database: SQLiteDatabase
|
||||||
): Long {
|
): Long {
|
||||||
return database.insertOrReplace(
|
return database.insertOrReplace(
|
||||||
shouldReplace,
|
shouldReplace,
|
||||||
@@ -415,7 +409,7 @@ object Database {
|
|||||||
put(Schema.Repository.ROW_ENABLED, if (repository.enabled) 1 else 0)
|
put(Schema.Repository.ROW_ENABLED, if (repository.enabled) 1 else 0)
|
||||||
put(Schema.Repository.ROW_DELETED, 0)
|
put(Schema.Repository.ROW_DELETED, 0)
|
||||||
put(Schema.Repository.ROW_DATA, jsonGenerate(repository::serialize))
|
put(Schema.Repository.ROW_DATA, jsonGenerate(repository::serialize))
|
||||||
},
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -448,8 +442,8 @@ object Database {
|
|||||||
Schema.Repository.name,
|
Schema.Repository.name,
|
||||||
selection = Pair(
|
selection = Pair(
|
||||||
"${Schema.Repository.ROW_ID} = ? AND ${Schema.Repository.ROW_DELETED} == 0",
|
"${Schema.Repository.ROW_ID} = ? AND ${Schema.Repository.ROW_DELETED} == 0",
|
||||||
arrayOf(id.toString()),
|
arrayOf(id.toString())
|
||||||
),
|
)
|
||||||
).use { it.firstOrNull()?.let(::transform) }
|
).use { it.firstOrNull()?.let(::transform) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -469,9 +463,9 @@ object Database {
|
|||||||
selection = Pair(
|
selection = Pair(
|
||||||
"${Schema.Repository.ROW_ENABLED} != 0 AND " +
|
"${Schema.Repository.ROW_ENABLED} != 0 AND " +
|
||||||
"${Schema.Repository.ROW_DELETED} == 0",
|
"${Schema.Repository.ROW_DELETED} == 0",
|
||||||
emptyArray(),
|
emptyArray()
|
||||||
),
|
),
|
||||||
signal = null,
|
signal = null
|
||||||
).use { it.asSequence().map(::transform).toList() }
|
).use { it.asSequence().map(::transform).toList() }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -479,7 +473,7 @@ object Database {
|
|||||||
return db.query(
|
return db.query(
|
||||||
Schema.Repository.name,
|
Schema.Repository.name,
|
||||||
selection = Pair("${Schema.Repository.ROW_DELETED} == 0", emptyArray()),
|
selection = Pair("${Schema.Repository.ROW_DELETED} == 0", emptyArray()),
|
||||||
signal = null,
|
signal = null
|
||||||
).use { it.asSequence().map(::transform).toList() }
|
).use { it.asSequence().map(::transform).toList() }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -495,9 +489,9 @@ object Database {
|
|||||||
selection = Pair(
|
selection = Pair(
|
||||||
"${Schema.Repository.ROW_ENABLED} == 0 OR " +
|
"${Schema.Repository.ROW_ENABLED} == 0 OR " +
|
||||||
"${Schema.Repository.ROW_DELETED} != 0",
|
"${Schema.Repository.ROW_DELETED} != 0",
|
||||||
emptyArray(),
|
emptyArray()
|
||||||
),
|
),
|
||||||
signal = null,
|
signal = null
|
||||||
).use { parentCursor ->
|
).use { parentCursor ->
|
||||||
parentCursor.asSequence().associate {
|
parentCursor.asSequence().associate {
|
||||||
val idIndex = it.getColumnIndexOrThrow(Schema.Repository.ROW_ID)
|
val idIndex = it.getColumnIndexOrThrow(Schema.Repository.ROW_ID)
|
||||||
@@ -514,7 +508,7 @@ object Database {
|
|||||||
put(Schema.Repository.ROW_DELETED, 1)
|
put(Schema.Repository.ROW_DELETED, 1)
|
||||||
},
|
},
|
||||||
"${Schema.Repository.ROW_ID} = ?",
|
"${Schema.Repository.ROW_ID} = ?",
|
||||||
arrayOf(id.toString()),
|
arrayOf(id.toString())
|
||||||
)
|
)
|
||||||
notifyChanged(Subject.Repositories, Subject.Repository(id), Subject.Products)
|
notifyChanged(Subject.Repositories, Subject.Repository(id), Subject.Products)
|
||||||
}
|
}
|
||||||
@@ -525,18 +519,18 @@ object Database {
|
|||||||
val productsCount = db.delete(
|
val productsCount = db.delete(
|
||||||
Schema.Product.name,
|
Schema.Product.name,
|
||||||
"${Schema.Product.ROW_REPOSITORY_ID} IN ($idsString)",
|
"${Schema.Product.ROW_REPOSITORY_ID} IN ($idsString)",
|
||||||
null,
|
null
|
||||||
)
|
)
|
||||||
val categoriesCount = db.delete(
|
val categoriesCount = db.delete(
|
||||||
Schema.Category.name,
|
Schema.Category.name,
|
||||||
"${Schema.Category.ROW_REPOSITORY_ID} IN ($idsString)",
|
"${Schema.Category.ROW_REPOSITORY_ID} IN ($idsString)",
|
||||||
null,
|
null
|
||||||
)
|
)
|
||||||
if (isDeleted) {
|
if (isDeleted) {
|
||||||
db.delete(
|
db.delete(
|
||||||
Schema.Repository.name,
|
Schema.Repository.name,
|
||||||
"${Schema.Repository.ROW_ID} IN ($id)",
|
"${Schema.Repository.ROW_ID} IN ($id)",
|
||||||
null,
|
null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
productsCount != 0 || categoriesCount != 0
|
productsCount != 0 || categoriesCount != 0
|
||||||
@@ -561,7 +555,7 @@ object Database {
|
|||||||
Schema.Repository.name,
|
Schema.Repository.name,
|
||||||
selection = Pair("${Schema.Repository.ROW_DELETED} == 0", emptyArray()),
|
selection = Pair("${Schema.Repository.ROW_DELETED} == 0", emptyArray()),
|
||||||
orderBy = "${Schema.Repository.ROW_ENABLED} DESC",
|
orderBy = "${Schema.Repository.ROW_ENABLED} DESC",
|
||||||
signal = signal,
|
signal = signal
|
||||||
).observable(Subject.Repositories)
|
).observable(Subject.Repositories)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -583,16 +577,14 @@ object Database {
|
|||||||
.map { get(packageName, null) }
|
.map { get(packageName, null) }
|
||||||
.flowOn(Dispatchers.IO)
|
.flowOn(Dispatchers.IO)
|
||||||
|
|
||||||
suspend fun getUpdates(skipSignatureCheck: Boolean): List<ProductItem> =
|
suspend fun getUpdates(): List<ProductItem> = withContext(Dispatchers.IO) {
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
query(
|
query(
|
||||||
installed = true,
|
installed = true,
|
||||||
updates = true,
|
updates = true,
|
||||||
searchQuery = "",
|
searchQuery = "",
|
||||||
skipSignatureCheck = skipSignatureCheck,
|
|
||||||
section = ProductItem.Section.All,
|
section = ProductItem.Section.All,
|
||||||
order = SortOrder.NAME,
|
order = SortOrder.NAME,
|
||||||
signal = null,
|
signal = null
|
||||||
).use {
|
).use {
|
||||||
it.asSequence()
|
it.asSequence()
|
||||||
.map(ProductAdapter::transformItem)
|
.map(ProductAdapter::transformItem)
|
||||||
@@ -600,11 +592,11 @@ object Database {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getUpdatesStream(skipSignatureCheck: Boolean): Flow<List<ProductItem>> = flowOf(Unit)
|
fun getUpdatesStream(): Flow<List<ProductItem>> = flowOf(Unit)
|
||||||
.onCompletion { if (it == null) emitAll(flowCollection(Subject.Products)) }
|
.onCompletion { if (it == null) emitAll(flowCollection(Subject.Products)) }
|
||||||
// Crashes due to immediate retrieval of data?
|
// Crashes due to immediate retrieval of data?
|
||||||
.onEach { delay(50) }
|
.onEach { delay(50) }
|
||||||
.map { getUpdates(skipSignatureCheck) }
|
.map { getUpdates() }
|
||||||
.flowOn(Dispatchers.IO)
|
.flowOn(Dispatchers.IO)
|
||||||
|
|
||||||
fun get(packageName: String, signal: CancellationSignal?): List<Product> {
|
fun get(packageName: String, signal: CancellationSignal?): List<Product> {
|
||||||
@@ -613,10 +605,10 @@ object Database {
|
|||||||
columns = arrayOf(
|
columns = arrayOf(
|
||||||
Schema.Product.ROW_REPOSITORY_ID,
|
Schema.Product.ROW_REPOSITORY_ID,
|
||||||
Schema.Product.ROW_DESCRIPTION,
|
Schema.Product.ROW_DESCRIPTION,
|
||||||
Schema.Product.ROW_DATA,
|
Schema.Product.ROW_DATA
|
||||||
),
|
),
|
||||||
selection = Pair("${Schema.Product.ROW_PACKAGE_NAME} = ?", arrayOf(packageName)),
|
selection = Pair("${Schema.Product.ROW_PACKAGE_NAME} = ?", arrayOf(packageName)),
|
||||||
signal = signal,
|
signal = signal
|
||||||
).use { it.asSequence().map(::transform).toList() }
|
).use { it.asSequence().map(::transform).toList() }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -631,24 +623,22 @@ object Database {
|
|||||||
columns = arrayOf("COUNT (*)"),
|
columns = arrayOf("COUNT (*)"),
|
||||||
selection = Pair(
|
selection = Pair(
|
||||||
"${Schema.Product.ROW_REPOSITORY_ID} = ?",
|
"${Schema.Product.ROW_REPOSITORY_ID} = ?",
|
||||||
arrayOf(repositoryId.toString()),
|
arrayOf(repositoryId.toString())
|
||||||
),
|
)
|
||||||
).use { it.firstOrNull()?.getInt(0) ?: 0 }
|
).use { it.firstOrNull()?.getInt(0) ?: 0 }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun query(
|
fun query(
|
||||||
installed: Boolean,
|
installed: Boolean,
|
||||||
updates: Boolean,
|
updates: Boolean,
|
||||||
skipSignatureCheck: Boolean = false,
|
|
||||||
searchQuery: String,
|
searchQuery: String,
|
||||||
section: ProductItem.Section,
|
section: ProductItem.Section,
|
||||||
order: SortOrder,
|
order: SortOrder,
|
||||||
signal: CancellationSignal?,
|
signal: CancellationSignal?
|
||||||
): Cursor {
|
): Cursor {
|
||||||
val builder = QueryBuilder()
|
val builder = QueryBuilder()
|
||||||
|
|
||||||
val signatureMatches = if (skipSignatureCheck) "1"
|
val signatureMatches = """installed.${Schema.Installed.ROW_SIGNATURE} IS NOT NULL AND
|
||||||
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} LIKE ('%.' || installed.${Schema.Installed.ROW_SIGNATURE} || '.%') AND
|
||||||
product.${Schema.Product.ROW_SIGNATURES} != ''"""
|
product.${Schema.Product.ROW_SIGNATURES} != ''"""
|
||||||
|
|
||||||
@@ -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 {
|
fun transformItem(cursor: Cursor): ProductItem {
|
||||||
return cursor.getBlob(cursor.getColumnIndexOrThrow(Schema.Product.ROW_DATA_ITEM))
|
return cursor.getBlob(cursor.getColumnIndexOrThrow(Schema.Product.ROW_DATA_ITEM))
|
||||||
.jsonParse {
|
.jsonParse {
|
||||||
@@ -807,10 +793,10 @@ object Database {
|
|||||||
Schema.Installed.ROW_PACKAGE_NAME,
|
Schema.Installed.ROW_PACKAGE_NAME,
|
||||||
Schema.Installed.ROW_VERSION,
|
Schema.Installed.ROW_VERSION,
|
||||||
Schema.Installed.ROW_VERSION_CODE,
|
Schema.Installed.ROW_VERSION_CODE,
|
||||||
Schema.Installed.ROW_SIGNATURE,
|
Schema.Installed.ROW_SIGNATURE
|
||||||
),
|
),
|
||||||
selection = Pair("${Schema.Installed.ROW_PACKAGE_NAME} = ?", arrayOf(packageName)),
|
selection = Pair("${Schema.Installed.ROW_PACKAGE_NAME} = ?", arrayOf(packageName)),
|
||||||
signal = signal,
|
signal = signal
|
||||||
).use { it.firstOrNull()?.let(::transform) }
|
).use { it.firstOrNull()?.let(::transform) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -823,7 +809,7 @@ object Database {
|
|||||||
put(Schema.Installed.ROW_VERSION, installedItem.version)
|
put(Schema.Installed.ROW_VERSION, installedItem.version)
|
||||||
put(Schema.Installed.ROW_VERSION_CODE, installedItem.versionCode)
|
put(Schema.Installed.ROW_VERSION_CODE, installedItem.versionCode)
|
||||||
put(Schema.Installed.ROW_SIGNATURE, installedItem.signature)
|
put(Schema.Installed.ROW_SIGNATURE, installedItem.signature)
|
||||||
},
|
}
|
||||||
)
|
)
|
||||||
if (notify) {
|
if (notify) {
|
||||||
notifyChanged(Subject.Products)
|
notifyChanged(Subject.Products)
|
||||||
@@ -843,7 +829,7 @@ object Database {
|
|||||||
val count = db.delete(
|
val count = db.delete(
|
||||||
Schema.Installed.name,
|
Schema.Installed.name,
|
||||||
"${Schema.Installed.ROW_PACKAGE_NAME} = ?",
|
"${Schema.Installed.ROW_PACKAGE_NAME} = ?",
|
||||||
arrayOf(packageName),
|
arrayOf(packageName)
|
||||||
)
|
)
|
||||||
if (count > 0) {
|
if (count > 0) {
|
||||||
notifyChanged(Subject.Products)
|
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_PACKAGE_NAME)),
|
||||||
cursor.getString(cursor.getColumnIndexOrThrow(Schema.Installed.ROW_VERSION)),
|
cursor.getString(cursor.getColumnIndexOrThrow(Schema.Installed.ROW_VERSION)),
|
||||||
cursor.getLong(cursor.getColumnIndexOrThrow(Schema.Installed.ROW_VERSION_CODE)),
|
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 {
|
ContentValues().apply {
|
||||||
put(Schema.Lock.ROW_PACKAGE_NAME, lock.first)
|
put(Schema.Lock.ROW_PACKAGE_NAME, lock.first)
|
||||||
put(Schema.Lock.ROW_VERSION_CODE, lock.second)
|
put(Schema.Lock.ROW_VERSION_CODE, lock.second)
|
||||||
},
|
}
|
||||||
)
|
)
|
||||||
if (notify) {
|
if (notify) {
|
||||||
notifyChanged(Subject.Products)
|
notifyChanged(Subject.Products)
|
||||||
@@ -924,9 +910,9 @@ object Database {
|
|||||||
put(Schema.Product.ROW_DATA, jsonGenerate(product::serialize))
|
put(Schema.Product.ROW_DATA, jsonGenerate(product::serialize))
|
||||||
put(
|
put(
|
||||||
Schema.Product.ROW_DATA_ITEM,
|
Schema.Product.ROW_DATA_ITEM,
|
||||||
jsonGenerate(product.item()::serialize),
|
jsonGenerate(product.item()::serialize)
|
||||||
)
|
)
|
||||||
},
|
}
|
||||||
)
|
)
|
||||||
for (category in product.categories) {
|
for (category in product.categories) {
|
||||||
db.insertOrReplace(
|
db.insertOrReplace(
|
||||||
@@ -936,7 +922,7 @@ object Database {
|
|||||||
put(Schema.Category.ROW_REPOSITORY_ID, product.repositoryId)
|
put(Schema.Category.ROW_REPOSITORY_ID, product.repositoryId)
|
||||||
put(Schema.Category.ROW_PACKAGE_NAME, product.packageName)
|
put(Schema.Category.ROW_PACKAGE_NAME, product.packageName)
|
||||||
put(Schema.Category.ROW_NAME, category)
|
put(Schema.Category.ROW_NAME, category)
|
||||||
},
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -949,20 +935,20 @@ object Database {
|
|||||||
db.delete(
|
db.delete(
|
||||||
Schema.Product.name,
|
Schema.Product.name,
|
||||||
"${Schema.Product.ROW_REPOSITORY_ID} = ?",
|
"${Schema.Product.ROW_REPOSITORY_ID} = ?",
|
||||||
arrayOf(repository.id.toString()),
|
arrayOf(repository.id.toString())
|
||||||
)
|
)
|
||||||
db.delete(
|
db.delete(
|
||||||
Schema.Category.name,
|
Schema.Category.name,
|
||||||
"${Schema.Category.ROW_REPOSITORY_ID} = ?",
|
"${Schema.Category.ROW_REPOSITORY_ID} = ?",
|
||||||
arrayOf(repository.id.toString()),
|
arrayOf(repository.id.toString())
|
||||||
)
|
)
|
||||||
db.execSQL(
|
db.execSQL(
|
||||||
"INSERT INTO ${Schema.Product.name} SELECT * " +
|
"INSERT INTO ${Schema.Product.name} SELECT * " +
|
||||||
"FROM ${Schema.Product.temporaryName}",
|
"FROM ${Schema.Product.temporaryName}"
|
||||||
)
|
)
|
||||||
db.execSQL(
|
db.execSQL(
|
||||||
"INSERT INTO ${Schema.Category.name} SELECT * " +
|
"INSERT INTO ${Schema.Category.name} SELECT * " +
|
||||||
"FROM ${Schema.Category.temporaryName}",
|
"FROM ${Schema.Category.temporaryName}"
|
||||||
)
|
)
|
||||||
RepositoryAdapter.putWithoutNotification(repository, true, db)
|
RepositoryAdapter.putWithoutNotification(repository, true, db)
|
||||||
db.execSQL("DROP TABLE IF EXISTS ${Schema.Product.temporaryName}")
|
db.execSQL("DROP TABLE IF EXISTS ${Schema.Product.temporaryName}")
|
||||||
@@ -971,7 +957,7 @@ object Database {
|
|||||||
notifyChanged(
|
notifyChanged(
|
||||||
Subject.Repositories,
|
Subject.Repositories,
|
||||||
Subject.Repository(repository.id),
|
Subject.Repository(repository.id),
|
||||||
Subject.Products,
|
Subject.Products
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
db.execSQL("DROP TABLE IF EXISTS ${Schema.Product.temporaryName}")
|
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.Cursor
|
||||||
import android.database.sqlite.SQLiteDatabase
|
import android.database.sqlite.SQLiteDatabase
|
||||||
import android.os.CancellationSignal
|
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.BuildConfig
|
||||||
import com.looker.droidify.utility.common.extension.asSequence
|
|
||||||
import com.looker.droidify.utility.common.log
|
|
||||||
|
|
||||||
class QueryBuilder {
|
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>()
|
private val arguments = mutableListOf<String>()
|
||||||
|
|
||||||
operator fun plusAssign(query: String) {
|
operator fun plusAssign(query: String) {
|
||||||
if (builder.isNotEmpty()) {
|
if (builder.isNotEmpty()) {
|
||||||
builder.append(" ")
|
builder.append(" ")
|
||||||
}
|
}
|
||||||
builder.trimAndJoin(query)
|
builder.append(trimQuery(query))
|
||||||
}
|
}
|
||||||
|
|
||||||
operator fun remAssign(argument: String) {
|
operator fun remAssign(argument: String) {
|
||||||
@@ -42,53 +48,3 @@ class QueryBuilder {
|
|||||||
return db.rawQuery(query, arguments, signal)
|
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.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import com.fasterxml.jackson.core.JsonToken
|
import com.fasterxml.jackson.core.JsonToken
|
||||||
import com.looker.droidify.utility.common.Exporter
|
import com.looker.core.common.Exporter
|
||||||
import com.looker.droidify.utility.common.extension.Json
|
import com.looker.core.common.extension.Json
|
||||||
import com.looker.droidify.utility.common.extension.forEach
|
import com.looker.core.common.extension.forEach
|
||||||
import com.looker.droidify.utility.common.extension.forEachKey
|
import com.looker.core.common.extension.forEachKey
|
||||||
import com.looker.droidify.utility.common.extension.parseDictionary
|
import com.looker.core.common.extension.parseDictionary
|
||||||
import com.looker.droidify.utility.common.extension.writeArray
|
import com.looker.core.common.extension.writeArray
|
||||||
import com.looker.droidify.utility.common.extension.writeDictionary
|
import com.looker.core.common.extension.writeDictionary
|
||||||
import com.looker.droidify.di.ApplicationScope
|
import com.looker.core.di.ApplicationScope
|
||||||
import com.looker.droidify.di.IoDispatcher
|
import com.looker.core.di.IoDispatcher
|
||||||
import com.looker.droidify.model.Repository
|
import com.looker.droidify.model.Repository
|
||||||
import com.looker.droidify.utility.serialization.repository
|
import com.looker.droidify.utility.serialization.repository
|
||||||
import com.looker.droidify.utility.serialization.serialize
|
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
|
drawable.colorFilter = colorFilter
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Deprecated("Deprecated in Java")
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
override fun getOpacity(): Int = drawable.opacity
|
override fun getOpacity(): Int = drawable.opacity
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,10 +3,11 @@ package com.looker.droidify.index
|
|||||||
import android.content.ContentValues
|
import android.content.ContentValues
|
||||||
import android.database.sqlite.SQLiteDatabase
|
import android.database.sqlite.SQLiteDatabase
|
||||||
import com.fasterxml.jackson.core.JsonToken
|
import com.fasterxml.jackson.core.JsonToken
|
||||||
import com.looker.droidify.utility.common.extension.Json
|
import com.looker.core.common.extension.Json
|
||||||
import com.looker.droidify.utility.common.extension.asSequence
|
import com.looker.core.common.extension.asSequence
|
||||||
import com.looker.droidify.utility.common.extension.collectNotNull
|
import com.looker.core.common.extension.collectNotNull
|
||||||
import com.looker.droidify.utility.common.extension.writeDictionary
|
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.Product
|
||||||
import com.looker.droidify.model.Release
|
import com.looker.droidify.model.Release
|
||||||
import com.looker.droidify.utility.serialization.product
|
import com.looker.droidify.utility.serialization.product
|
||||||
@@ -84,7 +85,7 @@ class IndexMerger(file: File) : Closeable {
|
|||||||
"""SELECT product.description, product.data AS pd, releases.data AS rd FROM product
|
"""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
|
null
|
||||||
).use { cursor ->
|
)?.use { cursor ->
|
||||||
cursor.asSequence().map { currentCursor ->
|
cursor.asSequence().map { currentCursor ->
|
||||||
val description = currentCursor.getString(0)
|
val description = currentCursor.getString(0)
|
||||||
val product = Json.factory.createParser(currentCursor.getBlob(1)).use {
|
val product = Json.factory.createParser(currentCursor.getBlob(1)).use {
|
||||||
@@ -111,8 +112,4 @@ class IndexMerger(file: File) : Closeable {
|
|||||||
override fun close() {
|
override fun close() {
|
||||||
db.use { closeTransaction() }
|
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 androidx.core.os.LocaleListCompat
|
||||||
import com.fasterxml.jackson.core.JsonParser
|
import com.fasterxml.jackson.core.JsonParser
|
||||||
import com.fasterxml.jackson.core.JsonToken
|
import com.fasterxml.jackson.core.JsonToken
|
||||||
import com.looker.droidify.utility.common.extension.Json
|
import com.looker.core.common.SdkCheck
|
||||||
import com.looker.droidify.utility.common.extension.collectDistinctNotEmptyStrings
|
import com.looker.core.common.extension.Json
|
||||||
import com.looker.droidify.utility.common.extension.collectNotNull
|
import com.looker.core.common.extension.collectDistinctNotEmptyStrings
|
||||||
import com.looker.droidify.utility.common.extension.forEach
|
import com.looker.core.common.extension.collectNotNull
|
||||||
import com.looker.droidify.utility.common.extension.forEachKey
|
import com.looker.core.common.extension.forEach
|
||||||
import com.looker.droidify.utility.common.extension.illegal
|
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
|
||||||
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.model.Release
|
||||||
import com.looker.droidify.utility.common.SdkCheck
|
|
||||||
import com.looker.droidify.utility.common.nullIfEmpty
|
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
|
||||||
object IndexV1Parser {
|
object IndexV1Parser {
|
||||||
@@ -43,12 +32,9 @@ object IndexV1Parser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private class Screenshots(
|
private class Screenshots(
|
||||||
val video: List<String>,
|
|
||||||
val phone: List<String>,
|
val phone: List<String>,
|
||||||
val smallTablet: List<String>,
|
val smallTablet: List<String>,
|
||||||
val largeTablet: List<String>,
|
val largeTablet: List<String>
|
||||||
val wear: List<String>,
|
|
||||||
val tv: List<String>,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
private class Localized(
|
private class Localized(
|
||||||
@@ -104,9 +90,10 @@ object IndexV1Parser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun <T> Map<String, Localized>.find(callback: (String, Localized) -> T?): T? {
|
private fun <T> Map<String, Localized>.find(callback: (String, Localized) -> T?): T? {
|
||||||
return getAndCall("en-US", callback)
|
return getAndCall("en-US", callback) ?: getAndCall("en_US", callback) ?: getAndCall(
|
||||||
?: getAndCall("en_US", callback)
|
"en",
|
||||||
?: getAndCall("en", callback)
|
callback
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun <T> Map<String, Localized>.findLocalized(callback: (Localized) -> T?): T? {
|
private fun <T> Map<String, Localized>.findLocalized(callback: (Localized) -> T?): T? {
|
||||||
@@ -135,11 +122,12 @@ object IndexV1Parser {
|
|||||||
|
|
||||||
internal object DonateComparator : Comparator<Product.Donate> {
|
internal object DonateComparator : Comparator<Product.Donate> {
|
||||||
private val classes = listOf(
|
private val classes = listOf(
|
||||||
Regular::class,
|
Product.Donate.Regular::class,
|
||||||
Bitcoin::class,
|
Product.Donate.Bitcoin::class,
|
||||||
Litecoin::class,
|
Product.Donate.Litecoin::class,
|
||||||
Liberapay::class,
|
Product.Donate.Flattr::class,
|
||||||
OpenCollective::class
|
Product.Donate.Liberapay::class,
|
||||||
|
Product.Donate.OpenCollective::class
|
||||||
)
|
)
|
||||||
|
|
||||||
override fun compare(donate1: Product.Donate, donate2: Product.Donate): Int {
|
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) {
|
fun parse(repositoryId: Long, inputStream: InputStream, callback: Callback) {
|
||||||
val jsonParser = Json.factory.createParser(inputStream)
|
val jsonParser = Json.factory.createParser(inputStream)
|
||||||
if (jsonParser.nextToken() != JsonToken.START_OBJECT) {
|
if (jsonParser.nextToken() != JsonToken.START_OBJECT) {
|
||||||
jsonParser.illegal()
|
jsonParser.illegal()
|
||||||
} else {
|
} else {
|
||||||
jsonParser.forEachKey { key ->
|
jsonParser.forEachKey { it ->
|
||||||
when {
|
when {
|
||||||
key.dictionary(DICT_REPO) -> {
|
it.dictionary("repo") -> {
|
||||||
var address = ""
|
var address = ""
|
||||||
var mirrors = emptyList<String>()
|
var mirrors = emptyList<String>()
|
||||||
var name = ""
|
var name = ""
|
||||||
@@ -180,14 +157,12 @@ object IndexV1Parser {
|
|||||||
var timestamp = 0L
|
var timestamp = 0L
|
||||||
forEachKey {
|
forEachKey {
|
||||||
when {
|
when {
|
||||||
it.string(KEY_REPO_ADDRESS) -> address = valueAsString
|
it.string("address") -> address = valueAsString
|
||||||
it.array(KEY_REPO_MIRRORS) -> mirrors =
|
it.array("mirrors") -> mirrors = collectDistinctNotEmptyStrings()
|
||||||
collectDistinctNotEmptyStrings()
|
it.string("name") -> name = valueAsString
|
||||||
|
it.string("description") -> description = valueAsString
|
||||||
it.string(KEY_REPO_NAME) -> name = valueAsString
|
it.number("version") -> version = valueAsInt
|
||||||
it.string(KEY_REPO_DESC) -> description = valueAsString
|
it.number("timestamp") -> timestamp = valueAsLong
|
||||||
it.number(KEY_REPO_VER) -> version = valueAsInt
|
|
||||||
it.number(KEY_REPO_TIME) -> timestamp = valueAsLong
|
|
||||||
else -> skipChildren()
|
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)
|
val product = parseProduct(repositoryId)
|
||||||
callback.onProduct(product)
|
callback.onProduct(product)
|
||||||
}
|
}
|
||||||
|
|
||||||
key.dictionary(DICT_RELEASE) -> forEachKey {
|
it.dictionary("packages") -> forEachKey {
|
||||||
if (it.token == JsonToken.START_ARRAY) {
|
if (it.token == JsonToken.START_ARRAY) {
|
||||||
val packageName = it.key
|
val packageName = it.key
|
||||||
val releases = collectNotNull(JsonToken.START_OBJECT) { parseRelease() }
|
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 {
|
private fun JsonParser.parseProduct(repositoryId: Long): Product {
|
||||||
var packageName = ""
|
var packageName = ""
|
||||||
var nameFallback = ""
|
var nameFallback = ""
|
||||||
@@ -281,42 +224,42 @@ object IndexV1Parser {
|
|||||||
val licenses = mutableListOf<String>()
|
val licenses = mutableListOf<String>()
|
||||||
val donates = mutableListOf<Product.Donate>()
|
val donates = mutableListOf<Product.Donate>()
|
||||||
val localizedMap = mutableMapOf<String, Localized>()
|
val localizedMap = mutableMapOf<String, Localized>()
|
||||||
forEachKey { key ->
|
forEachKey { it ->
|
||||||
when {
|
when {
|
||||||
key.string(KEY_PRODUCT_PACKAGENAME) -> packageName = valueAsString
|
it.string("packageName") -> packageName = valueAsString
|
||||||
key.string(KEY_PRODUCT_NAME) -> nameFallback = valueAsString
|
it.string("name") -> nameFallback = valueAsString
|
||||||
key.string(KEY_PRODUCT_SUMMARY) -> summaryFallback = valueAsString
|
it.string("summary") -> summaryFallback = valueAsString
|
||||||
key.string(KEY_PRODUCT_DESCRIPTION) -> descriptionFallback = valueAsString
|
it.string("description") -> descriptionFallback = valueAsString
|
||||||
key.string(KEY_PRODUCT_ICON) -> icon = validateIcon(valueAsString)
|
it.string("icon") -> icon = validateIcon(valueAsString)
|
||||||
key.string(KEY_PRODUCT_AUTHORNAME) -> authorName = valueAsString
|
it.string("authorName") -> authorName = valueAsString
|
||||||
key.string(KEY_PRODUCT_AUTHOREMAIL) -> authorEmail = valueAsString
|
it.string("authorEmail") -> authorEmail = valueAsString
|
||||||
key.string(KEY_PRODUCT_AUTHORWEBSITE) -> authorWeb = valueAsString
|
it.string("authorWebSite") -> authorWeb = valueAsString
|
||||||
key.string(KEY_PRODUCT_SOURCECODE) -> source = valueAsString
|
it.string("sourceCode") -> source = valueAsString
|
||||||
key.string(KEY_PRODUCT_CHANGELOG) -> changelog = valueAsString
|
it.string("changelog") -> changelog = valueAsString
|
||||||
key.string(KEY_PRODUCT_WEBSITE) -> web = valueAsString
|
it.string("webSite") -> web = valueAsString
|
||||||
key.string(KEY_PRODUCT_ISSUETRACKER) -> tracker = valueAsString
|
it.string("issueTracker") -> tracker = valueAsString
|
||||||
key.number(KEY_PRODUCT_ADDED) -> added = valueAsLong
|
it.number("added") -> added = valueAsLong
|
||||||
key.number(KEY_PRODUCT_LASTUPDATED) -> updated = valueAsLong
|
it.number("lastUpdated") -> updated = valueAsLong
|
||||||
key.string(KEY_PRODUCT_SUGGESTEDVERSIONCODE) ->
|
it.string("suggestedVersionCode") ->
|
||||||
suggestedVersionCode =
|
suggestedVersionCode =
|
||||||
valueAsString.toLongOrNull() ?: 0L
|
valueAsString.toLongOrNull() ?: 0L
|
||||||
|
|
||||||
key.array(KEY_PRODUCT_CATEGORIES) -> categories = collectDistinctNotEmptyStrings()
|
it.array("categories") -> categories = collectDistinctNotEmptyStrings()
|
||||||
key.array(KEY_PRODUCT_ANTIFEATURES) -> antiFeatures =
|
it.array("antiFeatures") -> antiFeatures = collectDistinctNotEmptyStrings()
|
||||||
collectDistinctNotEmptyStrings()
|
it.string("license") -> licenses += valueAsString.split(',')
|
||||||
|
|
||||||
key.string(KEY_PRODUCT_LICENSE) -> licenses += valueAsString.split(',')
|
|
||||||
.filter { it.isNotEmpty() }
|
.filter { it.isNotEmpty() }
|
||||||
|
|
||||||
key.string(KEY_PRODUCT_DONATE) -> donates += Regular(valueAsString)
|
it.string("donate") -> donates += Product.Donate.Regular(valueAsString)
|
||||||
key.string(KEY_PRODUCT_BITCOIN) -> donates += Bitcoin(valueAsString)
|
it.string("bitcoin") -> donates += Product.Donate.Bitcoin(valueAsString)
|
||||||
key.string(KEY_PRODUCT_LIBERAPAYID) -> donates += Liberapay(valueAsString)
|
it.string("flattrID") -> donates += Product.Donate.Flattr(valueAsString)
|
||||||
key.string(KEY_PRODUCT_LITECOIN) -> donates += Litecoin(valueAsString)
|
it.string("liberapayID") -> donates += Product.Donate.Liberapay(valueAsString)
|
||||||
key.string(KEY_PRODUCT_OPENCOLLECTIVE) -> donates += OpenCollective(valueAsString)
|
it.string("openCollective") -> donates += Product.Donate.OpenCollective(
|
||||||
|
valueAsString
|
||||||
|
)
|
||||||
|
|
||||||
key.dictionary(KEY_PRODUCT_LOCALIZED) -> forEachKey { localizedKey ->
|
it.dictionary("localized") -> forEachKey { it ->
|
||||||
if (localizedKey.token == JsonToken.START_OBJECT) {
|
if (it.token == JsonToken.START_OBJECT) {
|
||||||
val locale = localizedKey.key
|
val locale = it.key
|
||||||
var name = ""
|
var name = ""
|
||||||
var summary = ""
|
var summary = ""
|
||||||
var description = ""
|
var description = ""
|
||||||
@@ -325,52 +268,46 @@ object IndexV1Parser {
|
|||||||
var phone = emptyList<String>()
|
var phone = emptyList<String>()
|
||||||
var smallTablet = emptyList<String>()
|
var smallTablet = emptyList<String>()
|
||||||
var largeTablet = emptyList<String>()
|
var largeTablet = emptyList<String>()
|
||||||
var wear = emptyList<String>()
|
|
||||||
var tv = emptyList<String>()
|
|
||||||
var video = emptyList<String>()
|
|
||||||
forEachKey {
|
forEachKey {
|
||||||
when {
|
when {
|
||||||
it.string(KEY_PRODUCT_NAME) -> name = valueAsString
|
it.string("name") -> name = valueAsString
|
||||||
it.string(KEY_PRODUCT_SUMMARY) -> summary = valueAsString
|
it.string("summary") -> summary = valueAsString
|
||||||
it.string(KEY_PRODUCT_DESCRIPTION) -> description = valueAsString
|
it.string("description") -> description = valueAsString
|
||||||
it.string(KEY_PRODUCT_WHATSNEW) -> whatsNew = valueAsString
|
it.string("whatsNew") -> whatsNew = valueAsString
|
||||||
it.string(KEY_PRODUCT_ICON) -> metadataIcon = valueAsString
|
it.string("icon") -> metadataIcon = valueAsString
|
||||||
it.string(KEY_PRODUCT_VIDEO) -> video = listOf(valueAsString)
|
it.array("phoneScreenshots") ->
|
||||||
it.array(KEY_PRODUCT_PHONE_SCREENSHOTS) ->
|
phone =
|
||||||
phone = collectDistinctNotEmptyStrings()
|
collectDistinctNotEmptyStrings()
|
||||||
|
|
||||||
it.array(KEY_PRODUCT_SEVEN_INCH_SCREENSHOTS) ->
|
it.array("sevenInchScreenshots") ->
|
||||||
smallTablet = collectDistinctNotEmptyStrings()
|
smallTablet =
|
||||||
|
collectDistinctNotEmptyStrings()
|
||||||
|
|
||||||
it.array(KEY_PRODUCT_TEN_INCH_SCREENSHOTS) ->
|
it.array("tenInchScreenshots") ->
|
||||||
largeTablet = collectDistinctNotEmptyStrings()
|
largeTablet =
|
||||||
|
collectDistinctNotEmptyStrings()
|
||||||
it.array(KEY_PRODUCT_WEAR_SCREENSHOTS) ->
|
|
||||||
wear = collectDistinctNotEmptyStrings()
|
|
||||||
|
|
||||||
it.array(KEY_PRODUCT_TV_SCREENSHOTS) ->
|
|
||||||
tv = collectDistinctNotEmptyStrings()
|
|
||||||
|
|
||||||
else -> skipChildren()
|
else -> skipChildren()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val isScreenshotEmpty =
|
|
||||||
arrayOf(video, phone, smallTablet, largeTablet, wear, tv)
|
|
||||||
.any { it.isNotEmpty() }
|
|
||||||
val screenshots =
|
val screenshots =
|
||||||
if (isScreenshotEmpty) {
|
if (sequenceOf(
|
||||||
Screenshots(video, phone, smallTablet, largeTablet, wear, tv)
|
phone,
|
||||||
|
smallTablet,
|
||||||
|
largeTablet
|
||||||
|
).any { it.isNotEmpty() }
|
||||||
|
) {
|
||||||
|
Screenshots(phone, smallTablet, largeTablet)
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
localizedMap[locale] = Localized(
|
localizedMap[locale] = Localized(
|
||||||
name = name,
|
name,
|
||||||
summary = summary,
|
summary,
|
||||||
description = description,
|
description,
|
||||||
whatsNew = whatsNew,
|
whatsNew,
|
||||||
metadataIcon = metadataIcon.nullIfEmpty()?.let { "$locale/$it" }
|
metadataIcon.nullIfEmpty()?.let { "$locale/$it" }.orEmpty(),
|
||||||
.orEmpty(),
|
screenshots
|
||||||
screenshots = screenshots,
|
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
skipChildren()
|
skipChildren()
|
||||||
@@ -393,60 +330,53 @@ object IndexV1Parser {
|
|||||||
}
|
}
|
||||||
val screenshotPairs =
|
val screenshotPairs =
|
||||||
localizedMap.find { key, localized -> localized.screenshots?.let { Pair(key, it) } }
|
localizedMap.find { key, localized -> localized.screenshots?.let { Pair(key, it) } }
|
||||||
val screenshots = screenshotPairs?.let { (key, screenshots) ->
|
val screenshots = screenshotPairs
|
||||||
screenshots.video.map { Product.Screenshot(key, VIDEO, it) } +
|
?.let { (key, screenshots) ->
|
||||||
screenshots.phone.map { Product.Screenshot(key, PHONE, it) } +
|
screenshots.phone.asSequence()
|
||||||
screenshots.smallTablet.map { Product.Screenshot(key, SMALL_TABLET, it) } +
|
.map { Product.Screenshot(key, Product.Screenshot.Type.PHONE, it) } +
|
||||||
screenshots.largeTablet.map { Product.Screenshot(key, LARGE_TABLET, it) } +
|
screenshots.smallTablet.asSequence()
|
||||||
screenshots.wear.map { Product.Screenshot(key, WEAR, it) } +
|
.map {
|
||||||
screenshots.tv.map { Product.Screenshot(key, TV, it) }
|
Product.Screenshot(
|
||||||
}.orEmpty()
|
key,
|
||||||
return Product(
|
Product.Screenshot.Type.SMALL_TABLET,
|
||||||
repositoryId = repositoryId,
|
it
|
||||||
packageName = packageName,
|
)
|
||||||
name = name,
|
} +
|
||||||
summary = summary,
|
screenshots.largeTablet.asSequence()
|
||||||
description = description,
|
.map {
|
||||||
whatsNew = whatsNew,
|
Product.Screenshot(
|
||||||
icon = icon,
|
key,
|
||||||
metadataIcon = metadataIcon,
|
Product.Screenshot.Type.LARGE_TABLET,
|
||||||
author = Product.Author(authorName, authorEmail, authorWeb),
|
it
|
||||||
source = source,
|
)
|
||||||
changelog = changelog,
|
}
|
||||||
web = web,
|
}
|
||||||
tracker = tracker,
|
.orEmpty().toList()
|
||||||
added = added,
|
return Product(
|
||||||
updated = updated,
|
repositoryId,
|
||||||
suggestedVersionCode = suggestedVersionCode,
|
packageName,
|
||||||
categories = categories,
|
name,
|
||||||
antiFeatures = antiFeatures,
|
summary,
|
||||||
licenses = licenses,
|
description,
|
||||||
donates = donates.sortedWith(DonateComparator),
|
whatsNew,
|
||||||
screenshots = screenshots,
|
icon,
|
||||||
releases = emptyList()
|
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 {
|
private fun JsonParser.parseRelease(): Release {
|
||||||
var version = ""
|
var version = ""
|
||||||
@@ -468,28 +398,28 @@ object IndexV1Parser {
|
|||||||
val permissions = linkedSetOf<String>()
|
val permissions = linkedSetOf<String>()
|
||||||
var features = emptyList<String>()
|
var features = emptyList<String>()
|
||||||
var platforms = emptyList<String>()
|
var platforms = emptyList<String>()
|
||||||
forEachKey { key ->
|
forEachKey {
|
||||||
when {
|
when {
|
||||||
key.string(KEY_RELEASE_VERSIONNAME) -> version = valueAsString
|
it.string("versionName") -> version = valueAsString
|
||||||
key.number(KEY_RELEASE_VERSIONCODE) -> versionCode = valueAsLong
|
it.number("versionCode") -> versionCode = valueAsLong
|
||||||
key.number(KEY_RELEASE_ADDED) -> added = valueAsLong
|
it.number("added") -> added = valueAsLong
|
||||||
key.number(KEY_RELEASE_SIZE) -> size = valueAsLong
|
it.number("size") -> size = valueAsLong
|
||||||
key.number(KEY_RELEASE_MINSDKVERSION) -> minSdkVersion = valueAsInt
|
it.number("minSdkVersion") -> minSdkVersion = valueAsInt
|
||||||
key.number(KEY_RELEASE_TARGETSDKVERSION) -> targetSdkVersion = valueAsInt
|
it.number("targetSdkVersion") -> targetSdkVersion = valueAsInt
|
||||||
key.number(KEY_RELEASE_MAXSDKVERSION) -> maxSdkVersion = valueAsInt
|
it.number("maxSdkVersion") -> maxSdkVersion = valueAsInt
|
||||||
key.string(KEY_RELEASE_SRCNAME) -> source = valueAsString
|
it.string("srcname") -> source = valueAsString
|
||||||
key.string(KEY_RELEASE_APKNAME) -> release = valueAsString
|
it.string("apkName") -> release = valueAsString
|
||||||
key.string(KEY_RELEASE_HASH) -> hash = valueAsString
|
it.string("hash") -> hash = valueAsString
|
||||||
key.string(KEY_RELEASE_HASHTYPE) -> hashTypeCandidate = valueAsString
|
it.string("hashType") -> hashTypeCandidate = valueAsString
|
||||||
key.string(KEY_RELEASE_SIG) -> signature = valueAsString
|
it.string("sig") -> signature = valueAsString
|
||||||
key.string(KEY_RELEASE_OBBMAINFILE) -> obbMain = valueAsString
|
it.string("obbMainFile") -> obbMain = valueAsString
|
||||||
key.string(KEY_RELEASE_OBBMAINFILESHA256) -> obbMainHash = valueAsString
|
it.string("obbMainFileSha256") -> obbMainHash = valueAsString
|
||||||
key.string(KEY_RELEASE_OBBPATCHFILE) -> obbPatch = valueAsString
|
it.string("obbPatchFile") -> obbPatch = valueAsString
|
||||||
key.string(KEY_RELEASE_OBBPATCHFILESHA256) -> obbPatchHash = valueAsString
|
it.string("obbPatchFileSha256") -> obbPatchHash = valueAsString
|
||||||
key.array(KEY_RELEASE_USESPERMISSION) -> collectPermissions(permissions, 0)
|
it.array("uses-permission") -> collectPermissions(permissions, 0)
|
||||||
key.array(KEY_RELEASE_USESPERMISSIONSDK23) -> collectPermissions(permissions, 23)
|
it.array("uses-permission-sdk-23") -> collectPermissions(permissions, 23)
|
||||||
key.array(KEY_RELEASE_FEATURES) -> features = collectDistinctNotEmptyStrings()
|
it.array("features") -> features = collectDistinctNotEmptyStrings()
|
||||||
key.array(KEY_RELEASE_NATIVECODE) -> platforms = collectDistinctNotEmptyStrings()
|
it.array("nativecode") -> platforms = collectDistinctNotEmptyStrings()
|
||||||
else -> skipChildren()
|
else -> skipChildren()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -498,29 +428,29 @@ object IndexV1Parser {
|
|||||||
val obbMainHashType = if (obbMainHash.isNotEmpty()) "sha256" else ""
|
val obbMainHashType = if (obbMainHash.isNotEmpty()) "sha256" else ""
|
||||||
val obbPatchHashType = if (obbPatchHash.isNotEmpty()) "sha256" else ""
|
val obbPatchHashType = if (obbPatchHash.isNotEmpty()) "sha256" else ""
|
||||||
return Release(
|
return Release(
|
||||||
selected = false,
|
false,
|
||||||
version = version,
|
version,
|
||||||
versionCode = versionCode,
|
versionCode,
|
||||||
added = added,
|
added,
|
||||||
size = size,
|
size,
|
||||||
minSdkVersion = minSdkVersion,
|
minSdkVersion,
|
||||||
targetSdkVersion = targetSdkVersion,
|
targetSdkVersion,
|
||||||
maxSdkVersion = maxSdkVersion,
|
maxSdkVersion,
|
||||||
source = source,
|
source,
|
||||||
release = release,
|
release,
|
||||||
hash = hash,
|
hash,
|
||||||
hashType = hashType,
|
hashType,
|
||||||
signature = signature,
|
signature,
|
||||||
obbMain = obbMain,
|
obbMain,
|
||||||
obbMainHash = obbMainHash,
|
obbMainHash,
|
||||||
obbMainHashType = obbMainHashType,
|
obbMainHashType,
|
||||||
obbPatch = obbPatch,
|
obbPatch,
|
||||||
obbPatchHash = obbPatchHash,
|
obbPatchHash,
|
||||||
obbPatchHashType = obbPatchHashType,
|
obbPatchHashType,
|
||||||
permissions = permissions.toList(),
|
permissions.toList(),
|
||||||
features = features,
|
features,
|
||||||
platforms = platforms,
|
platforms,
|
||||||
incompatibilities = emptyList()
|
emptyList()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,28 +2,28 @@ package com.looker.droidify.index
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import com.looker.droidify.database.Database
|
import com.looker.core.common.SdkCheck
|
||||||
import com.looker.droidify.domain.model.fingerprint
|
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.Product
|
||||||
import com.looker.droidify.model.Release
|
import com.looker.droidify.model.Release
|
||||||
import com.looker.droidify.model.Repository
|
import com.looker.droidify.model.Repository
|
||||||
import com.looker.droidify.network.Downloader
|
import com.looker.droidify.database.Database
|
||||||
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.utility.extension.android.Android
|
import com.looker.droidify.utility.extension.android.Android
|
||||||
import com.looker.droidify.utility.getProgress
|
import com.looker.droidify.utility.getProgress
|
||||||
import kotlinx.coroutines.*
|
import com.looker.network.Downloader
|
||||||
import kotlinx.coroutines.flow.drop
|
import com.looker.network.NetworkResponse
|
||||||
import kotlinx.coroutines.flow.filter
|
|
||||||
import kotlinx.coroutines.flow.map
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.security.CodeSigner
|
import java.security.CodeSigner
|
||||||
import java.security.cert.Certificate
|
import java.security.cert.Certificate
|
||||||
import java.util.jar.JarEntry
|
import java.util.jar.JarEntry
|
||||||
import java.util.jar.JarFile
|
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 {
|
object RepositoryUpdater {
|
||||||
enum class Stage {
|
enum class Stage {
|
||||||
@@ -31,7 +31,7 @@ object RepositoryUpdater {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO Add support for Index-V2 and also cleanup everything here
|
// TODO Add support for Index-V2 and also cleanup everything here
|
||||||
enum class IndexType(
|
private enum class IndexType(
|
||||||
val jarName: String,
|
val jarName: String,
|
||||||
val contentName: String
|
val contentName: String
|
||||||
) {
|
) {
|
||||||
@@ -219,13 +219,12 @@ object RepositoryUpdater {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun processFile(
|
private fun processFile(
|
||||||
context: Context,
|
context: Context,
|
||||||
repository: Repository,
|
repository: Repository,
|
||||||
indexType: IndexType,
|
indexType: IndexType,
|
||||||
unstable: Boolean,
|
unstable: Boolean,
|
||||||
file: File,
|
file: File,
|
||||||
mergerFile: File = Cache.getTemporaryFile(context),
|
|
||||||
lastModified: String,
|
lastModified: String,
|
||||||
entityTag: String,
|
entityTag: String,
|
||||||
callback: (Stage, Long, Long?) -> Unit
|
callback: (Stage, Long, Long?) -> Unit
|
||||||
@@ -242,6 +241,7 @@ object RepositoryUpdater {
|
|||||||
|
|
||||||
var changedRepository: Repository? = null
|
var changedRepository: Repository? = null
|
||||||
|
|
||||||
|
val mergerFile = Cache.getTemporaryFile(context)
|
||||||
try {
|
try {
|
||||||
val unmergedProducts = mutableListOf<Product>()
|
val unmergedProducts = mutableListOf<Product>()
|
||||||
val unmergedReleases = mutableListOf<Pair<String, List<Release>>>()
|
val unmergedReleases = mutableListOf<Pair<String, List<Release>>>()
|
||||||
@@ -344,7 +344,6 @@ object RepositoryUpdater {
|
|||||||
.codeSigner
|
.codeSigner
|
||||||
.certificate
|
.certificate
|
||||||
.fingerprint()
|
.fingerprint()
|
||||||
.toString()
|
|
||||||
.uppercase()
|
.uppercase()
|
||||||
|
|
||||||
val commitRepository = if (!workRepository.fingerprint.equals(
|
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
|
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(
|
data class Product(
|
||||||
var repositoryId: Long,
|
var repositoryId: Long,
|
||||||
val packageName: String,
|
val packageName: String,
|
||||||
@@ -35,41 +30,20 @@ data class Product(
|
|||||||
data class Regular(val url: String) : Donate()
|
data class Regular(val url: String) : Donate()
|
||||||
data class Bitcoin(val address: String) : Donate()
|
data class Bitcoin(val address: String) : Donate()
|
||||||
data class Litecoin(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 Liberapay(val id: String) : Donate()
|
||||||
data class OpenCollective(val id: String) : Donate()
|
data class OpenCollective(val id: String) : Donate()
|
||||||
}
|
}
|
||||||
|
|
||||||
class Screenshot(val locale: String, val type: Type, val path: String) {
|
class Screenshot(val locale: String, val type: Type, val path: String) {
|
||||||
enum class Type(val jsonName: String) {
|
enum class Type(val jsonName: String) {
|
||||||
VIDEO("video"),
|
|
||||||
PHONE("phone"),
|
PHONE("phone"),
|
||||||
SMALL_TABLET("smallTablet"),
|
SMALL_TABLET("smallTablet"),
|
||||||
LARGE_TABLET("largeTablet"),
|
LARGE_TABLET("largeTablet")
|
||||||
WEAR("wear"),
|
|
||||||
TV("tv")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val identifier: String
|
val identifier: String
|
||||||
get() = "$locale.${type.name}.$path"
|
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
|
// Same releases with different signatures
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
package com.looker.droidify.model
|
package com.looker.droidify.model
|
||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import android.view.View
|
|
||||||
import com.looker.droidify.utility.common.extension.dpi
|
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
data class ProductItem(
|
data class ProductItem(
|
||||||
@@ -18,39 +16,15 @@ data class ProductItem(
|
|||||||
var canUpdate: Boolean,
|
var canUpdate: Boolean,
|
||||||
var matchRank: Int
|
var matchRank: Int
|
||||||
) {
|
) {
|
||||||
sealed interface Section : Parcelable {
|
sealed class Section : Parcelable {
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
object All : Section
|
data object All : Section()
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
class Category(val name: String) : Section
|
data class Category(val name: String) : Section()
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
class Repository(val id: Long, val name: String) : Section
|
data 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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ data class Release(
|
|||||||
object MinSdk : Incompatibility()
|
object MinSdk : Incompatibility()
|
||||||
object MaxSdk : Incompatibility()
|
object MaxSdk : Incompatibility()
|
||||||
object Platform : Incompatibility()
|
object Platform : Incompatibility()
|
||||||
class Feature(val feature: String) : Incompatibility()
|
data class Feature(val feature: String) : Incompatibility()
|
||||||
}
|
}
|
||||||
|
|
||||||
val identifier: String
|
val identifier: String
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.looker.droidify.model
|
package com.looker.droidify.model
|
||||||
|
|
||||||
|
import com.looker.core.common.extension.isOnion
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
|
|
||||||
data class Repository(
|
data class Repository(
|
||||||
@@ -15,9 +16,19 @@ data class Repository(
|
|||||||
val entityTag: String,
|
val entityTag: String,
|
||||||
val updated: Long,
|
val updated: Long,
|
||||||
val timestamp: 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 {
|
fun edit(address: String, fingerprint: String, authentication: String): Repository {
|
||||||
val isAddressChanged = this.address != address
|
val isAddressChanged = this.address != address
|
||||||
val isFingerprintChanged = this.fingerprint != fingerprint
|
val isFingerprintChanged = this.fingerprint != fingerprint
|
||||||
@@ -38,7 +49,7 @@ data class Repository(
|
|||||||
version: Int,
|
version: Int,
|
||||||
lastModified: String,
|
lastModified: String,
|
||||||
entityTag: String,
|
entityTag: String,
|
||||||
timestamp: Long,
|
timestamp: Long
|
||||||
): Repository {
|
): Repository {
|
||||||
return copy(
|
return copy(
|
||||||
mirrors = mirrors,
|
mirrors = mirrors,
|
||||||
@@ -62,7 +73,7 @@ data class Repository(
|
|||||||
fun newRepository(
|
fun newRepository(
|
||||||
address: String,
|
address: String,
|
||||||
fingerprint: String,
|
fingerprint: String,
|
||||||
authentication: String,
|
authentication: String
|
||||||
): Repository {
|
): Repository {
|
||||||
val name = try {
|
val name = try {
|
||||||
URL(address).let { "${it.host}${it.path}" }
|
URL(address).let { "${it.host}${it.path}" }
|
||||||
@@ -79,7 +90,7 @@ data class Repository(
|
|||||||
version: Int = 21,
|
version: Int = 21,
|
||||||
enabled: Boolean = false,
|
enabled: Boolean = false,
|
||||||
fingerprint: String,
|
fingerprint: String,
|
||||||
authentication: String = "",
|
authentication: String = ""
|
||||||
): Repository {
|
): Repository {
|
||||||
return Repository(
|
return Repository(
|
||||||
-1, address, emptyList(), name, description, version, enabled,
|
-1, address, emptyList(), name, description, version, enabled,
|
||||||
@@ -150,6 +161,14 @@ data class Repository(
|
|||||||
" by Netsyms Technologies.",
|
" by Netsyms Technologies.",
|
||||||
fingerprint = "2581BA7B32D3AB443180C4087CAB6A7E8FB258D3A6E98870ECB3C675E4D64489"
|
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(
|
defaultRepository(
|
||||||
address = "https://molly.im/fdroid/foss/fdroid/repo",
|
address = "https://molly.im/fdroid/foss/fdroid/repo",
|
||||||
name = "Molly",
|
name = "Molly",
|
||||||
@@ -339,7 +358,10 @@ data class Repository(
|
|||||||
name = "SimpleX Chat F-Droid",
|
name = "SimpleX Chat F-Droid",
|
||||||
description = "SimpleX Chat official F-Droid repository.",
|
description = "SimpleX Chat official F-Droid repository.",
|
||||||
fingerprint = "9F358FF284D1F71656A2BFAF0E005DEAE6AA14143720E089F11FF2DDCFEB01BA"
|
fingerprint = "9F358FF284D1F71656A2BFAF0E005DEAE6AA14143720E089F11FF2DDCFEB01BA"
|
||||||
),
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val newlyAdded = listOf<Repository>(
|
||||||
defaultRepository(
|
defaultRepository(
|
||||||
address = "https://f-droid.monerujo.io/fdroid/repo",
|
address = "https://f-droid.monerujo.io/fdroid/repo",
|
||||||
name = "Monerujo Wallet",
|
name = "Monerujo Wallet",
|
||||||
@@ -389,29 +411,5 @@ data class Repository(
|
|||||||
fingerprint = "5D87CE1FAD3772425C2A7ED987A57595A20B07543B9595A7FD2CED25DFF3CF12"
|
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.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager
|
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.database.Database
|
||||||
import com.looker.droidify.utility.extension.toInstalledItem
|
import com.looker.droidify.utility.extension.toInstalledItem
|
||||||
|
|
||||||
|
|||||||
@@ -6,34 +6,35 @@ import android.graphics.Color
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.core.app.NotificationCompat
|
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.BuildConfig
|
||||||
import com.looker.droidify.MainActivity
|
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.Release
|
||||||
import com.looker.droidify.model.Repository
|
import com.looker.droidify.model.Repository
|
||||||
import com.looker.droidify.network.DataSize
|
import com.looker.installer.InstallManager
|
||||||
import com.looker.droidify.network.Downloader
|
import com.looker.installer.model.InstallState
|
||||||
import com.looker.droidify.network.NetworkResponse
|
import com.looker.installer.model.installFrom
|
||||||
import com.looker.droidify.network.percentBy
|
import com.looker.installer.notification.createInstallNotification
|
||||||
import com.looker.droidify.network.validation.ValidationException
|
import com.looker.installer.notification.installNotification
|
||||||
import com.looker.droidify.utility.common.Constants
|
import com.looker.network.DataSize
|
||||||
import com.looker.droidify.utility.common.Constants.NOTIFICATION_CHANNEL_INSTALL
|
import com.looker.network.Downloader
|
||||||
import com.looker.droidify.utility.common.SdkCheck
|
import com.looker.network.NetworkResponse
|
||||||
import com.looker.droidify.utility.common.cache.Cache
|
import com.looker.network.validation.ValidationException
|
||||||
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 dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
@@ -50,7 +51,7 @@ import kotlinx.coroutines.sync.withLock
|
|||||||
import kotlinx.coroutines.yield
|
import kotlinx.coroutines.yield
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import com.looker.droidify.R.string as stringRes
|
import com.looker.core.common.R.string as stringRes
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class DownloadService : ConnectionService<DownloadService.Binder>() {
|
class DownloadService : ConnectionService<DownloadService.Binder>() {
|
||||||
@@ -174,7 +175,7 @@ class DownloadService : ConnectionService<DownloadService.Binder>() {
|
|||||||
)
|
)
|
||||||
createNotificationChannel(
|
createNotificationChannel(
|
||||||
id = NOTIFICATION_CHANNEL_INSTALL,
|
id = NOTIFICATION_CHANNEL_INSTALL,
|
||||||
name = getString(stringRes.install)
|
name = getString(R.string.install)
|
||||||
)
|
)
|
||||||
|
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
@@ -378,7 +379,7 @@ class DownloadService : ConnectionService<DownloadService.Binder>() {
|
|||||||
}
|
}
|
||||||
if (!started) {
|
if (!started) {
|
||||||
started = true
|
started = true
|
||||||
startServiceCompat()
|
startSelf()
|
||||||
}
|
}
|
||||||
val task = tasks.removeFirstOrNull() ?: return
|
val task = tasks.removeFirstOrNull() ?: return
|
||||||
with(stateNotificationBuilder) {
|
with(stateNotificationBuilder) {
|
||||||
|
|||||||
@@ -2,17 +2,17 @@ package com.looker.droidify.service
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import com.looker.droidify.utility.common.extension.calculateHash
|
import com.looker.core.common.extension.calculateHash
|
||||||
import com.looker.droidify.utility.common.extension.getPackageArchiveInfoCompat
|
import com.looker.core.common.extension.getPackageArchiveInfoCompat
|
||||||
import com.looker.droidify.utility.common.extension.singleSignature
|
import com.looker.core.common.extension.singleSignature
|
||||||
import com.looker.droidify.utility.common.extension.versionCodeCompat
|
import com.looker.core.common.extension.versionCodeCompat
|
||||||
import com.looker.droidify.network.validation.FileValidator
|
import com.looker.network.validation.FileValidator
|
||||||
import com.looker.droidify.utility.common.signature.Hash
|
import com.looker.core.common.signature.Hash
|
||||||
import com.looker.droidify.network.validation.invalid
|
import com.looker.network.validation.invalid
|
||||||
import com.looker.droidify.utility.common.signature.verifyHash
|
import com.looker.core.common.signature.verifyHash
|
||||||
import com.looker.droidify.model.Release
|
import com.looker.droidify.model.Release
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import com.looker.droidify.R.string as strings
|
import com.looker.core.common.R.string as strings
|
||||||
|
|
||||||
class ReleaseFileValidator(
|
class ReleaseFileValidator(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
|
|||||||
@@ -15,16 +15,17 @@ import android.text.style.ForegroundColorSpan
|
|||||||
import android.view.ContextThemeWrapper
|
import android.view.ContextThemeWrapper
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import com.looker.droidify.utility.common.Constants
|
import com.looker.core.common.Constants
|
||||||
import com.looker.droidify.utility.common.SdkCheck
|
import com.looker.core.common.SdkCheck
|
||||||
import com.looker.droidify.utility.common.createNotificationChannel
|
import com.looker.core.common.createNotificationChannel
|
||||||
import com.looker.droidify.utility.common.extension.getColorFromAttr
|
import com.looker.core.common.extension.getColorFromAttr
|
||||||
import com.looker.droidify.utility.common.extension.notificationManager
|
import com.looker.core.common.extension.notificationManager
|
||||||
import com.looker.droidify.utility.common.extension.startServiceCompat
|
import com.looker.core.common.extension.startSelf
|
||||||
import com.looker.droidify.utility.common.extension.stopForegroundCompat
|
import com.looker.core.common.extension.stopForegroundCompat
|
||||||
import com.looker.droidify.utility.common.result.Result
|
import com.looker.core.common.log
|
||||||
import com.looker.droidify.utility.common.sdkAbove
|
import com.looker.core.common.result.Result
|
||||||
import com.looker.droidify.datastore.SettingsRepository
|
import com.looker.core.common.sdkAbove
|
||||||
|
import com.looker.core.datastore.SettingsRepository
|
||||||
import com.looker.droidify.BuildConfig
|
import com.looker.droidify.BuildConfig
|
||||||
import com.looker.droidify.MainActivity
|
import com.looker.droidify.MainActivity
|
||||||
import com.looker.droidify.database.Database
|
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.ProductItem
|
||||||
import com.looker.droidify.model.Repository
|
import com.looker.droidify.model.Repository
|
||||||
import com.looker.droidify.utility.extension.startUpdate
|
import com.looker.droidify.utility.extension.startUpdate
|
||||||
import com.looker.droidify.network.DataSize
|
import com.looker.network.DataSize
|
||||||
import com.looker.droidify.network.percentBy
|
import com.looker.network.percentBy
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -51,12 +52,9 @@ import kotlinx.coroutines.sync.withLock
|
|||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.lang.ref.WeakReference
|
import java.lang.ref.WeakReference
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import com.looker.droidify.R
|
import com.looker.core.common.R as CommonR
|
||||||
import kotlinx.coroutines.FlowPreview
|
import com.looker.core.common.R.string as stringRes
|
||||||
import kotlin.math.roundToInt
|
import com.looker.core.common.R.style as styleRes
|
||||||
import android.R as AndroidR
|
|
||||||
import com.looker.droidify.R.string as stringRes
|
|
||||||
import com.looker.droidify.R.style as styleRes
|
|
||||||
import kotlinx.coroutines.Job as CoroutinesJob
|
import kotlinx.coroutines.Job as CoroutinesJob
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
@@ -70,16 +68,15 @@ class SyncService : ConnectionService<SyncService.Binder>() {
|
|||||||
private const val MAX_UPDATE_NOTIFICATION = 5
|
private const val MAX_UPDATE_NOTIFICATION = 5
|
||||||
private const val ACTION_CANCEL = "${BuildConfig.APPLICATION_ID}.intent.action.CANCEL"
|
private const val ACTION_CANCEL = "${BuildConfig.APPLICATION_ID}.intent.action.CANCEL"
|
||||||
|
|
||||||
val syncState = MutableSharedFlow<State>()
|
private val syncState = MutableSharedFlow<State>()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var settingsRepository: SettingsRepository
|
lateinit var settingsRepository: SettingsRepository
|
||||||
|
|
||||||
sealed class State(val name: String) {
|
sealed class State(val name: String) {
|
||||||
class Connecting(appName: String) : State(appName)
|
data class Connecting(val appName: String) : State(appName)
|
||||||
|
data class Syncing(
|
||||||
class Syncing(
|
|
||||||
val appName: String,
|
val appName: String,
|
||||||
val stage: RepositoryUpdater.Stage,
|
val stage: RepositoryUpdater.Stage,
|
||||||
val read: DataSize,
|
val read: DataSize,
|
||||||
@@ -87,18 +84,6 @@ class SyncService : ConnectionService<SyncService.Binder>() {
|
|||||||
) : State(appName)
|
) : State(appName)
|
||||||
|
|
||||||
data object Finish : State("")
|
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)
|
private class Task(val repositoryId: Long, val manual: Boolean)
|
||||||
@@ -140,7 +125,7 @@ class SyncService : ConnectionService<SyncService.Binder>() {
|
|||||||
handleNextTask(cancelledTask?.hasUpdates == true)
|
handleNextTask(cancelledTask?.hasUpdates == true)
|
||||||
if (request != SyncRequest.AUTO && started == Started.AUTO) {
|
if (request != SyncRequest.AUTO && started == Started.AUTO) {
|
||||||
started = Started.MANUAL
|
started = Started.MANUAL
|
||||||
startServiceCompat()
|
startSelf()
|
||||||
handleSetStarted()
|
handleSetStarted()
|
||||||
currentTask?.lastState?.let { publishForegroundState(true, it) }
|
currentTask?.lastState?.let { publishForegroundState(true, it) }
|
||||||
}
|
}
|
||||||
@@ -159,8 +144,7 @@ class SyncService : ConnectionService<SyncService.Binder>() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun updateAllApps() {
|
suspend fun updateAllApps() {
|
||||||
val skipSignature = settingsRepository.getInitial().ignoreSignature
|
updateAllAppsInternal()
|
||||||
updateAllAppsInternal(skipSignature)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setUpdateNotificationBlocker(fragment: Fragment?) {
|
fun setUpdateNotificationBlocker(fragment: Fragment?) {
|
||||||
@@ -213,7 +197,6 @@ class SyncService : ConnectionService<SyncService.Binder>() {
|
|||||||
private val binder = Binder()
|
private val binder = Binder()
|
||||||
override fun onBind(intent: Intent): Binder = binder
|
override fun onBind(intent: Intent): Binder = binder
|
||||||
|
|
||||||
@OptIn(FlowPreview::class)
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
|
||||||
@@ -293,10 +276,10 @@ class SyncService : ConnectionService<SyncService.Binder>() {
|
|||||||
Constants.NOTIFICATION_ID_SYNCING,
|
Constants.NOTIFICATION_ID_SYNCING,
|
||||||
NotificationCompat
|
NotificationCompat
|
||||||
.Builder(this, Constants.NOTIFICATION_CHANNEL_SYNCING)
|
.Builder(this, Constants.NOTIFICATION_CHANNEL_SYNCING)
|
||||||
.setSmallIcon(AndroidR.drawable.stat_sys_warning)
|
.setSmallIcon(android.R.drawable.stat_sys_warning)
|
||||||
.setColor(
|
.setColor(
|
||||||
ContextThemeWrapper(this, styleRes.Theme_Main_Light)
|
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))
|
.setContentTitle(getString(stringRes.could_not_sync_FORMAT, repository.name))
|
||||||
.setContentText(description)
|
.setContentText(description)
|
||||||
@@ -307,10 +290,10 @@ class SyncService : ConnectionService<SyncService.Binder>() {
|
|||||||
private val stateNotificationBuilder by lazy {
|
private val stateNotificationBuilder by lazy {
|
||||||
NotificationCompat
|
NotificationCompat
|
||||||
.Builder(this, Constants.NOTIFICATION_CHANNEL_SYNCING)
|
.Builder(this, Constants.NOTIFICATION_CHANNEL_SYNCING)
|
||||||
.setSmallIcon(R.drawable.ic_sync)
|
.setSmallIcon(CommonR.drawable.ic_sync)
|
||||||
.setColor(
|
.setColor(
|
||||||
ContextThemeWrapper(this, styleRes.Theme_Main_Light)
|
ContextThemeWrapper(this, styleRes.Theme_Main_Light)
|
||||||
.getColorFromAttr(AndroidR.attr.colorPrimary).defaultColor
|
.getColorFromAttr(android.R.attr.colorPrimary).defaultColor
|
||||||
)
|
)
|
||||||
.addAction(
|
.addAction(
|
||||||
0,
|
0,
|
||||||
@@ -385,7 +368,7 @@ class SyncService : ConnectionService<SyncService.Binder>() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
is State.Finish -> {}
|
is State.Finish -> {}
|
||||||
}
|
}::class
|
||||||
}.build()
|
}.build()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -405,8 +388,7 @@ class SyncService : ConnectionService<SyncService.Binder>() {
|
|||||||
handleUpdates(
|
handleUpdates(
|
||||||
hasUpdates = hasUpdates,
|
hasUpdates = hasUpdates,
|
||||||
notifyUpdates = setting.notifyUpdate,
|
notifyUpdates = setting.notifyUpdate,
|
||||||
autoUpdate = setting.autoUpdate,
|
autoUpdate = setting.autoUpdate
|
||||||
skipSignature = setting.ignoreSignature,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -423,7 +405,7 @@ class SyncService : ConnectionService<SyncService.Binder>() {
|
|||||||
}
|
}
|
||||||
started = newStarted
|
started = newStarted
|
||||||
if (newStarted == Started.MANUAL && lastStarted != Started.MANUAL) {
|
if (newStarted == Started.MANUAL && lastStarted != Started.MANUAL) {
|
||||||
startServiceCompat()
|
startSelf()
|
||||||
handleSetStarted()
|
handleSetStarted()
|
||||||
}
|
}
|
||||||
val initialState = State.Connecting(repository!!.name)
|
val initialState = State.Connecting(repository!!.name)
|
||||||
@@ -487,8 +469,7 @@ class SyncService : ConnectionService<SyncService.Binder>() {
|
|||||||
private suspend fun handleUpdates(
|
private suspend fun handleUpdates(
|
||||||
hasUpdates: Boolean,
|
hasUpdates: Boolean,
|
||||||
notifyUpdates: Boolean,
|
notifyUpdates: Boolean,
|
||||||
autoUpdate: Boolean,
|
autoUpdate: Boolean
|
||||||
skipSignature: Boolean,
|
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
if (!hasUpdates) {
|
if (!hasUpdates) {
|
||||||
@@ -499,16 +480,15 @@ class SyncService : ConnectionService<SyncService.Binder>() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
val blocked = updateNotificationBlockerFragment?.get()?.isAdded == true
|
val blocked = updateNotificationBlockerFragment?.get()?.isAdded == true
|
||||||
val updates = Database.ProductAdapter.getUpdates(skipSignature)
|
val updates = Database.ProductAdapter.getUpdates()
|
||||||
if (!blocked && updates.isNotEmpty()) {
|
if (!blocked && updates.isNotEmpty()) {
|
||||||
if (notifyUpdates) displayUpdatesNotification(updates)
|
if (notifyUpdates) displayUpdatesNotification(updates)
|
||||||
if (autoUpdate) updateAllAppsInternal(skipSignature)
|
if (autoUpdate) updateAllAppsInternal()
|
||||||
}
|
}
|
||||||
handleUpdates(
|
handleUpdates(
|
||||||
hasUpdates = false,
|
hasUpdates = false,
|
||||||
notifyUpdates = notifyUpdates,
|
notifyUpdates = notifyUpdates,
|
||||||
autoUpdate = autoUpdate,
|
autoUpdate = autoUpdate
|
||||||
skipSignature = skipSignature,
|
|
||||||
)
|
)
|
||||||
} finally {
|
} finally {
|
||||||
withContext(NonCancellable) {
|
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
|
Database.ProductAdapter
|
||||||
.getUpdates(skipSignature)
|
.getUpdates()
|
||||||
// Update Droid-ify the last
|
// Update Droid-ify the last
|
||||||
.sortedBy { if (it.packageName == packageName) 1 else -1 }
|
.sortedBy { if (it.packageName == packageName) 1 else -1 }
|
||||||
.map {
|
.map {
|
||||||
@@ -541,22 +522,23 @@ class SyncService : ConnectionService<SyncService.Binder>() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun displayUpdatesNotification(productItems: List<ProductItem>) {
|
private fun displayUpdatesNotification(productItems: List<ProductItem>) {
|
||||||
|
fun <T> T.applyHack(callback: T.() -> Unit): T = apply(callback)
|
||||||
notificationManager?.notify(
|
notificationManager?.notify(
|
||||||
Constants.NOTIFICATION_ID_UPDATES,
|
Constants.NOTIFICATION_ID_UPDATES,
|
||||||
NotificationCompat
|
NotificationCompat
|
||||||
.Builder(this, Constants.NOTIFICATION_CHANNEL_UPDATES)
|
.Builder(this, Constants.NOTIFICATION_CHANNEL_UPDATES)
|
||||||
.setSmallIcon(R.drawable.ic_new_releases)
|
.setSmallIcon(CommonR.drawable.ic_new_releases)
|
||||||
.setContentTitle(getString(stringRes.new_updates_available))
|
.setContentTitle(getString(stringRes.new_updates_available))
|
||||||
.setContentText(
|
.setContentText(
|
||||||
resources.getQuantityString(
|
resources.getQuantityString(
|
||||||
R.plurals.new_updates_DESC_FORMAT,
|
CommonR.plurals.new_updates_DESC_FORMAT,
|
||||||
productItems.size,
|
productItems.size,
|
||||||
productItems.size
|
productItems.size
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.setColor(
|
.setColor(
|
||||||
ContextThemeWrapper(this, styleRes.Theme_Main_Light)
|
ContextThemeWrapper(this, styleRes.Theme_Main_Light)
|
||||||
.getColorFromAttr(AndroidR.attr.colorPrimary).defaultColor
|
.getColorFromAttr(android.R.attr.colorPrimary).defaultColor
|
||||||
)
|
)
|
||||||
.setContentIntent(
|
.setContentIntent(
|
||||||
PendingIntent.getActivity(
|
PendingIntent.getActivity(
|
||||||
@@ -568,7 +550,7 @@ class SyncService : ConnectionService<SyncService.Binder>() {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
.setStyle(
|
.setStyle(
|
||||||
NotificationCompat.InboxStyle().also {
|
NotificationCompat.InboxStyle().applyHack {
|
||||||
for (productItem in productItems.take(MAX_UPDATE_NOTIFICATION)) {
|
for (productItem in productItems.take(MAX_UPDATE_NOTIFICATION)) {
|
||||||
val builder = SpannableStringBuilder(productItem.name)
|
val builder = SpannableStringBuilder(productItem.name)
|
||||||
builder.setSpan(
|
builder.setSpan(
|
||||||
@@ -578,7 +560,7 @@ class SyncService : ConnectionService<SyncService.Binder>() {
|
|||||||
SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE
|
SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||||
)
|
)
|
||||||
builder.append(' ').append(productItem.version)
|
builder.append(' ').append(productItem.version)
|
||||||
it.addLine(builder)
|
addLine(builder)
|
||||||
}
|
}
|
||||||
if (productItems.size > MAX_UPDATE_NOTIFICATION) {
|
if (productItems.size > MAX_UPDATE_NOTIFICATION) {
|
||||||
val summary =
|
val summary =
|
||||||
@@ -586,11 +568,7 @@ class SyncService : ConnectionService<SyncService.Binder>() {
|
|||||||
stringRes.plus_more_FORMAT,
|
stringRes.plus_more_FORMAT,
|
||||||
productItems.size - MAX_UPDATE_NOTIFICATION
|
productItems.size - MAX_UPDATE_NOTIFICATION
|
||||||
)
|
)
|
||||||
if (SdkCheck.isNougat) {
|
if (SdkCheck.isNougat) addLine(summary) else setSummaryText(summary)
|
||||||
it.addLine(summary)
|
|
||||||
} else {
|
|
||||||
it.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.DialogFragment
|
||||||
import androidx.fragment.app.FragmentManager
|
import androidx.fragment.app.FragmentManager
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import com.looker.droidify.utility.common.SdkCheck
|
import com.looker.core.common.SdkCheck
|
||||||
import com.looker.droidify.utility.common.nullIfEmpty
|
import com.looker.core.common.nullIfEmpty
|
||||||
import com.looker.droidify.model.Release
|
import com.looker.droidify.model.Release
|
||||||
import com.looker.droidify.ui.repository.RepositoryFragment
|
import com.looker.droidify.ui.repository.RepositoryFragment
|
||||||
import com.looker.droidify.utility.PackageItemResolver
|
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.Parceler
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import kotlinx.parcelize.TypeParceler
|
import kotlinx.parcelize.TypeParceler
|
||||||
import com.looker.droidify.R.string as stringRes
|
import com.looker.core.common.R.string as stringRes
|
||||||
|
|
||||||
class MessageDialog() : DialogFragment() {
|
class MessageDialog() : DialogFragment() {
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@@ -1,35 +1,20 @@
|
|||||||
package com.looker.droidify.ui.appDetail
|
package com.looker.droidify.ui.appDetail
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
import android.content.*
|
||||||
import android.content.Intent
|
|
||||||
import android.content.pm.PermissionGroupInfo
|
import android.content.pm.PermissionGroupInfo
|
||||||
import android.content.pm.PermissionInfo
|
import android.content.pm.PermissionInfo
|
||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
import android.graphics.Bitmap
|
import android.graphics.*
|
||||||
import android.graphics.Canvas
|
|
||||||
import android.graphics.Paint
|
|
||||||
import android.graphics.Typeface
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import android.text.SpannableStringBuilder
|
import android.text.SpannableStringBuilder
|
||||||
import android.text.format.DateFormat
|
import android.text.format.DateFormat
|
||||||
import android.text.method.LinkMovementMethod
|
import android.text.method.LinkMovementMethod
|
||||||
import android.text.style.BulletSpan
|
import android.text.style.*
|
||||||
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.util.Linkify
|
import android.text.util.Linkify
|
||||||
import android.view.Gravity
|
import android.view.*
|
||||||
import android.view.MotionEvent
|
import android.widget.*
|
||||||
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 androidx.annotation.DrawableRes
|
import androidx.annotation.DrawableRes
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
@@ -40,13 +25,17 @@ import androidx.core.text.util.LinkifyCompat
|
|||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import coil3.load
|
import coil.load
|
||||||
import com.google.android.material.button.MaterialButton
|
import com.google.android.material.button.MaterialButton
|
||||||
import com.google.android.material.card.MaterialCardView
|
import com.google.android.material.card.MaterialCardView
|
||||||
import com.google.android.material.imageview.ShapeableImageView
|
import com.google.android.material.imageview.ShapeableImageView
|
||||||
import com.google.android.material.materialswitch.MaterialSwitch
|
import com.google.android.material.materialswitch.MaterialSwitch
|
||||||
import com.google.android.material.progressindicator.LinearProgressIndicator
|
import com.google.android.material.progressindicator.LinearProgressIndicator
|
||||||
import com.google.android.material.snackbar.Snackbar
|
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.R
|
||||||
import com.looker.droidify.content.ProductPreferences
|
import com.looker.droidify.content.ProductPreferences
|
||||||
import com.looker.droidify.model.InstalledItem
|
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.Release
|
||||||
import com.looker.droidify.model.Repository
|
import com.looker.droidify.model.Repository
|
||||||
import com.looker.droidify.model.findSuggested
|
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.PackageItemResolver
|
||||||
import com.looker.droidify.utility.common.extension.authentication
|
import com.looker.droidify.utility.extension.ImageUtils.icon
|
||||||
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.android.Android
|
import com.looker.droidify.utility.extension.android.Android
|
||||||
import com.looker.droidify.utility.extension.resources.TypefaceExtra
|
import com.looker.droidify.utility.extension.resources.TypefaceExtra
|
||||||
import com.looker.droidify.utility.extension.resources.sizeScaled
|
import com.looker.droidify.utility.extension.resources.sizeScaled
|
||||||
@@ -88,8 +63,8 @@ import kotlin.math.PI
|
|||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
import kotlin.math.sin
|
import kotlin.math.sin
|
||||||
import com.google.android.material.R as MaterialR
|
import com.google.android.material.R as MaterialR
|
||||||
import com.looker.droidify.R.drawable as drawableRes
|
import com.looker.core.common.R.drawable as drawableRes
|
||||||
import com.looker.droidify.R.string as stringRes
|
import com.looker.core.common.R.string as stringRes
|
||||||
|
|
||||||
class AppDetailAdapter(private val callbacks: Callbacks) :
|
class AppDetailAdapter(private val callbacks: Callbacks) :
|
||||||
StableRecyclerAdapter<AppDetailAdapter.ViewType, RecyclerView.ViewHolder>() {
|
StableRecyclerAdapter<AppDetailAdapter.ViewType, RecyclerView.ViewHolder>() {
|
||||||
@@ -103,7 +78,7 @@ class AppDetailAdapter(private val callbacks: Callbacks) :
|
|||||||
fun onFavouriteClicked()
|
fun onFavouriteClicked()
|
||||||
fun onPreferenceChanged(preference: ProductPreference)
|
fun onPreferenceChanged(preference: ProductPreference)
|
||||||
fun onPermissionsClick(group: String?, permissions: List<String>)
|
fun onPermissionsClick(group: String?, permissions: List<String>)
|
||||||
fun onScreenshotClick(position: Int)
|
fun onScreenshotClick(screenshot: Product.Screenshot, parentView: ImageView)
|
||||||
fun onReleaseClick(release: Release)
|
fun onReleaseClick(release: Release)
|
||||||
fun onRequestAddRepository(address: String)
|
fun onRequestAddRepository(address: String)
|
||||||
fun onUriClick(uri: Uri, shouldConfirm: Boolean): Boolean
|
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.Regular -> drawableRes.ic_donate
|
||||||
is Product.Donate.Bitcoin -> drawableRes.ic_donate_bitcoin
|
is Product.Donate.Bitcoin -> drawableRes.ic_donate_bitcoin
|
||||||
is Product.Donate.Litecoin -> drawableRes.ic_donate_litecoin
|
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.Liberapay -> drawableRes.ic_donate_liberapay
|
||||||
is Product.Donate.OpenCollective -> drawableRes.ic_donate_opencollective
|
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.Regular -> context.getString(stringRes.website)
|
||||||
is Product.Donate.Bitcoin -> "Bitcoin"
|
is Product.Donate.Bitcoin -> "Bitcoin"
|
||||||
is Product.Donate.Litecoin -> "Litecoin"
|
is Product.Donate.Litecoin -> "Litecoin"
|
||||||
|
is Product.Donate.Flattr -> "Flattr"
|
||||||
is Product.Donate.Liberapay -> "Liberapay"
|
is Product.Donate.Liberapay -> "Liberapay"
|
||||||
is Product.Donate.OpenCollective -> "Open Collective"
|
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.Regular -> Uri.parse(donate.url)
|
||||||
is Product.Donate.Bitcoin -> Uri.parse("bitcoin:${donate.address}")
|
is Product.Donate.Bitcoin -> Uri.parse("bitcoin:${donate.address}")
|
||||||
is Product.Donate.Litecoin -> Uri.parse("litecoin:${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(
|
is Product.Donate.Liberapay -> Uri.parse(
|
||||||
"https://liberapay.com/${donate.id}"
|
"https://liberapay.com/~${donate.id}"
|
||||||
)
|
)
|
||||||
|
|
||||||
is Product.Donate.OpenCollective -> Uri.parse(
|
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 size = itemView.findViewById<TextView>(R.id.size)!!
|
||||||
val signature = itemView.findViewById<TextView>(R.id.signature)!!
|
val signature = itemView.findViewById<TextView>(R.id.signature)!!
|
||||||
val compatibility = itemView.findViewById<TextView>(R.id.compatibility)!!
|
val compatibility = itemView.findViewById<TextView>(R.id.compatibility)!!
|
||||||
val targetSdk = itemView.findViewById<TextView>(R.id.target_sdk)!!
|
|
||||||
|
|
||||||
val statefulViews: Sequence<View>
|
val statefulViews: Sequence<View>
|
||||||
get() = sequenceOf(
|
get() = sequenceOf(
|
||||||
@@ -568,8 +548,7 @@ class AppDetailAdapter(private val callbacks: Callbacks) :
|
|||||||
added,
|
added,
|
||||||
size,
|
size,
|
||||||
signature,
|
signature,
|
||||||
compatibility,
|
compatibility
|
||||||
targetSdk,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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 {
|
holder.dev.setOnClickListener {
|
||||||
product?.source?.let { link ->
|
product?.source?.let { link ->
|
||||||
@@ -1363,10 +1342,12 @@ class AppDetailAdapter(private val callbacks: Callbacks) :
|
|||||||
)
|
)
|
||||||
holder.progress.isIndeterminate = status.total == null
|
holder.progress.isIndeterminate = status.total == null
|
||||||
if (status.total != null) {
|
if (status.total != null) {
|
||||||
holder.progress.setProgressCompat(
|
holder.progress.progress =
|
||||||
status.read.value percentBy status.total.value,
|
(
|
||||||
true
|
holder.progress.max.toFloat() *
|
||||||
)
|
status.read.value /
|
||||||
|
status.total.value
|
||||||
|
).roundToInt()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1426,13 +1407,15 @@ class AppDetailAdapter(private val callbacks: Callbacks) :
|
|||||||
holder as ScreenShotViewHolder
|
holder as ScreenShotViewHolder
|
||||||
item as Item.ScreenshotItem
|
item as Item.ScreenshotItem
|
||||||
holder.screenshotsRecycler.run {
|
holder.screenshotsRecycler.run {
|
||||||
setHasFixedSize(true)
|
|
||||||
isNestedScrollingEnabled = false
|
isNestedScrollingEnabled = false
|
||||||
clipToPadding = false
|
clipToPadding = false
|
||||||
setPadding(8.dp, 8.dp, 8.dp, 8.dp)
|
setPadding(8.dp, 8.dp, 8.dp, 8.dp)
|
||||||
layoutManager =
|
layoutManager =
|
||||||
LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
|
LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
|
||||||
adapter = ScreenshotsAdapter(callbacks::onScreenshotClick).apply {
|
adapter =
|
||||||
|
ScreenshotsAdapter { screenshot, view ->
|
||||||
|
callbacks.onScreenshotClick(screenshot, view)
|
||||||
|
}.apply {
|
||||||
setScreenshots(item.repository, item.packageName, item.screenshots)
|
setScreenshots(item.repository, item.packageName, item.screenshots)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1620,7 +1603,7 @@ class AppDetailAdapter(private val callbacks: Callbacks) :
|
|||||||
holder.version.text =
|
holder.version.text =
|
||||||
context.getString(stringRes.version_FORMAT, item.release.version)
|
context.getString(stringRes.version_FORMAT, item.release.version)
|
||||||
|
|
||||||
with(holder.status) {
|
holder.status.apply {
|
||||||
isVisible = installed || suggested
|
isVisible = installed || suggested
|
||||||
setText(
|
setText(
|
||||||
when {
|
when {
|
||||||
@@ -1631,15 +1614,14 @@ class AppDetailAdapter(private val callbacks: Callbacks) :
|
|||||||
)
|
)
|
||||||
background = context.corneredBackground
|
background = context.corneredBackground
|
||||||
setPadding(15, 15, 15, 15)
|
setPadding(15, 15, 15, 15)
|
||||||
if (installed) {
|
val (background, foreground) = if (installed) {
|
||||||
backgroundTintList =
|
MaterialR.attr.colorSecondaryContainer to MaterialR.attr.colorOnSecondaryContainer
|
||||||
context.getColorFromAttr(MaterialR.attr.colorSecondaryContainer)
|
|
||||||
setTextColor(context.getColorFromAttr(MaterialR.attr.colorOnSecondaryContainer))
|
|
||||||
} else {
|
} else {
|
||||||
backgroundTintList =
|
MaterialR.attr.colorPrimaryContainer to MaterialR.attr.colorOnPrimaryContainer
|
||||||
context.getColorFromAttr(MaterialR.attr.colorPrimaryContainer)
|
|
||||||
setTextColor(context.getColorFromAttr(MaterialR.attr.colorOnPrimaryContainer))
|
|
||||||
}
|
}
|
||||||
|
backgroundTintList =
|
||||||
|
context.getColorFromAttr(background)
|
||||||
|
setTextColor(context.getColorFromAttr(foreground))
|
||||||
}
|
}
|
||||||
holder.source.text =
|
holder.source.text =
|
||||||
context.getString(stringRes.provided_by_FORMAT, item.repository.name)
|
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.dateFormat.format(item.release.added)
|
||||||
}
|
}
|
||||||
holder.added.text = dateFormat
|
holder.added.text = dateFormat
|
||||||
holder.size.text = DataSize(item.release.size).toString()
|
holder.size.text = item.release.size.formatSize()
|
||||||
holder.signature.isVisible =
|
holder.signature.isVisible =
|
||||||
item.showSignature && item.release.signature.isNotEmpty()
|
item.showSignature && item.release.signature.isNotEmpty()
|
||||||
if (item.showSignature && item.release.signature.isNotEmpty()) {
|
if (item.showSignature && item.release.signature.isNotEmpty()) {
|
||||||
@@ -1683,13 +1665,15 @@ class AppDetailAdapter(private val callbacks: Callbacks) :
|
|||||||
}
|
}
|
||||||
holder.signature.text = builder
|
holder.signature.text = builder
|
||||||
}
|
}
|
||||||
with(holder.compatibility) {
|
holder.compatibility.isVisible = incompatibility != null || singlePlatform != null
|
||||||
isVisible = incompatibility != null || singlePlatform != null
|
|
||||||
if (incompatibility != null) {
|
if (incompatibility != null) {
|
||||||
setTextColor(context.getColorFromAttr(MaterialR.attr.colorError))
|
holder.compatibility.setTextColor(
|
||||||
text = when (incompatibility) {
|
context.getColorFromAttr(MaterialR.attr.colorError)
|
||||||
|
)
|
||||||
|
holder.compatibility.text = when (incompatibility) {
|
||||||
is Release.Incompatibility.MinSdk,
|
is Release.Incompatibility.MinSdk,
|
||||||
is Release.Incompatibility.MaxSdk -> context.getString(
|
is Release.Incompatibility.MaxSdk
|
||||||
|
-> context.getString(
|
||||||
stringRes.incompatible_with_FORMAT,
|
stringRes.incompatible_with_FORMAT,
|
||||||
Android.name
|
Android.name
|
||||||
)
|
)
|
||||||
@@ -1705,22 +1689,11 @@ class AppDetailAdapter(private val callbacks: Callbacks) :
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else if (singlePlatform != null) {
|
} else if (singlePlatform != null) {
|
||||||
setTextColor(context.getColorFromAttr(android.R.attr.textColorSecondary))
|
holder.compatibility.setTextColor(
|
||||||
text = context.getString(
|
context.getColorFromAttr(android.R.attr.textColorSecondary)
|
||||||
stringRes.only_compatible_with_FORMAT,
|
|
||||||
singlePlatform,
|
|
||||||
)
|
)
|
||||||
}
|
holder.compatibility.text =
|
||||||
}
|
context.getString(stringRes.only_compatible_with_FORMAT, singlePlatform)
|
||||||
with(holder.targetSdk) {
|
|
||||||
val sdkVersion = sdkName.getOrDefault(
|
|
||||||
item.release.targetSdkVersion,
|
|
||||||
context.getString(
|
|
||||||
stringRes.label_unknown_sdk,
|
|
||||||
item.release.targetSdkVersion,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
text = context.getString(stringRes.label_targets_sdk, sdkVersion)
|
|
||||||
}
|
}
|
||||||
val enabled = status == Status.Idle
|
val enabled = status == Status.Idle
|
||||||
holder.statefulViews.forEach { it.isEnabled = enabled }
|
holder.statefulViews.forEach { it.isEnabled = enabled }
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package com.looker.droidify.ui.appDetail
|
package com.looker.droidify.ui.appDetail
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.ActivityNotFoundException
|
import android.content.ActivityNotFoundException
|
||||||
import android.content.ComponentName
|
import android.content.ComponentName
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
@@ -9,6 +8,7 @@ import android.os.Bundle
|
|||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import android.widget.ImageView
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.core.os.bundleOf
|
import androidx.core.os.bundleOf
|
||||||
@@ -20,13 +20,16 @@ import androidx.lifecycle.repeatOnLifecycle
|
|||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.recyclerview.widget.SimpleItemAnimator
|
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||||
import coil3.load
|
import coil.load
|
||||||
import coil3.request.allowHardware
|
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
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.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.InstalledItem
|
||||||
import com.looker.droidify.model.Product
|
import com.looker.droidify.model.Product
|
||||||
import com.looker.droidify.model.ProductPreference
|
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.ScreenFragment
|
||||||
import com.looker.droidify.ui.appDetail.AppDetailViewModel.Companion.ARG_PACKAGE_NAME
|
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.ui.appDetail.AppDetailViewModel.Companion.ARG_REPO_ADDRESS
|
||||||
import com.looker.droidify.utility.common.cache.Cache
|
import com.looker.droidify.utility.extension.ImageUtils.url
|
||||||
import com.looker.droidify.utility.common.extension.getLauncherActivities
|
import com.looker.droidify.utility.extension.screenActivity
|
||||||
import com.looker.droidify.utility.common.extension.getMutatedIcon
|
|
||||||
import com.looker.droidify.utility.common.extension.isFirstItemVisible
|
|
||||||
import com.looker.droidify.utility.common.extension.isSystemApplication
|
|
||||||
import com.looker.droidify.utility.common.extension.systemBarsPadding
|
|
||||||
import com.looker.droidify.utility.common.extension.updateAsMutable
|
|
||||||
import com.looker.droidify.utility.extension.mainActivity
|
|
||||||
import com.looker.droidify.utility.extension.startUpdate
|
import com.looker.droidify.utility.extension.startUpdate
|
||||||
|
import com.looker.installer.model.InstallState
|
||||||
|
import com.looker.installer.model.isCancellable
|
||||||
import com.stfalcon.imageviewer.StfalconImageViewer
|
import com.stfalcon.imageviewer.StfalconImageViewer
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import com.looker.droidify.R.string as stringRes
|
import com.looker.core.common.R.string as stringRes
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class AppDetailFragment() : ScreenFragment(), AppDetailAdapter.Callbacks {
|
class AppDetailFragment() : ScreenFragment(), AppDetailAdapter.Callbacks {
|
||||||
@@ -90,7 +89,6 @@ class AppDetailFragment() : ScreenFragment(), AppDetailAdapter.Callbacks {
|
|||||||
|
|
||||||
private val viewModel: AppDetailViewModel by viewModels()
|
private val viewModel: AppDetailViewModel by viewModels()
|
||||||
|
|
||||||
@SuppressLint("RestrictedApi")
|
|
||||||
private var layoutManagerState: LinearLayoutManager.SavedState? = null
|
private var layoutManagerState: LinearLayoutManager.SavedState? = null
|
||||||
|
|
||||||
private var actions = Pair(emptySet<Action>(), null as Action?)
|
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 recyclerView: RecyclerView? = null
|
||||||
private var detailAdapter: AppDetailAdapter? = null
|
private var detailAdapter: AppDetailAdapter? = null
|
||||||
private var imageViewer: StfalconImageViewer.Builder<Product.Screenshot>? = null
|
|
||||||
|
|
||||||
private val downloadConnection = Connection(
|
private val downloadConnection = Connection(
|
||||||
serviceClass = DownloadService::class.java,
|
serviceClass = DownloadService::class.java,
|
||||||
@@ -112,12 +109,11 @@ class AppDetailFragment() : ScreenFragment(), AppDetailAdapter.Callbacks {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@SuppressLint("RestrictedApi")
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
detailAdapter = AppDetailAdapter(this@AppDetailFragment)
|
detailAdapter = AppDetailAdapter(this@AppDetailFragment)
|
||||||
mainActivity.onToolbarCreated(toolbar)
|
screenActivity.onToolbarCreated(toolbar)
|
||||||
toolbar.menu.apply {
|
toolbar.menu.apply {
|
||||||
Action.entries.forEach { action ->
|
Action.entries.forEach { action ->
|
||||||
add(0, action.id, 0, action.adapterAction.titleResId)
|
add(0, action.id, 0, action.adapterAction.titleResId)
|
||||||
@@ -209,12 +205,10 @@ class AppDetailFragment() : ScreenFragment(), AppDetailAdapter.Callbacks {
|
|||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
recyclerView = null
|
recyclerView = null
|
||||||
detailAdapter = null
|
detailAdapter = null
|
||||||
imageViewer = null
|
|
||||||
|
|
||||||
downloadConnection.unbind(requireContext())
|
downloadConnection.unbind(requireContext())
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("RestrictedApi")
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
super.onSaveInstanceState(outState)
|
super.onSaveInstanceState(outState)
|
||||||
|
|
||||||
@@ -353,20 +347,10 @@ class AppDetailFragment() : ScreenFragment(), AppDetailAdapter.Callbacks {
|
|||||||
MessageDialog(Message.InsufficientStorage).show(childFragmentManager)
|
MessageDialog(Message.InsufficientStorage).show(childFragmentManager)
|
||||||
return
|
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(
|
downloadConnection.startUpdate(
|
||||||
packageName = viewModel.packageName,
|
viewModel.packageName,
|
||||||
installedItem = installed?.installedItem,
|
installed?.installedItem,
|
||||||
products = products,
|
products
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -452,27 +436,20 @@ class AppDetailFragment() : ScreenFragment(), AppDetailAdapter.Callbacks {
|
|||||||
.show(childFragmentManager)
|
.show(childFragmentManager)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onScreenshotClick(position: Int) {
|
override fun onScreenshotClick(screenshot: Product.Screenshot, parentView: ImageView) {
|
||||||
if (imageViewer == null) {
|
val product = products
|
||||||
val productRepository = products.findSuggested(installed?.installedItem) ?: return
|
.firstOrNull { (product, _) ->
|
||||||
val screenshots = productRepository.first.screenshots.mapNotNull {
|
product.screenshots.find { it === screenshot }?.identifier != null
|
||||||
if (it.type == Product.Screenshot.Type.VIDEO) null
|
|
||||||
else it
|
|
||||||
}
|
}
|
||||||
imageViewer = StfalconImageViewer
|
?: return
|
||||||
|
val screenshots = product.first.screenshots
|
||||||
|
val position = screenshots.indexOfFirst { screenshot.identifier == it.identifier }
|
||||||
|
StfalconImageViewer
|
||||||
.Builder(context, screenshots) { view, current ->
|
.Builder(context, screenshots) { view, current ->
|
||||||
val screenshotUrl = current.url(
|
view.load(current.url(product.second, viewModel.packageName))
|
||||||
context = requireContext(),
|
|
||||||
repository = productRepository.second,
|
|
||||||
packageName = viewModel.packageName
|
|
||||||
)
|
|
||||||
view.load(screenshotUrl) {
|
|
||||||
allowHardware(false)
|
|
||||||
}
|
}
|
||||||
}
|
.withStartPosition(position)
|
||||||
}
|
.show()
|
||||||
imageViewer?.withStartPosition(position)
|
|
||||||
imageViewer?.show()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onReleaseClick(release: Release) {
|
override fun onReleaseClick(release: Release) {
|
||||||
@@ -530,7 +507,7 @@ class AppDetailFragment() : ScreenFragment(), AppDetailAdapter.Callbacks {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onRequestAddRepository(address: String) {
|
override fun onRequestAddRepository(address: String) {
|
||||||
mainActivity.navigateAddRepository(address)
|
screenActivity.navigateAddRepository(address)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onUriClick(uri: Uri, shouldConfirm: Boolean): Boolean {
|
override fun onUriClick(uri: Uri, shouldConfirm: Boolean): Boolean {
|
||||||
|
|||||||
@@ -1,32 +1,25 @@
|
|||||||
package com.looker.droidify.ui.appDetail
|
package com.looker.droidify.ui.appDetail
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import androidx.lifecycle.SavedStateHandle
|
import androidx.lifecycle.SavedStateHandle
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.looker.droidify.utility.common.extension.asStateFlow
|
import com.looker.core.common.extension.asStateFlow
|
||||||
import com.looker.droidify.datastore.SettingsRepository
|
import com.looker.core.datastore.SettingsRepository
|
||||||
import com.looker.droidify.datastore.model.InstallerType
|
import com.looker.core.domain.model.toPackageName
|
||||||
import com.looker.droidify.domain.model.toPackageName
|
|
||||||
import com.looker.droidify.BuildConfig
|
import com.looker.droidify.BuildConfig
|
||||||
import com.looker.droidify.database.Database
|
import com.looker.droidify.database.Database
|
||||||
import com.looker.droidify.model.InstalledItem
|
import com.looker.droidify.model.InstalledItem
|
||||||
import com.looker.droidify.model.Product
|
import com.looker.droidify.model.Product
|
||||||
import com.looker.droidify.model.Repository
|
import com.looker.droidify.model.Repository
|
||||||
import com.looker.droidify.installer.InstallManager
|
import com.looker.installer.InstallManager
|
||||||
import com.looker.droidify.installer.installers.isShizukuAlive
|
import com.looker.installer.model.InstallState
|
||||||
import com.looker.droidify.installer.installers.isShizukuGranted
|
import com.looker.installer.model.installFrom
|
||||||
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 dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
import kotlinx.coroutines.flow.mapNotNull
|
import kotlinx.coroutines.flow.mapNotNull
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
@@ -69,31 +62,6 @@ class AppDetailViewModel @Inject constructor(
|
|||||||
)
|
)
|
||||||
}.asStateFlow(AppDetailUiState())
|
}.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 {
|
suspend fun shouldIgnoreSignature(): Boolean {
|
||||||
return settingsRepository.getInitial().ignoreSignature
|
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(
|
data class AppDetailUiState(
|
||||||
val products: List<Product> = emptyList(),
|
val products: List<Product> = emptyList(),
|
||||||
val repos: List<Repository> = emptyList(),
|
val repos: List<Repository> = emptyList(),
|
||||||
|
|||||||
@@ -5,71 +5,56 @@ import android.graphics.drawable.Drawable
|
|||||||
import android.view.Gravity
|
import android.view.Gravity
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
|
import android.widget.ImageView
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import coil3.asImage
|
import coil.dispose
|
||||||
import coil3.dispose
|
import coil.load
|
||||||
import coil3.load
|
import coil.size.Dimension
|
||||||
import coil3.request.placeholder
|
import coil.size.Scale
|
||||||
import coil3.size.Scale
|
|
||||||
import com.google.android.material.imageview.ShapeableImageView
|
import com.google.android.material.imageview.ShapeableImageView
|
||||||
import com.looker.droidify.databinding.VideoButtonBinding
|
import com.looker.core.common.extension.aspectRatio
|
||||||
import com.looker.droidify.graphics.PaddingDrawable
|
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.Product
|
||||||
import com.looker.droidify.model.Repository
|
import com.looker.droidify.model.Repository
|
||||||
import com.looker.droidify.utility.common.extension.aspectRatio
|
import com.looker.droidify.graphics.PaddingDrawable
|
||||||
import com.looker.droidify.utility.common.extension.authentication
|
import com.looker.droidify.utility.extension.ImageUtils.url
|
||||||
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.widget.StableRecyclerAdapter
|
import com.looker.droidify.widget.StableRecyclerAdapter
|
||||||
import com.google.android.material.R as MaterialR
|
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>() {
|
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(
|
private class ViewHolder(context: Context) :
|
||||||
binding: VideoButtonBinding,
|
RecyclerView.ViewHolder(FrameLayout(context)) {
|
||||||
) : RecyclerView.ViewHolder(binding.root) {
|
val image: ShapeableImageView = object : ShapeableImageView(context) {
|
||||||
val button = binding.videoButton
|
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||||
|
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
|
||||||
init {
|
setMeasuredDimension(measuredWidth, measuredHeight)
|
||||||
with(button) {
|
|
||||||
layoutParams = RecyclerView.LayoutParams(
|
|
||||||
RecyclerView.LayoutParams.WRAP_CONTENT,
|
|
||||||
150.dp,
|
|
||||||
)
|
|
||||||
setOnClickListener {
|
|
||||||
val item = items[absoluteAdapterPosition] as Item.VideoItem
|
|
||||||
it.context?.openLink(item.videoUrl)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private inner class ScreenshotViewHolder(
|
|
||||||
context: Context,
|
|
||||||
) : RecyclerView.ViewHolder(FrameLayout(context)) {
|
|
||||||
val image = ShapeableImageView(context)
|
|
||||||
val placeholderColor = context.getColorFromAttr(MaterialR.attr.colorPrimaryContainer)
|
val placeholderColor = context.getColorFromAttr(MaterialR.attr.colorPrimaryContainer)
|
||||||
val radius = context.resources.getDimension(dimenRes.shape_small_corner)
|
val radius = context.resources.getDimension(dimenRes.shape_small_corner)
|
||||||
|
|
||||||
val imageShapeModel = image.shapeAppearanceModel.toBuilder()
|
val imageShapeModel = image.shapeAppearanceModel.toBuilder()
|
||||||
.setAllCornerSizes(radius)
|
.setAllCornerSizes(radius)
|
||||||
.build()
|
.build()
|
||||||
val cameraIcon = context.camera.apply { setTintList(placeholderColor) }
|
val cameraIcon = context.camera
|
||||||
|
.apply { setTintList(placeholderColor) }
|
||||||
val placeholder: Drawable = PaddingDrawable(cameraIcon, 3f, context.aspectRatio)
|
val placeholder: Drawable = PaddingDrawable(cameraIcon, 3f, context.aspectRatio)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
with(image) {
|
with(image) {
|
||||||
layout(0, 0, 0, 0)
|
layout(0, 0, 0, 0)
|
||||||
|
adjustViewBounds = true
|
||||||
shapeAppearanceModel = imageShapeModel
|
shapeAppearanceModel = imageShapeModel
|
||||||
background = context.selectableBackground
|
background = context.selectableBackground
|
||||||
isFocusable = true
|
isFocusable = true
|
||||||
@@ -84,14 +69,6 @@ class ScreenshotsAdapter(private val onClick: (position: Int) -> Unit) :
|
|||||||
marginEnd = radius.toInt()
|
marginEnd = radius.toInt()
|
||||||
}
|
}
|
||||||
foregroundGravity = Gravity.CENTER
|
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(
|
fun setScreenshots(
|
||||||
repository: Repository,
|
repository: Repository,
|
||||||
packageName: String,
|
packageName: String,
|
||||||
screenshots: List<Product.Screenshot>,
|
screenshots: List<Product.Screenshot>
|
||||||
) {
|
) {
|
||||||
items.clear()
|
items.clear()
|
||||||
items += screenshots.map {
|
items += screenshots.map { Item.ScreenshotItem(repository, packageName, it) }
|
||||||
if (it.type == Product.Screenshot.Type.VIDEO) Item.VideoItem(it.path)
|
|
||||||
else Item.ScreenshotItem(repository, packageName, it)
|
|
||||||
}
|
|
||||||
notifyItemRangeInserted(0, screenshots.size)
|
notifyItemRangeInserted(0, screenshots.size)
|
||||||
}
|
}
|
||||||
|
|
||||||
override val viewTypeClass: Class<ViewType> get() = ViewType::class.java
|
override val viewTypeClass: Class<ViewType>
|
||||||
override fun getItemCount(): Int = items.size
|
get() = ViewType::class.java
|
||||||
override fun getItemEnumViewType(position: Int) = items[position].viewType
|
|
||||||
override fun getItemDescriptor(position: Int): String = items[position].descriptor
|
override fun getItemEnumViewType(position: Int): ViewType {
|
||||||
|
return ViewType.SCREENSHOT
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreateViewHolder(
|
override fun onCreateViewHolder(
|
||||||
parent: ViewGroup,
|
parent: ViewGroup,
|
||||||
viewType: ViewType,
|
viewType: ViewType
|
||||||
): RecyclerView.ViewHolder {
|
): RecyclerView.ViewHolder {
|
||||||
return when (viewType) {
|
return ViewHolder(parent.context).apply {
|
||||||
ViewType.VIDEO -> VideoViewHolder(VideoButtonBinding.inflate(parent.context.layoutInflater))
|
image.setOnClickListener {
|
||||||
ViewType.SCREENSHOT -> ScreenshotViewHolder(parent.context)
|
onClick(
|
||||||
|
items[absoluteAdapterPosition].screenshot,
|
||||||
|
it as ImageView
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemDescriptor(position: Int): String = items[position].descriptor
|
||||||
|
override fun getItemCount(): Int = items.size
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||||
when (getItemEnumViewType(position)) {
|
holder as ViewHolder
|
||||||
ViewType.SCREENSHOT -> {
|
val item = items[position]
|
||||||
holder as ScreenshotViewHolder
|
|
||||||
val item = items[position] as Item.ScreenshotItem
|
|
||||||
with(holder.image) {
|
with(holder.image) {
|
||||||
load(item.screenshot.url(context, item.repository, item.packageName)) {
|
load(item.screenshot.url(item.repository, item.packageName)) {
|
||||||
authentication(item.repository.authentication)
|
size(Dimension.Undefined, Dimension(150.dp.dpToPx.toInt()))
|
||||||
scale(Scale.FILL)
|
scale(Scale.FIT)
|
||||||
placeholder(holder.placeholder)
|
placeholder(holder.placeholder)
|
||||||
error(holder.placeholder.asImage())
|
error(holder.placeholder)
|
||||||
|
authentication(item.repository.authentication)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ViewType.VIDEO -> {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewRecycled(holder: RecyclerView.ViewHolder) {
|
override fun onViewRecycled(holder: RecyclerView.ViewHolder) {
|
||||||
super.onViewRecycled(holder)
|
super.onViewRecycled(holder)
|
||||||
if (holder is ScreenshotViewHolder) holder.image.dispose()
|
holder as ViewHolder
|
||||||
|
holder.image.dispose()
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed interface Item {
|
private sealed class Item {
|
||||||
|
abstract val descriptor: String
|
||||||
val descriptor: String
|
|
||||||
val viewType: ViewType
|
|
||||||
|
|
||||||
class ScreenshotItem(
|
class ScreenshotItem(
|
||||||
val repository: Repository,
|
val repository: Repository,
|
||||||
val packageName: String,
|
val packageName: String,
|
||||||
val screenshot: Product.Screenshot,
|
val screenshot: Product.Screenshot
|
||||||
) : Item {
|
) : Item() {
|
||||||
override val viewType: ViewType get() = ViewType.SCREENSHOT
|
|
||||||
override val descriptor: String
|
override val descriptor: String
|
||||||
get() = "screenshot.${repository.id}.${screenshot.identifier}"
|
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.annotation.SuppressLint
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.res.ColorStateList
|
|
||||||
import android.view.Gravity
|
import android.view.Gravity
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
@@ -10,44 +9,37 @@ import android.widget.FrameLayout
|
|||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
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.imageview.ShapeableImageView
|
||||||
import com.google.android.material.progressindicator.CircularProgressIndicator
|
import com.google.android.material.progressindicator.CircularProgressIndicator
|
||||||
import com.looker.droidify.R
|
import com.looker.core.common.extension.authentication
|
||||||
import com.looker.droidify.database.Database
|
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.ProductItem
|
||||||
import com.looker.droidify.model.Repository
|
import com.looker.droidify.model.Repository
|
||||||
import com.looker.droidify.utility.common.extension.authentication
|
import com.looker.droidify.R
|
||||||
import com.looker.droidify.utility.common.extension.corneredBackground
|
import com.looker.droidify.database.Database
|
||||||
import com.looker.droidify.utility.common.extension.dp
|
import com.looker.droidify.utility.extension.ImageUtils.icon
|
||||||
import com.looker.droidify.utility.common.extension.getColorFromAttr
|
|
||||||
import com.looker.droidify.utility.common.extension.inflate
|
|
||||||
import com.looker.droidify.utility.common.extension.setTextSizeScaled
|
|
||||||
import com.looker.droidify.utility.common.log
|
|
||||||
import com.looker.droidify.utility.common.nullIfEmpty
|
|
||||||
import com.looker.droidify.utility.extension.resources.TypefaceExtra
|
import com.looker.droidify.utility.extension.resources.TypefaceExtra
|
||||||
import com.looker.droidify.widget.CursorRecyclerAdapter
|
import com.looker.droidify.widget.CursorRecyclerAdapter
|
||||||
import kotlin.system.measureTimeMillis
|
|
||||||
import com.google.android.material.R as MaterialR
|
|
||||||
|
|
||||||
class AppListAdapter(
|
class AppListAdapter(
|
||||||
private val source: AppListFragment.Source,
|
private val source: AppListFragment.Source,
|
||||||
private val onClick: (packageName: String) -> Unit,
|
private val onClick: (ProductItem) -> Unit
|
||||||
) : CursorRecyclerAdapter<AppListAdapter.ViewType, RecyclerView.ViewHolder>() {
|
) : CursorRecyclerAdapter<AppListAdapter.ViewType, RecyclerView.ViewHolder>() {
|
||||||
|
|
||||||
enum class ViewType { PRODUCT, LOADING, EMPTY }
|
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 name = itemView.findViewById<TextView>(R.id.name)!!
|
||||||
val status = itemView.findViewById<TextView>(R.id.status)!!
|
val status = itemView.findViewById<TextView>(R.id.status)!!
|
||||||
val summary = itemView.findViewById<TextView>(R.id.summary)!!
|
val summary = itemView.findViewById<TextView>(R.id.summary)!!
|
||||||
val icon = itemView.findViewById<ShapeableImageView>(R.id.icon)!!
|
val icon = itemView.findViewById<ShapeableImageView>(R.id.icon)!!
|
||||||
|
|
||||||
init {
|
|
||||||
itemView.setOnClickListener {
|
|
||||||
log(measureTimeMillis { onClick(getPackageName(absoluteAdapterPosition)) }, "Bench")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private class LoadingViewHolder(context: Context) :
|
private class LoadingViewHolder(context: Context) :
|
||||||
@@ -55,14 +47,7 @@ class AppListAdapter(
|
|||||||
init {
|
init {
|
||||||
with(itemView as FrameLayout) {
|
with(itemView as FrameLayout) {
|
||||||
val progressBar = CircularProgressIndicator(context)
|
val progressBar = CircularProgressIndicator(context)
|
||||||
progressBar.isIndeterminate = true
|
addView(progressBar)
|
||||||
addView(
|
|
||||||
progressBar,
|
|
||||||
FrameLayout.LayoutParams(
|
|
||||||
FrameLayout.LayoutParams.WRAP_CONTENT,
|
|
||||||
FrameLayout.LayoutParams.WRAP_CONTENT,
|
|
||||||
).apply { gravity = Gravity.CENTER }
|
|
||||||
)
|
|
||||||
layoutParams = RecyclerView.LayoutParams(
|
layoutParams = RecyclerView.LayoutParams(
|
||||||
RecyclerView.LayoutParams.MATCH_PARENT,
|
RecyclerView.LayoutParams.MATCH_PARENT,
|
||||||
RecyclerView.LayoutParams.MATCH_PARENT
|
RecyclerView.LayoutParams.MATCH_PARENT
|
||||||
@@ -91,12 +76,10 @@ class AppListAdapter(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val repositories: HashMap<Long, Repository> = HashMap()
|
var repositories: Map<Long, Repository> = emptyMap()
|
||||||
|
@SuppressLint("NotifyDataSetChanged")
|
||||||
fun updateRepos(repos: List<Repository>) {
|
set(value) {
|
||||||
repos.forEach {
|
field = value
|
||||||
repositories[it.id] = it
|
|
||||||
}
|
|
||||||
notifyDataSetChanged()
|
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 {
|
private fun getProductItem(position: Int): ProductItem {
|
||||||
return Database.ProductAdapter.transformItem(moveTo(position.coerceAtLeast(0)))
|
return Database.ProductAdapter.transformItem(moveTo(position.coerceAtLeast(0)))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateViewHolder(
|
override fun onCreateViewHolder(
|
||||||
parent: ViewGroup,
|
parent: ViewGroup,
|
||||||
viewType: ViewType,
|
viewType: ViewType
|
||||||
): RecyclerView.ViewHolder {
|
): RecyclerView.ViewHolder {
|
||||||
return when (viewType) {
|
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.LOADING -> LoadingViewHolder(parent.context)
|
||||||
ViewType.EMPTY -> EmptyViewHolder(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) {
|
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||||
when (getItemEnumViewType(position)) {
|
when (getItemEnumViewType(position)) {
|
||||||
ViewType.PRODUCT -> {
|
ViewType.PRODUCT -> {
|
||||||
@@ -160,9 +136,9 @@ class AppListAdapter(
|
|||||||
val productItem = getProductItem(position)
|
val productItem = getProductItem(position)
|
||||||
holder.name.text = productItem.name
|
holder.name.text = productItem.name
|
||||||
holder.summary.text = productItem.summary
|
holder.summary.text = productItem.summary
|
||||||
holder.summary.isVisible = productItem.summary.isNotEmpty()
|
holder.summary.isVisible =
|
||||||
&& productItem.name != productItem.summary
|
productItem.summary.isNotEmpty() && productItem.name != productItem.summary
|
||||||
val repository = repositories[productItem.repositoryId]
|
val repository: Repository? = repositories[productItem.repositoryId]
|
||||||
if (repository != null) {
|
if (repository != null) {
|
||||||
val iconUrl = productItem.icon(view = holder.icon, repository = repository)
|
val iconUrl = productItem.icon(view = holder.icon, repository = repository)
|
||||||
holder.icon.load(iconUrl) {
|
holder.icon.load(iconUrl) {
|
||||||
@@ -179,38 +155,28 @@ class AppListAdapter(
|
|||||||
val isInstalled = productItem.installedVersion.nullIfEmpty() != null
|
val isInstalled = productItem.installedVersion.nullIfEmpty() != null
|
||||||
when {
|
when {
|
||||||
productItem.canUpdate -> {
|
productItem.canUpdate -> {
|
||||||
if (updateBackground == null) {
|
backgroundTintList =
|
||||||
updateBackground =
|
|
||||||
context.getColorFromAttr(MaterialR.attr.colorTertiaryContainer)
|
context.getColorFromAttr(MaterialR.attr.colorTertiaryContainer)
|
||||||
}
|
setTextColor(
|
||||||
if (updateForeground == null) {
|
|
||||||
updateForeground =
|
|
||||||
context.getColorFromAttr(MaterialR.attr.colorOnTertiaryContainer)
|
context.getColorFromAttr(MaterialR.attr.colorOnTertiaryContainer)
|
||||||
}
|
)
|
||||||
backgroundTintList = updateBackground
|
|
||||||
setTextColor(updateForeground)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
isInstalled -> {
|
isInstalled -> {
|
||||||
if (installedBackground == null) {
|
backgroundTintList =
|
||||||
installedBackground =
|
|
||||||
context.getColorFromAttr(MaterialR.attr.colorSecondaryContainer)
|
context.getColorFromAttr(MaterialR.attr.colorSecondaryContainer)
|
||||||
}
|
setTextColor(
|
||||||
if (installedForeground == null) {
|
|
||||||
installedForeground =
|
|
||||||
context.getColorFromAttr(MaterialR.attr.colorOnSecondaryContainer)
|
context.getColorFromAttr(MaterialR.attr.colorOnSecondaryContainer)
|
||||||
}
|
)
|
||||||
backgroundTintList = installedBackground
|
|
||||||
setTextColor(installedForeground)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
setPadding(0, 0, 0, 0)
|
setPadding(0, 0, 0, 0)
|
||||||
if (defaultForeground == null) {
|
setTextColor(
|
||||||
defaultForeground =
|
holder.status.context.getColorFromAttr(
|
||||||
context.getColorFromAttr(MaterialR.attr.colorOnBackground)
|
MaterialR.attr.colorOnBackground
|
||||||
}
|
)
|
||||||
setTextColor(defaultForeground)
|
)
|
||||||
background = null
|
background = null
|
||||||
return@with
|
return@with
|
||||||
}
|
}
|
||||||
@@ -219,9 +185,9 @@ class AppListAdapter(
|
|||||||
6.dp.let { setPadding(it, it, it, it) }
|
6.dp.let { setPadding(it, it, it, it) }
|
||||||
}
|
}
|
||||||
val enabled = productItem.compatible || productItem.installedVersion.isNotEmpty()
|
val enabled = productItem.compatible || productItem.installedVersion.isNotEmpty()
|
||||||
holder.name.isEnabled = enabled
|
sequenceOf(holder.name, holder.status, holder.summary).forEach {
|
||||||
holder.status.isEnabled = enabled
|
it.isEnabled = enabled
|
||||||
holder.summary.isEnabled = enabled
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ViewType.LOADING -> {
|
ViewType.LOADING -> {
|
||||||
@@ -232,6 +198,6 @@ class AppListAdapter(
|
|||||||
holder as EmptyViewHolder
|
holder as EmptyViewHolder
|
||||||
holder.text.text = emptyText
|
holder.text.text = emptyText
|
||||||
}
|
}
|
||||||
}
|
}::class
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,19 +14,19 @@ import androidx.lifecycle.lifecycleScope
|
|||||||
import androidx.lifecycle.repeatOnLifecycle
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
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.database.CursorOwner
|
||||||
import com.looker.droidify.databinding.RecyclerViewWithFabBinding
|
import com.looker.droidify.databinding.RecyclerViewWithFabBinding
|
||||||
import com.looker.droidify.model.ProductItem
|
import com.looker.droidify.utility.extension.screenActivity
|
||||||
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 dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import com.looker.droidify.R.string as stringRes
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class AppListFragment() : Fragment(), CursorOwner.Callback {
|
class AppListFragment() : Fragment(), CursorOwner.Callback {
|
||||||
@@ -46,7 +46,7 @@ class AppListFragment() : Fragment(), CursorOwner.Callback {
|
|||||||
val titleResId: Int,
|
val titleResId: Int,
|
||||||
val sections: Boolean,
|
val sections: Boolean,
|
||||||
val order: Boolean,
|
val order: Boolean,
|
||||||
val updateAll: Boolean,
|
val updateAll: Boolean
|
||||||
) {
|
) {
|
||||||
AVAILABLE(stringRes.available, true, true, false),
|
AVAILABLE(stringRes.available, true, true, false),
|
||||||
INSTALLED(stringRes.installed, false, 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)
|
get() = requireArguments().getString(EXTRA_SOURCE)!!.let(Source::valueOf)
|
||||||
|
|
||||||
private lateinit var recyclerView: RecyclerView
|
private lateinit var recyclerView: RecyclerView
|
||||||
private lateinit var appListAdapter: AppListAdapter
|
private lateinit var recyclerViewAdapter: AppListAdapter
|
||||||
private var scroller: Scroller? = null
|
|
||||||
private var shortAnimationDuration: Int = 0
|
private var shortAnimationDuration: Int = 0
|
||||||
private var layoutManagerState: Parcelable? = null
|
private var layoutManagerState: Parcelable? = null
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
container: ViewGroup?,
|
container: ViewGroup?,
|
||||||
savedInstanceState: Bundle?,
|
savedInstanceState: Bundle?
|
||||||
): View {
|
): View {
|
||||||
_binding = RecyclerViewWithFabBinding.inflate(inflater, container, false)
|
_binding = RecyclerViewWithFabBinding.inflate(inflater, container, false)
|
||||||
|
|
||||||
@@ -84,16 +83,18 @@ class AppListFragment() : Fragment(), CursorOwner.Callback {
|
|||||||
isMotionEventSplittingEnabled = false
|
isMotionEventSplittingEnabled = false
|
||||||
setHasFixedSize(true)
|
setHasFixedSize(true)
|
||||||
recycledViewPool.setMaxRecycledViews(AppListAdapter.ViewType.PRODUCT.ordinal, 30)
|
recycledViewPool.setMaxRecycledViews(AppListAdapter.ViewType.PRODUCT.ordinal, 30)
|
||||||
appListAdapter = AppListAdapter(source, mainActivity::navigateProduct)
|
recyclerViewAdapter = AppListAdapter(source) {
|
||||||
adapter = appListAdapter
|
screenActivity.navigateProduct(it.packageName)
|
||||||
|
}
|
||||||
|
adapter = recyclerViewAdapter
|
||||||
systemBarsPadding()
|
systemBarsPadding()
|
||||||
}
|
}
|
||||||
val fab = binding.scrollUp
|
val fab = binding.scrollUp
|
||||||
with(fab) {
|
with(fab) {
|
||||||
if (source.updateAll) {
|
if (source.updateAll) {
|
||||||
text = getString(R.string.update_all)
|
text = getString(CommonR.string.update_all)
|
||||||
setOnClickListener { viewModel.updateAll() }
|
setOnClickListener { viewModel.updateAll() }
|
||||||
setIconResource(R.drawable.ic_download)
|
setIconResource(CommonR.drawable.ic_download)
|
||||||
alpha = 1f
|
alpha = 1f
|
||||||
viewLifecycleOwner.lifecycleScope.launch {
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
viewModel.showUpdateAllButton.collect {
|
viewModel.showUpdateAllButton.collect {
|
||||||
@@ -102,13 +103,11 @@ class AppListFragment() : Fragment(), CursorOwner.Callback {
|
|||||||
}
|
}
|
||||||
systemBarsMargin(16.dp)
|
systemBarsMargin(16.dp)
|
||||||
} else {
|
} else {
|
||||||
text = null
|
text = ""
|
||||||
setIconResource(R.drawable.arrow_up)
|
setIconResource(CommonR.drawable.arrow_up)
|
||||||
setOnClickListener {
|
setOnClickListener {
|
||||||
if (scroller == null) {
|
val scroller = Scroller(requireContext())
|
||||||
scroller = Scroller(requireContext())
|
scroller.targetPosition = 0
|
||||||
}
|
|
||||||
scroller!!.targetPosition = 0
|
|
||||||
recyclerView.layoutManager?.startSmoothScroll(scroller)
|
recyclerView.layoutManager?.startSmoothScroll(scroller)
|
||||||
}
|
}
|
||||||
alpha = 0f
|
alpha = 0f
|
||||||
@@ -139,7 +138,7 @@ class AppListFragment() : Fragment(), CursorOwner.Callback {
|
|||||||
repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
||||||
launch {
|
launch {
|
||||||
viewModel.reposStream.collect { repos ->
|
viewModel.reposStream.collect { repos ->
|
||||||
appListAdapter.updateRepos(repos)
|
recyclerViewAdapter.repositories = repos.associateBy { it.id }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
launch {
|
launch {
|
||||||
@@ -161,13 +160,12 @@ class AppListFragment() : Fragment(), CursorOwner.Callback {
|
|||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
viewModel.syncConnection.unbind(requireContext())
|
viewModel.syncConnection.unbind(requireContext())
|
||||||
_binding = null
|
_binding = null
|
||||||
scroller = null
|
screenActivity.cursorOwner.detach(this)
|
||||||
mainActivity.cursorOwner.detach(this)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCursorData(request: CursorOwner.Request, cursor: Cursor?) {
|
override fun onCursorData(request: CursorOwner.Request, cursor: Cursor?) {
|
||||||
appListAdapter.cursor = cursor
|
recyclerViewAdapter.cursor = cursor
|
||||||
appListAdapter.emptyText = when {
|
recyclerViewAdapter.emptyText = when {
|
||||||
cursor == null -> ""
|
cursor == null -> ""
|
||||||
viewModel.searchQuery.value.isNotEmpty() -> {
|
viewModel.searchQuery.value.isNotEmpty() -> {
|
||||||
getString(stringRes.no_matching_applications_found)
|
getString(stringRes.no_matching_applications_found)
|
||||||
@@ -199,7 +197,7 @@ class AppListFragment() : Fragment(), CursorOwner.Callback {
|
|||||||
|
|
||||||
private fun updateRequest() {
|
private fun updateRequest() {
|
||||||
if (view != null) {
|
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.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.looker.droidify.database.CursorOwner
|
import com.looker.core.common.extension.asStateFlow
|
||||||
import com.looker.droidify.database.CursorOwner.Request.Available
|
import com.looker.core.datastore.SettingsRepository
|
||||||
import com.looker.droidify.database.CursorOwner.Request.Installed
|
import com.looker.core.datastore.get
|
||||||
import com.looker.droidify.database.CursorOwner.Request.Updates
|
import com.looker.core.datastore.model.SortOrder
|
||||||
import com.looker.droidify.database.Database
|
|
||||||
import com.looker.droidify.datastore.SettingsRepository
|
|
||||||
import com.looker.droidify.datastore.get
|
|
||||||
import com.looker.droidify.datastore.model.SortOrder
|
|
||||||
import com.looker.droidify.model.ProductItem
|
import com.looker.droidify.model.ProductItem
|
||||||
import com.looker.droidify.model.ProductItem.Section.All
|
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.Connection
|
||||||
import com.looker.droidify.service.SyncService
|
import com.looker.droidify.service.SyncService
|
||||||
import com.looker.droidify.utility.common.extension.asStateFlow
|
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import javax.inject.Inject
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.flatMapConcat
|
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class AppListViewModel
|
class AppListViewModel
|
||||||
@Inject constructor(
|
@Inject constructor(
|
||||||
settingsRepository: SettingsRepository,
|
settingsRepository: SettingsRepository
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
private val skipSignatureStream = settingsRepository
|
|
||||||
.get { ignoreSignature }
|
|
||||||
.asStateFlow(false)
|
|
||||||
|
|
||||||
val sortOrderFlow = settingsRepository
|
|
||||||
.get { sortOrder }
|
|
||||||
.asStateFlow(SortOrder.UPDATED)
|
|
||||||
|
|
||||||
val reposStream = Database.RepositoryAdapter
|
val reposStream = Database.RepositoryAdapter
|
||||||
.getAllStream()
|
.getAllStream()
|
||||||
.asStateFlow(emptyList())
|
.asStateFlow(emptyList())
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
val showUpdateAllButton = Database.ProductAdapter
|
||||||
val showUpdateAllButton = skipSignatureStream.flatMapConcat { skip ->
|
.getUpdatesStream()
|
||||||
Database.ProductAdapter
|
|
||||||
.getUpdatesStream(skip)
|
|
||||||
.map { it.isNotEmpty() }
|
.map { it.isNotEmpty() }
|
||||||
}.asStateFlow(false)
|
.asStateFlow(false)
|
||||||
|
|
||||||
|
val sortOrderFlow = settingsRepository.get { sortOrder }
|
||||||
|
.asStateFlow(SortOrder.UPDATED)
|
||||||
|
|
||||||
private val sections = MutableStateFlow<ProductItem.Section>(All)
|
private val sections = MutableStateFlow<ProductItem.Section>(All)
|
||||||
|
|
||||||
@@ -63,23 +51,22 @@ class AppListViewModel
|
|||||||
|
|
||||||
fun request(source: AppListFragment.Source): CursorOwner.Request {
|
fun request(source: AppListFragment.Source): CursorOwner.Request {
|
||||||
return when (source) {
|
return when (source) {
|
||||||
AppListFragment.Source.AVAILABLE -> Available(
|
AppListFragment.Source.AVAILABLE -> CursorOwner.Request.ProductsAvailable(
|
||||||
searchQuery = searchQuery.value,
|
searchQuery.value,
|
||||||
section = sections.value,
|
sections.value,
|
||||||
order = sortOrderFlow.value,
|
sortOrderFlow.value
|
||||||
)
|
)
|
||||||
|
|
||||||
AppListFragment.Source.INSTALLED -> Installed(
|
AppListFragment.Source.INSTALLED -> CursorOwner.Request.ProductsInstalled(
|
||||||
searchQuery = searchQuery.value,
|
searchQuery.value,
|
||||||
section = sections.value,
|
sections.value,
|
||||||
order = sortOrderFlow.value,
|
sortOrderFlow.value
|
||||||
)
|
)
|
||||||
|
|
||||||
AppListFragment.Source.UPDATES -> Updates(
|
AppListFragment.Source.UPDATES -> CursorOwner.Request.ProductsUpdates(
|
||||||
searchQuery = searchQuery.value,
|
searchQuery.value,
|
||||||
section = sections.value,
|
sections.value,
|
||||||
order = sortOrderFlow.value,
|
sortOrderFlow.value
|
||||||
skipSignatureCheck = skipSignatureStream.value,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,16 +4,17 @@ import android.view.LayoutInflater
|
|||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import coil3.load
|
import coil.load
|
||||||
import com.looker.droidify.databinding.ProductItemBinding
|
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.Product
|
||||||
import com.looker.droidify.model.Repository
|
import com.looker.droidify.model.Repository
|
||||||
import com.looker.droidify.utility.common.extension.authentication
|
import com.looker.droidify.databinding.ProductItemBinding
|
||||||
import com.looker.droidify.utility.common.extension.corneredBackground
|
import com.looker.droidify.utility.extension.ImageUtils.icon
|
||||||
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
|
|
||||||
|
|
||||||
class FavouriteFragmentAdapter(
|
class FavouriteFragmentAdapter(
|
||||||
private val onProductClick: (String) -> Unit
|
private val onProductClick: (String) -> Unit
|
||||||
|
|||||||
@@ -11,11 +11,11 @@ import androidx.lifecycle.lifecycleScope
|
|||||||
import androidx.lifecycle.repeatOnLifecycle
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.looker.droidify.R
|
import com.looker.core.common.R as CommonR
|
||||||
import com.looker.droidify.utility.common.extension.systemBarsPadding
|
import com.looker.core.common.extension.systemBarsPadding
|
||||||
import com.looker.droidify.database.Database
|
import com.looker.droidify.database.Database
|
||||||
import com.looker.droidify.ui.ScreenFragment
|
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 dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -43,7 +43,7 @@ class FavouritesFragment : ScreenFragment() {
|
|||||||
isVerticalScrollBarEnabled = false
|
isVerticalScrollBarEnabled = false
|
||||||
setHasFixedSize(true)
|
setHasFixedSize(true)
|
||||||
recyclerViewAdapter =
|
recyclerViewAdapter =
|
||||||
FavouriteFragmentAdapter { mainActivity.navigateProduct(it) }
|
FavouriteFragmentAdapter { screenActivity.navigateProduct(it) }
|
||||||
this.adapter = recyclerViewAdapter
|
this.adapter = recyclerViewAdapter
|
||||||
systemBarsPadding(includeFab = false)
|
systemBarsPadding(includeFab = false)
|
||||||
recyclerView = this
|
recyclerView = this
|
||||||
@@ -68,12 +68,12 @@ class FavouritesFragment : ScreenFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
toolbar.title = getString(R.string.favourites)
|
toolbar.title = getString(CommonR.string.favourites)
|
||||||
return view
|
return view
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
mainActivity.onToolbarCreated(toolbar)
|
screenActivity.onToolbarCreated(toolbar)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,21 @@
|
|||||||
package com.looker.droidify.ui.favourites
|
package com.looker.droidify.ui.favourites
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import com.looker.droidify.database.Database
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.looker.droidify.datastore.SettingsRepository
|
import com.looker.core.common.extension.asStateFlow
|
||||||
import com.looker.droidify.datastore.get
|
import com.looker.core.datastore.SettingsRepository
|
||||||
|
import com.looker.core.datastore.get
|
||||||
import com.looker.droidify.model.Product
|
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 dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import javax.inject.Inject
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import javax.inject.Inject
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class FavouritesViewModel @Inject constructor(
|
class FavouritesViewModel @Inject constructor(
|
||||||
settingsRepository: SettingsRepository,
|
private val settingsRepository: SettingsRepository
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
val favouriteApps: StateFlow<List<List<Product>>> =
|
val favouriteApps: StateFlow<List<List<Product>>> =
|
||||||
@@ -25,4 +27,9 @@ class FavouritesViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
}.asStateFlow(emptyList())
|
}.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 androidx.lifecycle.lifecycleScope
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import com.google.android.material.snackbar.Snackbar
|
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.database.Database
|
||||||
import com.looker.droidify.databinding.EditRepositoryBinding
|
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.Connection
|
||||||
import com.looker.droidify.service.SyncService
|
import com.looker.droidify.service.SyncService
|
||||||
import com.looker.droidify.ui.Message
|
import com.looker.droidify.ui.Message
|
||||||
import com.looker.droidify.ui.MessageDialog
|
import com.looker.droidify.ui.MessageDialog
|
||||||
import com.looker.droidify.ui.ScreenFragment
|
import com.looker.droidify.ui.ScreenFragment
|
||||||
import com.looker.droidify.utility.common.extension.clipboardManager
|
import com.looker.droidify.utility.extension.screenActivity
|
||||||
import com.looker.droidify.utility.common.extension.get
|
import com.looker.network.Downloader
|
||||||
import com.looker.droidify.utility.common.extension.getMutatedIcon
|
import com.looker.network.NetworkResponse
|
||||||
import com.looker.droidify.utility.common.nullIfEmpty
|
|
||||||
import com.looker.droidify.utility.extension.mainActivity
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.awaitAll
|
||||||
import kotlinx.coroutines.coroutineScope
|
import kotlinx.coroutines.coroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
@@ -43,7 +44,8 @@ import java.nio.charset.Charset
|
|||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlin.math.min
|
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
|
@AndroidEntryPoint
|
||||||
class EditRepositoryFragment() : ScreenFragment() {
|
class EditRepositoryFragment() : ScreenFragment() {
|
||||||
@@ -80,12 +82,14 @@ class EditRepositoryFragment() : ScreenFragment() {
|
|||||||
|
|
||||||
syncConnection.bind(requireContext())
|
syncConnection.bind(requireContext())
|
||||||
|
|
||||||
mainActivity.onToolbarCreated(toolbar)
|
screenActivity.onToolbarCreated(toolbar)
|
||||||
toolbar.title =
|
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)
|
saveMenuItem = toolbar.menu.add(stringRes.save)
|
||||||
.setIcon(toolbar.context.getMutatedIcon(R.drawable.ic_save))
|
.setIcon(toolbar.context.getMutatedIcon(CommonR.drawable.ic_save))
|
||||||
.setEnabled(false)
|
.setEnabled(false)
|
||||||
.setShowAsActionFlags(MenuItem.SHOW_AS_ACTION_ALWAYS).setOnMenuItemClickListener {
|
.setShowAsActionFlags(MenuItem.SHOW_AS_ACTION_ALWAYS).setOnMenuItemClickListener {
|
||||||
onSaveRepositoryClick(true)
|
onSaveRepositoryClick(true)
|
||||||
@@ -167,7 +171,7 @@ class EditRepositoryFragment() : ScreenFragment() {
|
|||||||
val mirrors = repository.mirrors.map { it.withoutKnownPath }
|
val mirrors = repository.mirrors.map { it.withoutKnownPath }
|
||||||
binding.addressContainer.apply {
|
binding.addressContainer.apply {
|
||||||
isEndIconVisible = mirrors.isNotEmpty()
|
isEndIconVisible = mirrors.isNotEmpty()
|
||||||
setEndIconDrawable(R.drawable.ic_arrow_down)
|
setEndIconDrawable(CommonR.drawable.ic_arrow_down)
|
||||||
setEndIconOnClickListener {
|
setEndIconOnClickListener {
|
||||||
SelectMirrorDialog(mirrors).show(
|
SelectMirrorDialog(mirrors).show(
|
||||||
childFragmentManager,
|
childFragmentManager,
|
||||||
@@ -236,8 +240,6 @@ class EditRepositoryFragment() : ScreenFragment() {
|
|||||||
|
|
||||||
saveMenuItem = null
|
saveMenuItem = null
|
||||||
syncConnection.unbind(requireContext())
|
syncConnection.unbind(requireContext())
|
||||||
checkJob?.cancel()
|
|
||||||
checkJob = null
|
|
||||||
_binding = null
|
_binding = null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -392,22 +394,22 @@ class EditRepositoryFragment() : ScreenFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun checkAddress(
|
private suspend fun checkAddress(
|
||||||
rawAddress: String,
|
address: String,
|
||||||
authentication: String
|
authentication: String
|
||||||
): String? = coroutineScope {
|
): String? = coroutineScope {
|
||||||
checkInProgress = true
|
checkInProgress = true
|
||||||
invalidateState()
|
invalidateState()
|
||||||
val allAddresses = addressSuffixes.map { "$rawAddress/$it" } + rawAddress
|
val allAddresses = addressSuffixes.map { "$address/$it" } + address
|
||||||
allAddresses
|
val pathCheck = allAddresses.map {
|
||||||
.sortedBy { it.length }
|
async {
|
||||||
.forEach { address ->
|
downloader.headCall(
|
||||||
val response = downloader.headCall(
|
url = "$it/index-v1.jar",
|
||||||
url = "$address/index-v1.jar",
|
|
||||||
headers = { authentication(authentication) }
|
headers = { authentication(authentication) }
|
||||||
)
|
) is NetworkResponse.Success
|
||||||
if (response is NetworkResponse.Success) return@coroutineScope address
|
|
||||||
}
|
}
|
||||||
null
|
}
|
||||||
|
val indexOfValidAddress = pathCheck.awaitAll().indexOf(true)
|
||||||
|
allAddresses[indexOfValidAddress].nullIfEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onSaveRepositoryProceedInvalidate(
|
private fun onSaveRepositoryProceedInvalidate(
|
||||||
@@ -429,7 +431,7 @@ class EditRepositoryFragment() : ScreenFragment() {
|
|||||||
if (repositoryId == null && changedRepository.enabled) {
|
if (repositoryId == null && changedRepository.enabled) {
|
||||||
binder.sync(changedRepository)
|
binder.sync(changedRepository)
|
||||||
}
|
}
|
||||||
mainActivity.onBackPressedDispatcher.onBackPressed()
|
screenActivity.onBackPressedDispatcher.onBackPressed()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
invalidateState()
|
invalidateState()
|
||||||
@@ -441,7 +443,7 @@ class EditRepositoryFragment() : ScreenFragment() {
|
|||||||
invalidateState()
|
invalidateState()
|
||||||
Snackbar.make(
|
Snackbar.make(
|
||||||
requireView(),
|
requireView(),
|
||||||
R.string.repository_unreachable,
|
CommonR.string.repository_unreachable,
|
||||||
Snackbar.LENGTH_SHORT
|
Snackbar.LENGTH_SHORT
|
||||||
).show()
|
).show()
|
||||||
}
|
}
|
||||||
@@ -468,6 +470,6 @@ class EditRepositoryFragment() : ScreenFragment() {
|
|||||||
const val EXTRA_REPOSITORY_ID = "repositoryId"
|
const val EXTRA_REPOSITORY_ID = "repositoryId"
|
||||||
const val EXTRA_REPOSITORY_ADDRESS = "repositoryAddress"
|
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.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import com.looker.droidify.R
|
import com.looker.core.common.R as CommonR
|
||||||
import com.looker.droidify.utility.common.extension.dp
|
import com.looker.core.common.extension.dp
|
||||||
import com.looker.droidify.utility.common.extension.systemBarsMargin
|
import com.looker.core.common.extension.systemBarsMargin
|
||||||
import com.looker.droidify.utility.common.extension.systemBarsPadding
|
import com.looker.core.common.extension.systemBarsPadding
|
||||||
import com.looker.droidify.database.CursorOwner
|
import com.looker.droidify.database.CursorOwner
|
||||||
import com.looker.droidify.databinding.RecyclerViewWithFabBinding
|
import com.looker.droidify.databinding.RecyclerViewWithFabBinding
|
||||||
import com.looker.droidify.service.Connection
|
import com.looker.droidify.service.Connection
|
||||||
import com.looker.droidify.service.SyncService
|
import com.looker.droidify.service.SyncService
|
||||||
import com.looker.droidify.ui.ScreenFragment
|
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
|
import com.looker.droidify.widget.addDivider
|
||||||
|
|
||||||
class RepositoriesFragment : ScreenFragment(), CursorOwner.Callback {
|
class RepositoriesFragment : ScreenFragment(), CursorOwner.Callback {
|
||||||
@@ -34,9 +34,9 @@ class RepositoriesFragment : ScreenFragment(), CursorOwner.Callback {
|
|||||||
_binding = RecyclerViewWithFabBinding.inflate(inflater, container, false)
|
_binding = RecyclerViewWithFabBinding.inflate(inflater, container, false)
|
||||||
val view = fragmentBinding.root.apply {
|
val view = fragmentBinding.root.apply {
|
||||||
binding.scrollUp.apply {
|
binding.scrollUp.apply {
|
||||||
setIconResource(R.drawable.ic_add)
|
setIconResource(CommonR.drawable.ic_add)
|
||||||
setText(R.string.add_repository)
|
setText(CommonR.string.add_repository)
|
||||||
setOnClickListener { mainActivity.navigateAddRepository() }
|
setOnClickListener { screenActivity.navigateAddRepository() }
|
||||||
systemBarsMargin(16.dp)
|
systemBarsMargin(16.dp)
|
||||||
}
|
}
|
||||||
binding.recyclerView.apply {
|
binding.recyclerView.apply {
|
||||||
@@ -44,7 +44,7 @@ class RepositoriesFragment : ScreenFragment(), CursorOwner.Callback {
|
|||||||
isMotionEventSplittingEnabled = false
|
isMotionEventSplittingEnabled = false
|
||||||
setHasFixedSize(true)
|
setHasFixedSize(true)
|
||||||
adapter = RepositoriesAdapter(
|
adapter = RepositoriesAdapter(
|
||||||
navigate = { mainActivity.navigateRepository(it.id) }
|
navigate = { screenActivity.navigateRepository(it.id) }
|
||||||
) { repository, isEnabled ->
|
) { repository, isEnabled ->
|
||||||
repository.enabled != isEnabled &&
|
repository.enabled != isEnabled &&
|
||||||
syncConnection.binder?.setEnabled(repository, isEnabled) == true
|
syncConnection.binder?.setEnabled(repository, isEnabled) == true
|
||||||
@@ -79,9 +79,9 @@ class RepositoriesFragment : ScreenFragment(), CursorOwner.Callback {
|
|||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
syncConnection.bind(requireContext())
|
syncConnection.bind(requireContext())
|
||||||
mainActivity.cursorOwner.attach(this, CursorOwner.Request.Repositories)
|
screenActivity.cursorOwner.attach(this, CursorOwner.Request.Repositories)
|
||||||
mainActivity.onToolbarCreated(toolbar)
|
screenActivity.onToolbarCreated(toolbar)
|
||||||
toolbar.title = getString(R.string.repositories)
|
toolbar.title = getString(CommonR.string.repositories)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroyView() {
|
override fun onDestroyView() {
|
||||||
@@ -89,7 +89,7 @@ class RepositoriesFragment : ScreenFragment(), CursorOwner.Callback {
|
|||||||
|
|
||||||
_binding = null
|
_binding = null
|
||||||
syncConnection.unbind(requireContext())
|
syncConnection.unbind(requireContext())
|
||||||
mainActivity.cursorOwner.detach(this)
|
screenActivity.cursorOwner.detach(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCursorData(request: CursorOwner.Request, cursor: Cursor?) {
|
override fun onCursorData(request: CursorOwner.Request, cursor: Cursor?) {
|
||||||
|
|||||||
@@ -15,21 +15,21 @@ import androidx.fragment.app.viewModels
|
|||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.lifecycle.repeatOnLifecycle
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import com.looker.droidify.utility.common.extension.getColorFromAttr
|
import com.looker.core.common.extension.getColorFromAttr
|
||||||
import com.looker.droidify.utility.common.extension.systemBarsPadding
|
import com.looker.core.common.extension.systemBarsPadding
|
||||||
import com.looker.droidify.model.Repository
|
import com.looker.droidify.model.Repository
|
||||||
import com.looker.droidify.databinding.RepositoryPageBinding
|
import com.looker.droidify.databinding.RepositoryPageBinding
|
||||||
import com.looker.droidify.ui.Message
|
import com.looker.droidify.ui.Message
|
||||||
import com.looker.droidify.ui.MessageDialog
|
import com.looker.droidify.ui.MessageDialog
|
||||||
import com.looker.droidify.ui.ScreenFragment
|
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 dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import com.google.android.material.R as MaterialR
|
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
|
@AndroidEntryPoint
|
||||||
class RepositoryFragment() : ScreenFragment() {
|
class RepositoryFragment() : ScreenFragment() {
|
||||||
@@ -53,7 +53,7 @@ class RepositoryFragment() : ScreenFragment() {
|
|||||||
super.onCreateView(inflater, container, savedInstanceState)
|
super.onCreateView(inflater, container, savedInstanceState)
|
||||||
_binding = RepositoryPageBinding.inflate(inflater, container, false)
|
_binding = RepositoryPageBinding.inflate(inflater, container, false)
|
||||||
viewModel.bindService(requireContext())
|
viewModel.bindService(requireContext())
|
||||||
mainActivity.onToolbarCreated(toolbar)
|
screenActivity.onToolbarCreated(toolbar)
|
||||||
toolbar.title = getString(stringRes.repository)
|
toolbar.title = getString(stringRes.repository)
|
||||||
val scroll = NestedScrollView(binding.root.context)
|
val scroll = NestedScrollView(binding.root.context)
|
||||||
scroll.addView(binding.root)
|
scroll.addView(binding.root)
|
||||||
@@ -149,7 +149,7 @@ class RepositoryFragment() : ScreenFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
editRepoButton.setOnClickListener {
|
editRepoButton.setOnClickListener {
|
||||||
mainActivity.navigateEditRepository(viewModel.id)
|
screenActivity.navigateEditRepository(viewModel.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteRepoButton.setOnClickListener {
|
deleteRepoButton.setOnClickListener {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import android.content.Context
|
|||||||
import androidx.lifecycle.SavedStateHandle
|
import androidx.lifecycle.SavedStateHandle
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
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.model.Repository
|
||||||
import com.looker.droidify.database.Database
|
import com.looker.droidify.database.Database
|
||||||
import com.looker.droidify.service.Connection
|
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.dialog.MaterialAlertDialogBuilder
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import com.google.android.material.textfield.TextInputEditText
|
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.BuildConfig
|
||||||
import com.looker.droidify.R
|
|
||||||
import com.looker.droidify.databinding.EnumTypeBinding
|
import com.looker.droidify.databinding.EnumTypeBinding
|
||||||
import com.looker.droidify.databinding.SettingsPageBinding
|
import com.looker.droidify.databinding.SettingsPageBinding
|
||||||
import com.looker.droidify.databinding.SwitchTypeBinding
|
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 dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
import kotlin.time.Duration.Companion.days
|
import kotlin.time.Duration.Companion.days
|
||||||
import kotlin.time.Duration.Companion.hours
|
import kotlin.time.Duration.Companion.hours
|
||||||
import com.google.android.material.R as MaterialR
|
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
|
@AndroidEntryPoint
|
||||||
class SettingsFragment : Fragment() {
|
class SettingsFragment : Fragment() {
|
||||||
@@ -64,7 +66,7 @@ class SettingsFragment : Fragment() {
|
|||||||
private const val REPO_BACKUP_NAME = "droidify_repos"
|
private const val REPO_BACKUP_NAME = "droidify_repos"
|
||||||
private const val SETTINGS_BACKUP_NAME = "droidify_settings"
|
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()
|
.toList()
|
||||||
.updateAsMutable { add(0, "system") }
|
.updateAsMutable { add(0, "system") }
|
||||||
|
|
||||||
@@ -91,7 +93,7 @@ class SettingsFragment : Fragment() {
|
|||||||
if (fileUri != null) {
|
if (fileUri != null) {
|
||||||
viewModel.importSettings(fileUri)
|
viewModel.importSettings(fileUri)
|
||||||
} else {
|
} 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) {
|
if (fileUri != null) {
|
||||||
viewModel.importRepos(fileUri)
|
viewModel.importRepos(fileUri)
|
||||||
} else {
|
} 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 {
|
): View {
|
||||||
_binding = SettingsPageBinding.inflate(inflater, container, false)
|
_binding = SettingsPageBinding.inflate(inflater, container, false)
|
||||||
binding.nestedScrollView.systemBarsPadding()
|
binding.nestedScrollView.systemBarsPadding()
|
||||||
viewModel.toggleBackgroundAccess(requireContext().isIgnoreBatteryEnabled())
|
if (requireContext().isIgnoreBatteryEnabled()) {
|
||||||
|
viewModel.allowBackground()
|
||||||
|
}
|
||||||
val toolbar = binding.toolbar
|
val toolbar = binding.toolbar
|
||||||
toolbar.navigationIcon = toolbar.context.homeAsUp
|
toolbar.navigationIcon = toolbar.context.homeAsUp
|
||||||
toolbar.setNavigationOnClickListener { activity?.onBackPressedDispatcher?.onBackPressed() }
|
toolbar.setNavigationOnClickListener { activity?.onBackPressedDispatcher?.onBackPressed() }
|
||||||
toolbar.title = getString(R.string.settings)
|
toolbar.title = getString(CommonR.string.settings)
|
||||||
with(binding) {
|
with(binding) {
|
||||||
dynamicTheme.root.isVisible = SdkCheck.isSnowCake
|
dynamicTheme.root.isVisible = SdkCheck.isSnowCake
|
||||||
dynamicTheme.connect(
|
dynamicTheme.connect(
|
||||||
titleText = getString(R.string.material_you),
|
titleText = getString(CommonR.string.material_you),
|
||||||
contentText = getString(R.string.material_you_desc),
|
contentText = getString(CommonR.string.material_you_desc),
|
||||||
setting = viewModel.getInitialSetting { dynamicTheme }
|
setting = viewModel.getInitialSetting { dynamicTheme }
|
||||||
)
|
)
|
||||||
homeScreenSwiping.connect(
|
homeScreenSwiping.connect(
|
||||||
titleText = getString(R.string.home_screen_swiping),
|
titleText = getString(CommonR.string.home_screen_swiping),
|
||||||
contentText = getString(R.string.home_screen_swiping_DESC),
|
contentText = getString(CommonR.string.home_screen_swiping_DESC),
|
||||||
setting = viewModel.getInitialSetting { homeScreenSwiping }
|
setting = viewModel.getInitialSetting { homeScreenSwiping }
|
||||||
)
|
)
|
||||||
autoUpdate.connect(
|
autoUpdate.connect(
|
||||||
titleText = getString(R.string.auto_update),
|
titleText = getString(CommonR.string.auto_update),
|
||||||
contentText = getString(R.string.auto_update_apps),
|
contentText = getString(CommonR.string.auto_update_apps),
|
||||||
setting = viewModel.getInitialSetting { autoUpdate }
|
setting = viewModel.getInitialSetting { autoUpdate }
|
||||||
)
|
)
|
||||||
notifyUpdates.connect(
|
notifyUpdates.connect(
|
||||||
titleText = getString(R.string.notify_about_updates),
|
titleText = getString(CommonR.string.notify_about_updates),
|
||||||
contentText = getString(R.string.notify_about_updates_summary),
|
contentText = getString(CommonR.string.notify_about_updates_summary),
|
||||||
setting = viewModel.getInitialSetting { notifyUpdate }
|
setting = viewModel.getInitialSetting { notifyUpdate }
|
||||||
)
|
)
|
||||||
unstableUpdates.connect(
|
unstableUpdates.connect(
|
||||||
titleText = getString(R.string.unstable_updates),
|
titleText = getString(CommonR.string.unstable_updates),
|
||||||
contentText = getString(R.string.unstable_updates_summary),
|
contentText = getString(CommonR.string.unstable_updates_summary),
|
||||||
setting = viewModel.getInitialSetting { unstableUpdate }
|
setting = viewModel.getInitialSetting { unstableUpdate }
|
||||||
)
|
)
|
||||||
ignoreSignature.connect(
|
ignoreSignature.connect(
|
||||||
titleText = getString(R.string.ignore_signature),
|
titleText = getString(CommonR.string.ignore_signature),
|
||||||
contentText = getString(R.string.ignore_signature_summary),
|
contentText = getString(CommonR.string.ignore_signature_summary),
|
||||||
setting = viewModel.getInitialSetting { ignoreSignature }
|
setting = viewModel.getInitialSetting { ignoreSignature }
|
||||||
)
|
)
|
||||||
incompatibleUpdates.connect(
|
incompatibleUpdates.connect(
|
||||||
titleText = getString(R.string.incompatible_versions),
|
titleText = getString(CommonR.string.incompatible_versions),
|
||||||
contentText = getString(R.string.incompatible_versions_summary),
|
contentText = getString(CommonR.string.incompatible_versions_summary),
|
||||||
setting = viewModel.getInitialSetting { incompatibleVersions }
|
setting = viewModel.getInitialSetting { incompatibleVersions }
|
||||||
)
|
)
|
||||||
language.connect(
|
language.connect(
|
||||||
titleText = getString(R.string.prefs_language_title),
|
titleText = getString(CommonR.string.prefs_language_title),
|
||||||
map = { translateLocale(getLocaleOfCode(it)) },
|
map = { translateLocale(getLocaleOfCode(it)) },
|
||||||
setting = viewModel.getSetting { language }
|
setting = viewModel.getSetting { language }
|
||||||
) { selectedLocale, valueToString ->
|
) { selectedLocale, valueToString ->
|
||||||
addSingleCorrectDialog(
|
addSingleCorrectDialog(
|
||||||
initialValue = selectedLocale,
|
initialValue = selectedLocale,
|
||||||
values = localeCodesList,
|
values = localeCodesList,
|
||||||
title = R.string.prefs_language_title,
|
title = CommonR.string.prefs_language_title,
|
||||||
iconRes = R.drawable.ic_language,
|
iconRes = CommonR.drawable.ic_language,
|
||||||
valueToString = valueToString,
|
valueToString = valueToString,
|
||||||
onClick = viewModel::setLanguage
|
onClick = viewModel::setLanguage
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
theme.connect(
|
theme.connect(
|
||||||
titleText = getString(R.string.theme),
|
titleText = getString(CommonR.string.theme),
|
||||||
setting = viewModel.getSetting { theme },
|
setting = viewModel.getSetting { theme },
|
||||||
map = { themeName(it) }
|
map = { themeName(it) }
|
||||||
) { theme, valueToString ->
|
) { theme, valueToString ->
|
||||||
addSingleCorrectDialog(
|
addSingleCorrectDialog(
|
||||||
initialValue = theme,
|
initialValue = theme,
|
||||||
values = Theme.entries,
|
values = Theme.entries,
|
||||||
title = R.string.themes,
|
title = CommonR.string.themes,
|
||||||
iconRes = R.drawable.ic_themes,
|
iconRes = CommonR.drawable.ic_themes,
|
||||||
valueToString = valueToString,
|
valueToString = valueToString,
|
||||||
onClick = viewModel::setTheme
|
onClick = viewModel::setTheme
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
cleanUp.connect(
|
cleanUp.connect(
|
||||||
titleText = getString(R.string.cleanup_title),
|
titleText = getString(CommonR.string.cleanup_title),
|
||||||
setting = viewModel.getSetting { cleanUpInterval },
|
setting = viewModel.getSetting { cleanUpInterval },
|
||||||
map = { toTime(it) }
|
map = { toTime(it) }
|
||||||
) { duration, valueToString ->
|
) { duration, valueToString ->
|
||||||
addSingleCorrectDialog(
|
addSingleCorrectDialog(
|
||||||
initialValue = duration,
|
initialValue = duration,
|
||||||
values = cleanUpIntervals,
|
values = cleanUpIntervals,
|
||||||
title = R.string.cleanup_title,
|
title = CommonR.string.cleanup_title,
|
||||||
iconRes = R.drawable.ic_time,
|
iconRes = CommonR.drawable.ic_time,
|
||||||
valueToString = valueToString,
|
valueToString = valueToString,
|
||||||
onClick = viewModel::setCleanUpInterval
|
onClick = viewModel::setCleanUpInterval
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
autoSync.connect(
|
autoSync.connect(
|
||||||
titleText = getString(R.string.sync_repositories_automatically),
|
titleText = getString(CommonR.string.sync_repositories_automatically),
|
||||||
setting = viewModel.getSetting { autoSync },
|
setting = viewModel.getSetting { autoSync },
|
||||||
map = { autoSyncName(it) }
|
map = { autoSyncName(it) }
|
||||||
) { autoSync, valueToString ->
|
) { autoSync, valueToString ->
|
||||||
addSingleCorrectDialog(
|
addSingleCorrectDialog(
|
||||||
initialValue = autoSync,
|
initialValue = autoSync,
|
||||||
values = AutoSync.entries,
|
values = AutoSync.entries,
|
||||||
title = R.string.sync_repositories_automatically,
|
title = CommonR.string.sync_repositories_automatically,
|
||||||
iconRes = R.drawable.ic_sync_type,
|
iconRes = CommonR.drawable.ic_sync_type,
|
||||||
valueToString = valueToString,
|
valueToString = valueToString,
|
||||||
onClick = viewModel::setAutoSync
|
onClick = viewModel::setAutoSync
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
installer.connect(
|
installer.connect(
|
||||||
titleText = getString(R.string.installer),
|
titleText = getString(CommonR.string.installer),
|
||||||
setting = viewModel.getSetting { installerType },
|
setting = viewModel.getSetting { installerType },
|
||||||
map = { installerName(it) }
|
map = { installerName(it) }
|
||||||
) { installerType, valueToString ->
|
) { installerType, valueToString ->
|
||||||
addSingleCorrectDialog(
|
addSingleCorrectDialog(
|
||||||
initialValue = installerType,
|
initialValue = installerType,
|
||||||
values = InstallerType.entries,
|
values = InstallerType.entries,
|
||||||
title = R.string.installer,
|
title = CommonR.string.installer,
|
||||||
iconRes = R.drawable.ic_apk_install,
|
iconRes = CommonR.drawable.ic_apk_install,
|
||||||
valueToString = valueToString,
|
valueToString = valueToString,
|
||||||
onClick = { viewModel.setInstaller(requireContext(), it) }
|
onClick = viewModel::setInstaller
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
proxyType.connect(
|
proxyType.connect(
|
||||||
titleText = getString(R.string.proxy_type),
|
titleText = getString(CommonR.string.proxy_type),
|
||||||
setting = viewModel.getSetting { proxy.type },
|
setting = viewModel.getSetting { proxy.type },
|
||||||
map = { proxyName(it) }
|
map = { proxyName(it) }
|
||||||
) { proxyType, valueToString ->
|
) { proxyType, valueToString ->
|
||||||
addSingleCorrectDialog(
|
addSingleCorrectDialog(
|
||||||
initialValue = proxyType,
|
initialValue = proxyType,
|
||||||
values = ProxyType.entries,
|
values = ProxyType.entries,
|
||||||
title = R.string.proxy_type,
|
title = CommonR.string.proxy_type,
|
||||||
iconRes = R.drawable.ic_proxy,
|
iconRes = CommonR.drawable.ic_proxy,
|
||||||
valueToString = valueToString,
|
valueToString = valueToString,
|
||||||
onClick = viewModel::setProxyType
|
onClick = viewModel::setProxyType
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
proxyHost.connect(
|
proxyHost.connect(
|
||||||
titleText = getString(R.string.proxy_host),
|
titleText = getString(CommonR.string.proxy_host),
|
||||||
setting = viewModel.getSetting { proxy.host },
|
setting = viewModel.getSetting { proxy.host },
|
||||||
map = { it }
|
map = { it }
|
||||||
) { host, _ ->
|
) { host, _ ->
|
||||||
addEditTextDialog(
|
addEditTextDialog(
|
||||||
initialValue = host,
|
initialValue = host,
|
||||||
title = R.string.proxy_host,
|
title = CommonR.string.proxy_host,
|
||||||
onFinish = viewModel::setProxyHost
|
onFinish = viewModel::setProxyHost
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
proxyPort.connect(
|
proxyPort.connect(
|
||||||
titleText = getString(R.string.proxy_port),
|
titleText = getString(CommonR.string.proxy_port),
|
||||||
setting = viewModel.getSetting { proxy.port },
|
setting = viewModel.getSetting { proxy.port },
|
||||||
map = { it.toString() }
|
map = { it.toString() }
|
||||||
) { port, _ ->
|
) { port, _ ->
|
||||||
addEditTextDialog(
|
addEditTextDialog(
|
||||||
initialValue = port.toString(),
|
initialValue = port.toString(),
|
||||||
title = R.string.proxy_port,
|
title = CommonR.string.proxy_port,
|
||||||
onFinish = viewModel::setProxyPort
|
onFinish = viewModel::setProxyPort
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
forceCleanUp.title.text = getString(R.string.force_clean_up)
|
forceCleanUp.title.text = getString(CommonR.string.force_clean_up)
|
||||||
forceCleanUp.content.text = getString(R.string.force_clean_up_DESC)
|
forceCleanUp.content.text = getString(CommonR.string.force_clean_up_DESC)
|
||||||
|
|
||||||
importSettings.title.text = getString(R.string.import_settings_title)
|
importSettings.title.text = getString(CommonR.string.import_settings_title)
|
||||||
importSettings.content.text = getString(R.string.import_settings_DESC)
|
importSettings.content.text = getString(CommonR.string.import_settings_DESC)
|
||||||
exportSettings.title.text = getString(R.string.export_settings_title)
|
exportSettings.title.text = getString(CommonR.string.export_settings_title)
|
||||||
exportSettings.content.text = getString(R.string.export_settings_DESC)
|
exportSettings.content.text = getString(CommonR.string.export_settings_DESC)
|
||||||
|
|
||||||
importRepos.title.text = getString(R.string.import_repos_title)
|
importRepos.title.text = getString(CommonR.string.import_repos_title)
|
||||||
importRepos.content.text = getString(R.string.import_repos_DESC)
|
importRepos.content.text = getString(CommonR.string.import_repos_DESC)
|
||||||
exportRepos.title.text = getString(R.string.export_repos_title)
|
exportRepos.title.text = getString(CommonR.string.export_repos_title)
|
||||||
exportRepos.content.text = getString(R.string.export_repos_DESC)
|
exportRepos.content.text = getString(CommonR.string.export_repos_DESC)
|
||||||
|
|
||||||
allowBackgroundWork.root.isVisible = false
|
allowBackgroundWork.title.text = getString(CommonR.string.require_background_access)
|
||||||
allowBackgroundWork.title.text = getString(R.string.require_background_access)
|
|
||||||
allowBackgroundWork.content.text =
|
allowBackgroundWork.content.text =
|
||||||
getString(R.string.require_background_access_DESC)
|
getString(CommonR.string.require_background_access_DESC)
|
||||||
allowBackgroundWork.root.setBackgroundColor(
|
allowBackgroundWork.root.setBackgroundColor(
|
||||||
requireContext()
|
requireContext()
|
||||||
.getColorFromAttr(MaterialR.attr.colorErrorContainer)
|
.getColorFromAttr(MaterialR.attr.colorErrorContainer)
|
||||||
@@ -297,7 +300,7 @@ class SettingsFragment : Fragment() {
|
|||||||
requireContext()
|
requireContext()
|
||||||
.getColorFromAttr(MaterialR.attr.colorOnErrorContainer)
|
.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
|
creditFoxy.content.text = FOXY_DROID_TITLE
|
||||||
droidify.title.text = DROID_IFY_TITLE
|
droidify.title.text = DROID_IFY_TITLE
|
||||||
droidify.content.text = BuildConfig.VERSION_NAME
|
droidify.content.text = BuildConfig.VERSION_NAME
|
||||||
@@ -313,8 +316,8 @@ class SettingsFragment : Fragment() {
|
|||||||
launch {
|
launch {
|
||||||
viewModel.settingsFlow.collect { setting ->
|
viewModel.settingsFlow.collect { setting ->
|
||||||
updateSettings(setting)
|
updateSettings(setting)
|
||||||
binding.allowBackgroundWork.root.isVisible =
|
binding.allowBackgroundWork.root.isVisible = !viewModel.backgroundTask.first()
|
||||||
!viewModel.isBackgroundAllowed && setting.autoSync != AutoSync.NEVER
|
&& setting.autoSync != AutoSync.NEVER
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -324,7 +327,9 @@ class SettingsFragment : Fragment() {
|
|||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
viewModel.toggleBackgroundAccess(requireContext().isIgnoreBatteryEnabled())
|
if (requireContext().isIgnoreBatteryEnabled()) {
|
||||||
|
viewModel.allowBackground()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroyView() {
|
override fun onDestroyView() {
|
||||||
@@ -372,7 +377,9 @@ class SettingsFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
allowBackgroundWork.root.setOnClickListener {
|
allowBackgroundWork.root.setOnClickListener {
|
||||||
requireContext().requestBatteryFreedom()
|
requireContext().requestBatteryFreedom()
|
||||||
viewModel.toggleBackgroundAccess(requireContext().isIgnoreBatteryEnabled())
|
if (requireContext().isIgnoreBatteryEnabled()) {
|
||||||
|
viewModel.allowBackground()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
creditFoxy.root.setOnClickListener {
|
creditFoxy.root.setOnClickListener {
|
||||||
openLink(FOXY_DROID_URL)
|
openLink(FOXY_DROID_URL)
|
||||||
@@ -414,7 +421,7 @@ class SettingsFragment : Fragment() {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
getString(R.string.system)
|
getString(CommonR.string.system)
|
||||||
}
|
}
|
||||||
return languageDisplay
|
return languageDisplay
|
||||||
}
|
}
|
||||||
@@ -423,7 +430,7 @@ class SettingsFragment : Fragment() {
|
|||||||
try {
|
try {
|
||||||
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(link)))
|
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(link)))
|
||||||
} catch (e: IllegalStateException) {
|
} 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))
|
onClick(values.elementAt(newValue))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.setNegativeButton(R.string.cancel, null)
|
.setNegativeButton(CommonR.string.cancel, null)
|
||||||
.create()
|
.create()
|
||||||
|
|
||||||
private fun View.addEditTextDialog(
|
private fun View.addEditTextDialog(
|
||||||
@@ -519,7 +526,7 @@ class SettingsFragment : Fragment() {
|
|||||||
val scroll = NestedScrollView(context)
|
val scroll = NestedScrollView(context)
|
||||||
val customEditText = TextInputEditText(context)
|
val customEditText = TextInputEditText(context)
|
||||||
customEditText.id = android.R.id.edit
|
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)
|
scroll.setPadding(paddingValue, 0, paddingValue, 0)
|
||||||
customEditText.setText(initialValue)
|
customEditText.setText(initialValue)
|
||||||
customEditText.hint = customEditText.text.toString()
|
customEditText.hint = customEditText.text.toString()
|
||||||
@@ -533,10 +540,10 @@ class SettingsFragment : Fragment() {
|
|||||||
return MaterialAlertDialogBuilder(context)
|
return MaterialAlertDialogBuilder(context)
|
||||||
.setTitle(title)
|
.setTitle(title)
|
||||||
.setView(scroll)
|
.setView(scroll)
|
||||||
.setPositiveButton(R.string.ok) { _, _ ->
|
.setPositiveButton(CommonR.string.ok) { _, _ ->
|
||||||
post { onFinish(customEditText.text.toString()) }
|
post { onFinish(customEditText.text.toString()) }
|
||||||
}
|
}
|
||||||
.setNegativeButton(R.string.cancel, null)
|
.setNegativeButton(CommonR.string.cancel, null)
|
||||||
.create()
|
.create()
|
||||||
.apply {
|
.apply {
|
||||||
window!!.setSoftInputMode(
|
window!!.setSoftInputMode(
|
||||||
|
|||||||
@@ -7,39 +7,38 @@ import androidx.appcompat.app.AppCompatDelegate
|
|||||||
import androidx.core.os.LocaleListCompat
|
import androidx.core.os.LocaleListCompat
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
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.Database
|
||||||
import com.looker.droidify.database.RepositoryExporter
|
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.droidify.work.CleanUpWorker
|
||||||
|
import com.looker.installer.installers.shizuku.ShizukuPermissionHandler
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.asSharedFlow
|
import kotlinx.coroutines.flow.asSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.util.Locale
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
|
import com.looker.core.common.R as CommonR
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class SettingsViewModel
|
class SettingsViewModel
|
||||||
@Inject constructor(
|
@Inject constructor(
|
||||||
private val settingsRepository: SettingsRepository,
|
private val settingsRepository: SettingsRepository,
|
||||||
|
private val shizukuPermissionHandler: ShizukuPermissionHandler,
|
||||||
private val repositoryExporter: RepositoryExporter
|
private val repositoryExporter: RepositoryExporter
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
@@ -48,8 +47,8 @@ class SettingsViewModel
|
|||||||
}
|
}
|
||||||
val settingsFlow get() = settingsRepository.data
|
val settingsFlow get() = settingsRepository.data
|
||||||
|
|
||||||
var isBackgroundAllowed = true
|
private val _backgroundTask = MutableStateFlow(false)
|
||||||
private set
|
val backgroundTask = _backgroundTask.asStateFlow()
|
||||||
|
|
||||||
private val _snackbarStringId = MutableSharedFlow<Int>()
|
private val _snackbarStringId = MutableSharedFlow<Int>()
|
||||||
val snackbarStringId = _snackbarStringId.asSharedFlow()
|
val snackbarStringId = _snackbarStringId.asSharedFlow()
|
||||||
@@ -58,8 +57,10 @@ class SettingsViewModel
|
|||||||
|
|
||||||
fun <T> getInitialSetting(block: Settings.() -> T): Flow<T> = initialSetting.map { it.block() }
|
fun <T> getInitialSetting(block: Settings.() -> T): Flow<T> = initialSetting.map { it.block() }
|
||||||
|
|
||||||
fun toggleBackgroundAccess(enable: Boolean) {
|
fun allowBackground() {
|
||||||
isBackgroundAllowed = enable
|
viewModelScope.launch {
|
||||||
|
_backgroundTask.emit(true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setLanguage(language: String) {
|
fun setLanguage(language: String) {
|
||||||
@@ -152,42 +153,16 @@ class SettingsViewModel
|
|||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
try {
|
||||||
settingsRepository.setProxyPort(proxyPort.toInt())
|
settingsRepository.setProxyPort(proxyPort.toInt())
|
||||||
} catch (_: NumberFormatException) {
|
} catch (e: NumberFormatException) {
|
||||||
createSnackbar(R.string.proxy_port_error_not_int)
|
createSnackbar(CommonR.string.proxy_port_error_not_int)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setInstaller(context: Context, installerType: InstallerType) {
|
fun setInstaller(installerType: InstallerType) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
when (installerType) {
|
|
||||||
SHIZUKU -> {
|
|
||||||
if (isShizukuInstalled(context)) {
|
|
||||||
if (!isShizukuAlive()) {
|
|
||||||
createSnackbar(R.string.shizuku_not_alive)
|
|
||||||
return@launch
|
|
||||||
} else if (isShizukuGranted()) {
|
|
||||||
settingsRepository.setInstallerType(installerType)
|
settingsRepository.setInstallerType(installerType)
|
||||||
} else if (!isShizukuGranted()) {
|
if (installerType == InstallerType.SHIZUKU) handleShizuku()
|
||||||
if (requestPermissionListener()) {
|
|
||||||
settingsRepository.setInstallerType(installerType)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
createSnackbar(R.string.shizuku_not_installed)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ROOT -> {
|
|
||||||
if (isMagiskGranted()) {
|
|
||||||
settingsRepository.setInstallerType(installerType)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
settingsRepository.setInstallerType(installerType)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,18 +197,18 @@ class SettingsViewModel
|
|||||||
_snackbarStringId.emit(message)
|
_snackbarStringId.emit(message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
private fun handleShizuku() {
|
||||||
private fun String.toLocale(): Locale = when {
|
viewModelScope.launch {
|
||||||
contains("-r") -> Locale(
|
val state = shizukuPermissionHandler.state.first()
|
||||||
substring(0, 2),
|
if (state.isAlive && state.isPermissionGranted) cancel()
|
||||||
substring(4)
|
if (state.isInstalled) {
|
||||||
)
|
if (!state.isAlive) {
|
||||||
|
createSnackbar(CommonR.string.shizuku_not_alive)
|
||||||
contains("_") -> Locale(
|
}
|
||||||
substring(0, 2),
|
} else {
|
||||||
substring(3)
|
createSnackbar(CommonR.string.shizuku_not_installed)
|
||||||
)
|
}
|
||||||
|
}
|
||||||
else -> Locale(this)
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import android.widget.FrameLayout
|
|||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.activity.OnBackPressedCallback
|
import androidx.activity.OnBackPressedCallback
|
||||||
import androidx.appcompat.widget.SearchView
|
import androidx.appcompat.widget.SearchView
|
||||||
import androidx.core.view.isGone
|
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.viewModels
|
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.MaterialShapeDrawable
|
||||||
import com.google.android.material.shape.ShapeAppearanceModel
|
import com.google.android.material.shape.ShapeAppearanceModel
|
||||||
import com.google.android.material.tabs.TabLayoutMediator
|
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.R
|
||||||
import com.looker.droidify.databinding.TabsToolbarBinding
|
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.model.ProductItem
|
||||||
import com.looker.droidify.service.Connection
|
import com.looker.droidify.service.Connection
|
||||||
import com.looker.droidify.service.SyncService
|
import com.looker.droidify.service.SyncService
|
||||||
import com.looker.droidify.ui.ScreenFragment
|
import com.looker.droidify.ui.ScreenFragment
|
||||||
import com.looker.droidify.ui.appList.AppListFragment
|
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.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.DividerConfiguration
|
||||||
import com.looker.droidify.widget.FocusSearchView
|
import com.looker.droidify.widget.FocusSearchView
|
||||||
import com.looker.droidify.widget.StableRecyclerAdapter
|
import com.looker.droidify.widget.StableRecyclerAdapter
|
||||||
@@ -54,7 +53,8 @@ import dagger.hilt.android.AndroidEntryPoint
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
import kotlin.math.roundToInt
|
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
|
@AndroidEntryPoint
|
||||||
class TabsFragment : ScreenFragment() {
|
class TabsFragment : ScreenFragment() {
|
||||||
@@ -152,7 +152,7 @@ class TabsFragment : ScreenFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mainActivity.onToolbarCreated(toolbar)
|
screenActivity.onToolbarCreated(toolbar)
|
||||||
toolbar.title = getString(R.string.application_name)
|
toolbar.title = getString(R.string.application_name)
|
||||||
// Move focus from SearchView to Toolbar
|
// Move focus from SearchView to Toolbar
|
||||||
toolbar.isFocusable = true
|
toolbar.isFocusable = true
|
||||||
@@ -184,7 +184,7 @@ class TabsFragment : ScreenFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
searchMenuItem = add(0, R.id.toolbar_search, 0, stringRes.search)
|
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)
|
.setActionView(searchView)
|
||||||
.setShowAsActionFlags(
|
.setShowAsActionFlags(
|
||||||
MenuItem.SHOW_AS_ACTION_ALWAYS or MenuItem.SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW
|
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)
|
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 {
|
.setOnMenuItemClickListener {
|
||||||
|
// SyncWorker.startSyncWork(requireContext())
|
||||||
syncConnection.binder?.sync(SyncService.SyncRequest.MANUAL)
|
syncConnection.binder?.sync(SyncService.SyncRequest.MANUAL)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
sortOrderMenu = addSubMenu(0, 0, 0, stringRes.sorting_order)
|
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 ->
|
.let { menu ->
|
||||||
menu.item.setShowAsActionFlags(MenuItem.SHOW_AS_ACTION_ALWAYS)
|
menu.item.setShowAsActionFlags(MenuItem.SHOW_AS_ACTION_ALWAYS)
|
||||||
val menuItems = SortOrder.entries.map { sortOrder ->
|
val menuItems = SortOrder.entries.map { sortOrder ->
|
||||||
@@ -225,22 +226,22 @@ class TabsFragment : ScreenFragment() {
|
|||||||
|
|
||||||
favouritesItem = add(1, 0, 0, stringRes.favourites)
|
favouritesItem = add(1, 0, 0, stringRes.favourites)
|
||||||
.setIcon(
|
.setIcon(
|
||||||
toolbar.context.getMutatedIcon(R.drawable.ic_favourite_checked)
|
toolbar.context.getMutatedIcon(CommonR.drawable.ic_favourite_checked)
|
||||||
)
|
)
|
||||||
.setOnMenuItemClickListener {
|
.setOnMenuItemClickListener {
|
||||||
view.post { mainActivity.navigateFavourites() }
|
view.post { screenActivity.navigateFavourites() }
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
add(1, 0, 0, stringRes.repositories)
|
add(1, 0, 0, stringRes.repositories)
|
||||||
.setOnMenuItemClickListener {
|
.setOnMenuItemClickListener {
|
||||||
view.post { mainActivity.navigateRepositories() }
|
view.post { screenActivity.navigateRepositories() }
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
add(1, 0, 0, stringRes.settings)
|
add(1, 0, 0, stringRes.settings)
|
||||||
.setOnMenuItemClickListener {
|
.setOnMenuItemClickListener {
|
||||||
view.post { mainActivity.navigatePreferences() }
|
view.post { screenActivity.navigatePreferences() }
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -297,31 +298,12 @@ class TabsFragment : ScreenFragment() {
|
|||||||
onBackPressedCallback?.isEnabled = it != BackAction.None
|
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()
|
val backgroundPath = ShapeAppearanceModel.builder()
|
||||||
.setAllCornerSizes(
|
.setAllCornerSizes(
|
||||||
context?.resources?.getDimension(R.dimen.shape_large_corner) ?: 0F
|
context?.resources?.getDimension(CommonR.dimen.shape_large_corner) ?: 0F
|
||||||
)
|
)
|
||||||
.build()
|
.build()
|
||||||
val sectionBackground = MaterialShapeDrawable(backgroundPath)
|
val sectionBackground = MaterialShapeDrawable(backgroundPath)
|
||||||
|
|||||||
@@ -3,19 +3,19 @@ package com.looker.droidify.ui.tabsFragment
|
|||||||
import androidx.lifecycle.SavedStateHandle
|
import androidx.lifecycle.SavedStateHandle
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.looker.droidify.database.Database
|
import com.looker.core.common.extension.asStateFlow
|
||||||
import com.looker.droidify.datastore.SettingsRepository
|
import com.looker.core.datastore.SettingsRepository
|
||||||
import com.looker.droidify.datastore.get
|
import com.looker.core.datastore.get
|
||||||
import com.looker.droidify.datastore.model.SortOrder
|
import com.looker.core.datastore.model.SortOrder
|
||||||
import com.looker.droidify.model.ProductItem
|
import com.looker.droidify.model.ProductItem
|
||||||
|
import com.looker.droidify.database.Database
|
||||||
import com.looker.droidify.ui.tabsFragment.TabsFragment.BackAction
|
import com.looker.droidify.ui.tabsFragment.TabsFragment.BackAction
|
||||||
import com.looker.droidify.utility.common.extension.asStateFlow
|
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import javax.inject.Inject
|
||||||
import kotlinx.coroutines.flow.catch
|
import kotlinx.coroutines.flow.catch
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class TabsViewModel @Inject constructor(
|
class TabsViewModel @Inject constructor(
|
||||||
@@ -57,18 +57,7 @@ class TabsViewModel @Inject constructor(
|
|||||||
|
|
||||||
val showSections = MutableStateFlow(false)
|
val showSections = MutableStateFlow(false)
|
||||||
|
|
||||||
val backAction = combine(
|
val backAction = combine(currentSection, isSearchActionItemExpanded, showSections, ::calcBackAction).asStateFlow(BackAction.None)
|
||||||
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)
|
|
||||||
|
|
||||||
fun setSection(section: ProductItem.Section) {
|
fun setSection(section: ProductItem.Section) {
|
||||||
savedStateHandle[STATE_SECTION] = 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 {
|
companion object {
|
||||||
private const val STATE_SECTION = "section"
|
private const val STATE_SECTION = "section"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import android.content.pm.PackageItemInfo
|
|||||||
import android.content.pm.PermissionInfo
|
import android.content.pm.PermissionInfo
|
||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import com.looker.droidify.utility.common.SdkCheck
|
import com.looker.core.common.SdkCheck
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
object PackageItemResolver {
|
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
|
package com.looker.droidify.utility.extension
|
||||||
|
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import com.looker.droidify.MainActivity
|
import com.looker.droidify.ScreenActivity
|
||||||
|
|
||||||
inline val Fragment.mainActivity: MainActivity
|
inline val Fragment.screenActivity: ScreenActivity
|
||||||
get() = requireActivity() as MainActivity
|
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
|
package com.looker.droidify.utility.extension
|
||||||
|
|
||||||
import android.content.pm.PackageInfo
|
import android.content.pm.PackageInfo
|
||||||
import com.looker.droidify.utility.common.extension.calculateHash
|
import com.looker.core.common.extension.calculateHash
|
||||||
import com.looker.droidify.utility.common.extension.singleSignature
|
import com.looker.core.common.extension.singleSignature
|
||||||
import com.looker.droidify.utility.common.extension.versionCodeCompat
|
import com.looker.core.common.extension.versionCodeCompat
|
||||||
import com.looker.droidify.model.InstalledItem
|
import com.looker.droidify.model.InstalledItem
|
||||||
|
|
||||||
fun PackageInfo.toInstalledItem(): 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.JsonGenerator
|
||||||
import com.fasterxml.jackson.core.JsonParser
|
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
|
import com.looker.droidify.model.ProductItem
|
||||||
|
|
||||||
fun ProductItem.serialize(generator: JsonGenerator) {
|
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.JsonGenerator
|
||||||
import com.fasterxml.jackson.core.JsonParser
|
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
|
import com.looker.droidify.model.ProductPreference
|
||||||
|
|
||||||
fun ProductPreference.serialize(generator: JsonGenerator) {
|
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.JsonGenerator
|
||||||
import com.fasterxml.jackson.core.JsonParser
|
import com.fasterxml.jackson.core.JsonParser
|
||||||
import com.fasterxml.jackson.core.JsonToken
|
import com.fasterxml.jackson.core.JsonToken
|
||||||
import com.looker.droidify.utility.common.extension.collectNotNull
|
import com.looker.core.common.extension.collectNotNull
|
||||||
import com.looker.droidify.utility.common.extension.collectNotNullStrings
|
import com.looker.core.common.extension.collectNotNullStrings
|
||||||
import com.looker.droidify.utility.common.extension.forEachKey
|
import com.looker.core.common.extension.forEachKey
|
||||||
import com.looker.droidify.utility.common.extension.writeArray
|
import com.looker.core.common.extension.writeArray
|
||||||
import com.looker.droidify.utility.common.extension.writeDictionary
|
import com.looker.core.common.extension.writeDictionary
|
||||||
import com.looker.droidify.model.Product
|
import com.looker.droidify.model.Product
|
||||||
import com.looker.droidify.model.Release
|
import com.looker.droidify.model.Release
|
||||||
|
|
||||||
fun Product.serialize(generator: JsonGenerator) {
|
fun Product.serialize(generator: JsonGenerator) {
|
||||||
generator.writeNumberField(REPOSITORYID, repositoryId)
|
generator.writeNumberField("repositoryId", repositoryId)
|
||||||
generator.writeNumberField(SERIALVERSION, 1)
|
generator.writeNumberField("serialVersion", 1)
|
||||||
generator.writeStringField(PACKAGENAME, packageName)
|
generator.writeStringField("packageName", packageName)
|
||||||
generator.writeStringField(NAME, name)
|
generator.writeStringField("name", name)
|
||||||
generator.writeStringField(SUMMARY, summary)
|
generator.writeStringField("summary", summary)
|
||||||
generator.writeStringField(DESCRIPTION, description)
|
generator.writeStringField("description", description)
|
||||||
generator.writeStringField(WHATSNEW, whatsNew)
|
generator.writeStringField("whatsNew", whatsNew)
|
||||||
generator.writeStringField(ICON, icon)
|
generator.writeStringField("icon", icon)
|
||||||
generator.writeStringField(METADATAICON, metadataIcon)
|
generator.writeStringField("metadataIcon", metadataIcon)
|
||||||
generator.writeStringField(AUTHORNAME, author.name)
|
generator.writeStringField("authorName", author.name)
|
||||||
generator.writeStringField(AUTHOREMAIL, author.email)
|
generator.writeStringField("authorEmail", author.email)
|
||||||
generator.writeStringField(AUTHORWEB, author.web)
|
generator.writeStringField("authorWeb", author.web)
|
||||||
generator.writeStringField(SOURCE, source)
|
generator.writeStringField("source", source)
|
||||||
generator.writeStringField(CHANGELOG, changelog)
|
generator.writeStringField("changelog", changelog)
|
||||||
generator.writeStringField(WEB, web)
|
generator.writeStringField("web", web)
|
||||||
generator.writeStringField(TRACKER, tracker)
|
generator.writeStringField("tracker", tracker)
|
||||||
generator.writeNumberField(ADDED, added)
|
generator.writeNumberField("added", added)
|
||||||
generator.writeNumberField(UPDATED, updated)
|
generator.writeNumberField("updated", updated)
|
||||||
generator.writeNumberField(SUGGESTEDVERSIONCODE, suggestedVersionCode)
|
generator.writeNumberField("suggestedVersionCode", suggestedVersionCode)
|
||||||
generator.writeArray(CATEGORIES) { categories.forEach(::writeString) }
|
generator.writeArray("categories") { categories.forEach(::writeString) }
|
||||||
generator.writeArray(ANTIFEATURES) { antiFeatures.forEach(::writeString) }
|
generator.writeArray("antiFeatures") { antiFeatures.forEach(::writeString) }
|
||||||
generator.writeArray(LICENSES) { licenses.forEach(::writeString) }
|
generator.writeArray("licenses") { licenses.forEach(::writeString) }
|
||||||
generator.writeArray(DONATES) {
|
generator.writeArray("donates") {
|
||||||
donates.forEach {
|
donates.forEach {
|
||||||
writeDictionary {
|
writeDictionary {
|
||||||
when (it) {
|
when (it) {
|
||||||
is Product.Donate.Regular -> {
|
is Product.Donate.Regular -> {
|
||||||
writeStringField(TYPE, DONATION_EMPTY)
|
writeStringField("type", "")
|
||||||
writeStringField(URL, it.url)
|
writeStringField("url", it.url)
|
||||||
}
|
}
|
||||||
|
|
||||||
is Product.Donate.Bitcoin -> {
|
is Product.Donate.Bitcoin -> {
|
||||||
writeStringField(TYPE, DONATION_BITCOIN)
|
writeStringField("type", "bitcoin")
|
||||||
writeStringField(ADDRESS, it.address)
|
writeStringField("address", it.address)
|
||||||
}
|
}
|
||||||
|
|
||||||
is Product.Donate.Litecoin -> {
|
is Product.Donate.Litecoin -> {
|
||||||
writeStringField(TYPE, DONATION_LITECOIN)
|
writeStringField("type", "litecoin")
|
||||||
writeStringField(ADDRESS, it.address)
|
writeStringField("address", it.address)
|
||||||
|
}
|
||||||
|
|
||||||
|
is Product.Donate.Flattr -> {
|
||||||
|
writeStringField("type", "flattr")
|
||||||
|
writeStringField("id", it.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
is Product.Donate.Liberapay -> {
|
is Product.Donate.Liberapay -> {
|
||||||
writeStringField(TYPE, DONATION_LIBERAPAY)
|
writeStringField("type", "liberapay")
|
||||||
writeStringField(ID, it.id)
|
writeStringField("id", it.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
is Product.Donate.OpenCollective -> {
|
is Product.Donate.OpenCollective -> {
|
||||||
writeStringField(TYPE, DONATION_OPENCOLLECTIVE)
|
writeStringField("type", "openCollective")
|
||||||
writeStringField(ID, it.id)
|
writeStringField("id", it.id)
|
||||||
}
|
}
|
||||||
}::class
|
}::class
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
generator.writeArray(SCREENSHOTS) {
|
generator.writeArray("screenshots") {
|
||||||
screenshots.forEach {
|
screenshots.forEach {
|
||||||
writeDictionary {
|
writeDictionary {
|
||||||
writeStringField(LOCALE, it.locale)
|
writeStringField("locale", it.locale)
|
||||||
writeStringField(TYPE, it.type.jsonName)
|
writeStringField("type", it.type.jsonName)
|
||||||
writeStringField(PATH, it.path)
|
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 {
|
fun JsonParser.product(): Product {
|
||||||
var repositoryId = 0L
|
var repositoryId = 0L
|
||||||
var packageName = KEY_EMPTY
|
var packageName = ""
|
||||||
var name = KEY_EMPTY
|
var name = ""
|
||||||
var summary = KEY_EMPTY
|
var summary = ""
|
||||||
var description = KEY_EMPTY
|
var description = ""
|
||||||
var whatsNew = KEY_EMPTY
|
var whatsNew = ""
|
||||||
var icon = KEY_EMPTY
|
var icon = ""
|
||||||
var metadataIcon = KEY_EMPTY
|
var metadataIcon = ""
|
||||||
var authorName = KEY_EMPTY
|
var authorName = ""
|
||||||
var authorEmail = KEY_EMPTY
|
var authorEmail = ""
|
||||||
var authorWeb = KEY_EMPTY
|
var authorWeb = ""
|
||||||
var source = KEY_EMPTY
|
var source = ""
|
||||||
var changelog = KEY_EMPTY
|
var changelog = ""
|
||||||
var web = KEY_EMPTY
|
var web = ""
|
||||||
var tracker = KEY_EMPTY
|
var tracker = ""
|
||||||
var added = 0L
|
var added = 0L
|
||||||
var updated = 0L
|
var updated = 0L
|
||||||
var suggestedVersionCode = 0L
|
var suggestedVersionCode = 0L
|
||||||
@@ -103,64 +108,65 @@ fun JsonParser.product(): Product {
|
|||||||
var donates = emptyList<Product.Donate>()
|
var donates = emptyList<Product.Donate>()
|
||||||
var screenshots = emptyList<Product.Screenshot>()
|
var screenshots = emptyList<Product.Screenshot>()
|
||||||
var releases = emptyList<Release>()
|
var releases = emptyList<Release>()
|
||||||
forEachKey { key ->
|
forEachKey { it ->
|
||||||
when {
|
when {
|
||||||
key.string(REPOSITORYID) -> repositoryId = valueAsLong
|
it.string("repositoryId") -> repositoryId = valueAsLong
|
||||||
key.string(PACKAGENAME) -> packageName = valueAsString
|
it.string("packageName") -> packageName = valueAsString
|
||||||
key.string(NAME) -> name = valueAsString
|
it.string("name") -> name = valueAsString
|
||||||
key.string(SUMMARY) -> summary = valueAsString
|
it.string("summary") -> summary = valueAsString
|
||||||
key.string(DESCRIPTION) -> description = valueAsString
|
it.string("description") -> description = valueAsString
|
||||||
key.string(WHATSNEW) -> whatsNew = valueAsString
|
it.string("whatsNew") -> whatsNew = valueAsString
|
||||||
key.string(ICON) -> icon = valueAsString
|
it.string("icon") -> icon = valueAsString
|
||||||
key.string(METADATAICON) -> metadataIcon = valueAsString
|
it.string("metadataIcon") -> metadataIcon = valueAsString
|
||||||
key.string(AUTHORNAME) -> authorName = valueAsString
|
it.string("authorName") -> authorName = valueAsString
|
||||||
key.string(AUTHOREMAIL) -> authorEmail = valueAsString
|
it.string("authorEmail") -> authorEmail = valueAsString
|
||||||
key.string(AUTHORWEB) -> authorWeb = valueAsString
|
it.string("authorWeb") -> authorWeb = valueAsString
|
||||||
key.string(SOURCE) -> source = valueAsString
|
it.string("source") -> source = valueAsString
|
||||||
key.string(CHANGELOG) -> changelog = valueAsString
|
it.string("changelog") -> changelog = valueAsString
|
||||||
key.string(WEB) -> web = valueAsString
|
it.string("web") -> web = valueAsString
|
||||||
key.string(TRACKER) -> tracker = valueAsString
|
it.string("tracker") -> tracker = valueAsString
|
||||||
key.number(ADDED) -> added = valueAsLong
|
it.number("added") -> added = valueAsLong
|
||||||
key.number(UPDATED) -> updated = valueAsLong
|
it.number("updated") -> updated = valueAsLong
|
||||||
key.number(SUGGESTEDVERSIONCODE) -> suggestedVersionCode = valueAsLong
|
it.number("suggestedVersionCode") -> suggestedVersionCode = valueAsLong
|
||||||
key.array(CATEGORIES) -> categories = collectNotNullStrings()
|
it.array("categories") -> categories = collectNotNullStrings()
|
||||||
key.array(ANTIFEATURES) -> antiFeatures = collectNotNullStrings()
|
it.array("antiFeatures") -> antiFeatures = collectNotNullStrings()
|
||||||
key.array(LICENSES) -> licenses = collectNotNullStrings()
|
it.array("licenses") -> licenses = collectNotNullStrings()
|
||||||
key.array(DONATES) -> donates = collectNotNull(JsonToken.START_OBJECT) {
|
it.array("donates") -> donates = collectNotNull(JsonToken.START_OBJECT) {
|
||||||
var type = KEY_EMPTY
|
var type = ""
|
||||||
var url = KEY_EMPTY
|
var url = ""
|
||||||
var address = KEY_EMPTY
|
var address = ""
|
||||||
var id = KEY_EMPTY
|
var id = ""
|
||||||
forEachKey {
|
forEachKey {
|
||||||
when {
|
when {
|
||||||
it.string(TYPE) -> type = valueAsString
|
it.string("type") -> type = valueAsString
|
||||||
it.string(URL) -> url = valueAsString
|
it.string("url") -> url = valueAsString
|
||||||
it.string(ADDRESS) -> address = valueAsString
|
it.string("address") -> address = valueAsString
|
||||||
it.string(ID) -> id = valueAsString
|
it.string("id") -> id = valueAsString
|
||||||
else -> skipChildren()
|
else -> skipChildren()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
when (type) {
|
when (type) {
|
||||||
DONATION_EMPTY -> Product.Donate.Regular(url)
|
"" -> Product.Donate.Regular(url)
|
||||||
DONATION_BITCOIN -> Product.Donate.Bitcoin(address)
|
"bitcoin" -> Product.Donate.Bitcoin(address)
|
||||||
DONATION_LITECOIN -> Product.Donate.Litecoin(address)
|
"litecoin" -> Product.Donate.Litecoin(address)
|
||||||
DONATION_LIBERAPAY -> Product.Donate.Liberapay(id)
|
"flattr" -> Product.Donate.Flattr(id)
|
||||||
DONATION_OPENCOLLECTIVE -> Product.Donate.OpenCollective(id)
|
"liberapay" -> Product.Donate.Liberapay(id)
|
||||||
|
"openCollective" -> Product.Donate.OpenCollective(id)
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
key.array(SCREENSHOTS) ->
|
it.array("screenshots") ->
|
||||||
screenshots =
|
screenshots =
|
||||||
collectNotNull(JsonToken.START_OBJECT) {
|
collectNotNull(JsonToken.START_OBJECT) {
|
||||||
var locale = KEY_EMPTY
|
var locale = ""
|
||||||
var type = KEY_EMPTY
|
var type = ""
|
||||||
var path = KEY_EMPTY
|
var path = ""
|
||||||
forEachKey {
|
forEachKey {
|
||||||
when {
|
when {
|
||||||
it.string(LOCALE) -> locale = valueAsString
|
it.string("locale") -> locale = valueAsString
|
||||||
it.string(TYPE) -> type = valueAsString
|
it.string("type") -> type = valueAsString
|
||||||
it.string(PATH) -> path = valueAsString
|
it.string("path") -> path = valueAsString
|
||||||
else -> skipChildren()
|
else -> skipChildren()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -168,7 +174,7 @@ fun JsonParser.product(): Product {
|
|||||||
?.let { Product.Screenshot(locale, it, path) }
|
?.let { Product.Screenshot(locale, it, path) }
|
||||||
}
|
}
|
||||||
|
|
||||||
key.array(RELEASES) ->
|
it.array("releases") ->
|
||||||
releases =
|
releases =
|
||||||
collectNotNull(JsonToken.START_OBJECT) { release() }
|
collectNotNull(JsonToken.START_OBJECT) { release() }
|
||||||
|
|
||||||
@@ -176,66 +182,27 @@ fun JsonParser.product(): Product {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Product(
|
return Product(
|
||||||
repositoryId = repositoryId,
|
repositoryId,
|
||||||
packageName = packageName,
|
packageName,
|
||||||
name = name,
|
name,
|
||||||
summary = summary,
|
summary,
|
||||||
description = description,
|
description,
|
||||||
whatsNew = whatsNew,
|
whatsNew,
|
||||||
icon = icon,
|
icon,
|
||||||
metadataIcon = metadataIcon,
|
metadataIcon,
|
||||||
author = Product.Author(authorName, authorEmail, authorWeb),
|
Product.Author(authorName, authorEmail, authorWeb),
|
||||||
source = source,
|
source,
|
||||||
changelog = changelog,
|
changelog,
|
||||||
web = web,
|
web,
|
||||||
tracker = tracker,
|
tracker,
|
||||||
added = added,
|
added,
|
||||||
updated = updated,
|
updated,
|
||||||
suggestedVersionCode = suggestedVersionCode,
|
suggestedVersionCode,
|
||||||
categories = categories,
|
categories,
|
||||||
antiFeatures = antiFeatures,
|
antiFeatures,
|
||||||
licenses = licenses,
|
licenses,
|
||||||
donates = donates,
|
donates,
|
||||||
screenshots = screenshots,
|
screenshots,
|
||||||
releases = releases
|
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.JsonGenerator
|
||||||
import com.fasterxml.jackson.core.JsonParser
|
import com.fasterxml.jackson.core.JsonParser
|
||||||
import com.fasterxml.jackson.core.JsonToken
|
import com.fasterxml.jackson.core.JsonToken
|
||||||
import com.looker.droidify.utility.common.extension.collectNotNull
|
import com.looker.core.common.extension.collectNotNull
|
||||||
import com.looker.droidify.utility.common.extension.collectNotNullStrings
|
import com.looker.core.common.extension.collectNotNullStrings
|
||||||
import com.looker.droidify.utility.common.extension.forEachKey
|
import com.looker.core.common.extension.forEachKey
|
||||||
import com.looker.droidify.utility.common.extension.writeArray
|
import com.looker.core.common.extension.writeArray
|
||||||
import com.looker.droidify.utility.common.extension.writeDictionary
|
import com.looker.core.common.extension.writeDictionary
|
||||||
import com.looker.droidify.model.Release
|
import com.looker.droidify.model.Release
|
||||||
|
|
||||||
fun Release.serialize(generator: JsonGenerator) {
|
fun Release.serialize(generator: JsonGenerator) {
|
||||||
generator.writeNumberField(SERIALVERSION, 1)
|
generator.writeNumberField("serialVersion", 1)
|
||||||
generator.writeBooleanField(SELECTED, selected)
|
generator.writeBooleanField("selected", selected)
|
||||||
generator.writeStringField(VERSION, version)
|
generator.writeStringField("version", version)
|
||||||
generator.writeNumberField(VERSIONCODE, versionCode)
|
generator.writeNumberField("versionCode", versionCode)
|
||||||
generator.writeNumberField(ADDED, added)
|
generator.writeNumberField("added", added)
|
||||||
generator.writeNumberField(SIZE, size)
|
generator.writeNumberField("size", size)
|
||||||
generator.writeNumberField(MINSDKVERSION, minSdkVersion)
|
generator.writeNumberField("minSdkVersion", minSdkVersion)
|
||||||
generator.writeNumberField(TARGETSDKVERSION, targetSdkVersion)
|
generator.writeNumberField("targetSdkVersion", targetSdkVersion)
|
||||||
generator.writeNumberField(MAXSDKVERSION, maxSdkVersion)
|
generator.writeNumberField("maxSdkVersion", maxSdkVersion)
|
||||||
generator.writeStringField(SOURCE, source)
|
generator.writeStringField("source", source)
|
||||||
generator.writeStringField(RELEASE, release)
|
generator.writeStringField("release", release)
|
||||||
generator.writeStringField(HASH, hash)
|
generator.writeStringField("hash", hash)
|
||||||
generator.writeStringField(HASHTYPE, hashType)
|
generator.writeStringField("hashType", hashType)
|
||||||
generator.writeStringField(SIGNATURE, signature)
|
generator.writeStringField("signature", signature)
|
||||||
generator.writeStringField(OBBMAIN, obbMain)
|
generator.writeStringField("obbMain", obbMain)
|
||||||
generator.writeStringField(OBBMAINHASH, obbMainHash)
|
generator.writeStringField("obbMainHash", obbMainHash)
|
||||||
generator.writeStringField(OBBMAINHASHTYPE, obbMainHashType)
|
generator.writeStringField("obbMainHashType", obbMainHashType)
|
||||||
generator.writeStringField(OBBPATCH, obbPatch)
|
generator.writeStringField("obbPatch", obbPatch)
|
||||||
generator.writeStringField(OBBPATCHHASH, obbPatchHash)
|
generator.writeStringField("obbPatchHash", obbPatchHash)
|
||||||
generator.writeStringField(OBBPATCHHASHTYPE, obbPatchHashType)
|
generator.writeStringField("obbPatchHashType", obbPatchHashType)
|
||||||
generator.writeArray(PERMISSIONS) { permissions.forEach { writeString(it) } }
|
generator.writeArray("permissions") { permissions.forEach { writeString(it) } }
|
||||||
generator.writeArray(FEATURES) { features.forEach { writeString(it) } }
|
generator.writeArray("features") { features.forEach { writeString(it) } }
|
||||||
generator.writeArray(PLATFORMS) { platforms.forEach { writeString(it) } }
|
generator.writeArray("platforms") { platforms.forEach { writeString(it) } }
|
||||||
generator.writeArray(INCOMPATIBILITIES) {
|
generator.writeArray("incompatibilities") {
|
||||||
incompatibilities.forEach {
|
incompatibilities.forEach {
|
||||||
writeDictionary {
|
writeDictionary {
|
||||||
when (it) {
|
when (it) {
|
||||||
is Release.Incompatibility.MinSdk -> {
|
is Release.Incompatibility.MinSdk -> {
|
||||||
writeStringField(INCOMPATIBILITY_TYPE, MIN_SDK)
|
writeStringField("type", "minSdk")
|
||||||
}
|
}
|
||||||
|
|
||||||
is Release.Incompatibility.MaxSdk -> {
|
is Release.Incompatibility.MaxSdk -> {
|
||||||
writeStringField(INCOMPATIBILITY_TYPE, MAX_SDK)
|
writeStringField("type", "maxSdk")
|
||||||
}
|
}
|
||||||
|
|
||||||
is Release.Incompatibility.Platform -> {
|
is Release.Incompatibility.Platform -> {
|
||||||
writeStringField(INCOMPATIBILITY_TYPE, PLATFORM)
|
writeStringField("type", "platform")
|
||||||
}
|
}
|
||||||
|
|
||||||
is Release.Incompatibility.Feature -> {
|
is Release.Incompatibility.Feature -> {
|
||||||
writeStringField(INCOMPATIBILITY_TYPE, INCOMPATIBILITY_FEATURE)
|
writeStringField("type", "feature")
|
||||||
writeStringField(INCOMPATIBILITY_FEATURE, it.feature)
|
writeStringField("feature", it.feature)
|
||||||
}
|
}
|
||||||
}::class
|
}::class
|
||||||
}
|
}
|
||||||
@@ -60,72 +60,71 @@ fun Release.serialize(generator: JsonGenerator) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun JsonParser.release(): Release {
|
fun JsonParser.release(): Release {
|
||||||
var selected = false
|
var selected = false
|
||||||
var version = KEY_EMPTY
|
var version = ""
|
||||||
var versionCode = 0L
|
var versionCode = 0L
|
||||||
var added = 0L
|
var added = 0L
|
||||||
var size = 0L
|
var size = 0L
|
||||||
var minSdkVersion = 0
|
var minSdkVersion = 0
|
||||||
var targetSdkVersion = 0
|
var targetSdkVersion = 0
|
||||||
var maxSdkVersion = 0
|
var maxSdkVersion = 0
|
||||||
var source = KEY_EMPTY
|
var source = ""
|
||||||
var release = KEY_EMPTY
|
var release = ""
|
||||||
var hash = KEY_EMPTY
|
var hash = ""
|
||||||
var hashType = KEY_EMPTY
|
var hashType = ""
|
||||||
var signature = KEY_EMPTY
|
var signature = ""
|
||||||
var obbMain = KEY_EMPTY
|
var obbMain = ""
|
||||||
var obbMainHash = KEY_EMPTY
|
var obbMainHash = ""
|
||||||
var obbMainHashType = KEY_EMPTY
|
var obbMainHashType = ""
|
||||||
var obbPatch = KEY_EMPTY
|
var obbPatch = ""
|
||||||
var obbPatchHash = KEY_EMPTY
|
var obbPatchHash = ""
|
||||||
var obbPatchHashType = KEY_EMPTY
|
var obbPatchHashType = ""
|
||||||
var permissions = emptyList<String>()
|
var permissions = emptyList<String>()
|
||||||
var features = emptyList<String>()
|
var features = emptyList<String>()
|
||||||
var platforms = emptyList<String>()
|
var platforms = emptyList<String>()
|
||||||
var incompatibilities = emptyList<Release.Incompatibility>()
|
var incompatibilities = emptyList<Release.Incompatibility>()
|
||||||
forEachKey { key ->
|
forEachKey { it ->
|
||||||
when {
|
when {
|
||||||
key.boolean(SELECTED) -> selected = valueAsBoolean
|
it.boolean("selected") -> selected = valueAsBoolean
|
||||||
key.string(VERSION) -> version = valueAsString
|
it.string("version") -> version = valueAsString
|
||||||
key.number(VERSIONCODE) -> versionCode = valueAsLong
|
it.number("versionCode") -> versionCode = valueAsLong
|
||||||
key.number(ADDED) -> added = valueAsLong
|
it.number("added") -> added = valueAsLong
|
||||||
key.number(SIZE) -> size = valueAsLong
|
it.number("size") -> size = valueAsLong
|
||||||
key.number(MINSDKVERSION) -> minSdkVersion = valueAsInt
|
it.number("minSdkVersion") -> minSdkVersion = valueAsInt
|
||||||
key.number(TARGETSDKVERSION) -> targetSdkVersion = valueAsInt
|
it.number("targetSdkVersion") -> targetSdkVersion = valueAsInt
|
||||||
key.number(MAXSDKVERSION) -> maxSdkVersion = valueAsInt
|
it.number("maxSdkVersion") -> maxSdkVersion = valueAsInt
|
||||||
key.string(SOURCE) -> source = valueAsString
|
it.string("source") -> source = valueAsString
|
||||||
key.string(RELEASE) -> release = valueAsString
|
it.string("release") -> release = valueAsString
|
||||||
key.string(HASH) -> hash = valueAsString
|
it.string("hash") -> hash = valueAsString
|
||||||
key.string(HASHTYPE) -> hashType = valueAsString
|
it.string("hashType") -> hashType = valueAsString
|
||||||
key.string(SIGNATURE) -> signature = valueAsString
|
it.string("signature") -> signature = valueAsString
|
||||||
key.string(OBBMAIN) -> obbMain = valueAsString
|
it.string("obbMain") -> obbMain = valueAsString
|
||||||
key.string(OBBMAINHASH) -> obbMainHash = valueAsString
|
it.string("obbMainHash") -> obbMainHash = valueAsString
|
||||||
key.string(OBBMAINHASHTYPE) -> obbMainHashType = valueAsString
|
it.string("obbMainHashType") -> obbMainHashType = valueAsString
|
||||||
key.string(OBBPATCH) -> obbPatch = valueAsString
|
it.string("obbPatch") -> obbPatch = valueAsString
|
||||||
key.string(OBBPATCHHASH) -> obbPatchHash = valueAsString
|
it.string("obbPatchHash") -> obbPatchHash = valueAsString
|
||||||
key.string(OBBPATCHHASHTYPE) -> obbPatchHashType = valueAsString
|
it.string("obbPatchHashType") -> obbPatchHashType = valueAsString
|
||||||
key.array(PERMISSIONS) -> permissions = collectNotNullStrings()
|
it.array("permissions") -> permissions = collectNotNullStrings()
|
||||||
key.array(FEATURES) -> features = collectNotNullStrings()
|
it.array("features") -> features = collectNotNullStrings()
|
||||||
key.array(PLATFORMS) -> platforms = collectNotNullStrings()
|
it.array("platforms") -> platforms = collectNotNullStrings()
|
||||||
key.array(INCOMPATIBILITIES) ->
|
it.array("incompatibilities") ->
|
||||||
incompatibilities =
|
incompatibilities =
|
||||||
collectNotNull(JsonToken.START_OBJECT) {
|
collectNotNull(JsonToken.START_OBJECT) {
|
||||||
var type = KEY_EMPTY
|
var type = ""
|
||||||
var feature = KEY_EMPTY
|
var feature = ""
|
||||||
forEachKey {
|
forEachKey {
|
||||||
when {
|
when {
|
||||||
it.string(INCOMPATIBILITY_TYPE) -> type = valueAsString
|
it.string("type") -> type = valueAsString
|
||||||
it.string(INCOMPATIBILITY_FEATURE) -> feature = valueAsString
|
it.string("feature") -> feature = valueAsString
|
||||||
else -> skipChildren()
|
else -> skipChildren()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
when (type) {
|
when (type) {
|
||||||
MIN_SDK -> Release.Incompatibility.MinSdk
|
"minSdk" -> Release.Incompatibility.MinSdk
|
||||||
MAX_SDK -> Release.Incompatibility.MaxSdk
|
"maxSdk" -> Release.Incompatibility.MaxSdk
|
||||||
PLATFORM -> Release.Incompatibility.Platform
|
"platform" -> Release.Incompatibility.Platform
|
||||||
INCOMPATIBILITY_FEATURE -> Release.Incompatibility.Feature(feature)
|
"feature" -> Release.Incompatibility.Feature(feature)
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -134,59 +133,28 @@ fun JsonParser.release(): Release {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Release(
|
return Release(
|
||||||
selected = selected,
|
selected,
|
||||||
version = version,
|
version,
|
||||||
versionCode = versionCode,
|
versionCode,
|
||||||
added = added,
|
added,
|
||||||
size = size,
|
size,
|
||||||
minSdkVersion = minSdkVersion,
|
minSdkVersion,
|
||||||
targetSdkVersion = targetSdkVersion,
|
targetSdkVersion,
|
||||||
maxSdkVersion = maxSdkVersion,
|
maxSdkVersion,
|
||||||
source = source,
|
source,
|
||||||
release = release,
|
release,
|
||||||
hash = hash,
|
hash,
|
||||||
hashType = hashType,
|
hashType,
|
||||||
signature = signature,
|
signature,
|
||||||
obbMain = obbMain,
|
obbMain,
|
||||||
obbMainHash = obbMainHash,
|
obbMainHash,
|
||||||
obbMainHashType = obbMainHashType,
|
obbMainHashType,
|
||||||
obbPatch = obbPatch,
|
obbPatch,
|
||||||
obbPatchHash = obbPatchHash,
|
obbPatchHash,
|
||||||
obbPatchHashType = obbPatchHashType,
|
obbPatchHashType,
|
||||||
permissions = permissions,
|
permissions,
|
||||||
features = features,
|
features,
|
||||||
platforms = platforms,
|
platforms,
|
||||||
incompatibilities = incompatibilities
|
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.JsonGenerator
|
||||||
import com.fasterxml.jackson.core.JsonParser
|
import com.fasterxml.jackson.core.JsonParser
|
||||||
import com.looker.droidify.utility.common.extension.collectNotNullStrings
|
import com.looker.core.common.extension.collectNotNullStrings
|
||||||
import com.looker.droidify.utility.common.extension.forEachKey
|
import com.looker.core.common.extension.forEachKey
|
||||||
import com.looker.droidify.utility.common.extension.writeArray
|
import com.looker.core.common.extension.writeArray
|
||||||
import com.looker.droidify.model.Repository
|
import com.looker.droidify.model.Repository
|
||||||
|
|
||||||
fun Repository.serialize(generator: JsonGenerator) {
|
fun Repository.serialize(generator: JsonGenerator) {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import android.graphics.Canvas
|
|||||||
import android.graphics.Rect
|
import android.graphics.Rect
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
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 com.looker.droidify.R
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ import androidx.work.OneTimeWorkRequestBuilder
|
|||||||
import androidx.work.PeriodicWorkRequestBuilder
|
import androidx.work.PeriodicWorkRequestBuilder
|
||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
import androidx.work.WorkerParameters
|
import androidx.work.WorkerParameters
|
||||||
import com.looker.droidify.utility.common.cache.Cache
|
import com.looker.core.common.cache.Cache
|
||||||
import com.looker.droidify.datastore.SettingsRepository
|
import com.looker.core.datastore.SettingsRepository
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import kotlin.time.Duration
|
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>
|
</LinearLayout>
|
||||||
|
|
||||||
<com.google.android.material.divider.MaterialDivider
|
<com.google.android.material.divider.MaterialDivider
|
||||||
|
android:id="@+id/divider1"
|
||||||
android:layout_width="2dp"
|
android:layout_width="2dp"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:layout_marginVertical="12dp" />
|
android:layout_marginVertical="12dp" />
|
||||||
@@ -119,6 +120,7 @@
|
|||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<com.google.android.material.divider.MaterialDivider
|
<com.google.android.material.divider.MaterialDivider
|
||||||
|
android:id="@+id/divider2"
|
||||||
android:layout_width="2dp"
|
android:layout_width="2dp"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:layout_marginVertical="12dp" />
|
android:layout_marginVertical="12dp" />
|
||||||
|
|||||||
@@ -10,6 +10,13 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<com.google.android.material.card.MaterialCardView
|
||||||
|
style="?materialCardViewElevatedStyle"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginHorizontal="12dp"
|
||||||
|
android:layout_marginTop="12dp">
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
@@ -72,6 +79,8 @@
|
|||||||
</com.google.android.material.textfield.TextInputLayout>
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
||||||
</androidx.core.widget.NestedScrollView>
|
</androidx.core.widget.NestedScrollView>
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
@@ -81,7 +90,6 @@
|
|||||||
android:background="?android:attr/colorBackground"
|
android:background="?android:attr/colorBackground"
|
||||||
android:clickable="true"
|
android:clickable="true"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:visibility="gone"
|
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
tools:ignore="KeyboardInaccessibleWidget">
|
tools:ignore="KeyboardInaccessibleWidget">
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
android:orientation="horizontal">
|
android:orientation="horizontal">
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="0dp"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:gravity="center_vertical"
|
android:gravity="center_vertical"
|
||||||
@@ -96,13 +96,6 @@
|
|||||||
android:textColor="?android:attr/textColorSecondary"
|
android:textColor="?android:attr/textColorSecondary"
|
||||||
android:textSize="14sp" />
|
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>
|
</LinearLayout>
|
||||||
|
|
||||||
</com.google.android.material.card.MaterialCardView>
|
</com.google.android.material.card.MaterialCardView>
|
||||||
@@ -2,8 +2,7 @@
|
|||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical">
|
||||||
android:paddingBottom="20dp">
|
|
||||||
|
|
||||||
<com.google.android.material.materialswitch.MaterialSwitch
|
<com.google.android.material.materialswitch.MaterialSwitch
|
||||||
android:id="@+id/repo_switch"
|
android:id="@+id/repo_switch"
|
||||||
@@ -66,4 +65,9 @@
|
|||||||
android:text="@string/delete"
|
android:text="@string/delete"
|
||||||
android:textColor="?colorOnError" />
|
android:textColor="?colorOnError" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="20dp"/>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|||||||
@@ -44,12 +44,8 @@
|
|||||||
android:layout_marginEnd="20dp"
|
android:layout_marginEnd="20dp"
|
||||||
android:src="@drawable/ic_arrow_down"
|
android:src="@drawable/ic_arrow_down"
|
||||||
tools:ignore="ContentDescription" />
|
tools:ignore="ContentDescription" />
|
||||||
</LinearLayout>
|
|
||||||
</FrameLayout>
|
|
||||||
|
|
||||||
<com.google.android.material.progressindicator.LinearProgressIndicator
|
</LinearLayout>
|
||||||
android:id="@+id/sync_state"
|
|
||||||
android:visibility="gone"
|
</FrameLayout>
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content" />
|
|
||||||
</LinearLayout>
|
</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