-- BIG Update --
@@ -1,6 +1,7 @@
|
||||
/*
|
||||
!/.gitignore
|
||||
!/build.gradle
|
||||
!/settings.gradle
|
||||
!/COPYING
|
||||
!/extra
|
||||
!/gradle
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
# Foxy Droid
|
||||
# Michas Droid
|
||||
|
||||
Yet another F-Droid client.
|
||||
|
||||
[](https://github.com/kitsunyan/foxy-droid/releases)
|
||||
[](https://f-droid.org/packages/nya.kitsunyan.foxydroid/)
|
||||
[](https://github.com/michatec/michas-droid/releases/latest)
|
||||
|
||||
## Description
|
||||
|
||||
@@ -48,4 +47,4 @@ Run `./gradlew assembleRelease` to build the package, which can be installed usi
|
||||
|
||||
## License
|
||||
|
||||
Foxy Droid is available under the terms of the GNU General Public License v3 or later. Copyright © 2020 kitsunyan.
|
||||
Michas Droid is available under the terms of the GNU General Public License v3 or later. Copyright © 2026 Michatec.
|
||||
|
||||
@@ -1,52 +1,46 @@
|
||||
buildscript {
|
||||
ext.versions = [
|
||||
android: '3.4.1',
|
||||
kotlin: '1.3.72'
|
||||
]
|
||||
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:' + versions.android
|
||||
classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:' + versions.kotlin
|
||||
classpath 'com.android.tools.build:gradle:9.0.1'
|
||||
classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:2.2.10'
|
||||
}
|
||||
}
|
||||
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
|
||||
android {
|
||||
compileSdkVersion 29
|
||||
buildToolsVersion '29.0.3'
|
||||
namespace 'nya.kitsunyan.foxydroid'
|
||||
compileSdk 36
|
||||
|
||||
defaultConfig {
|
||||
archivesBaseName = 'foxy-droid'
|
||||
applicationId 'nya.kitsunyan.foxydroid'
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 29
|
||||
versionCode 4
|
||||
versionName '1.3'
|
||||
minSdk 30
|
||||
targetSdk 36
|
||||
versionCode 15
|
||||
versionName '1.5'
|
||||
|
||||
def languages = [ 'en' ]
|
||||
buildConfigField 'String[]', 'LANGUAGES', '{ "' + languages.join('", "') + '" }'
|
||||
resConfigs languages
|
||||
}
|
||||
|
||||
sourceSets.all {
|
||||
def javaDir = it.java.srcDirs.find { it.name == 'java' }
|
||||
it.java.srcDirs += new File(javaDir.parentFile, 'kotlin')
|
||||
buildFeatures {
|
||||
buildConfig = true
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main {
|
||||
java.srcDirs += 'src/main/kotlin'
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = compileOptions.sourceCompatibility.toString()
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
targetCompatibility JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
@@ -58,25 +52,6 @@ android {
|
||||
minifyEnabled true
|
||||
shrinkResources false
|
||||
}
|
||||
all {
|
||||
crunchPngs false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard.pro'
|
||||
}
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
warning 'InvalidPackage'
|
||||
ignore 'InvalidVectorPath'
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
exclude '/DebugProbesKt.bin'
|
||||
exclude '/kotlin/**.kotlin_builtins'
|
||||
exclude '/kotlin/**.kotlin_metadata'
|
||||
exclude '/META-INF/**.kotlin_module'
|
||||
exclude '/META-INF/**.pro'
|
||||
exclude '/META-INF/**.version'
|
||||
exclude '/okhttp3/internal/publicsuffix/*'
|
||||
}
|
||||
|
||||
def keystorePropertiesFile = rootProject.file('keystore.properties')
|
||||
@@ -98,31 +73,26 @@ android {
|
||||
storePassword signing.storePassword
|
||||
keyAlias signing.keyAlias
|
||||
keyPassword signing.keyPassword
|
||||
v2SigningEnabled false
|
||||
enableV2Signing false
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
debug.signingConfig signingConfigs.primary
|
||||
release.signingConfig signingConfigs.primary
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk7:' + versions.kotlin
|
||||
implementation 'androidx.fragment:fragment:1.2.5'
|
||||
implementation 'androidx.viewpager2:viewpager2:1.0.0'
|
||||
implementation 'androidx.vectordrawable:vectordrawable:1.1.0'
|
||||
implementation 'com.squareup.okhttp3:okhttp:4.7.2'
|
||||
implementation 'io.reactivex.rxjava3:rxjava:3.0.4'
|
||||
implementation 'io.reactivex.rxjava3:rxandroid:3.0.0'
|
||||
implementation 'com.fasterxml.jackson.core:jackson-core:2.11.0'
|
||||
implementation 'org.jetbrains.kotlin:kotlin-stdlib:2.3.10'
|
||||
implementation 'androidx.fragment:fragment-ktx:1.8.9'
|
||||
implementation 'androidx.viewpager2:viewpager2:1.1.0'
|
||||
implementation 'androidx.vectordrawable:vectordrawable:1.2.0'
|
||||
implementation 'com.squareup.okhttp3:okhttp:5.3.2'
|
||||
implementation 'io.reactivex.rxjava3:rxjava:3.1.12'
|
||||
implementation 'io.reactivex.rxjava3:rxandroid:3.0.2'
|
||||
implementation 'com.fasterxml.jackson.core:jackson-core:2.21.1'
|
||||
implementation 'com.squareup.picasso:picasso:2.71828'
|
||||
}
|
||||
|
||||
@@ -1,273 +1,87 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
style="enable-background:new"
|
||||
id="svg3"
|
||||
version="1.1"
|
||||
viewBox="0 0 108 108"
|
||||
height="108"
|
||||
width="108">
|
||||
<metadata
|
||||
id="metadata55">
|
||||
<svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns="http://www.w3.org/2000/svg" style="enable-background:new" id="svg3" version="1.1" viewBox="0 0 108 108"
|
||||
height="108" width="108" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://purl.org/dc/elements/1.1/">
|
||||
<metadata id="metadata55">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<cc:Work rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title>-</dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs17">
|
||||
<clipPath
|
||||
id="paper-clip"
|
||||
clipPathUnits="userSpaceOnUse">
|
||||
<path
|
||||
id="paper-clip-path"
|
||||
d="m 67.759585,28.469613 -18.355472,18.146485 -21.453125,-4.732422 9.087891,24.064453 25.894536,12.974609 12.37695,-23.86914 -1.93359,-0.66211 z" />
|
||||
<defs id="defs17">
|
||||
<clipPath id="paper-clip" clipPathUnits="userSpaceOnUse">
|
||||
<path id="paper-clip-path" d="m 67.759585,28.469613 -18.355472,18.146485 -21.453125,-4.732422 9.087891,24.064453 25.894536,12.974609 12.37695,-23.86914 -1.93359,-0.66211 z" />
|
||||
</clipPath>
|
||||
<filter
|
||||
id="paper-inner-shadow">
|
||||
<feFlood
|
||||
id="feFlood6"
|
||||
result="flood"
|
||||
flood-color="rgb(0,0,0)"
|
||||
flood-opacity="0.2" />
|
||||
<feComposite
|
||||
id="feComposite8"
|
||||
result="composite1"
|
||||
operator="in"
|
||||
in2="SourceGraphic"
|
||||
in="flood" />
|
||||
<feGaussianBlur
|
||||
id="feGaussianBlur10"
|
||||
result="blur"
|
||||
stdDeviation="2"
|
||||
in="composite1" />
|
||||
<feOffset
|
||||
id="feOffset12"
|
||||
result="offset"
|
||||
dy="0"
|
||||
dx="0" />
|
||||
<feComposite
|
||||
id="feComposite14"
|
||||
result="composite2"
|
||||
operator="over"
|
||||
in2="offset"
|
||||
in="SourceGraphic" />
|
||||
<filter id="paper-inner-shadow">
|
||||
<feFlood id="feFlood6" result="flood" flood-color="rgb(0,0,0)" flood-opacity="0.2" />
|
||||
<feComposite id="feComposite8" result="composite1" operator="in" in2="SourceGraphic" in="flood" />
|
||||
<feGaussianBlur id="feGaussianBlur10" result="blur" stdDeviation="2" in="composite1" />
|
||||
<feOffset id="feOffset12" result="offset" dy="0" dx="0" />
|
||||
<feComposite id="feComposite14" result="composite2" operator="over" in2="offset" in="SourceGraphic" />
|
||||
</filter>
|
||||
<filter
|
||||
id="paper-edge-1"
|
||||
style="color-interpolation-filters:sRGB">
|
||||
<feFlood
|
||||
id="feFlood958"
|
||||
result="flood"
|
||||
flood-color="rgb(0,0,0)"
|
||||
flood-opacity="0.2" />
|
||||
<feComposite
|
||||
id="feComposite960"
|
||||
result="composite1"
|
||||
operator="out"
|
||||
in2="SourceGraphic"
|
||||
<filter id="paper-edge-1" style="color-interpolation-filters:sRGB">
|
||||
<feFlood id="feFlood958" result="flood" flood-color="rgb(0,0,0)" flood-opacity="0.2" />
|
||||
<feComposite id="feComposite960" result="composite1" operator="out" in2="SourceGraphic"
|
||||
in="flood" />
|
||||
<feGaussianBlur
|
||||
id="feGaussianBlur962"
|
||||
result="blur"
|
||||
stdDeviation="0"
|
||||
in="composite1" />
|
||||
<feOffset
|
||||
id="feOffset964"
|
||||
result="offset"
|
||||
dy="0.2"
|
||||
dx="0.2" />
|
||||
<feComposite
|
||||
id="feComposite966"
|
||||
result="composite2"
|
||||
operator="atop"
|
||||
in2="SourceGraphic"
|
||||
<feGaussianBlur id="feGaussianBlur962" result="blur" stdDeviation="0" in="composite1" />
|
||||
<feOffset id="feOffset964" result="offset" dy="0.2" dx="0.2" />
|
||||
<feComposite id="feComposite966" result="composite2" operator="atop" in2="SourceGraphic"
|
||||
in="offset" />
|
||||
</filter>
|
||||
<filter
|
||||
id="paper-edge-2"
|
||||
style="color-interpolation-filters:sRGB">
|
||||
<feFlood
|
||||
id="feFlood1388"
|
||||
result="flood"
|
||||
flood-color="rgb(255,255,255)"
|
||||
flood-opacity="0.2" />
|
||||
<feComposite
|
||||
id="feComposite1390"
|
||||
result="composite1"
|
||||
operator="out"
|
||||
in2="SourceGraphic"
|
||||
<filter id="paper-edge-2" style="color-interpolation-filters:sRGB">
|
||||
<feFlood id="feFlood1388" result="flood" flood-color="rgb(255,255,255)" flood-opacity="0.2" />
|
||||
<feComposite id="feComposite1390" result="composite1" operator="out" in2="SourceGraphic"
|
||||
in="flood" />
|
||||
<feGaussianBlur
|
||||
id="feGaussianBlur1392"
|
||||
result="blur"
|
||||
stdDeviation="0"
|
||||
in="composite1" />
|
||||
<feOffset
|
||||
id="feOffset1394"
|
||||
result="offset"
|
||||
dy="-0.1"
|
||||
dx="0.3" />
|
||||
<feComposite
|
||||
id="feComposite1396"
|
||||
result="composite2"
|
||||
operator="atop"
|
||||
in2="SourceGraphic"
|
||||
<feGaussianBlur id="feGaussianBlur1392" result="blur" stdDeviation="0" in="composite1" />
|
||||
<feOffset id="feOffset1394" result="offset" dy="-0.1" dx="0.3" />
|
||||
<feComposite id="feComposite1396" result="composite2" operator="atop" in2="SourceGraphic"
|
||||
in="offset" />
|
||||
</filter>
|
||||
<filter
|
||||
id="paper-shadow"
|
||||
style="color-interpolation-filters:sRGB">
|
||||
<feFlood
|
||||
id="feFlood1506"
|
||||
result="flood"
|
||||
flood-color="rgb(0,0,0)"
|
||||
flood-opacity="0.4" />
|
||||
<feComposite
|
||||
id="feComposite1508"
|
||||
result="composite1"
|
||||
operator="in"
|
||||
in2="SourceGraphic"
|
||||
<filter id="paper-shadow" style="color-interpolation-filters:sRGB">
|
||||
<feFlood id="feFlood1506" result="flood" flood-color="rgb(0,0,0)" flood-opacity="0.4" />
|
||||
<feComposite id="feComposite1508" result="composite1" operator="in" in2="SourceGraphic"
|
||||
in="flood" />
|
||||
<feGaussianBlur
|
||||
id="feGaussianBlur1510"
|
||||
result="blur"
|
||||
stdDeviation="2"
|
||||
in="composite1" />
|
||||
<feOffset
|
||||
id="feOffset1512"
|
||||
result="offset"
|
||||
dy="0"
|
||||
dx="0" />
|
||||
<feComposite
|
||||
id="feComposite1514"
|
||||
result="composite2"
|
||||
operator="over"
|
||||
in2="offset"
|
||||
in="SourceGraphic" />
|
||||
<feGaussianBlur id="feGaussianBlur1510" result="blur" stdDeviation="2" in="composite1" />
|
||||
<feOffset id="feOffset1512" result="offset" dy="0" dx="0" />
|
||||
<feComposite id="feComposite1514" result="composite2" operator="over" in2="offset" in="SourceGraphic" />
|
||||
</filter>
|
||||
<filter
|
||||
id="circle-shadow"
|
||||
style="color-interpolation-filters:sRGB">
|
||||
<feFlood
|
||||
id="feFlood1626"
|
||||
result="flood"
|
||||
flood-color="rgb(255,255,255)"
|
||||
flood-opacity="0.2" />
|
||||
<feComposite
|
||||
id="feComposite1628"
|
||||
result="composite1"
|
||||
operator="out"
|
||||
in2="SourceGraphic"
|
||||
<filter id="circle-shadow" style="color-interpolation-filters:sRGB">
|
||||
<feFlood id="feFlood1626" result="flood" flood-color="rgb(255,255,255)" flood-opacity="0.2" />
|
||||
<feComposite id="feComposite1628" result="composite1" operator="out" in2="SourceGraphic"
|
||||
in="flood" />
|
||||
<feGaussianBlur
|
||||
id="feGaussianBlur1630"
|
||||
result="blur"
|
||||
stdDeviation="0"
|
||||
in="composite1" />
|
||||
<feOffset
|
||||
id="feOffset1632"
|
||||
result="offset"
|
||||
dy="0.5"
|
||||
dx="0" />
|
||||
<feComposite
|
||||
id="feComposite1634"
|
||||
result="fbSourceGraphic"
|
||||
operator="atop"
|
||||
in2="SourceGraphic"
|
||||
<feGaussianBlur id="feGaussianBlur1630" result="blur" stdDeviation="0" in="composite1" />
|
||||
<feOffset id="feOffset1632" result="offset" dy="0.5" dx="0" />
|
||||
<feComposite id="feComposite1634" result="fbSourceGraphic" operator="atop" in2="SourceGraphic"
|
||||
in="offset" />
|
||||
<feFlood
|
||||
in="fbSourceGraphic"
|
||||
result="flood"
|
||||
flood-color="rgb(0,0,0)"
|
||||
flood-opacity="0.2"
|
||||
<feFlood in="fbSourceGraphic" result="flood" flood-color="rgb(0,0,0)" flood-opacity="0.2"
|
||||
id="feFlood1640" />
|
||||
<feComposite
|
||||
result="composite1"
|
||||
operator="out"
|
||||
in="flood"
|
||||
id="feComposite1642"
|
||||
<feComposite result="composite1" operator="out" in="flood" id="feComposite1642" in2="fbSourceGraphic" />
|
||||
<feGaussianBlur result="blur" stdDeviation="0" in="composite1" id="feGaussianBlur1644" />
|
||||
<feOffset result="offset" dy="-0.5" dx="0" id="feOffset1646" />
|
||||
<feComposite result="fbSourceGraphic" operator="atop" in="offset" id="feComposite1648"
|
||||
in2="fbSourceGraphic" />
|
||||
<feGaussianBlur
|
||||
result="blur"
|
||||
stdDeviation="0"
|
||||
in="composite1"
|
||||
id="feGaussianBlur1644" />
|
||||
<feOffset
|
||||
result="offset"
|
||||
dy="-0.5"
|
||||
dx="0"
|
||||
id="feOffset1646" />
|
||||
<feComposite
|
||||
result="fbSourceGraphic"
|
||||
operator="atop"
|
||||
in="offset"
|
||||
id="feComposite1648"
|
||||
in2="fbSourceGraphic" />
|
||||
<feFlood
|
||||
in="fbSourceGraphic"
|
||||
result="flood"
|
||||
flood-color="rgb(0,0,0)"
|
||||
flood-opacity="0.2"
|
||||
<feFlood in="fbSourceGraphic" result="flood" flood-color="rgb(0,0,0)" flood-opacity="0.2"
|
||||
id="feFlood1664" />
|
||||
<feComposite
|
||||
result="composite1"
|
||||
operator="in"
|
||||
in="flood"
|
||||
id="feComposite1666"
|
||||
in2="fbSourceGraphic" />
|
||||
<feGaussianBlur
|
||||
result="blur"
|
||||
stdDeviation="1"
|
||||
in="composite1"
|
||||
id="feGaussianBlur1668" />
|
||||
<feOffset
|
||||
result="offset"
|
||||
dy="1"
|
||||
dx="0"
|
||||
id="feOffset1670" />
|
||||
<feComposite
|
||||
result="composite2"
|
||||
operator="over"
|
||||
in="fbSourceGraphic"
|
||||
id="feComposite1672"
|
||||
<feComposite result="composite1" operator="in" in="flood" id="feComposite1666" in2="fbSourceGraphic" />
|
||||
<feGaussianBlur result="blur" stdDeviation="1" in="composite1" id="feGaussianBlur1668" />
|
||||
<feOffset result="offset" dy="1" dx="0" id="feOffset1670" />
|
||||
<feComposite result="composite2" operator="over" in="fbSourceGraphic" id="feComposite1672"
|
||||
in2="offset" />
|
||||
</filter>
|
||||
</defs>
|
||||
<circle
|
||||
r="36"
|
||||
cy="54"
|
||||
cx="54"
|
||||
id="circle"
|
||||
style="fill:#262c38;filter:url(#circle-shadow)" />
|
||||
<g
|
||||
style="filter:url(#paper-shadow)"
|
||||
id="paper-group">
|
||||
<path
|
||||
d="m 67.75836,28.470764 -23.382292,23.115602 -0.0887,1.254642 29.189963,2.017946 z"
|
||||
style="fill:#1976d2"
|
||||
id="paper-4" />
|
||||
<path
|
||||
style="fill:#47a2fc;filter:url(#paper-inner-shadow)"
|
||||
d="m 27.949219,41.884766 9.08789,24.064453 25.894532,12.974609 12.376953,-23.86914 -22.298828,-7.638672 v -0.002 z"
|
||||
clip-path="url(#paper-clip)"
|
||||
id="paper-3" />
|
||||
<path
|
||||
d="m 53.009473,47.414648 -15.970174,18.53113 25.894116,12.97696 z"
|
||||
style="fill:#1976d2;filter:url(#paper-edge-1)"
|
||||
<circle r="36" cy="54" cx="54" id="circle" style="fill:#262c38;filter:url(#circle-shadow)" />
|
||||
<g style="filter:url(#paper-shadow)" id="paper-group">
|
||||
<path d="m 67.75836,28.470764 -23.382292,23.115602 -0.0887,1.254642 29.189963,2.017946 z"
|
||||
style="fill:#1976d2" id="paper-4" />
|
||||
<path style="fill:#47a2fc;filter:url(#paper-inner-shadow)" d="m 27.949219,41.884766 9.08789,24.064453 25.894532,12.974609 12.376953,-23.86914 -22.298828,-7.638672 v -0.002 z"
|
||||
clip-path="url(#paper-clip)" id="paper-3" />
|
||||
<path d="m 53.009473,47.414648 -15.970174,18.53113 25.894116,12.97696 z" style="fill:#1976d2;filter:url(#paper-edge-1)"
|
||||
id="paper-2" />
|
||||
<path
|
||||
style="fill:#47a2fc;filter:url(#paper-edge-2)"
|
||||
d="m 53.009766,47.414016 9.923828,31.503906 12.375,-23.865234 z"
|
||||
<path style="fill:#47a2fc;filter:url(#paper-edge-2)" d="m 53.009766,47.414016 9.923828,31.503906 12.375,-23.865234 z"
|
||||
id="paper-1" />
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 6.0 KiB |
@@ -1,5 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
-dontobfuscate
|
||||
|
||||
# Disable ServiceLoader reproducibility-breaking optimizations
|
||||
-keep class kotlinx.coroutines.CoroutineExceptionHandler
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
rootProject.name = "michas-droid"
|
||||
include ':'
|
||||
@@ -1,26 +1,33 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="nya.kitsunyan.foxydroid">
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
|
||||
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent>
|
||||
</queries>
|
||||
|
||||
<application
|
||||
android:name=".MainApplication"
|
||||
android:label="@string/application_name"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:supportsRtl="true"
|
||||
android:allowBackup="false"
|
||||
android:theme="@style/Theme.Main.Light"
|
||||
tools:ignore="GoogleAppIndexingWarning">
|
||||
android:theme="@style/Theme.Main.Light">
|
||||
|
||||
<receiver
|
||||
android:name=".MainApplication$BootReceiver">
|
||||
android:name=".MainApplication$BootReceiver"
|
||||
android:exported="true">
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
@@ -30,6 +37,7 @@
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTask"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
|
||||
@@ -52,7 +60,7 @@
|
||||
<data android:scheme="market" android:host="details" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<intent-filter android:autoVerify="true">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
@@ -69,7 +77,8 @@
|
||||
</activity>
|
||||
|
||||
<service
|
||||
android:name=".service.SyncService" />
|
||||
android:name=".service.SyncService"
|
||||
android:foregroundServiceType="dataSync" />
|
||||
|
||||
<service
|
||||
android:name=".service.SyncService$Job"
|
||||
@@ -77,7 +86,8 @@
|
||||
android:permission="android.permission.BIND_JOB_SERVICE" />
|
||||
|
||||
<service
|
||||
android:name=".service.DownloadService" />
|
||||
android:name=".service.DownloadService"
|
||||
android:foregroundServiceType="dataSync" />
|
||||
|
||||
<receiver
|
||||
android:name=".service.DownloadService$Receiver" />
|
||||
|
||||
|
After Width: | Height: | Size: 50 KiB |
@@ -12,6 +12,7 @@ import android.content.IntentFilter
|
||||
import android.content.pm.PackageInfo
|
||||
import com.squareup.picasso.OkHttp3Downloader
|
||||
import com.squareup.picasso.Picasso
|
||||
import io.reactivex.rxjava3.disposables.Disposable
|
||||
import nya.kitsunyan.foxydroid.content.Cache
|
||||
import nya.kitsunyan.foxydroid.content.Preferences
|
||||
import nya.kitsunyan.foxydroid.content.ProductPreferences
|
||||
@@ -27,7 +28,6 @@ import nya.kitsunyan.foxydroid.utility.extension.android.*
|
||||
import java.net.InetSocketAddress
|
||||
import java.net.Proxy
|
||||
|
||||
@Suppress("unused")
|
||||
class MainApplication: Application() {
|
||||
private fun PackageInfo.toInstalledItem(): InstalledItem {
|
||||
val signatureString = singleSignature?.let(Utils::calculateHash).orEmpty()
|
||||
@@ -38,13 +38,15 @@ class MainApplication: Application() {
|
||||
super.attachBaseContext(Utils.configureLocale(base))
|
||||
}
|
||||
|
||||
private var preferencesDisposable: Disposable? = null
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
val databaseUpdated = Database.init(this)
|
||||
Preferences.init(this)
|
||||
ProductPreferences.init(this)
|
||||
RepositoryUpdater.init(this)
|
||||
RepositoryUpdater.init()
|
||||
listenApplications()
|
||||
listenPreferences()
|
||||
|
||||
@@ -69,7 +71,7 @@ class MainApplication: Application() {
|
||||
Intent.ACTION_PACKAGE_REMOVED -> {
|
||||
val packageInfo = try {
|
||||
packageManager.getPackageInfo(packageName, Android.PackageManager.signaturesFlag)
|
||||
} catch (e: Exception) {
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
if (packageInfo != null) {
|
||||
@@ -93,21 +95,20 @@ class MainApplication: Application() {
|
||||
|
||||
private fun listenPreferences() {
|
||||
updateProxy()
|
||||
var lastAutoSync = Preferences[Preferences.Key.AutoSync]
|
||||
var lastUpdateUnstable = Preferences[Preferences.Key.UpdateUnstable]
|
||||
Preferences.observable.subscribe {
|
||||
val lastAutoSync = Preferences[Preferences.Key.AutoSync]
|
||||
val lastUpdateUnstable = Preferences[Preferences.Key.UpdateUnstable]
|
||||
preferencesDisposable?.dispose()
|
||||
preferencesDisposable = Preferences.observable.subscribe {
|
||||
if (it == Preferences.Key.ProxyType || it == Preferences.Key.ProxyHost || it == Preferences.Key.ProxyPort) {
|
||||
updateProxy()
|
||||
} else if (it == Preferences.Key.AutoSync) {
|
||||
val autoSync = Preferences[Preferences.Key.AutoSync]
|
||||
if (lastAutoSync != autoSync) {
|
||||
lastAutoSync = autoSync
|
||||
updateSyncJob(true)
|
||||
}
|
||||
} else if (it == Preferences.Key.UpdateUnstable) {
|
||||
val updateUnstable = Preferences[Preferences.Key.UpdateUnstable]
|
||||
if (lastUpdateUnstable != updateUnstable) {
|
||||
lastUpdateUnstable = updateUnstable
|
||||
forceSyncAll()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ object Cache {
|
||||
fun getReleaseUri(context: Context, cacheFileName: String): Uri {
|
||||
val file = getReleaseFile(context, cacheFileName)
|
||||
val packageInfo = context.packageManager.getPackageInfo(context.packageName, PackageManager.GET_PROVIDERS)
|
||||
val authority = packageInfo.providers.find { it.name == Provider::class.java.name }!!.authority
|
||||
val authority = packageInfo.providers?.find { it.name == Provider::class.java.name }!!.authority
|
||||
return Uri.Builder().scheme("content").authority(authority)
|
||||
.encodedPath(subPath(context.cacheDir, file)).build()
|
||||
}
|
||||
@@ -106,7 +106,7 @@ object Cache {
|
||||
try {
|
||||
val stat = Os.lstat(it.path)
|
||||
stat.st_atime < olderThan
|
||||
} catch (e: Exception) {
|
||||
} catch (_: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
@@ -138,7 +138,7 @@ object Cache {
|
||||
override fun onCreate(): Boolean = true
|
||||
|
||||
override fun query(uri: Uri, projection: Array<String>?,
|
||||
selection: String?, selectionArgs: Array<out String>?, sortOrder: String?): Cursor? {
|
||||
selection: String?, selectionArgs: Array<out String>?, sortOrder: String?): Cursor {
|
||||
val file = getFileAndTypeForUri(uri).first
|
||||
val columns = (projection ?: defaultColumns).mapNotNull {
|
||||
when (it) {
|
||||
@@ -150,12 +150,12 @@ object Cache {
|
||||
return MatrixCursor(columns.first.toTypedArray()).apply { addRow(columns.second.toTypedArray()) }
|
||||
}
|
||||
|
||||
override fun getType(uri: Uri): String? = getFileAndTypeForUri(uri).second
|
||||
override fun getType(uri: Uri): String = getFileAndTypeForUri(uri).second
|
||||
|
||||
private val unsupported: Nothing
|
||||
get() = throw UnsupportedOperationException()
|
||||
|
||||
override fun insert(uri: Uri, contentValues: ContentValues?): Uri? = unsupported
|
||||
override fun insert(uri: Uri, contentValues: ContentValues?): Uri = unsupported
|
||||
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int = unsupported
|
||||
override fun update(uri: Uri, contentValues: ContentValues?,
|
||||
selection: String?, selectionArgs: Array<out String>?): Int = unsupported
|
||||
|
||||
@@ -9,6 +9,7 @@ import nya.kitsunyan.foxydroid.R
|
||||
import nya.kitsunyan.foxydroid.entity.ProductItem
|
||||
import nya.kitsunyan.foxydroid.utility.extension.android.*
|
||||
import java.net.Proxy
|
||||
import androidx.core.content.edit
|
||||
|
||||
object Preferences {
|
||||
private lateinit var preferences: SharedPreferences
|
||||
@@ -18,9 +19,13 @@ object Preferences {
|
||||
private val keys = sequenceOf(Key.AutoSync, Key.IncompatibleVersions, Key.ProxyHost, Key.ProxyPort, Key.ProxyType,
|
||||
Key.SortOrder, Key.Theme, Key.UpdateNotify, Key.UpdateUnstable).map { Pair(it.name, it) }.toMap()
|
||||
|
||||
private val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, keyString ->
|
||||
keys[keyString]?.let(subject::onNext)
|
||||
}
|
||||
|
||||
fun init(context: Context) {
|
||||
preferences = context.getSharedPreferences("${context.packageName}_preferences", Context.MODE_PRIVATE)
|
||||
preferences.registerOnSharedPreferenceChangeListener { _, keyString -> keys[keyString]?.let(subject::onNext) }
|
||||
preferences.registerOnSharedPreferenceChangeListener(listener)
|
||||
}
|
||||
|
||||
val observable: Observable<Key<*>>
|
||||
@@ -38,7 +43,7 @@ object Preferences {
|
||||
}
|
||||
|
||||
override fun set(preferences: SharedPreferences, key: String, value: Boolean) {
|
||||
preferences.edit().putBoolean(key, value).apply()
|
||||
preferences.edit(commit = true) { putBoolean(key, value) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,7 +53,7 @@ object Preferences {
|
||||
}
|
||||
|
||||
override fun set(preferences: SharedPreferences, key: String, value: Int) {
|
||||
preferences.edit().putInt(key, value).apply()
|
||||
preferences.edit(commit = true) { putInt(key, value) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,7 +63,7 @@ object Preferences {
|
||||
}
|
||||
|
||||
override fun set(preferences: SharedPreferences, key: String, value: String) {
|
||||
preferences.edit().putString(key, value).apply()
|
||||
preferences.edit(commit = true) { putString(key, value) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,7 +74,7 @@ object Preferences {
|
||||
}
|
||||
|
||||
override fun set(preferences: SharedPreferences, key: String, value: T) {
|
||||
preferences.edit().putString(key, value.valueString).apply()
|
||||
preferences.edit(commit = true) { putString(key, value.valueString) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package nya.kitsunyan.foxydroid.content
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import io.reactivex.rxjava3.disposables.Disposable
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import io.reactivex.rxjava3.subjects.PublishSubject
|
||||
import nya.kitsunyan.foxydroid.database.Database
|
||||
@@ -9,17 +10,20 @@ import nya.kitsunyan.foxydroid.entity.ProductPreference
|
||||
import nya.kitsunyan.foxydroid.utility.extension.json.*
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.nio.charset.Charset
|
||||
import androidx.core.content.edit
|
||||
|
||||
object ProductPreferences {
|
||||
private val defaultProductPreference = ProductPreference(false, 0L)
|
||||
private lateinit var preferences: SharedPreferences
|
||||
private val subject = PublishSubject.create<Pair<String, Long?>>()
|
||||
private var disposable: Disposable? = null
|
||||
|
||||
fun init(context: Context) {
|
||||
preferences = context.getSharedPreferences("product_preferences", Context.MODE_PRIVATE)
|
||||
Database.LockAdapter.putAll(preferences.all.keys
|
||||
.mapNotNull { packageName -> this[packageName].databaseVersionCode?.let { Pair(packageName, it) } })
|
||||
subject
|
||||
disposable?.dispose()
|
||||
disposable = subject
|
||||
.observeOn(Schedulers.io())
|
||||
.subscribe { (packageName, versionCode) ->
|
||||
if (versionCode != null) {
|
||||
@@ -53,9 +57,15 @@ object ProductPreferences {
|
||||
|
||||
operator fun set(packageName: String, productPreference: ProductPreference) {
|
||||
val oldProductPreference = this[packageName]
|
||||
preferences.edit().putString(packageName, ByteArrayOutputStream()
|
||||
.apply { Json.factory.createGenerator(this).use { it.writeDictionary(productPreference::serialize) } }
|
||||
.toByteArray().toString(Charset.defaultCharset())).apply()
|
||||
preferences.edit {
|
||||
putString(
|
||||
packageName, ByteArrayOutputStream()
|
||||
.apply {
|
||||
Json.factory.createGenerator(this)
|
||||
.use { it.writeDictionary(productPreference::serialize) }
|
||||
}
|
||||
.toByteArray().toString(Charset.defaultCharset()))
|
||||
}
|
||||
if (oldProductPreference.ignoreUpdates != productPreference.ignoreUpdates ||
|
||||
oldProductPreference.ignoreVersionCode != productPreference.ignoreVersionCode) {
|
||||
subject.onNext(Pair(packageName, productPreference.databaseVersionCode))
|
||||
|
||||
@@ -3,6 +3,8 @@ package nya.kitsunyan.foxydroid.database
|
||||
import android.database.Cursor
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.loader.app.LoaderManager
|
||||
import androidx.loader.content.Loader
|
||||
import nya.kitsunyan.foxydroid.entity.ProductItem
|
||||
@@ -39,13 +41,20 @@ class CursorOwner: Fragment(), LoaderManager.LoaderCallbacks<Cursor> {
|
||||
fun onCursorData(request: Request, cursor: Cursor?)
|
||||
}
|
||||
|
||||
private data class ActiveRequest(val request: Request, val callback: Callback?, val cursor: Cursor?)
|
||||
data class ActiveRequest(val request: Request, val callback: Callback?, val cursor: Cursor?)
|
||||
|
||||
init {
|
||||
retainInstance = true
|
||||
class CursorViewModel : ViewModel() {
|
||||
internal val activeRequests = mutableMapOf<Int, ActiveRequest>()
|
||||
|
||||
override fun onCleared() {
|
||||
activeRequests.values.forEach { it.cursor?.close() }
|
||||
activeRequests.clear()
|
||||
}
|
||||
}
|
||||
|
||||
private val activeRequests = mutableMapOf<Int, ActiveRequest>()
|
||||
private val viewModel by lazy { ViewModelProvider(this)[CursorViewModel::class.java] }
|
||||
private val activeRequests: MutableMap<Int, ActiveRequest>
|
||||
get() = viewModel.activeRequests
|
||||
|
||||
fun attach(callback: Callback, request: Request) {
|
||||
val oldActiveRequest = activeRequests[request.id]
|
||||
@@ -79,11 +88,32 @@ class CursorOwner: Fragment(), LoaderManager.LoaderCallbacks<Cursor> {
|
||||
return QueryLoader(requireContext()) {
|
||||
when (request) {
|
||||
is Request.ProductsAvailable -> Database.ProductAdapter
|
||||
.query(false, false, request.searchQuery, request.section, request.order, it)
|
||||
.query(
|
||||
installed = false,
|
||||
updates = false,
|
||||
searchQuery = request.searchQuery,
|
||||
section = request.section,
|
||||
order = request.order,
|
||||
signal = it
|
||||
)
|
||||
is Request.ProductsInstalled -> Database.ProductAdapter
|
||||
.query(true, false, request.searchQuery, request.section, request.order, it)
|
||||
.query(
|
||||
installed = true,
|
||||
updates = false,
|
||||
searchQuery = request.searchQuery,
|
||||
section = request.section,
|
||||
order = request.order,
|
||||
signal = it
|
||||
)
|
||||
is Request.ProductsUpdates -> Database.ProductAdapter
|
||||
.query(true, true, request.searchQuery, request.section, request.order, it)
|
||||
.query(
|
||||
installed = true,
|
||||
updates = true,
|
||||
searchQuery = request.searchQuery,
|
||||
section = request.section,
|
||||
order = request.order,
|
||||
signal = it
|
||||
)
|
||||
is Request.Repositories -> Database.RepositoryAdapter.query(it)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import nya.kitsunyan.foxydroid.entity.Repository
|
||||
import nya.kitsunyan.foxydroid.utility.extension.android.*
|
||||
import nya.kitsunyan.foxydroid.utility.extension.json.*
|
||||
import java.io.ByteArrayOutputStream
|
||||
import androidx.core.database.sqlite.transaction
|
||||
|
||||
object Database {
|
||||
fun init(context: Context): Boolean {
|
||||
@@ -170,6 +171,7 @@ object Database {
|
||||
}
|
||||
|
||||
override fun onOpen(db: SQLiteDatabase) {
|
||||
db.enableWriteAheadLogging()
|
||||
val create = handleTables(db, false, Schema.Repository)
|
||||
val updated = handleTables(db, create, Schema.Product, Schema.Category)
|
||||
db.execSQL("ATTACH DATABASE ':memory:' AS memory")
|
||||
@@ -182,9 +184,11 @@ object Database {
|
||||
}
|
||||
|
||||
private fun handleTables(db: SQLiteDatabase, recreate: Boolean, vararg tables: Table): Boolean {
|
||||
val shouldRecreate = recreate || tables.any {
|
||||
val sql = db.query("${it.databasePrefix}sqlite_master", columns = arrayOf("sql"),
|
||||
selection = Pair("type = ? AND name = ?", arrayOf("table", it.innerName)))
|
||||
val shouldRecreate = recreate || tables.any { it ->
|
||||
val sql = db.query(
|
||||
"${it.databasePrefix}sqlite_master", columns = arrayOf("sql"),
|
||||
selection = Pair("type = ? AND name = ?", arrayOf("table", it.innerName))
|
||||
)
|
||||
.use { it.firstOrNull()?.getString(0) }.orEmpty()
|
||||
it.formatCreateTable(it.innerName) != sql
|
||||
}
|
||||
@@ -202,10 +206,12 @@ object Database {
|
||||
}
|
||||
|
||||
private fun handleIndexes(db: SQLiteDatabase, vararg tables: Table) {
|
||||
val shouldVacuum = tables.map {
|
||||
val sqls = db.query("${it.databasePrefix}sqlite_master", columns = arrayOf("name", "sql"),
|
||||
selection = Pair("type = ? AND tbl_name = ?", arrayOf("index", it.innerName)))
|
||||
.use { it.asSequence().mapNotNull { it.getString(1)?.let { sql -> Pair(it.getString(0), sql) } }.toList() }
|
||||
val shouldVacuum = tables.map { it ->
|
||||
val sqls = db.query(
|
||||
"${it.databasePrefix}sqlite_master", columns = arrayOf("name", "sql"),
|
||||
selection = Pair("type = ? AND tbl_name = ?", arrayOf("index", it.innerName))
|
||||
)
|
||||
.use { it -> it.asSequence().mapNotNull { it.getString(1)?.let { sql -> Pair(it.getString(0), sql) } }.toList() }
|
||||
.filter { !it.first.startsWith("sqlite_") }
|
||||
val createIndexes = it.createIndexPairFormatted?.let { listOf(it) }.orEmpty()
|
||||
createIndexes.map { it.first } != sqls.map { it.second } && run {
|
||||
@@ -224,11 +230,13 @@ object Database {
|
||||
}
|
||||
|
||||
private fun dropOldTables(db: SQLiteDatabase, vararg neededTables: Table) {
|
||||
val tables = db.query("sqlite_master", columns = arrayOf("name"),
|
||||
selection = Pair("type = ?", arrayOf("table")))
|
||||
.use { it.asSequence().mapNotNull { it.getString(0) }.toList() }
|
||||
val tables = db.query(
|
||||
"sqlite_master", columns = arrayOf("name"),
|
||||
selection = Pair("type = ?", arrayOf("table"))
|
||||
)
|
||||
.use { it -> it.asSequence().mapNotNull { it.getString(0) }.toList() }
|
||||
.filter { !it.startsWith("sqlite_") && !it.startsWith("android_") }
|
||||
.toSet() - neededTables.mapNotNull { if (it.memory) null else it.name }
|
||||
.toSet() - neededTables.mapNotNull { if (it.memory) null else it.name }.toSet()
|
||||
if (tables.isNotEmpty()) {
|
||||
for (table in tables) {
|
||||
db.execSQL("DROP TABLE IF EXISTS $table")
|
||||
@@ -281,9 +289,11 @@ object Database {
|
||||
return if (replace) replace(table, null, contentValues) else insert(table, null, contentValues)
|
||||
}
|
||||
|
||||
private fun SQLiteDatabase.query(table: String, columns: Array<String>? = null,
|
||||
private fun SQLiteDatabase.query(
|
||||
table: String, columns: Array<String>? = null,
|
||||
selection: Pair<String, Array<String>>? = null, orderBy: String? = null,
|
||||
signal: CancellationSignal? = null): Cursor {
|
||||
signal: CancellationSignal? = null
|
||||
): Cursor {
|
||||
return query(false, table, columns, selection?.first, selection?.second, null, null, orderBy, null, signal)
|
||||
}
|
||||
|
||||
@@ -313,33 +323,40 @@ object Database {
|
||||
})
|
||||
}
|
||||
|
||||
fun put(repository: Repository): Repository {
|
||||
fun put(repository: Repository): Long {
|
||||
val shouldReplace = repository.id >= 0L
|
||||
val newId = putWithoutNotification(repository, shouldReplace)
|
||||
val id = if (shouldReplace) repository.id else newId
|
||||
notifyChanged(Subject.Repositories, Subject.Repository(id), Subject.Products)
|
||||
return if (newId != repository.id) repository.copy(id = newId) else repository
|
||||
return id
|
||||
}
|
||||
|
||||
fun get(id: Long): Repository? {
|
||||
return db.query(Schema.Repository.name,
|
||||
return db.query(
|
||||
Schema.Repository.name,
|
||||
selection = Pair("${Schema.Repository.ROW_ID} = ? AND ${Schema.Repository.ROW_DELETED} == 0",
|
||||
arrayOf(id.toString())))
|
||||
arrayOf(id.toString()))
|
||||
)
|
||||
.use { it.firstOrNull()?.let(::transform) }
|
||||
}
|
||||
|
||||
fun getAll(signal: CancellationSignal?): List<Repository> {
|
||||
return db.query(Schema.Repository.name,
|
||||
return db.query(
|
||||
Schema.Repository.name,
|
||||
selection = Pair("${Schema.Repository.ROW_DELETED} == 0", emptyArray()),
|
||||
signal = signal).use { it.asSequence().map(::transform).toList() }
|
||||
signal = signal
|
||||
).use { it.asSequence().map(::transform).toList() }
|
||||
}
|
||||
|
||||
fun getAllDisabledDeleted(signal: CancellationSignal?): Set<Pair<Long, Boolean>> {
|
||||
return db.query(Schema.Repository.name,
|
||||
return db.query(
|
||||
Schema.Repository.name,
|
||||
columns = arrayOf(Schema.Repository.ROW_ID, Schema.Repository.ROW_DELETED),
|
||||
selection = Pair("${Schema.Repository.ROW_ENABLED} == 0 OR ${Schema.Repository.ROW_DELETED} != 0", emptyArray()),
|
||||
signal = signal).use { it.asSequence().map { Pair(it.getLong(it.getColumnIndex(Schema.Repository.ROW_ID)),
|
||||
it.getInt(it.getColumnIndex(Schema.Repository.ROW_DELETED)) != 0) }.toSet() }
|
||||
signal = signal
|
||||
).use { it ->
|
||||
it.asSequence().map { Pair(it.getLong(it.getColumnIndexOrThrow(Schema.Repository.ROW_ID)),
|
||||
it.getInt(it.getColumnIndexOrThrow(Schema.Repository.ROW_DELETED)) != 0) }.toSet() }
|
||||
}
|
||||
|
||||
fun markAsDeleted(id: Long) {
|
||||
@@ -350,7 +367,7 @@ object Database {
|
||||
}
|
||||
|
||||
fun cleanup(pairs: Set<Pair<Long, Boolean>>) {
|
||||
val result = pairs.windowed(10, 10, true).map {
|
||||
val result = pairs.windowed(10, 10, true).map { it ->
|
||||
val idsString = it.joinToString(separator = ", ") { it.first.toString() }
|
||||
val productsCount = db.delete(Schema.Product.name,
|
||||
"${Schema.Product.ROW_REPOSITORY_ID} IN ($idsString)", null)
|
||||
@@ -369,28 +386,34 @@ object Database {
|
||||
}
|
||||
|
||||
fun query(signal: CancellationSignal?): Cursor {
|
||||
return db.query(Schema.Repository.name,
|
||||
return db.query(
|
||||
Schema.Repository.name,
|
||||
selection = Pair("${Schema.Repository.ROW_DELETED} == 0", emptyArray()),
|
||||
signal = signal).observable(Subject.Repositories)
|
||||
signal = signal
|
||||
).observable(Subject.Repositories)
|
||||
}
|
||||
|
||||
fun transform(cursor: Cursor): Repository {
|
||||
return cursor.getBlob(cursor.getColumnIndex(Schema.Repository.ROW_DATA))
|
||||
.jsonParse { Repository.deserialize(cursor.getLong(cursor.getColumnIndex(Schema.Repository.ROW_ID)), it) }
|
||||
return cursor.getBlob(cursor.getColumnIndexOrThrow(Schema.Repository.ROW_DATA))
|
||||
.jsonParse { Repository.deserialize(cursor.getLong(cursor.getColumnIndexOrThrow(Schema.Repository.ROW_ID)), it) }
|
||||
}
|
||||
}
|
||||
|
||||
object ProductAdapter {
|
||||
fun get(packageName: String, signal: CancellationSignal?): List<Product> {
|
||||
return db.query(Schema.Product.name,
|
||||
return db.query(
|
||||
Schema.Product.name,
|
||||
columns = arrayOf(Schema.Product.ROW_REPOSITORY_ID, Schema.Product.ROW_DESCRIPTION, Schema.Product.ROW_DATA),
|
||||
selection = Pair("${Schema.Product.ROW_PACKAGE_NAME} = ?", arrayOf(packageName)),
|
||||
signal = signal).use { it.asSequence().map(::transform).toList() }
|
||||
signal = signal
|
||||
).use { it.asSequence().map(::transform).toList() }
|
||||
}
|
||||
|
||||
fun getCount(repositoryId: Long): Int {
|
||||
return db.query(Schema.Product.name, columns = arrayOf("COUNT (*)"),
|
||||
selection = Pair("${Schema.Product.ROW_REPOSITORY_ID} = ?", arrayOf(repositoryId.toString())))
|
||||
return db.query(
|
||||
Schema.Product.name, columns = arrayOf("COUNT (*)"),
|
||||
selection = Pair("${Schema.Product.ROW_REPOSITORY_ID} = ?", arrayOf(repositoryId.toString()))
|
||||
)
|
||||
.use { it.firstOrNull()?.getInt(0) ?: 0 }
|
||||
}
|
||||
|
||||
@@ -464,28 +487,28 @@ object Database {
|
||||
ProductItem.Order.NAME -> Unit
|
||||
ProductItem.Order.DATE_ADDED -> builder += "product.${Schema.Product.ROW_ADDED} DESC,"
|
||||
ProductItem.Order.LAST_UPDATE -> builder += "product.${Schema.Product.ROW_UPDATED} DESC,"
|
||||
}::class
|
||||
}
|
||||
builder += "product.${Schema.Product.ROW_NAME} COLLATE LOCALIZED ASC"
|
||||
|
||||
return builder.query(db, signal).observable(Subject.Products)
|
||||
}
|
||||
|
||||
private fun transform(cursor: Cursor): Product {
|
||||
return cursor.getBlob(cursor.getColumnIndex(Schema.Product.ROW_DATA))
|
||||
.jsonParse { Product.deserialize(cursor.getLong(cursor.getColumnIndex(Schema.Product.ROW_REPOSITORY_ID)),
|
||||
cursor.getString(cursor.getColumnIndex(Schema.Product.ROW_DESCRIPTION)), it) }
|
||||
return cursor.getBlob(cursor.getColumnIndexOrThrow(Schema.Product.ROW_DATA))
|
||||
.jsonParse { Product.deserialize(cursor.getLong(cursor.getColumnIndexOrThrow(Schema.Product.ROW_REPOSITORY_ID)),
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(Schema.Product.ROW_DESCRIPTION)), it) }
|
||||
}
|
||||
|
||||
fun transformItem(cursor: Cursor): ProductItem {
|
||||
return cursor.getBlob(cursor.getColumnIndex(Schema.Product.ROW_DATA_ITEM))
|
||||
.jsonParse { ProductItem.deserialize(cursor.getLong(cursor.getColumnIndex(Schema.Product.ROW_REPOSITORY_ID)),
|
||||
cursor.getString(cursor.getColumnIndex(Schema.Product.ROW_PACKAGE_NAME)),
|
||||
cursor.getString(cursor.getColumnIndex(Schema.Product.ROW_NAME)),
|
||||
cursor.getString(cursor.getColumnIndex(Schema.Product.ROW_SUMMARY)),
|
||||
cursor.getString(cursor.getColumnIndex(Schema.Installed.ROW_VERSION)).orEmpty(),
|
||||
cursor.getInt(cursor.getColumnIndex(Schema.Product.ROW_COMPATIBLE)) != 0,
|
||||
cursor.getInt(cursor.getColumnIndex(Schema.Synthetic.ROW_CAN_UPDATE)) != 0,
|
||||
cursor.getInt(cursor.getColumnIndex(Schema.Synthetic.ROW_MATCH_RANK)), it) }
|
||||
return cursor.getBlob(cursor.getColumnIndexOrThrow(Schema.Product.ROW_DATA_ITEM))
|
||||
.jsonParse { ProductItem.deserialize(cursor.getLong(cursor.getColumnIndexOrThrow(Schema.Product.ROW_REPOSITORY_ID)),
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(Schema.Product.ROW_PACKAGE_NAME)),
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(Schema.Product.ROW_NAME)),
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(Schema.Product.ROW_SUMMARY)),
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(Schema.Installed.ROW_VERSION)).orEmpty(),
|
||||
cursor.getInt(cursor.getColumnIndexOrThrow(Schema.Product.ROW_COMPATIBLE)) != 0,
|
||||
cursor.getInt(cursor.getColumnIndexOrThrow(Schema.Synthetic.ROW_CAN_UPDATE)) != 0,
|
||||
cursor.getInt(cursor.getColumnIndexOrThrow(Schema.Synthetic.ROW_MATCH_RANK)), it) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -500,18 +523,21 @@ object Database {
|
||||
WHERE repository.${Schema.Repository.ROW_ENABLED} != 0 AND
|
||||
repository.${Schema.Repository.ROW_DELETED} == 0"""
|
||||
|
||||
return builder.query(db, signal).use { it.asSequence()
|
||||
.map { it.getString(it.getColumnIndex(Schema.Category.ROW_NAME)) }.toSet() }
|
||||
return builder.query(db, signal).use { it ->
|
||||
it.asSequence()
|
||||
.map { it.getString(it.getColumnIndexOrThrow(Schema.Category.ROW_NAME)) }.toSet() }
|
||||
}
|
||||
}
|
||||
|
||||
object InstalledAdapter {
|
||||
fun get(packageName: String, signal: CancellationSignal?): InstalledItem? {
|
||||
return db.query(Schema.Installed.name,
|
||||
return db.query(
|
||||
Schema.Installed.name,
|
||||
columns = arrayOf(Schema.Installed.ROW_PACKAGE_NAME, Schema.Installed.ROW_VERSION,
|
||||
Schema.Installed.ROW_VERSION_CODE, Schema.Installed.ROW_SIGNATURE),
|
||||
selection = Pair("${Schema.Installed.ROW_PACKAGE_NAME} = ?", arrayOf(packageName)),
|
||||
signal = signal).use { it.firstOrNull()?.let(::transform) }
|
||||
signal = signal
|
||||
).use { it.firstOrNull()?.let(::transform) }
|
||||
}
|
||||
|
||||
private fun put(installedItem: InstalledItem, notify: Boolean) {
|
||||
@@ -529,13 +555,12 @@ object Database {
|
||||
fun put(installedItem: InstalledItem) = put(installedItem, true)
|
||||
|
||||
fun putAll(installedItems: List<InstalledItem>) {
|
||||
db.beginTransaction()
|
||||
db.transaction {
|
||||
try {
|
||||
db.delete(Schema.Installed.name, null, null)
|
||||
delete(Schema.Installed.name, null, null)
|
||||
installedItems.forEach { put(it, false) }
|
||||
db.setTransactionSuccessful()
|
||||
} finally {
|
||||
db.endTransaction()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -547,10 +572,10 @@ object Database {
|
||||
}
|
||||
|
||||
private fun transform(cursor: Cursor): InstalledItem {
|
||||
return InstalledItem(cursor.getString(cursor.getColumnIndex(Schema.Installed.ROW_PACKAGE_NAME)),
|
||||
cursor.getString(cursor.getColumnIndex(Schema.Installed.ROW_VERSION)),
|
||||
cursor.getLong(cursor.getColumnIndex(Schema.Installed.ROW_VERSION_CODE)),
|
||||
cursor.getString(cursor.getColumnIndex(Schema.Installed.ROW_SIGNATURE)))
|
||||
return InstalledItem(cursor.getString(cursor.getColumnIndexOrThrow(Schema.Installed.ROW_PACKAGE_NAME)),
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(Schema.Installed.ROW_VERSION)),
|
||||
cursor.getLong(cursor.getColumnIndexOrThrow(Schema.Installed.ROW_VERSION_CODE)),
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(Schema.Installed.ROW_SIGNATURE)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -568,13 +593,12 @@ object Database {
|
||||
fun put(lock: Pair<String, Long>) = put(lock, true)
|
||||
|
||||
fun putAll(locks: List<Pair<String, Long>>) {
|
||||
db.beginTransaction()
|
||||
db.transaction {
|
||||
try {
|
||||
db.delete(Schema.Lock.name, null, null)
|
||||
delete(Schema.Lock.name, null, null)
|
||||
locks.forEach { put(it, false) }
|
||||
db.setTransactionSuccessful()
|
||||
} finally {
|
||||
db.endTransaction()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -596,13 +620,13 @@ object Database {
|
||||
}
|
||||
|
||||
fun putTemporary(products: List<Product>) {
|
||||
db.beginTransaction()
|
||||
db.transaction {
|
||||
try {
|
||||
for (product in products) {
|
||||
// Format signatures like ".signature1.signature2." for easier select
|
||||
val signatures = product.signatures.joinToString { ".$it" }
|
||||
.let { if (it.isNotEmpty()) "$it." else "" }
|
||||
db.insertOrReplace(true, Schema.Product.temporaryName, ContentValues().apply {
|
||||
insertOrReplace(true, Schema.Product.temporaryName, ContentValues().apply {
|
||||
put(Schema.Product.ROW_REPOSITORY_ID, product.repositoryId)
|
||||
put(Schema.Product.ROW_PACKAGE_NAME, product.packageName)
|
||||
put(Schema.Product.ROW_NAME, product.name)
|
||||
@@ -617,39 +641,39 @@ object Database {
|
||||
put(Schema.Product.ROW_DATA_ITEM, jsonGenerate(product.item()::serialize))
|
||||
})
|
||||
for (category in product.categories) {
|
||||
db.insertOrReplace(true, Schema.Category.temporaryName, ContentValues().apply {
|
||||
insertOrReplace(true, Schema.Category.temporaryName, ContentValues().apply {
|
||||
put(Schema.Category.ROW_REPOSITORY_ID, product.repositoryId)
|
||||
put(Schema.Category.ROW_PACKAGE_NAME, product.packageName)
|
||||
put(Schema.Category.ROW_NAME, category)
|
||||
})
|
||||
}
|
||||
}
|
||||
db.setTransactionSuccessful()
|
||||
} finally {
|
||||
db.endTransaction()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun finishTemporary(repository: Repository, success: Boolean) {
|
||||
if (success) {
|
||||
db.beginTransaction()
|
||||
db.transaction {
|
||||
try {
|
||||
db.delete(Schema.Product.name, "${Schema.Product.ROW_REPOSITORY_ID} = ?",
|
||||
arrayOf(repository.id.toString()))
|
||||
db.delete(Schema.Category.name, "${Schema.Category.ROW_REPOSITORY_ID} = ?",
|
||||
arrayOf(repository.id.toString()))
|
||||
db.execSQL("INSERT INTO ${Schema.Product.name} SELECT * FROM ${Schema.Product.temporaryName}")
|
||||
db.execSQL("INSERT INTO ${Schema.Category.name} SELECT * FROM ${Schema.Category.temporaryName}")
|
||||
delete(
|
||||
Schema.Product.name, "${Schema.Product.ROW_REPOSITORY_ID} = ?",
|
||||
arrayOf(repository.id.toString())
|
||||
)
|
||||
delete(
|
||||
Schema.Category.name, "${Schema.Category.ROW_REPOSITORY_ID} = ?",
|
||||
arrayOf(repository.id.toString())
|
||||
)
|
||||
execSQL("INSERT INTO ${Schema.Product.name} SELECT * FROM ${Schema.Product.temporaryName}")
|
||||
execSQL("INSERT INTO ${Schema.Category.name} SELECT * FROM ${Schema.Category.temporaryName}")
|
||||
RepositoryAdapter.putWithoutNotification(repository, true)
|
||||
db.execSQL("DROP TABLE IF EXISTS ${Schema.Product.temporaryName}")
|
||||
db.execSQL("DROP TABLE IF EXISTS ${Schema.Category.temporaryName}")
|
||||
db.setTransactionSuccessful()
|
||||
execSQL("DROP TABLE IF EXISTS ${Schema.Product.temporaryName}")
|
||||
execSQL("DROP TABLE IF EXISTS ${Schema.Category.temporaryName}")
|
||||
} finally {
|
||||
db.endTransaction()
|
||||
}
|
||||
if (success) {
|
||||
}
|
||||
notifyChanged(Subject.Repositories, Subject.Repository(repository.id), Subject.Products)
|
||||
}
|
||||
} else {
|
||||
db.execSQL("DROP TABLE IF EXISTS ${Schema.Product.temporaryName}")
|
||||
db.execSQL("DROP TABLE IF EXISTS ${Schema.Category.temporaryName}")
|
||||
|
||||
@@ -1,57 +1,37 @@
|
||||
package nya.kitsunyan.foxydroid.database
|
||||
|
||||
import android.database.ContentObservable
|
||||
import android.database.ContentObserver
|
||||
import android.database.Cursor
|
||||
import android.database.CursorWrapper
|
||||
import java.util.concurrent.CopyOnWriteArraySet
|
||||
|
||||
class ObservableCursor(cursor: Cursor, private val observable: (register: Boolean,
|
||||
observer: () -> Unit) -> Unit): CursorWrapper(cursor) {
|
||||
private var registered = false
|
||||
private val contentObservable = ContentObservable()
|
||||
private val observers = CopyOnWriteArraySet<ContentObserver>()
|
||||
|
||||
private val onChange: () -> Unit = {
|
||||
contentObservable.dispatchChange(false, null)
|
||||
for (observer in observers) {
|
||||
observer.dispatchChange(false, null)
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
observable(true, onChange)
|
||||
registered = true
|
||||
}
|
||||
|
||||
override fun registerContentObserver(observer: ContentObserver) {
|
||||
super.registerContentObserver(observer)
|
||||
contentObservable.registerObserver(observer)
|
||||
observers.add(observer)
|
||||
}
|
||||
|
||||
override fun unregisterContentObserver(observer: ContentObserver) {
|
||||
super.unregisterContentObserver(observer)
|
||||
contentObservable.unregisterObserver(observer)
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
override fun requery(): Boolean {
|
||||
if (!registered) {
|
||||
observable(true, onChange)
|
||||
registered = true
|
||||
}
|
||||
return super.requery()
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
override fun deactivate() {
|
||||
super.deactivate()
|
||||
deactivateOrClose()
|
||||
observers.remove(observer)
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
super.close()
|
||||
contentObservable.unregisterAll()
|
||||
deactivateOrClose()
|
||||
}
|
||||
|
||||
private fun deactivateOrClose() {
|
||||
observers.clear()
|
||||
observable(false, onChange)
|
||||
registered = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,14 +32,15 @@ class QueryBuilder {
|
||||
this.arguments += arguments
|
||||
}
|
||||
|
||||
fun query(db: SQLiteDatabase, signal: CancellationSignal?): Cursor {
|
||||
fun query(db: SQLiteDatabase, signal: CancellationSignal? = null): Cursor {
|
||||
val query = builder.toString()
|
||||
val arguments = arguments.toTypedArray()
|
||||
if (BuildConfig.DEBUG) {
|
||||
synchronized(QueryBuilder::class.java) {
|
||||
debug(query)
|
||||
db.rawQuery("EXPLAIN QUERY PLAN $query", arguments).use { it.asSequence()
|
||||
.forEach { debug(":: ${it.getString(it.getColumnIndex("detail"))}") } }
|
||||
db.rawQuery("EXPLAIN QUERY PLAN $query", arguments).use { it ->
|
||||
it.asSequence()
|
||||
.forEach { debug(":: ${it.getString(it.getColumnIndexOrThrow("detail"))}") } }
|
||||
}
|
||||
}
|
||||
return db.rawQuery(query, arguments, signal)
|
||||
|
||||
@@ -156,7 +156,7 @@ data class Product(val repositoryId: Long, val packageName: String, val name: St
|
||||
var donates = emptyList<Donate>()
|
||||
var screenshots = emptyList<Screenshot>()
|
||||
var releases = emptyList<Release>()
|
||||
parser.forEachKey {
|
||||
parser.forEachKey { it ->
|
||||
when {
|
||||
it.string("packageName") -> packageName = valueAsString
|
||||
it.string("name") -> name = valueAsString
|
||||
@@ -213,7 +213,7 @@ data class Product(val repositoryId: Long, val packageName: String, val name: St
|
||||
else -> skipChildren()
|
||||
}
|
||||
}
|
||||
Screenshot.Type.values().find { it.jsonName == type }?.let { Screenshot(locale, it, path) }
|
||||
Screenshot.Type.entries.find { it.jsonName == type }?.let { Screenshot(locale, it, path) }
|
||||
}
|
||||
it.array("releases") -> releases = collectNotNull(JsonToken.START_OBJECT, Release.Companion::deserialize)
|
||||
else -> skipChildren()
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
package nya.kitsunyan.foxydroid.entity
|
||||
|
||||
import android.net.Uri
|
||||
import com.fasterxml.jackson.core.JsonGenerator
|
||||
import com.fasterxml.jackson.core.JsonParser
|
||||
import com.fasterxml.jackson.core.JsonToken
|
||||
import nya.kitsunyan.foxydroid.utility.extension.json.*
|
||||
import androidx.core.net.toUri
|
||||
|
||||
data class Release(val selected: Boolean, val version: String, val versionCode: Long,
|
||||
val added: Long, val size: Long, val minSdkVersion: Int, val targetSdkVersion: Int, val maxSdkVersion: Int,
|
||||
@@ -24,7 +24,7 @@ data class Release(val selected: Boolean, val version: String, val versionCode:
|
||||
get() = "$versionCode.$hash"
|
||||
|
||||
fun getDownloadUrl(repository: Repository): String {
|
||||
return Uri.parse(repository.address).buildUpon().appendPath(release).build().toString()
|
||||
return repository.address.toUri().buildUpon().appendPath(release).build().toString()
|
||||
}
|
||||
|
||||
val cacheFileName: String
|
||||
@@ -102,7 +102,7 @@ data class Release(val selected: Boolean, val version: String, val versionCode:
|
||||
var features = emptyList<String>()
|
||||
var platforms = emptyList<String>()
|
||||
var incompatibilities = emptyList<Incompatibility>()
|
||||
parser.forEachKey {
|
||||
parser.forEachKey { it ->
|
||||
when {
|
||||
it.boolean("selected") -> selected = valueAsBoolean
|
||||
it.string("version") -> version = valueAsString
|
||||
|
||||
@@ -3,20 +3,13 @@ package nya.kitsunyan.foxydroid.entity
|
||||
import com.fasterxml.jackson.core.JsonGenerator
|
||||
import com.fasterxml.jackson.core.JsonParser
|
||||
import nya.kitsunyan.foxydroid.utility.extension.json.*
|
||||
import java.net.URL
|
||||
|
||||
data class Repository(val id: Long, val address: String, val mirrors: List<String>,
|
||||
data class Repository(
|
||||
val id: Long, val address: String, val mirrors: List<String>,
|
||||
val name: String, val description: String, val version: Int, val enabled: Boolean,
|
||||
val fingerprint: String, val lastModified: String, val entityTag: String,
|
||||
val updated: Long, val timestamp: Long, val authentication: String) {
|
||||
fun edit(address: String, fingerprint: String, authentication: String): Repository {
|
||||
val addressChanged = this.address != address
|
||||
val fingerprintChanged = this.fingerprint != fingerprint
|
||||
val changed = addressChanged || fingerprintChanged
|
||||
return copy(address = address, fingerprint = fingerprint, lastModified = if (changed) "" else lastModified,
|
||||
entityTag = if (changed) "" else entityTag, authentication = authentication)
|
||||
}
|
||||
|
||||
val updated: Long, val timestamp: Long, val authentication: String
|
||||
) {
|
||||
fun update(mirrors: List<String>, name: String, description: String, version: Int,
|
||||
lastModified: String, entityTag: String, timestamp: Long): Repository {
|
||||
return copy(mirrors = mirrors, name = name, description = description,
|
||||
@@ -79,15 +72,6 @@ data class Repository(val id: Long, val address: String, val mirrors: List<Strin
|
||||
lastModified, entityTag, updated, timestamp, authentication)
|
||||
}
|
||||
|
||||
fun newRepository(address: String, fingerprint: String, authentication: String): Repository {
|
||||
val name = try {
|
||||
URL(address).let { "${it.host}${it.path}" }
|
||||
} catch (e: Exception) {
|
||||
address
|
||||
}
|
||||
return defaultRepository(address, name, "", 0, true, fingerprint, authentication)
|
||||
}
|
||||
|
||||
private fun defaultRepository(address: String, name: String, description: String,
|
||||
version: Int, enabled: Boolean, fingerprint: String, authentication: String): Repository {
|
||||
return Repository(-1, address, emptyList(), name, description, version, enabled,
|
||||
@@ -95,9 +79,14 @@ data class Repository(val id: Long, val address: String, val mirrors: List<Strin
|
||||
}
|
||||
|
||||
val defaultRepositories = listOf(run {
|
||||
defaultRepository("https://repo.dgplayser.duckdns.org/fdroid/repo", "Michachatz F-Droid Repo", "Michachatz official repository. " +
|
||||
"Everything in this repository is always built from the source code.",
|
||||
21, true, "3546DCBDD900F280EE2161CC163C1156BE2C2F3EB810415115039E0C7D3242C0", "")
|
||||
},
|
||||
run {
|
||||
defaultRepository("https://f-droid.org/repo", "F-Droid", "The official F-Droid Free Software repository. " +
|
||||
"Everything in this repository is always built from the source code.",
|
||||
21, true, "43238D512C1E5EB2D6569F4A3AFBF5523418B82E0A3ED1552770ABB9A9C9CCAB", "")
|
||||
21, false, "43238D512C1E5EB2D6569F4A3AFBF5523418B82E0A3ED1552770ABB9A9C9CCAB", "")
|
||||
}, run {
|
||||
defaultRepository("https://f-droid.org/archive", "F-Droid Archive", "The archive of the official F-Droid Free " +
|
||||
"Software repository. Apps here are old and can contain known vulnerabilities and security issues!",
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
package nya.kitsunyan.foxydroid.graphics
|
||||
|
||||
import android.content.res.ColorStateList
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.ColorFilter
|
||||
import android.graphics.PorterDuff
|
||||
import android.graphics.Rect
|
||||
import android.graphics.drawable.Drawable
|
||||
import androidx.core.graphics.drawable.DrawableCompat
|
||||
|
||||
open class DrawableWrapper(val drawable: Drawable): Drawable() {
|
||||
init {
|
||||
@@ -36,7 +39,7 @@ open class DrawableWrapper(val drawable: Drawable): Drawable() {
|
||||
}
|
||||
|
||||
override fun getAlpha(): Int {
|
||||
return drawable.alpha
|
||||
return DrawableCompat.getAlpha(drawable)
|
||||
}
|
||||
|
||||
override fun setAlpha(alpha: Int) {
|
||||
@@ -44,13 +47,32 @@ open class DrawableWrapper(val drawable: Drawable): Drawable() {
|
||||
}
|
||||
|
||||
override fun getColorFilter(): ColorFilter? {
|
||||
return drawable.colorFilter
|
||||
return DrawableCompat.getColorFilter(drawable)
|
||||
}
|
||||
|
||||
override fun setColorFilter(colorFilter: ColorFilter?) {
|
||||
drawable.colorFilter = colorFilter
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
override fun setTint(tintColor: Int) {
|
||||
DrawableCompat.setTint(drawable, tintColor)
|
||||
}
|
||||
|
||||
override fun setTintList(tint: ColorStateList?) {
|
||||
DrawableCompat.setTintList(drawable, tint)
|
||||
}
|
||||
|
||||
override fun setTintMode(tintMode: PorterDuff.Mode?) {
|
||||
DrawableCompat.setTintMode(drawable, tintMode ?: PorterDuff.Mode.SRC_IN)
|
||||
}
|
||||
|
||||
override fun setHotspot(x: Float, y: Float) {
|
||||
DrawableCompat.setHotspot(drawable, x, y)
|
||||
}
|
||||
|
||||
override fun setHotspotBounds(left: Int, top: Int, right: Int, bottom: Int) {
|
||||
DrawableCompat.setHotspotBounds(drawable, left, top, right, bottom)
|
||||
}
|
||||
|
||||
override fun getOpacity(): Int = drawable.opacity
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ class IndexHandler(private val repositoryId: Long, private val callback: Callbac
|
||||
private fun String.parseDate(): Long {
|
||||
return try {
|
||||
dateFormat.parse(this)?.time ?: 0L
|
||||
} catch (e: Exception) {
|
||||
} catch (_: Exception) {
|
||||
0L
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,13 +63,15 @@ class IndexMerger(file: File): Closeable {
|
||||
closeTransaction()
|
||||
db.rawQuery("""SELECT product.description, product.data AS pd, releases.data AS rd FROM product
|
||||
LEFT JOIN releases ON product.package_name = releases.package_name""", null)
|
||||
?.use { it.asSequence().map {
|
||||
.use { it ->
|
||||
it.asSequence().map { it ->
|
||||
val description = it.getString(0)
|
||||
val product = Json.factory.createParser(it.getBlob(1)).use {
|
||||
it.nextToken()
|
||||
Product.deserialize(repositoryId, description, it)
|
||||
}
|
||||
val releases = it.getBlob(2)?.let { Json.factory.createParser(it).use {
|
||||
val releases = it.getBlob(2)?.let { it ->
|
||||
Json.factory.createParser(it).use {
|
||||
it.nextToken()
|
||||
it.collectNotNull(JsonToken.START_OBJECT, Release.Companion::deserialize)
|
||||
} }.orEmpty()
|
||||
|
||||
@@ -37,7 +37,7 @@ object IndexV1Parser {
|
||||
if (jsonParser.nextToken() != JsonToken.START_OBJECT) {
|
||||
jsonParser.illegal()
|
||||
} else {
|
||||
jsonParser.forEachKey {
|
||||
jsonParser.forEachKey { it ->
|
||||
when {
|
||||
it.dictionary("repo") -> {
|
||||
var address = ""
|
||||
@@ -100,7 +100,7 @@ object IndexV1Parser {
|
||||
val licenses = mutableListOf<String>()
|
||||
val donates = mutableListOf<Product.Donate>()
|
||||
val localizedMap = mutableMapOf<String, Localized>()
|
||||
forEachKey {
|
||||
forEachKey { it ->
|
||||
when {
|
||||
it.string("packageName") -> packageName = valueAsString
|
||||
it.string("name") -> nameFallback = valueAsString
|
||||
@@ -125,7 +125,7 @@ object IndexV1Parser {
|
||||
it.string("flattrID") -> donates += Product.Donate.Flattr(valueAsString)
|
||||
it.string("liberapayID") -> donates += Product.Donate.Liberapay(valueAsString)
|
||||
it.string("openCollective") -> donates += Product.Donate.OpenCollective(valueAsString)
|
||||
it.dictionary("localized") -> forEachKey {
|
||||
it.dictionary("localized") -> forEachKey { it ->
|
||||
if (it.token == JsonToken.START_OBJECT) {
|
||||
val locale = it.key
|
||||
var name = ""
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package nya.kitsunyan.foxydroid.index
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.disposables.Disposable
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import nya.kitsunyan.foxydroid.content.Cache
|
||||
import nya.kitsunyan.foxydroid.database.Database
|
||||
@@ -23,6 +23,7 @@ import java.util.Locale
|
||||
import java.util.jar.JarEntry
|
||||
import java.util.jar.JarFile
|
||||
import javax.xml.parsers.SAXParserFactory
|
||||
import androidx.core.net.toUri
|
||||
|
||||
object RepositoryUpdater {
|
||||
enum class Stage {
|
||||
@@ -50,23 +51,19 @@ object RepositoryUpdater {
|
||||
}
|
||||
}
|
||||
|
||||
private lateinit var context: Context
|
||||
private val updaterLock = Any()
|
||||
private val cleanupLock = Any()
|
||||
|
||||
fun init(context: Context) {
|
||||
this.context = context
|
||||
|
||||
var lastDisabled = setOf<Long>()
|
||||
Observable.just(Unit)
|
||||
fun init(): Disposable {
|
||||
val lastDisabled = setOf<Long>()
|
||||
return Observable.just(Unit)
|
||||
.concatWith(Database.observable(Database.Subject.Repositories))
|
||||
.observeOn(Schedulers.io())
|
||||
.flatMapSingle { RxUtils.querySingle { Database.RepositoryAdapter.getAllDisabledDeleted(it) } }
|
||||
.forEach {
|
||||
val newDisabled = it.asSequence().filter { !it.second }.map { it.first }.toSet()
|
||||
.flatMapSingle { RxUtils.querySingle { signal -> Database.RepositoryAdapter.getAllDisabledDeleted(signal) } }
|
||||
.subscribe { result ->
|
||||
val newDisabled = result.asSequence().filter { !it.second }.map { it.first }.toSet()
|
||||
val disabled = newDisabled - lastDisabled
|
||||
lastDisabled = newDisabled
|
||||
val deleted = it.asSequence().filter { it.second }.map { it.first }.toSet()
|
||||
val deleted = result.asSequence().filter { it.second }.map { it.first }.toSet()
|
||||
if (disabled.isNotEmpty() || deleted.isNotEmpty()) {
|
||||
val pairs = (disabled.asSequence().map { Pair(it, false) } +
|
||||
deleted.asSequence().map { Pair(it, true) }).toSet()
|
||||
@@ -79,15 +76,15 @@ object RepositoryUpdater {
|
||||
synchronized(updaterLock) { }
|
||||
}
|
||||
|
||||
fun update(repository: Repository, unstable: Boolean,
|
||||
fun update(context: Context, repository: Repository, unstable: Boolean,
|
||||
callback: (Stage, Long, Long?) -> Unit): Single<Boolean> {
|
||||
return update(repository, listOf(IndexType.INDEX_V1, IndexType.INDEX), unstable, callback)
|
||||
return update(context, repository, listOf(IndexType.INDEX_V1, IndexType.INDEX), unstable, callback)
|
||||
}
|
||||
|
||||
private fun update(repository: Repository, indexTypes: List<IndexType>, unstable: Boolean,
|
||||
private fun update(context: Context, repository: Repository, indexTypes: List<IndexType>, unstable: Boolean,
|
||||
callback: (Stage, Long, Long?) -> Unit): Single<Boolean> {
|
||||
val indexType = indexTypes[0]
|
||||
return downloadIndex(repository, indexType, callback)
|
||||
return downloadIndex(context, repository, indexType, callback)
|
||||
.flatMap { (result, file) ->
|
||||
when {
|
||||
result.isNotChanged -> {
|
||||
@@ -96,26 +93,27 @@ object RepositoryUpdater {
|
||||
}
|
||||
!result.success -> {
|
||||
file.delete()
|
||||
if (result.code == 404 && indexTypes.isNotEmpty()) {
|
||||
update(repository, indexTypes.subList(1, indexTypes.size), unstable, callback)
|
||||
if (result.code == 404 && indexTypes.size > 1) {
|
||||
update(context, repository, indexTypes.subList(1, indexTypes.size), unstable, callback)
|
||||
} else {
|
||||
Single.error(UpdateException(ErrorType.HTTP, "Invalid response: HTTP ${result.code}"))
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
RxUtils.managedSingle { processFile(repository, indexType, unstable,
|
||||
RxUtils.managedSingle { processFile(context, repository, indexType, unstable,
|
||||
file, result.lastModified, result.entityTag, callback) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun downloadIndex(repository: Repository, indexType: IndexType,
|
||||
private fun downloadIndex(context: Context, repository: Repository, indexType: IndexType,
|
||||
callback: (Stage, Long, Long?) -> Unit): Single<Pair<Downloader.Result, File>> {
|
||||
return Single.just(Unit)
|
||||
.map { Cache.getTemporaryFile(context) }
|
||||
.flatMap { file -> Downloader
|
||||
.download(Uri.parse(repository.address).buildUpon()
|
||||
.download(
|
||||
repository.address.toUri().buildUpon()
|
||||
.appendPath(indexType.jarName).build().toString(), file, repository.lastModified, repository.entityTag,
|
||||
repository.authentication) { read, total -> callback(Stage.DOWNLOAD, read, total) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
@@ -130,7 +128,7 @@ object RepositoryUpdater {
|
||||
} }
|
||||
}
|
||||
|
||||
private fun processFile(repository: Repository, indexType: IndexType, unstable: Boolean,
|
||||
private fun processFile(context: Context, repository: Repository, indexType: IndexType, unstable: Boolean,
|
||||
file: File, lastModified: String, entityTag: String, callback: (Stage, Long, Long?) -> Unit): Boolean {
|
||||
var rollback = true
|
||||
return synchronized(updaterLock) {
|
||||
@@ -157,7 +155,7 @@ object RepositoryUpdater {
|
||||
certificate: String, version: Int, timestamp: Long) {
|
||||
changedRepository = repository.update(mirrors, name, description, version,
|
||||
lastModified, entityTag, timestamp)
|
||||
certificateFromIndex = certificate.toLowerCase(Locale.US)
|
||||
certificateFromIndex = certificate.lowercase(Locale.US)
|
||||
}
|
||||
|
||||
override fun onProduct(product: Product) {
|
||||
@@ -191,7 +189,7 @@ object RepositoryUpdater {
|
||||
val unmergedProducts = mutableListOf<Product>()
|
||||
val unmergedReleases = mutableListOf<Pair<String, List<Release>>>()
|
||||
IndexMerger(mergerFile).use { indexMerger ->
|
||||
ProgressInputStream(jarFile.getInputStream(indexEntry)) { callback(Stage.PROCESS, it, total) }.use {
|
||||
ProgressInputStream(jarFile.getInputStream(indexEntry)) { callback(Stage.PROCESS, it, total) }.use { it ->
|
||||
IndexV1Parser.parse(repository.id, it, object: IndexV1Parser.Callback {
|
||||
override fun onRepository(mirrors: List<String>, name: String, description: String,
|
||||
version: Int, timestamp: Long) {
|
||||
@@ -315,7 +313,7 @@ object RepositoryUpdater {
|
||||
}
|
||||
|
||||
private fun transformProduct(product: Product, features: Set<String>, unstable: Boolean): Product {
|
||||
val releasePairs = product.releases.distinctBy { it.identifier }.sortedByDescending { it.versionCode }.map {
|
||||
val releasePairs = product.releases.distinctBy { it.identifier }.sortedByDescending { it.versionCode }.map { it ->
|
||||
val incompatibilities = mutableListOf<Release.Incompatibility>()
|
||||
if (it.minSdkVersion > 0 && Android.sdk < it.minSdkVersion) {
|
||||
incompatibilities += Release.Incompatibility.MinSdk
|
||||
|
||||
@@ -85,13 +85,13 @@ object Downloader {
|
||||
.callSingle { createCall(request, authentication, null) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.flatMap { result -> RxUtils
|
||||
.managedSingle { result.use {
|
||||
.managedSingle { result.use { it ->
|
||||
if (result.code == 304) {
|
||||
Result(it.code, lastModified, entityTag)
|
||||
} else {
|
||||
val body = it.body!!
|
||||
val body = it.body
|
||||
val append = start != null && it.header("Content-Range") != null
|
||||
val progressStart = if (append && start != null) start else 0L
|
||||
val progressStart = if (append) start else 0L
|
||||
val progressTotal = body.contentLength().let { if (it >= 0L) it else null }
|
||||
?.let { progressStart + it }
|
||||
val inputStream = ProgressInputStream(body.byteStream()) {
|
||||
|
||||
@@ -65,7 +65,7 @@ object PicassoDownloader {
|
||||
} else {
|
||||
Downloader.createCall(request.newBuilder().url(address.toHttpUrl()
|
||||
.newBuilder().addPathSegment(packageName.orEmpty()).addPathSegment(locale.orEmpty())
|
||||
.addPathSegment(device.orEmpty()).addPathSegment(screenshot.orEmpty()).build()),
|
||||
.addPathSegment(device.orEmpty()).addPathSegment(screenshot).build()),
|
||||
authentication.orEmpty(), cache)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.graphics.PorterDuff
|
||||
import android.graphics.PorterDuffColorFilter
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.text.Editable
|
||||
import android.text.Selection
|
||||
@@ -42,6 +41,7 @@ import java.net.URL
|
||||
import java.nio.charset.Charset
|
||||
import java.util.Locale
|
||||
import kotlin.math.*
|
||||
import androidx.core.net.toUri
|
||||
|
||||
class EditRepositoryFragment(): ScreenFragment() {
|
||||
companion object {
|
||||
@@ -149,7 +149,7 @@ class EditRepositoryFragment(): ScreenFragment() {
|
||||
|
||||
override fun afterTextChanged(s: Editable) {
|
||||
val inputString = s.toString()
|
||||
val outputString = inputString.toUpperCase(Locale.US)
|
||||
val outputString = inputString.uppercase(Locale.US)
|
||||
.filter(validChar).windowed(2, 2, true).take(32).joinToString(separator = " ")
|
||||
if (inputString != outputString) {
|
||||
val inputStart = logicalPosition(inputString, Selection.getSelectionStart(s))
|
||||
@@ -161,19 +161,19 @@ class EditRepositoryFragment(): ScreenFragment() {
|
||||
})
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
val repository = repositoryId?.let(Database.RepositoryAdapter::get)
|
||||
val repository = repositoryId?.let { Database.RepositoryAdapter.get(it) }
|
||||
if (repository == null) {
|
||||
val clipboardManager = requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
val text = clipboardManager.primaryClip
|
||||
?.let { if (it.itemCount > 0) it else null }
|
||||
?.getItemAt(0)?.text?.toString().orEmpty()
|
||||
val (addressText, fingerprintText) = try {
|
||||
val uri = Uri.parse(URL(text).toString())
|
||||
val uri = URL(text).toString().toUri()
|
||||
val fingerprintText = uri.getQueryParameter("fingerprint")?.nullIfEmpty()
|
||||
?: uri.getQueryParameter("FINGERPRINT")?.nullIfEmpty()
|
||||
Pair(uri.buildUpon().path(uri.path?.pathCropped)
|
||||
.query(null).fragment(null).build().toString(), fingerprintText)
|
||||
} catch (e: Exception) {
|
||||
} catch (_: Exception) {
|
||||
Pair(null, null)
|
||||
}
|
||||
layout.address.setText(addressText?.nullIfEmpty() ?: layout.address.hint)
|
||||
@@ -209,10 +209,26 @@ class EditRepositoryFragment(): ScreenFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
layout.address.addTextChangedListener(SimpleTextWatcher { invalidateAddress() })
|
||||
layout.fingerprint.addTextChangedListener(SimpleTextWatcher { invalidateFingerprint() })
|
||||
layout.username.addTextChangedListener(SimpleTextWatcher { invalidateUsernamePassword() })
|
||||
layout.password.addTextChangedListener(SimpleTextWatcher { invalidateUsernamePassword() })
|
||||
layout.address.addTextChangedListener(object: TextWatcher {
|
||||
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) = Unit
|
||||
override fun onTextChanged(s: CharSequence, start: Int, count: Int, after: Int) = Unit
|
||||
override fun afterTextChanged(s: Editable) = invalidateAddress()
|
||||
})
|
||||
layout.fingerprint.addTextChangedListener(object: TextWatcher {
|
||||
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) = Unit
|
||||
override fun onTextChanged(s: CharSequence, start: Int, count: Int, after: Int) = Unit
|
||||
override fun afterTextChanged(s: Editable) = invalidateFingerprint()
|
||||
})
|
||||
layout.username.addTextChangedListener(object: TextWatcher {
|
||||
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) = Unit
|
||||
override fun onTextChanged(s: CharSequence, start: Int, count: Int, after: Int) = Unit
|
||||
override fun afterTextChanged(s: Editable) = invalidateUsernamePassword()
|
||||
})
|
||||
layout.password.addTextChangedListener(object: TextWatcher {
|
||||
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) = Unit
|
||||
override fun onTextChanged(s: CharSequence, start: Int, count: Int, after: Int) = Unit
|
||||
override fun afterTextChanged(s: Editable) = invalidateUsernamePassword()
|
||||
})
|
||||
|
||||
(layout.overlay.parent as ViewGroup).layoutTransition?.setDuration(200L)
|
||||
layout.overlay.background!!.apply {
|
||||
@@ -230,14 +246,18 @@ class EditRepositoryFragment(): ScreenFragment() {
|
||||
repositoriesDisposable = Observable.just(Unit)
|
||||
.concatWith(Database.observable(Database.Subject.Repositories))
|
||||
.observeOn(Schedulers.io())
|
||||
.flatMapSingle { RxUtils.querySingle { Database.RepositoryAdapter.getAll(it) } }
|
||||
.flatMapSingle { RxUtils.querySingle { signal -> Database.RepositoryAdapter.getAll(signal) } }
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe {
|
||||
takenAddresses = it.asSequence().filter { it.id != repositoryId }
|
||||
.subscribe { result ->
|
||||
takenAddresses = result.asSequence().filter { it.id != repositoryId }
|
||||
.flatMap { (it.mirrors + it.address).asSequence() }
|
||||
.map { it.withoutKnownPath }.toSet()
|
||||
invalidateAddress()
|
||||
}
|
||||
|
||||
invalidateAddress()
|
||||
invalidateFingerprint()
|
||||
invalidateUsernamePassword()
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
@@ -253,14 +273,6 @@ class EditRepositoryFragment(): ScreenFragment() {
|
||||
checkDisposable = null
|
||||
}
|
||||
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
|
||||
invalidateAddress()
|
||||
invalidateFingerprint()
|
||||
invalidateUsernamePassword()
|
||||
}
|
||||
|
||||
private var addressError = false
|
||||
private var fingerprintError = false
|
||||
private var usernamePasswordError = false
|
||||
@@ -328,7 +340,7 @@ class EditRepositoryFragment(): ScreenFragment() {
|
||||
|
||||
private fun invalidateState() {
|
||||
val layout = layout!!
|
||||
saveMenuItem!!.isEnabled = !addressError && !fingerprintError &&
|
||||
saveMenuItem?.isEnabled = !addressError && !fingerprintError &&
|
||||
!usernamePasswordError && checkDisposable == null
|
||||
layout.apply { sequenceOf(address, addressMirror, fingerprint, username, password)
|
||||
.forEach { it.isEnabled = checkDisposable == null } }
|
||||
@@ -346,21 +358,21 @@ class EditRepositoryFragment(): ScreenFragment() {
|
||||
val cropped = pathCropped
|
||||
val endsWith = checkPaths.asSequence().filter { it.isNotEmpty() }
|
||||
.sortedByDescending { it.length }.find { cropped.endsWith("/$it") }
|
||||
return if (endsWith != null) cropped.substring(0, cropped.length - endsWith.length - 1) else cropped
|
||||
return if (endsWith != null) cropped.take(cropped.length - endsWith.length - 1) else cropped
|
||||
}
|
||||
|
||||
private fun normalizeAddress(address: String): String? {
|
||||
val uri = try {
|
||||
val uri = URI(address)
|
||||
if (uri.isAbsolute) uri.normalize() else null
|
||||
} catch (e: Exception) {
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
val path = uri?.path?.pathCropped
|
||||
return if (uri != null && path != null) {
|
||||
try {
|
||||
URI(uri.scheme, uri.userInfo, uri.host, uri.port, path, uri.query, uri.fragment).toString()
|
||||
} catch (e: Exception) {
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
} else {
|
||||
@@ -394,7 +406,7 @@ class EditRepositoryFragment(): ScreenFragment() {
|
||||
.fold(Single.just("")) { oldAddressSingle, checkPath -> oldAddressSingle
|
||||
.flatMap { oldAddress ->
|
||||
if (oldAddress.isEmpty()) {
|
||||
val builder = Uri.parse(address).buildUpon()
|
||||
val builder = address.toUri().buildUpon()
|
||||
.let { if (checkPath.isEmpty()) it else it.appendEncodedPath(checkPath) }
|
||||
val newAddress = builder.build()
|
||||
val indexAddress = builder.appendPath("index.jar").build()
|
||||
@@ -413,71 +425,67 @@ class EditRepositoryFragment(): ScreenFragment() {
|
||||
.subscribe { result, throwable ->
|
||||
checkDisposable = null
|
||||
throwable?.printStackTrace()
|
||||
val resultAddress = result?.let { if (it.isEmpty()) null else it } ?: address
|
||||
val allow = resultAddress == address || run {
|
||||
layout.address.setText(resultAddress)
|
||||
invalidateAddress(resultAddress)
|
||||
!addressError
|
||||
}
|
||||
if (allow) {
|
||||
onSaveRepositoryProceedInvalidate(resultAddress, fingerprint, authentication)
|
||||
val resultAddress = result?.let { it.ifEmpty { null } } ?: address
|
||||
val allow = resultAddress == address || resultAddress == "$address/"
|
||||
if (!allow) {
|
||||
AlertDialog.Builder(requireContext())
|
||||
.setMessage(getString(R.string.address_redirect_FORMAT, resultAddress))
|
||||
.setPositiveButton(android.R.string.ok) { _, _ -> saveRepository(resultAddress, fingerprint, authentication) }
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show()
|
||||
} else {
|
||||
invalidateState()
|
||||
saveRepository(resultAddress, fingerprint, authentication)
|
||||
}
|
||||
}
|
||||
invalidateState()
|
||||
} else {
|
||||
onSaveRepositoryProceedInvalidate(address, fingerprint, authentication)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onSaveRepositoryProceedInvalidate(address: String, fingerprint: String, authentication: String) {
|
||||
val binder = syncConnection.binder
|
||||
if (binder != null) {
|
||||
val repositoryId = repositoryId
|
||||
if (repositoryId != null && binder.isCurrentlySyncing(repositoryId)) {
|
||||
MessageDialog(MessageDialog.Message.CantEditSyncing).show(childFragmentManager)
|
||||
invalidateState()
|
||||
} else {
|
||||
val repository = repositoryId?.let(Database.RepositoryAdapter::get)
|
||||
?.edit(address, fingerprint, authentication)
|
||||
?: Repository.newRepository(address, fingerprint, authentication)
|
||||
val changedRepository = Database.RepositoryAdapter.put(repository)
|
||||
if (repositoryId == null && changedRepository.enabled) {
|
||||
binder.sync(changedRepository)
|
||||
}
|
||||
requireActivity().onBackPressed()
|
||||
}
|
||||
} else {
|
||||
saveRepository(address, fingerprint, authentication)
|
||||
}
|
||||
invalidateState()
|
||||
}
|
||||
}
|
||||
|
||||
private class SimpleTextWatcher(private val callback: (Editable) -> Unit): TextWatcher {
|
||||
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) = Unit
|
||||
override fun onTextChanged(s: CharSequence, start: Int, count: Int, after: Int) = Unit
|
||||
override fun afterTextChanged(s: Editable) = callback(s)
|
||||
private fun saveRepository(address: String, fingerprint: String, authentication: String) {
|
||||
val repository = repositoryId?.let { Database.RepositoryAdapter.get(it) }
|
||||
if (repository != null) {
|
||||
Database.RepositoryAdapter.put(repository.copy(address = address, fingerprint = fingerprint,
|
||||
authentication = authentication))
|
||||
} else {
|
||||
val id = Database.RepositoryAdapter.put(Repository(
|
||||
id = 0,
|
||||
address = address,
|
||||
mirrors = emptyList(),
|
||||
name = "",
|
||||
description = "",
|
||||
version = 0,
|
||||
enabled = true,
|
||||
fingerprint = fingerprint,
|
||||
lastModified = "",
|
||||
entityTag = "",
|
||||
updated = 0L,
|
||||
timestamp = 0L,
|
||||
authentication = authentication
|
||||
))
|
||||
Database.RepositoryAdapter.get(id)?.let { syncConnection.binder?.sync(it) }
|
||||
}
|
||||
screenActivity.onBackPressedDispatcher.onBackPressed()
|
||||
}
|
||||
|
||||
class SelectMirrorDialog(): DialogFragment() {
|
||||
companion object {
|
||||
private const val EXTRA_MIRRORS = "mirrors"
|
||||
}
|
||||
private val mirrors: List<String>
|
||||
get() = requireArguments().getStringArrayList("mirrors")!!
|
||||
|
||||
constructor(mirrors: List<String>): this() {
|
||||
arguments = Bundle().apply {
|
||||
putStringArrayList(EXTRA_MIRRORS, ArrayList(mirrors))
|
||||
putStringArrayList("mirrors", ArrayList(mirrors))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): AlertDialog {
|
||||
val mirrors = requireArguments().getStringArrayList(EXTRA_MIRRORS)!!
|
||||
return AlertDialog.Builder(requireContext())
|
||||
.setTitle(R.string.select_mirror)
|
||||
.setItems(mirrors.toTypedArray()) { _, position -> (parentFragment as EditRepositoryFragment)
|
||||
.setMirror(mirrors[position]) }
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.setItems(mirrors.toTypedArray()) { _, which ->
|
||||
(parentFragment as EditRepositoryFragment).setMirror(mirrors[which])
|
||||
}
|
||||
.create()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.Parcel
|
||||
import androidx.core.os.BundleCompat
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import nya.kitsunyan.foxydroid.R
|
||||
@@ -14,6 +15,7 @@ import nya.kitsunyan.foxydroid.utility.KParcelable
|
||||
import nya.kitsunyan.foxydroid.utility.PackageItemResolver
|
||||
import nya.kitsunyan.foxydroid.utility.extension.android.*
|
||||
import nya.kitsunyan.foxydroid.utility.extension.text.*
|
||||
import androidx.core.net.toUri
|
||||
|
||||
class MessageDialog(): DialogFragment() {
|
||||
companion object {
|
||||
@@ -36,7 +38,7 @@ class MessageDialog(): DialogFragment() {
|
||||
|
||||
companion object {
|
||||
@Suppress("unused") @JvmField val CREATOR = KParcelable.creator {
|
||||
val uri = Uri.parse(it.readString()!!)
|
||||
val uri = it.readString()!!.toUri()
|
||||
Link(uri)
|
||||
}
|
||||
}
|
||||
@@ -124,7 +126,8 @@ class MessageDialog(): DialogFragment() {
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): AlertDialog {
|
||||
val dialog = AlertDialog.Builder(requireContext())
|
||||
when (val message = requireArguments().getParcelable<Message>(EXTRA_MESSAGE)!!) {
|
||||
val message = BundleCompat.getParcelable(requireArguments(), EXTRA_MESSAGE, Message::class.java)!!
|
||||
when (message) {
|
||||
is Message.DeleteRepositoryConfirm -> {
|
||||
dialog.setTitle(R.string.confirmation)
|
||||
dialog.setMessage(R.string.delete_repository_DESC)
|
||||
@@ -157,7 +160,7 @@ class MessageDialog(): DialogFragment() {
|
||||
val permissionGroupInfo = packageManager.getPermissionGroupInfo(message.group, 0)
|
||||
PackageItemResolver.loadLabel(requireContext(), localCache, permissionGroupInfo)
|
||||
?.nullIfEmpty()?.let { if (it == message.group) null else it }
|
||||
} catch (e: Exception) {
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
name ?: getString(R.string.unknown)
|
||||
@@ -169,7 +172,7 @@ class MessageDialog(): DialogFragment() {
|
||||
val permissionInfo = packageManager.getPermissionInfo(permission, 0)
|
||||
PackageItemResolver.loadDescription(requireContext(), localCache, permissionInfo)
|
||||
?.nullIfEmpty()?.let { if (it == permission) null else it }
|
||||
} catch (e: Exception) {
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
description?.let { builder.append(it).append("\n\n") }
|
||||
|
||||
@@ -18,11 +18,12 @@ import android.widget.Switch
|
||||
import android.widget.TextView
|
||||
import android.widget.Toolbar
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.Fragment
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.disposables.Disposable
|
||||
import nya.kitsunyan.foxydroid.R
|
||||
import nya.kitsunyan.foxydroid.content.Preferences
|
||||
import nya.kitsunyan.foxydroid.utility.extension.resources.*
|
||||
import androidx.core.view.isNotEmpty
|
||||
|
||||
class PreferencesFragment: ScreenFragment() {
|
||||
private val preferences = mutableMapOf<Preferences.Key<*>, Preference<*>>()
|
||||
@@ -46,11 +47,11 @@ class PreferencesFragment: ScreenFragment() {
|
||||
content.addView(scroll, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
|
||||
val scrollLayout = FrameLayout(content.context)
|
||||
scroll.addView(scrollLayout, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
|
||||
val preferences = LinearLayout(scrollLayout.context)
|
||||
preferences.orientation = LinearLayout.VERTICAL
|
||||
scrollLayout.addView(preferences, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
|
||||
val preferencesLayout = LinearLayout(scrollLayout.context)
|
||||
preferencesLayout.orientation = LinearLayout.VERTICAL
|
||||
scrollLayout.addView(preferencesLayout, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
|
||||
|
||||
preferences.addCategory(getString(R.string.updates)) {
|
||||
preferencesLayout.addCategory(getString(R.string.updates)) {
|
||||
addEnumeration(Preferences.Key.AutoSync, getString(R.string.sync_repositories_automatically)) {
|
||||
when (it) {
|
||||
Preferences.AutoSync.Never -> getString(R.string.never)
|
||||
@@ -63,7 +64,7 @@ class PreferencesFragment: ScreenFragment() {
|
||||
addSwitch(Preferences.Key.UpdateUnstable, getString(R.string.unstable_updates),
|
||||
getString(R.string.unstable_updates_summary))
|
||||
}
|
||||
preferences.addCategory(getString(R.string.proxy)) {
|
||||
preferencesLayout.addCategory(getString(R.string.proxy)) {
|
||||
addEnumeration(Preferences.Key.ProxyType, getString(R.string.proxy_type)) {
|
||||
when (it) {
|
||||
is Preferences.ProxyType.Direct -> getString(R.string.no_proxy)
|
||||
@@ -74,7 +75,7 @@ class PreferencesFragment: ScreenFragment() {
|
||||
addEditString(Preferences.Key.ProxyHost, getString(R.string.proxy_host))
|
||||
addEditInt(Preferences.Key.ProxyPort, getString(R.string.proxy_port), 1 .. 65535)
|
||||
}
|
||||
preferences.addCategory(getString(R.string.other)) {
|
||||
preferencesLayout.addCategory(getString(R.string.other)) {
|
||||
addEnumeration(Preferences.Key.Theme, getString(R.string.theme)) {
|
||||
when (it) {
|
||||
is Preferences.Theme.System -> getString(R.string.system)
|
||||
@@ -86,7 +87,9 @@ class PreferencesFragment: ScreenFragment() {
|
||||
getString(R.string.incompatible_versions_summary))
|
||||
}
|
||||
|
||||
disposable = Preferences.observable.subscribe(this::updatePreference)
|
||||
disposable = Preferences.observable
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(this::updatePreference)
|
||||
updatePreference(null)
|
||||
}
|
||||
|
||||
@@ -138,12 +141,12 @@ class PreferencesFragment: ScreenFragment() {
|
||||
callback()
|
||||
val divider = addDivider(true)
|
||||
// Negative margin for last divider
|
||||
(layoutParams as ViewGroup.MarginLayoutParams).bottomMargin = -divider.layoutParams.height
|
||||
(divider.layoutParams as ViewGroup.MarginLayoutParams).bottomMargin = -divider.layoutParams.height
|
||||
}
|
||||
|
||||
private fun <T> LinearLayout.addPreference(key: Preferences.Key<T>, title: String,
|
||||
summaryProvider: () -> String, dialogProvider: ((Context) -> AlertDialog)?): Preference<T> {
|
||||
if (childCount > 0 && getChildAt(childCount - 1) !is TextView) {
|
||||
if (isNotEmpty() && getChildAt(childCount - 1) !is TextView) {
|
||||
addDivider(false)
|
||||
}
|
||||
val preference = Preference(key, this@PreferencesFragment, this, title, summaryProvider, dialogProvider)
|
||||
@@ -160,10 +163,10 @@ class PreferencesFragment: ScreenFragment() {
|
||||
|
||||
private fun <T> LinearLayout.addEdit(key: Preferences.Key<T>, title: String, valueToString: (T) -> String,
|
||||
stringToValue: (String) -> T?, configureEdit: (EditText) -> Unit) {
|
||||
addPreference(key, title, { valueToString(Preferences[key]) }) {
|
||||
val scroll = ScrollView(it)
|
||||
addPreference(key, title, { valueToString(Preferences[key]) }) { context ->
|
||||
val scroll = ScrollView(context)
|
||||
scroll.resources.sizeScaled(20).let { scroll.setPadding(it, 0, it, 0) }
|
||||
val edit = EditText(it)
|
||||
val edit = EditText(context)
|
||||
configureEdit(edit)
|
||||
edit.id = android.R.id.edit
|
||||
edit.setTextSizeScaled(16)
|
||||
@@ -173,12 +176,12 @@ class PreferencesFragment: ScreenFragment() {
|
||||
edit.setSelection(edit.text.length)
|
||||
edit.requestFocus()
|
||||
scroll.addView(edit, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
|
||||
AlertDialog.Builder(it)
|
||||
AlertDialog.Builder(context)
|
||||
.setTitle(title)
|
||||
.setView(scroll)
|
||||
.setPositiveButton(R.string.ok) { _, _ ->
|
||||
val value = stringToValue(edit.text.toString()) ?: key.default.value
|
||||
post { Preferences[key] = value }
|
||||
Preferences[key] = value
|
||||
}
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.create()
|
||||
@@ -194,11 +197,10 @@ class PreferencesFragment: ScreenFragment() {
|
||||
|
||||
private fun LinearLayout.addEditInt(key: Preferences.Key<Int>, title: String, range: IntRange?) {
|
||||
addEdit(key, title, { it.toString() }, { it.toIntOrNull() }) {
|
||||
it.inputType = InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_FLAG_DECIMAL
|
||||
it.inputType = InputType.TYPE_CLASS_NUMBER
|
||||
if (range != null) {
|
||||
it.filters = arrayOf(InputFilter { source, start, end, dest, dstart, dend ->
|
||||
val value = (dest.substring(0, dstart) + source.substring(start, end) +
|
||||
dest.substring(dend, dest.length)).toIntOrNull()
|
||||
val value = "${dest.subSequence(0, dstart)}${source.subSequence(start, end)}${dest.subSequence(dend, dest.length)}".toIntOrNull()
|
||||
if (value != null && value in range) null else ""
|
||||
})
|
||||
}
|
||||
@@ -207,14 +209,14 @@ class PreferencesFragment: ScreenFragment() {
|
||||
|
||||
private fun <T: Preferences.Enumeration<T>> LinearLayout
|
||||
.addEnumeration(key: Preferences.Key<T>, title: String, valueToString: (T) -> String) {
|
||||
addPreference(key, title, { valueToString(Preferences[key]) }) {
|
||||
addPreference(key, title, { valueToString(Preferences[key]) }) { context ->
|
||||
val values = key.default.value.values
|
||||
AlertDialog.Builder(it)
|
||||
AlertDialog.Builder(context)
|
||||
.setTitle(title)
|
||||
.setSingleChoiceItems(values.map(valueToString).toTypedArray(),
|
||||
values.indexOf(Preferences[key])) { dialog, which ->
|
||||
dialog.dismiss()
|
||||
post { Preferences[key] = values[which] }
|
||||
Preferences[key] = values[which]
|
||||
}
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.create()
|
||||
@@ -222,7 +224,7 @@ class PreferencesFragment: ScreenFragment() {
|
||||
}
|
||||
|
||||
private class Preference<T>(private val key: Preferences.Key<T>,
|
||||
fragment: Fragment, parent: ViewGroup, titleText: String,
|
||||
private val fragment: PreferencesFragment, parent: ViewGroup, titleText: String,
|
||||
private val summaryProvider: () -> String, private val dialogProvider: ((Context) -> AlertDialog)?) {
|
||||
val view = parent.inflate(R.layout.preference_item)
|
||||
val title = view.findViewById<TextView>(R.id.title)!!
|
||||
@@ -276,10 +278,10 @@ class PreferencesFragment: ScreenFragment() {
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val preferences = (parentFragment as PreferencesFragment).preferences
|
||||
val preferencesFragment = parentFragment as PreferencesFragment
|
||||
val key = requireArguments().getString(EXTRA_KEY)!!
|
||||
.let { name -> preferences.keys.find { it.name == name }!! }
|
||||
val preference = preferences[key]!!
|
||||
.let { name -> preferencesFragment.preferences.keys.find { it.name == name }!! }
|
||||
val preference = preferencesFragment.preferences[key]!!
|
||||
return preference.createDialog(requireContext())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ import android.widget.Toast
|
||||
import androidx.core.graphics.ColorUtils
|
||||
import androidx.core.text.HtmlCompat
|
||||
import androidx.core.text.util.LinkifyCompat
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import nya.kitsunyan.foxydroid.R
|
||||
import nya.kitsunyan.foxydroid.content.Preferences
|
||||
@@ -61,6 +62,7 @@ import nya.kitsunyan.foxydroid.widget.StableRecyclerAdapter
|
||||
import java.lang.ref.WeakReference
|
||||
import java.util.Locale
|
||||
import kotlin.math.*
|
||||
import androidx.core.net.toUri
|
||||
|
||||
class ProductAdapter(private val callbacks: Callbacks, private val columns: Int):
|
||||
StableRecyclerAdapter<ProductAdapter.ViewType, RecyclerView.ViewHolder>() {
|
||||
@@ -130,7 +132,7 @@ class ProductAdapter(private val callbacks: Callbacks, private val columns: Int)
|
||||
abstract val descriptor: String
|
||||
abstract val viewType: ViewType
|
||||
|
||||
class HeaderItem(val repository: Repository, val product: Product): Item() {
|
||||
data class HeaderItem(val repository: Repository, val product: Product): Item() {
|
||||
override val descriptor: String
|
||||
get() = "header"
|
||||
|
||||
@@ -138,7 +140,7 @@ class ProductAdapter(private val callbacks: Callbacks, private val columns: Int)
|
||||
get() = ViewType.HEADER
|
||||
}
|
||||
|
||||
class SwitchItem(val switchType: SwitchType, val packageName: String, val versionCode: Long): Item() {
|
||||
data class SwitchItem(val switchType: SwitchType, val packageName: String, val versionCode: Long): Item() {
|
||||
override val descriptor: String
|
||||
get() = "switch.${switchType.name}"
|
||||
|
||||
@@ -146,7 +148,7 @@ class ProductAdapter(private val callbacks: Callbacks, private val columns: Int)
|
||||
get() = ViewType.SWITCH
|
||||
}
|
||||
|
||||
class SectionItem(val sectionType: SectionType, val expandType: ExpandType,
|
||||
data class SectionItem(val sectionType: SectionType, val expandType: ExpandType,
|
||||
val items: List<Item>, val collapseCount: Int): Item() {
|
||||
constructor(sectionType: SectionType): this(sectionType, ExpandType.NOTHING, emptyList(), 0)
|
||||
|
||||
@@ -157,7 +159,7 @@ class ProductAdapter(private val callbacks: Callbacks, private val columns: Int)
|
||||
get() = ViewType.SECTION
|
||||
}
|
||||
|
||||
class ExpandItem(val expandType: ExpandType, val replace: Boolean, val items: List<Item>): Item() {
|
||||
data class ExpandItem(val expandType: ExpandType, val replace: Boolean, val items: List<Item>): Item() {
|
||||
override val descriptor: String
|
||||
get() = "expand.${expandType.name}"
|
||||
|
||||
@@ -165,7 +167,7 @@ class ProductAdapter(private val callbacks: Callbacks, private val columns: Int)
|
||||
get() = ViewType.EXPAND
|
||||
}
|
||||
|
||||
class TextItem(val textType: TextType, val text: CharSequence): Item() {
|
||||
data class TextItem(val textType: TextType, val text: CharSequence): Item() {
|
||||
override val descriptor: String
|
||||
get() = "text.${textType.name}"
|
||||
|
||||
@@ -185,7 +187,7 @@ class ProductAdapter(private val callbacks: Callbacks, private val columns: Int)
|
||||
get() = uri?.schemeSpecificPart?.nullIfEmpty()
|
||||
?.let { if (it.startsWith("//")) null else it } ?: uri?.toString()
|
||||
|
||||
class Typed(val linkType: LinkType, val text: String, override val uri: Uri?): LinkItem() {
|
||||
data class Typed(val linkType: LinkType, val text: String, override val uri: Uri?): LinkItem() {
|
||||
override val descriptor: String
|
||||
get() = "link.typed.${linkType.name}"
|
||||
|
||||
@@ -198,7 +200,7 @@ class ProductAdapter(private val callbacks: Callbacks, private val columns: Int)
|
||||
}
|
||||
}
|
||||
|
||||
class Donate(val donate: Product.Donate): LinkItem() {
|
||||
data class Donate(val donate: Product.Donate): LinkItem() {
|
||||
override val descriptor: String
|
||||
get() = "link.donate.$donate"
|
||||
|
||||
@@ -222,17 +224,17 @@ class ProductAdapter(private val callbacks: Callbacks, private val columns: Int)
|
||||
}
|
||||
|
||||
override val uri: Uri? = when (donate) {
|
||||
is Product.Donate.Regular -> Uri.parse(donate.url)
|
||||
is Product.Donate.Bitcoin -> Uri.parse("bitcoin:${donate.address}")
|
||||
is Product.Donate.Litecoin -> Uri.parse("litecoin:${donate.address}")
|
||||
is Product.Donate.Flattr -> Uri.parse("https://flattr.com/thing/${donate.id}")
|
||||
is Product.Donate.Liberapay -> Uri.parse("https://liberapay.com/~${donate.id}")
|
||||
is Product.Donate.OpenCollective -> Uri.parse("https://opencollective.com/${donate.id}")
|
||||
is Product.Donate.Regular -> donate.url.toUri()
|
||||
is Product.Donate.Bitcoin -> "bitcoin:${donate.address}".toUri()
|
||||
is Product.Donate.Litecoin -> "litecoin:${donate.address}".toUri()
|
||||
is Product.Donate.Flattr -> "https://flattr.com/thing/${donate.id}".toUri()
|
||||
is Product.Donate.Liberapay -> "https://liberapay.com/~${donate.id}".toUri()
|
||||
is Product.Donate.OpenCollective -> "https://opencollective.com/${donate.id}".toUri()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class PermissionsItem(val group: PermissionGroupInfo?, val permissions: List<PermissionInfo>): Item() {
|
||||
data class PermissionsItem(val group: PermissionGroupInfo?, val permissions: List<PermissionInfo>): Item() {
|
||||
override val descriptor: String
|
||||
get() = "permissions.${group?.name}.${permissions.joinToString(separator = ".") { it.name }}"
|
||||
|
||||
@@ -240,7 +242,7 @@ class ProductAdapter(private val callbacks: Callbacks, private val columns: Int)
|
||||
get() = ViewType.PERMISSIONS
|
||||
}
|
||||
|
||||
class ScreenshotItem(val repository: Repository, val packageName: String,
|
||||
data class ScreenshotItem(val repository: Repository, val packageName: String,
|
||||
val screenshot: Product.Screenshot): Item() {
|
||||
override val descriptor: String
|
||||
get() = "screenshot.${repository.id}.${screenshot.identifier}"
|
||||
@@ -249,7 +251,7 @@ class ProductAdapter(private val callbacks: Callbacks, private val columns: Int)
|
||||
get() = ViewType.SCREENSHOT
|
||||
}
|
||||
|
||||
class ReleaseItem(val repository: Repository, val release: Release, val selectedRepository: Boolean,
|
||||
data class ReleaseItem(val repository: Repository, val release: Release, val selectedRepository: Boolean,
|
||||
val showSignature: Boolean): Item() {
|
||||
override val descriptor: String
|
||||
get() = "release.${repository.id}.${release.identifier}"
|
||||
@@ -258,7 +260,7 @@ class ProductAdapter(private val callbacks: Callbacks, private val columns: Int)
|
||||
get() = ViewType.RELEASE
|
||||
}
|
||||
|
||||
class EmptyItem(val packageName: String): Item() {
|
||||
data class EmptyItem(val packageName: String): Item() {
|
||||
override val descriptor: String
|
||||
get() = "empty"
|
||||
|
||||
@@ -269,7 +271,7 @@ class ProductAdapter(private val callbacks: Callbacks, private val columns: Int)
|
||||
|
||||
private class Measurement<T: Any> {
|
||||
private var density = 0f
|
||||
private var scaledDensity = 0f
|
||||
private var fontScale = 0f
|
||||
private lateinit var metric: T
|
||||
|
||||
fun measure(view: View) {
|
||||
@@ -277,10 +279,10 @@ class ProductAdapter(private val callbacks: Callbacks, private val columns: Int)
|
||||
}
|
||||
|
||||
fun invalidate(resources: Resources, callback: () -> T): T {
|
||||
val (density, scaledDensity) = resources.displayMetrics.let { Pair(it.density, it.scaledDensity) }
|
||||
if (this.density != density || this.scaledDensity != scaledDensity) {
|
||||
val (density, fontScale) = resources.displayMetrics.density to resources.configuration.fontScale
|
||||
if (this.density != density || this.fontScale != fontScale) {
|
||||
this.density = density
|
||||
this.scaledDensity = scaledDensity
|
||||
this.fontScale = fontScale
|
||||
metric = callback()
|
||||
}
|
||||
return metric
|
||||
@@ -334,12 +336,12 @@ class ProductAdapter(private val callbacks: Callbacks, private val columns: Int)
|
||||
|
||||
init {
|
||||
itemView as TextView
|
||||
itemView.typeface = TypefaceExtra.medium
|
||||
itemView.setTextSizeScaled(14)
|
||||
itemView.setTextColor(itemView.context.getColorFromAttr(android.R.attr.colorAccent))
|
||||
(itemView as TextView).typeface = TypefaceExtra.medium
|
||||
(itemView as TextView).setTextSizeScaled(14)
|
||||
(itemView as TextView).setTextColor(itemView.context.getColorFromAttr(android.R.attr.colorAccent))
|
||||
itemView.background = itemView.context.getDrawableFromAttr(android.R.attr.selectableItemBackground)
|
||||
itemView.gravity = Gravity.CENTER
|
||||
itemView.isAllCaps = true
|
||||
(itemView as TextView).gravity = Gravity.CENTER
|
||||
(itemView as TextView).isAllCaps = true
|
||||
itemView.layoutParams = RecyclerView.LayoutParams(RecyclerView.LayoutParams.MATCH_PARENT,
|
||||
itemView.resources.sizeScaled(48)).apply { topMargin = -itemView.resources.sizeScaled(16) }
|
||||
}
|
||||
@@ -351,10 +353,10 @@ class ProductAdapter(private val callbacks: Callbacks, private val columns: Int)
|
||||
|
||||
init {
|
||||
itemView as TextView
|
||||
itemView.setTextSizeScaled(14)
|
||||
itemView.setTextColor(itemView.context.getColorFromAttr(android.R.attr.textColorPrimary))
|
||||
(itemView as TextView).setTextSizeScaled(14)
|
||||
(itemView as TextView).setTextColor(itemView.context.getColorFromAttr(android.R.attr.textColorPrimary))
|
||||
itemView.resources.sizeScaled(16).let { itemView.setPadding(it, it, it, it) }
|
||||
itemView.movementMethod = ClickableMovementMethod
|
||||
(itemView as TextView).movementMethod = ClickableMovementMethod
|
||||
itemView.layoutParams = RecyclerView.LayoutParams(RecyclerView.LayoutParams.MATCH_PARENT,
|
||||
RecyclerView.LayoutParams.WRAP_CONTENT)
|
||||
}
|
||||
@@ -363,11 +365,10 @@ class ProductAdapter(private val callbacks: Callbacks, private val columns: Int)
|
||||
private open class OverlappingViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) {
|
||||
init {
|
||||
// Block touch events if touched above negative margin
|
||||
itemView.setOnTouchListener { _, event ->
|
||||
event.action == MotionEvent.ACTION_DOWN && run {
|
||||
val top = (itemView.layoutParams as ViewGroup.MarginLayoutParams).topMargin
|
||||
top < 0 && event.y < -top
|
||||
}
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
itemView.setOnTouchListener { v, event ->
|
||||
val top = (v.layoutParams as ViewGroup.MarginLayoutParams).topMargin
|
||||
event.action == MotionEvent.ACTION_DOWN && top < 0 && event.y < -top
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -441,7 +442,7 @@ class ProductAdapter(private val callbacks: Callbacks, private val columns: Int)
|
||||
}
|
||||
image.scaleType = ImageView.ScaleType.CENTER_CROP
|
||||
image.setBackgroundColor(ColorUtils.blendARGB(backgroundColor, accentColor, 0.1f))
|
||||
itemView.addView(image, FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT)
|
||||
(itemView as ViewGroup).addView(image, FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT)
|
||||
itemView.layoutParams = RecyclerView.LayoutParams(RecyclerView.LayoutParams.MATCH_PARENT,
|
||||
RecyclerView.LayoutParams.WRAP_CONTENT)
|
||||
|
||||
@@ -482,8 +483,8 @@ class ProductAdapter(private val callbacks: Callbacks, private val columns: Int)
|
||||
|
||||
init {
|
||||
itemView as LinearLayout
|
||||
itemView.orientation = LinearLayout.VERTICAL
|
||||
itemView.gravity = Gravity.CENTER
|
||||
(itemView as LinearLayout).orientation = LinearLayout.VERTICAL
|
||||
(itemView as LinearLayout).gravity = Gravity.CENTER
|
||||
itemView.resources.sizeScaled(20).let { itemView.setPadding(it, it, it, it) }
|
||||
val title = TextView(itemView.context)
|
||||
title.gravity = Gravity.CENTER
|
||||
@@ -491,12 +492,12 @@ class ProductAdapter(private val callbacks: Callbacks, private val columns: Int)
|
||||
title.setTextColor(context.getColorFromAttr(android.R.attr.textColorPrimary))
|
||||
title.setTextSizeScaled(20)
|
||||
title.setText(R.string.application_not_found)
|
||||
itemView.addView(title, LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT)
|
||||
(itemView as ViewGroup).addView(title, LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT)
|
||||
val packageName = TextView(itemView.context)
|
||||
packageName.gravity = Gravity.CENTER
|
||||
packageName.setTextColor(context.getColorFromAttr(android.R.attr.textColorPrimary))
|
||||
packageName.setTextSizeScaled(16)
|
||||
itemView.addView(packageName, LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT)
|
||||
(itemView as ViewGroup).addView(packageName, LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT)
|
||||
itemView.layoutParams = RecyclerView.LayoutParams(RecyclerView.LayoutParams.MATCH_PARENT,
|
||||
RecyclerView.LayoutParams.MATCH_PARENT)
|
||||
this.packageName = packageName
|
||||
@@ -529,7 +530,7 @@ class ProductAdapter(private val callbacks: Callbacks, private val columns: Int)
|
||||
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
|
||||
val holder = parent.getChildViewHolder(view)
|
||||
if (holder is ScreenshotViewHolder) {
|
||||
val position = holder.adapterPosition
|
||||
val position = holder.bindingAdapterPosition
|
||||
if (position >= 0) {
|
||||
val first = items.subList(0, position).indexOfLast { it !is Item.ScreenshotItem } + 1
|
||||
val gridCount = items.subList(first, items.size).indexOfFirst { it !is Item.ScreenshotItem }
|
||||
@@ -555,21 +556,31 @@ class ProductAdapter(private val callbacks: Callbacks, private val columns: Int)
|
||||
when {
|
||||
nextItem == null || currentItem is Item.HeaderItem ||
|
||||
nextItem is Item.TextItem && nextItem.textType == TextType.DESCRIPTION -> {
|
||||
configuration.set(true, false, 0, 0)
|
||||
configuration.set(needDivider = true, toTop = false, paddingStart = 0, paddingEnd = 0)
|
||||
}
|
||||
nextItem is Item.SectionItem -> {
|
||||
configuration.set(true, true, 0, 0)
|
||||
configuration.set(needDivider = true, toTop = true, paddingStart = 0, paddingEnd = 0)
|
||||
}
|
||||
currentItem is Item.LinkItem && nextItem is Item.LinkItem ||
|
||||
currentItem is Item.PermissionsItem && nextItem is Item.PermissionsItem -> {
|
||||
configuration.set(true, false, context.resources.sizeScaled(72), 0)
|
||||
configuration.set(
|
||||
needDivider = true,
|
||||
toTop = false,
|
||||
paddingStart = context.resources.sizeScaled(72),
|
||||
paddingEnd = 0
|
||||
)
|
||||
}
|
||||
currentItem is Item.SwitchItem && nextItem is Item.SwitchItem ||
|
||||
currentItem is Item.ReleaseItem && nextItem is Item.ReleaseItem -> {
|
||||
configuration.set(true, false, context.resources.sizeScaled(16), 0)
|
||||
configuration.set(
|
||||
needDivider = true,
|
||||
toTop = false,
|
||||
paddingStart = context.resources.sizeScaled(16),
|
||||
paddingEnd = 0
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
configuration.set(false, false, 0, 0)
|
||||
configuration.set(needDivider = false, toTop = false, paddingStart = 0, paddingEnd = 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -579,24 +590,37 @@ class ProductAdapter(private val callbacks: Callbacks, private val columns: Int)
|
||||
private var product: Product? = null
|
||||
private var installedItem: InstalledItem? = null
|
||||
|
||||
private class ItemDiffCallback(private val oldList: List<Item>, private val newList: List<Item>): DiffUtil.Callback() {
|
||||
override fun getOldListSize(): Int = oldList.size
|
||||
override fun getNewListSize(): Int = newList.size
|
||||
|
||||
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
||||
return oldList[oldItemPosition].descriptor == newList[newItemPosition].descriptor
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
||||
return oldList[oldItemPosition] == newList[newItemPosition]
|
||||
}
|
||||
}
|
||||
|
||||
fun setProducts(context: Context, packageName: String,
|
||||
products: List<Pair<Product, Repository>>, installedItem: InstalledItem?) {
|
||||
val productRepository = Product.findSuggested(products, installedItem) { it.first }
|
||||
items.clear()
|
||||
val newItems = mutableListOf<Item>()
|
||||
|
||||
if (productRepository != null) {
|
||||
items += Item.HeaderItem(productRepository.second, productRepository.first)
|
||||
newItems += Item.HeaderItem(productRepository.second, productRepository.first)
|
||||
|
||||
if (installedItem != null) {
|
||||
items.add(Item.SwitchItem(SwitchType.IGNORE_ALL_UPDATES, packageName, productRepository.first.versionCode))
|
||||
newItems.add(Item.SwitchItem(SwitchType.IGNORE_ALL_UPDATES, packageName, productRepository.first.versionCode))
|
||||
if (productRepository.first.canUpdate(installedItem)) {
|
||||
items.add(Item.SwitchItem(SwitchType.IGNORE_THIS_UPDATE, packageName, productRepository.first.versionCode))
|
||||
newItems.add(Item.SwitchItem(SwitchType.IGNORE_THIS_UPDATE, packageName, productRepository.first.versionCode))
|
||||
}
|
||||
}
|
||||
|
||||
val textViewHolder = TextViewHolder(context)
|
||||
val textViewWidthSpec = context.resources.displayMetrics.widthPixels
|
||||
.let { View.MeasureSpec.makeMeasureSpec(it, View.MeasureSpec.EXACTLY) }
|
||||
val textViewWidthSpec = View.MeasureSpec.makeMeasureSpec(
|
||||
context.resources.displayMetrics.widthPixels, View.MeasureSpec.EXACTLY)
|
||||
val textViewHeightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
|
||||
|
||||
fun CharSequence.lineCropped(maxLines: Int, cropLines: Int): CharSequence? {
|
||||
@@ -638,10 +662,10 @@ class ProductAdapter(private val callbacks: Callbacks, private val columns: Int)
|
||||
val cropped = if (ExpandType.DESCRIPTION !in expanded) description.lineCropped(12, 10) else null
|
||||
val item = Item.TextItem(TextType.DESCRIPTION, description)
|
||||
if (cropped != null) {
|
||||
items += listOf(Item.TextItem(TextType.DESCRIPTION, cropped),
|
||||
newItems += listOf(Item.TextItem(TextType.DESCRIPTION, cropped),
|
||||
Item.ExpandItem(ExpandType.DESCRIPTION, true, listOf(item)))
|
||||
} else {
|
||||
items += item
|
||||
newItems += item
|
||||
}
|
||||
}
|
||||
|
||||
@@ -662,20 +686,20 @@ class ProductAdapter(private val callbacks: Callbacks, private val columns: Int)
|
||||
}
|
||||
}.joinToString(separator = "\n") { "\u2022 $it" }
|
||||
if (antiFeatures.isNotEmpty()) {
|
||||
items += Item.SectionItem(SectionType.ANTI_FEATURES)
|
||||
items += Item.TextItem(TextType.ANTI_FEATURES, antiFeatures)
|
||||
newItems += Item.SectionItem(SectionType.ANTI_FEATURES)
|
||||
newItems += Item.TextItem(TextType.ANTI_FEATURES, antiFeatures)
|
||||
}
|
||||
|
||||
val changes = formatHtml(productRepository.first.whatsNew)
|
||||
if (changes.isNotEmpty()) {
|
||||
items += Item.SectionItem(SectionType.CHANGES)
|
||||
newItems += Item.SectionItem(SectionType.CHANGES)
|
||||
val cropped = if (ExpandType.CHANGES !in expanded) changes.lineCropped(12, 10) else null
|
||||
val item = Item.TextItem(TextType.CHANGES, changes)
|
||||
if (cropped != null) {
|
||||
items += listOf(Item.TextItem(TextType.CHANGES, cropped),
|
||||
newItems += listOf(Item.TextItem(TextType.CHANGES, cropped),
|
||||
Item.ExpandItem(ExpandType.CHANGES, true, listOf(item)))
|
||||
} else {
|
||||
items += item
|
||||
newItems += item
|
||||
}
|
||||
}
|
||||
|
||||
@@ -685,30 +709,32 @@ class ProductAdapter(private val callbacks: Callbacks, private val columns: Int)
|
||||
linkItems += Item.LinkItem.Typed(LinkType.AUTHOR, author.name, author.web.nullIfEmpty()?.let(Uri::parse))
|
||||
}
|
||||
author.email.nullIfEmpty()?.let { linkItems += Item.LinkItem
|
||||
.Typed(LinkType.EMAIL, "", Uri.parse("mailto:$it")) }
|
||||
.Typed(LinkType.EMAIL, "", "mailto:$it".toUri()) }
|
||||
linkItems += licenses.asSequence().map { Item.LinkItem.Typed(LinkType.LICENSE, it,
|
||||
Uri.parse("https://spdx.org/licenses/$it.html")) }
|
||||
source.nullIfEmpty()?.let { linkItems += Item.LinkItem.Typed(LinkType.SOURCE, "", Uri.parse(it)) }
|
||||
tracker.nullIfEmpty()?.let { linkItems += Item.LinkItem.Typed(LinkType.TRACKER, "", Uri.parse(it)) }
|
||||
changelog.nullIfEmpty()?.let { linkItems += Item.LinkItem.Typed(LinkType.CHANGELOG, "", Uri.parse(it)) }
|
||||
web.nullIfEmpty()?.let { linkItems += Item.LinkItem.Typed(LinkType.WEB, "", Uri.parse(it)) }
|
||||
"https://spdx.org/licenses/$it.html".toUri()) }
|
||||
source.nullIfEmpty()?.let { linkItems += Item.LinkItem.Typed(LinkType.SOURCE, "", it.toUri()) }
|
||||
tracker.nullIfEmpty()?.let { linkItems += Item.LinkItem.Typed(LinkType.TRACKER, "",
|
||||
it.toUri()) }
|
||||
changelog.nullIfEmpty()?.let { linkItems += Item.LinkItem.Typed(LinkType.CHANGELOG, "",
|
||||
it.toUri()) }
|
||||
web.nullIfEmpty()?.let { linkItems += Item.LinkItem.Typed(LinkType.WEB, "", it.toUri()) }
|
||||
}
|
||||
if (linkItems.isNotEmpty()) {
|
||||
if (ExpandType.LINKS in expanded) {
|
||||
items += Item.SectionItem(SectionType.LINKS, ExpandType.LINKS, emptyList(), linkItems.size)
|
||||
items += linkItems
|
||||
newItems += Item.SectionItem(SectionType.LINKS, ExpandType.LINKS, emptyList(), linkItems.size)
|
||||
newItems += linkItems
|
||||
} else {
|
||||
items += Item.SectionItem(SectionType.LINKS, ExpandType.LINKS, linkItems, 0)
|
||||
newItems += Item.SectionItem(SectionType.LINKS, ExpandType.LINKS, linkItems, 0)
|
||||
}
|
||||
}
|
||||
|
||||
val donateItems = productRepository.first.donates.map(Item.LinkItem::Donate)
|
||||
if (donateItems.isNotEmpty()) {
|
||||
if (ExpandType.DONATES in expanded) {
|
||||
items += Item.SectionItem(SectionType.DONATE, ExpandType.DONATES, emptyList(), donateItems.size)
|
||||
items += donateItems
|
||||
newItems += Item.SectionItem(SectionType.DONATE, ExpandType.DONATES, emptyList(), donateItems.size)
|
||||
newItems += donateItems
|
||||
} else {
|
||||
items += Item.SectionItem(SectionType.DONATE, ExpandType.DONATES, donateItems, 0)
|
||||
newItems += Item.SectionItem(SectionType.DONATE, ExpandType.DONATES, donateItems, 0)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -719,7 +745,7 @@ class ProductAdapter(private val callbacks: Callbacks, private val columns: Int)
|
||||
.asSequence().mapNotNull {
|
||||
try {
|
||||
packageManager.getPermissionInfo(it, 0)
|
||||
} catch (e: Exception) {
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
@@ -727,7 +753,7 @@ class ProductAdapter(private val callbacks: Callbacks, private val columns: Int)
|
||||
.asSequence().map { (group, permissionInfo) ->
|
||||
val permissionGroupInfo = try {
|
||||
group?.let { packageManager.getPermissionGroupInfo(it, 0) }
|
||||
} catch (e: Exception) {
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
Pair(permissionGroupInfo, permissionInfo)
|
||||
@@ -740,11 +766,11 @@ class ProductAdapter(private val callbacks: Callbacks, private val columns: Int)
|
||||
permissions.asSequence().find { it.key == null }
|
||||
?.let { permissionsItems += Item.PermissionsItem(null, it.value.flatten()) }
|
||||
if (ExpandType.PERMISSIONS in expanded) {
|
||||
items += Item.SectionItem(SectionType.PERMISSIONS, ExpandType.PERMISSIONS,
|
||||
newItems += Item.SectionItem(SectionType.PERMISSIONS, ExpandType.PERMISSIONS,
|
||||
emptyList(), permissionsItems.size)
|
||||
items += permissionsItems
|
||||
newItems += permissionsItems
|
||||
} else {
|
||||
items += Item.SectionItem(SectionType.PERMISSIONS, ExpandType.PERMISSIONS, permissionsItems, 0)
|
||||
newItems += Item.SectionItem(SectionType.PERMISSIONS, ExpandType.PERMISSIONS, permissionsItems, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -753,10 +779,10 @@ class ProductAdapter(private val callbacks: Callbacks, private val columns: Int)
|
||||
.map { Item.ScreenshotItem(productRepository.second, packageName, it) }
|
||||
if (screenshotItems.isNotEmpty()) {
|
||||
if (ExpandType.SCREENSHOTS in expanded) {
|
||||
items += Item.SectionItem(SectionType.SCREENSHOTS, ExpandType.SCREENSHOTS, emptyList(), screenshotItems.size)
|
||||
items += screenshotItems
|
||||
newItems += Item.SectionItem(SectionType.SCREENSHOTS, ExpandType.SCREENSHOTS, emptyList(), screenshotItems.size)
|
||||
newItems += screenshotItems
|
||||
} else {
|
||||
items += Item.SectionItem(SectionType.SCREENSHOTS, ExpandType.SCREENSHOTS, screenshotItems, 0)
|
||||
newItems += Item.SectionItem(SectionType.SCREENSHOTS, ExpandType.SCREENSHOTS, screenshotItems, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -778,22 +804,26 @@ class ProductAdapter(private val callbacks: Callbacks, private val columns: Int)
|
||||
.sortedByDescending { it.release.versionCode }
|
||||
.toList()
|
||||
if (releaseItems.isNotEmpty()) {
|
||||
items += Item.SectionItem(SectionType.VERSIONS)
|
||||
newItems += Item.SectionItem(SectionType.VERSIONS)
|
||||
val maxReleases = 5
|
||||
if (releaseItems.size > maxReleases && ExpandType.VERSIONS !in expanded) {
|
||||
items += releaseItems.take(maxReleases)
|
||||
items += Item.ExpandItem(ExpandType.VERSIONS, false, releaseItems.takeLast(releaseItems.size - maxReleases))
|
||||
newItems += releaseItems.take(maxReleases)
|
||||
newItems += Item.ExpandItem(ExpandType.VERSIONS, false, releaseItems.takeLast(releaseItems.size - maxReleases))
|
||||
} else {
|
||||
items += releaseItems
|
||||
newItems += releaseItems
|
||||
}
|
||||
}
|
||||
|
||||
if (items.isEmpty()) {
|
||||
items += Item.EmptyItem(packageName)
|
||||
if (newItems.isEmpty()) {
|
||||
newItems += Item.EmptyItem(packageName)
|
||||
}
|
||||
|
||||
val diffResult = DiffUtil.calculateDiff(ItemDiffCallback(items, newItems))
|
||||
items.clear()
|
||||
items.addAll(newItems)
|
||||
this.product = productRepository?.first
|
||||
this.installedItem = installedItem
|
||||
notifyDataSetChanged()
|
||||
diffResult.dispatchUpdatesTo(this)
|
||||
}
|
||||
|
||||
private var action: Action? = null
|
||||
@@ -850,7 +880,7 @@ class ProductAdapter(private val callbacks: Callbacks, private val columns: Int)
|
||||
}
|
||||
ViewType.SWITCH -> SwitchViewHolder(parent.inflate(R.layout.switch_item)).apply {
|
||||
itemView.setOnClickListener {
|
||||
val switchItem = items[adapterPosition] as Item.SwitchItem
|
||||
val switchItem = items[bindingAdapterPosition] as Item.SwitchItem
|
||||
val productPreference = when (switchItem.switchType) {
|
||||
SwitchType.IGNORE_ALL_UPDATES -> {
|
||||
ProductPreferences[switchItem.packageName].let { it.copy(ignoreUpdates = !it.ignoreUpdates) }
|
||||
@@ -868,20 +898,20 @@ class ProductAdapter(private val callbacks: Callbacks, private val columns: Int)
|
||||
}
|
||||
ViewType.SECTION -> SectionViewHolder(parent.inflate(R.layout.section_item)).apply {
|
||||
itemView.setOnClickListener {
|
||||
val position = adapterPosition
|
||||
val position = bindingAdapterPosition
|
||||
val sectionItem = items[position] as Item.SectionItem
|
||||
if (sectionItem.items.isNotEmpty()) {
|
||||
expanded += sectionItem.expandType
|
||||
items[position] = Item.SectionItem(sectionItem.sectionType, sectionItem.expandType, emptyList(),
|
||||
sectionItem.items.size + sectionItem.collapseCount)
|
||||
notifyItemChanged(adapterPosition, Payload.REFRESH)
|
||||
notifyItemChanged(bindingAdapterPosition, Payload.REFRESH)
|
||||
items.addAll(position + 1, sectionItem.items)
|
||||
notifyItemRangeInserted(position + 1, sectionItem.items.size)
|
||||
} else if (sectionItem.collapseCount > 0) {
|
||||
expanded -= sectionItem.expandType
|
||||
items[position] = Item.SectionItem(sectionItem.sectionType, sectionItem.expandType,
|
||||
items.subList(position + 1, position + 1 + sectionItem.collapseCount).toList(), 0)
|
||||
notifyItemChanged(adapterPosition, Payload.REFRESH)
|
||||
notifyItemChanged(bindingAdapterPosition, Payload.REFRESH)
|
||||
repeat(sectionItem.collapseCount) { items.removeAt(position + 1) }
|
||||
notifyItemRangeRemoved(position + 1, sectionItem.collapseCount)
|
||||
}
|
||||
@@ -889,7 +919,7 @@ class ProductAdapter(private val callbacks: Callbacks, private val columns: Int)
|
||||
}
|
||||
ViewType.EXPAND -> ExpandViewHolder(parent.context).apply {
|
||||
itemView.setOnClickListener {
|
||||
val position = adapterPosition
|
||||
val position = bindingAdapterPosition
|
||||
val expandItem = items[position] as Item.ExpandItem
|
||||
items.removeAt(position)
|
||||
expanded += expandItem.expandType
|
||||
@@ -913,32 +943,32 @@ class ProductAdapter(private val callbacks: Callbacks, private val columns: Int)
|
||||
ViewType.TEXT -> TextViewHolder(parent.context)
|
||||
ViewType.LINK -> LinkViewHolder(parent.inflate(R.layout.link_item)).apply {
|
||||
itemView.setOnClickListener {
|
||||
val linkItem = items[adapterPosition] as Item.LinkItem
|
||||
val linkItem = items[bindingAdapterPosition] as Item.LinkItem
|
||||
if (linkItem.uri?.let { callbacks.onUriClick(it, false) } != true) {
|
||||
linkItem.displayLink?.let { copyLinkToClipboard(itemView.context, it) }
|
||||
}
|
||||
}
|
||||
itemView.setOnLongClickListener {
|
||||
val linkItem = items[adapterPosition] as Item.LinkItem
|
||||
val linkItem = items[bindingAdapterPosition] as Item.LinkItem
|
||||
linkItem.displayLink?.let { copyLinkToClipboard(itemView.context, it) }
|
||||
true
|
||||
}
|
||||
}
|
||||
ViewType.PERMISSIONS -> PermissionsViewHolder(parent.inflate(R.layout.permissions_item)).apply {
|
||||
itemView.setOnClickListener {
|
||||
val permissionsItem = items[adapterPosition] as Item.PermissionsItem
|
||||
val permissionsItem = items[bindingAdapterPosition] as Item.PermissionsItem
|
||||
callbacks.onPermissionsClick(permissionsItem.group?.name, permissionsItem.permissions.map { it.name })
|
||||
}
|
||||
}
|
||||
ViewType.SCREENSHOT -> ScreenshotViewHolder(parent.context).apply {
|
||||
itemView.setOnClickListener {
|
||||
val screenshotItem = items[adapterPosition] as Item.ScreenshotItem
|
||||
val screenshotItem = items[bindingAdapterPosition] as Item.ScreenshotItem
|
||||
callbacks.onScreenshotClick(screenshotItem.screenshot)
|
||||
}
|
||||
}
|
||||
ViewType.RELEASE -> ReleaseViewHolder(parent.inflate(R.layout.release_item)).apply {
|
||||
itemView.setOnClickListener {
|
||||
val releaseItem = items[adapterPosition] as Item.ReleaseItem
|
||||
val releaseItem = items[bindingAdapterPosition] as Item.ReleaseItem
|
||||
callbacks.onReleaseClick(releaseItem.release)
|
||||
}
|
||||
}
|
||||
@@ -989,7 +1019,6 @@ class ProductAdapter(private val callbacks: Callbacks, private val columns: Int)
|
||||
holder.actionTintCancel else holder.actionTintNormal
|
||||
}
|
||||
}
|
||||
if (updateAll || updateStatus) {
|
||||
val status = status
|
||||
holder.statusLayout.visibility = if (status != null) View.VISIBLE else View.GONE
|
||||
if (status != null) {
|
||||
@@ -1013,7 +1042,6 @@ class ProductAdapter(private val callbacks: Callbacks, private val columns: Int)
|
||||
}
|
||||
}::class
|
||||
}
|
||||
}
|
||||
Unit
|
||||
}
|
||||
ViewType.SWITCH -> {
|
||||
@@ -1094,10 +1122,10 @@ class ProductAdapter(private val callbacks: Callbacks, private val columns: Int)
|
||||
val labelFromPackage = PackageItemResolver.loadLabel(context, localCache, permission)
|
||||
val label = labelFromPackage ?: run {
|
||||
val prefixes = listOf("android.permission.", "com.android.browser.permission.")
|
||||
prefixes.find { permission.name.startsWith(it) }?.let {
|
||||
prefixes.find { permission.name.startsWith(it) }?.let { it ->
|
||||
val transform = permission.name.substring(it.length)
|
||||
if (transform.matches("[A-Z_]+".toRegex())) {
|
||||
transform.split("_").joinToString(separator = " ") { it.toLowerCase(Locale.US) }
|
||||
transform.split("_").joinToString(separator = " ") { it.lowercase(Locale.US) }
|
||||
} else {
|
||||
null
|
||||
}
|
||||
@@ -1106,7 +1134,7 @@ class ProductAdapter(private val callbacks: Callbacks, private val columns: Int)
|
||||
if (label == null) {
|
||||
Pair(false, permission.name)
|
||||
} else {
|
||||
Pair(true, label.first().toUpperCase() + label.substring(1, label.length))
|
||||
Pair(true, label.first().uppercaseChar() + label.substring(1, label.length))
|
||||
}
|
||||
}
|
||||
val builder = SpannableStringBuilder()
|
||||
@@ -1171,7 +1199,7 @@ class ProductAdapter(private val callbacks: Callbacks, private val columns: Int)
|
||||
holder.signature.visibility = if (item.showSignature && item.release.signature.isNotEmpty())
|
||||
View.VISIBLE else View.GONE
|
||||
if (item.showSignature && item.release.signature.isNotEmpty()) {
|
||||
val bytes = item.release.signature.toUpperCase(Locale.US).windowed(2, 2, false).take(8)
|
||||
val bytes = item.release.signature.uppercase(Locale.US).windowed(2, 2, false).take(8)
|
||||
val signature = bytes.joinToString(separator = " ")
|
||||
val builder = SpannableStringBuilder(context.getString(R.string.signature_FORMAT, signature))
|
||||
val index = builder.indexOf(signature)
|
||||
@@ -1255,7 +1283,7 @@ class ProductAdapter(private val callbacks: Callbacks, private val columns: Int)
|
||||
override fun onClick(view: View) {
|
||||
val productAdapter = productAdapterReference.get()
|
||||
val uri = try {
|
||||
Uri.parse(url)
|
||||
url.toUri()
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
|
||||
@@ -7,6 +7,7 @@ import android.content.Intent
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.provider.Settings
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MenuItem
|
||||
@@ -14,6 +15,7 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.Toolbar
|
||||
import androidx.core.os.BundleCompat
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
@@ -36,6 +38,7 @@ import nya.kitsunyan.foxydroid.utility.RxUtils
|
||||
import nya.kitsunyan.foxydroid.utility.Utils
|
||||
import nya.kitsunyan.foxydroid.utility.extension.android.*
|
||||
import nya.kitsunyan.foxydroid.widget.DividerItemDecoration
|
||||
import androidx.core.net.toUri
|
||||
|
||||
class ProductFragment(): ScreenFragment(), ProductAdapter.Callbacks {
|
||||
companion object {
|
||||
@@ -67,7 +70,7 @@ class ProductFragment(): ScreenFragment(), ProductAdapter.Callbacks {
|
||||
val packageName: String
|
||||
get() = requireArguments().getString(EXTRA_PACKAGE_NAME)!!
|
||||
|
||||
private var layoutManagerState: LinearLayoutManager.SavedState? = null
|
||||
private var layoutManagerState: Parcelable? = null
|
||||
|
||||
private var actions = Pair(emptySet<Action>(), null as Action?)
|
||||
private var products = emptyList<Pair<Product, Repository>>()
|
||||
@@ -100,7 +103,7 @@ class ProductFragment(): ScreenFragment(), ProductAdapter.Callbacks {
|
||||
this.toolbar = toolbar
|
||||
|
||||
toolbar.menu.apply {
|
||||
for (action in Action.values()) {
|
||||
for (action in Action.entries) {
|
||||
add(0, action.id, 0, action.adapterAction.titleResId)
|
||||
.setIcon(Utils.getToolbarIcon(toolbar.context, action.iconResId))
|
||||
.setVisible(false)
|
||||
@@ -130,42 +133,43 @@ class ProductFragment(): ScreenFragment(), ProductAdapter.Callbacks {
|
||||
addOnScrollListener(scrollListener)
|
||||
addItemDecoration(adapter.gridItemDecoration)
|
||||
addItemDecoration(DividerItemDecoration(context, adapter::configureDivider))
|
||||
savedInstanceState?.getParcelable<ProductAdapter.SavedState>(STATE_ADAPTER)?.let(adapter::restoreState)
|
||||
layoutManagerState = savedInstanceState?.getParcelable(STATE_LAYOUT_MANAGER)
|
||||
savedInstanceState?.let { bundle ->
|
||||
BundleCompat.getParcelable(bundle, STATE_ADAPTER, ProductAdapter.SavedState::class.java)?.let(adapter::restoreState)
|
||||
layoutManagerState = BundleCompat.getParcelable(bundle, STATE_LAYOUT_MANAGER, Parcelable::class.java)
|
||||
}
|
||||
recyclerView = this
|
||||
}, FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)
|
||||
|
||||
var first = true
|
||||
val first = true
|
||||
productDisposable = Observable.just(Unit)
|
||||
.concatWith(Database.observable(Database.Subject.Products))
|
||||
.observeOn(Schedulers.io())
|
||||
.flatMapSingle { RxUtils.querySingle { Database.ProductAdapter.get(packageName, it) } }
|
||||
.flatMapSingle { RxUtils.querySingle { signal -> Database.ProductAdapter.get(packageName, signal) } }
|
||||
.flatMapSingle { products -> RxUtils
|
||||
.querySingle { Database.RepositoryAdapter.getAll(it) }
|
||||
.map { it.asSequence().map { Pair(it.id, it) }.toMap()
|
||||
.let { products.mapNotNull { product -> it[product.repositoryId]?.let { Pair(product, it) } } } } }
|
||||
.querySingle { signal -> Database.RepositoryAdapter.getAll(signal) }
|
||||
.map { result ->
|
||||
result.asSequence().map { Pair(it.id, it) }.toMap()
|
||||
.let { map -> products.mapNotNull { product -> map[product.repositoryId]?.let { Pair(product, it) } } } } }
|
||||
.flatMapSingle { products -> RxUtils
|
||||
.querySingle { Nullable(Database.InstalledAdapter.get(packageName, it)) }
|
||||
.map { Pair(products, it) } }
|
||||
.querySingle { signal -> Nullable(Database.InstalledAdapter.get(packageName, signal)) }
|
||||
.map { result -> Pair(products, result) } }
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe {
|
||||
val (products, installedItem) = it
|
||||
val firstChanged = first
|
||||
first = false
|
||||
.subscribe { result ->
|
||||
val (products, installedItem) = result
|
||||
val productChanged = this.products != products
|
||||
val installedItemChanged = this.installed?.installedItem != installedItem.value
|
||||
if (firstChanged || productChanged || installedItemChanged) {
|
||||
if (first || productChanged || installedItemChanged) {
|
||||
layoutManagerState?.let { recyclerView?.layoutManager!!.onRestoreInstanceState(it) }
|
||||
layoutManagerState = null
|
||||
if (firstChanged || productChanged) {
|
||||
if (first || productChanged) {
|
||||
this.products = products
|
||||
}
|
||||
if (firstChanged || installedItemChanged) {
|
||||
installed = installedItem.value?.let {
|
||||
if (first || installedItemChanged) {
|
||||
installed = installedItem.value?.let { it ->
|
||||
val isSystem = try {
|
||||
((requireContext().packageManager.getApplicationInfo(packageName, 0).flags)
|
||||
and ApplicationInfo.FLAG_SYSTEM) != 0
|
||||
} catch (e: Exception) {
|
||||
} catch (_: Exception) {
|
||||
false
|
||||
}
|
||||
val launcherActivities = if (packageName == requireContext().packageName) {
|
||||
@@ -173,9 +177,9 @@ class ProductFragment(): ScreenFragment(), ProductAdapter.Callbacks {
|
||||
emptyList()
|
||||
} else {
|
||||
val packageManager = requireContext().packageManager
|
||||
packageManager
|
||||
val activities = packageManager
|
||||
.queryIntentActivities(Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_LAUNCHER), 0)
|
||||
.asSequence().mapNotNull { it.activityInfo }.filter { it.packageName == packageName }
|
||||
activities.asSequence().mapNotNull { it.activityInfo }.filter { it.packageName == packageName }
|
||||
.mapNotNull { activityInfo ->
|
||||
val label = try {
|
||||
activityInfo.loadLabel(packageManager).toString()
|
||||
@@ -190,10 +194,9 @@ class ProductFragment(): ScreenFragment(), ProductAdapter.Callbacks {
|
||||
Installed(it, isSystem, launcherActivities)
|
||||
}
|
||||
}
|
||||
val recyclerView = recyclerView!!
|
||||
val adapter = recyclerView.adapter as ProductAdapter
|
||||
if (firstChanged || productChanged || installedItemChanged) {
|
||||
adapter.setProducts(recyclerView.context, packageName, products, installedItem.value)
|
||||
recyclerView?.let {
|
||||
val adapter = it.adapter as ProductAdapter
|
||||
adapter.setProducts(it.context, packageName, products, installedItem.value)
|
||||
}
|
||||
updateButtons()
|
||||
}
|
||||
@@ -289,7 +292,7 @@ class ProductFragment(): ScreenFragment(), ProductAdapter.Callbacks {
|
||||
}
|
||||
val toolbar = toolbar
|
||||
if (toolbar != null) {
|
||||
for (action in Action.values()) {
|
||||
for (action in Action.entries) {
|
||||
toolbar.menu.findItem(action.id).isVisible = action in displayActions
|
||||
}
|
||||
}
|
||||
@@ -339,8 +342,6 @@ class ProductFragment(): ScreenFragment(), ProductAdapter.Callbacks {
|
||||
compatibleReleases
|
||||
.filter { it.platforms.contains(Android.primaryPlatform) }
|
||||
.minBy { it.platforms.size }
|
||||
?: compatibleReleases.minBy { it.platforms.size }
|
||||
?: compatibleReleases.firstOrNull()
|
||||
} else {
|
||||
compatibleReleases.firstOrNull()
|
||||
}
|
||||
@@ -361,13 +362,11 @@ class ProductFragment(): ScreenFragment(), ProductAdapter.Callbacks {
|
||||
}
|
||||
ProductAdapter.Action.DETAILS -> {
|
||||
startActivity(Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
|
||||
.setData(Uri.parse("package:$packageName")))
|
||||
.setData("package:$packageName".toUri()))
|
||||
}
|
||||
ProductAdapter.Action.UNINSTALL -> {
|
||||
// TODO Handle deprecation
|
||||
@Suppress("DEPRECATION")
|
||||
startActivity(Intent(Intent.ACTION_UNINSTALL_PACKAGE)
|
||||
.setData(Uri.parse("package:$packageName")))
|
||||
startActivity(Intent(Intent.ACTION_DELETE)
|
||||
.setData("package:$packageName".toUri()))
|
||||
}
|
||||
ProductAdapter.Action.CANCEL -> {
|
||||
val binder = downloadConnection.binder
|
||||
@@ -400,7 +399,7 @@ class ProductFragment(): ScreenFragment(), ProductAdapter.Callbacks {
|
||||
|
||||
override fun onScreenshotClick(screenshot: Product.Screenshot) {
|
||||
val pair = products.asSequence()
|
||||
.map { Pair(it.second, it.first.screenshots.find { it === screenshot }?.identifier) }
|
||||
.map { it -> Pair(it.second, it.first.screenshots.find { it === screenshot }?.identifier) }
|
||||
.filter { it.second != null }.firstOrNull()
|
||||
if (pair != null) {
|
||||
val (repository, identifier) = pair
|
||||
@@ -424,7 +423,7 @@ class ProductFragment(): ScreenFragment(), ProductAdapter.Callbacks {
|
||||
MessageDialog(MessageDialog.Message.ReleaseSignatureMismatch).show(childFragmentManager)
|
||||
}
|
||||
else -> {
|
||||
val productRepository = products.asSequence().filter { it.first.releases.any { it === release } }.firstOrNull()
|
||||
val productRepository = products.asSequence().filter { it -> it.first.releases.any { it === release } }.firstOrNull()
|
||||
if (productRepository != null) {
|
||||
downloadConnection.binder?.enqueue(packageName, productRepository.first.name,
|
||||
productRepository.second, release)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package nya.kitsunyan.foxydroid.screen
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.graphics.drawable.GradientDrawable
|
||||
import android.view.Gravity
|
||||
@@ -46,7 +48,7 @@ class ProductsAdapter(private val onClick: (ProductItem) -> Unit):
|
||||
init {
|
||||
itemView as FrameLayout
|
||||
val progressBar = ProgressBar(itemView.context)
|
||||
itemView.addView(progressBar, FrameLayout.LayoutParams(FrameLayout.LayoutParams.WRAP_CONTENT,
|
||||
(itemView as ViewGroup).addView(progressBar, FrameLayout.LayoutParams(FrameLayout.LayoutParams.WRAP_CONTENT,
|
||||
FrameLayout.LayoutParams.WRAP_CONTENT).apply { gravity = Gravity.CENTER })
|
||||
itemView.layoutParams = RecyclerView.LayoutParams(RecyclerView.LayoutParams.MATCH_PARENT,
|
||||
RecyclerView.LayoutParams.MATCH_PARENT)
|
||||
@@ -59,11 +61,11 @@ class ProductsAdapter(private val onClick: (ProductItem) -> Unit):
|
||||
|
||||
init {
|
||||
itemView as TextView
|
||||
itemView.gravity = Gravity.CENTER
|
||||
(itemView as TextView).gravity = Gravity.CENTER
|
||||
itemView.resources.sizeScaled(20).let { itemView.setPadding(it, it, it, it) }
|
||||
itemView.typeface = TypefaceExtra.light
|
||||
itemView.setTextColor(context.getColorFromAttr(android.R.attr.textColorPrimary))
|
||||
itemView.setTextSizeScaled(20)
|
||||
(itemView as TextView).typeface = TypefaceExtra.light
|
||||
(itemView as TextView).setTextColor(context.getColorFromAttr(android.R.attr.textColorPrimary))
|
||||
(itemView as TextView).setTextSizeScaled(20)
|
||||
itemView.layoutParams = RecyclerView.LayoutParams(RecyclerView.LayoutParams.MATCH_PARENT,
|
||||
RecyclerView.LayoutParams.MATCH_PARENT)
|
||||
}
|
||||
@@ -75,18 +77,25 @@ class ProductsAdapter(private val onClick: (ProductItem) -> Unit):
|
||||
getProductItem(position + 1) else null
|
||||
when {
|
||||
currentItem != null && nextItem != null && currentItem.matchRank != nextItem.matchRank -> {
|
||||
configuration.set(true, false, 0, 0)
|
||||
configuration.set(needDivider = true, toTop = false, paddingStart = 0, paddingEnd = 0)
|
||||
}
|
||||
else -> {
|
||||
configuration.set(true, false, context.resources.sizeScaled(72), 0)
|
||||
configuration.set(
|
||||
needDivider = true,
|
||||
toTop = false,
|
||||
paddingStart = context.resources.sizeScaled(72),
|
||||
paddingEnd = 0
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var repositories: Map<Long, Repository> = emptyMap()
|
||||
set(value) {
|
||||
if (field != value) {
|
||||
field = value
|
||||
notifyDataSetChanged()
|
||||
notifyItemRangeChanged(0, itemCount)
|
||||
}
|
||||
}
|
||||
|
||||
var emptyText: String = ""
|
||||
@@ -94,7 +103,7 @@ class ProductsAdapter(private val onClick: (ProductItem) -> Unit):
|
||||
if (field != value) {
|
||||
field = value
|
||||
if (isEmpty) {
|
||||
notifyDataSetChanged()
|
||||
notifyItemChanged(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -123,7 +132,12 @@ class ProductsAdapter(private val onClick: (ProductItem) -> Unit):
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: ViewType): RecyclerView.ViewHolder {
|
||||
return when (viewType) {
|
||||
ViewType.PRODUCT -> ProductViewHolder(parent.inflate(R.layout.product_item)).apply {
|
||||
itemView.setOnClickListener { onClick(getProductItem(adapterPosition)) }
|
||||
itemView.setOnClickListener {
|
||||
val position = bindingAdapterPosition
|
||||
if (position != RecyclerView.NO_POSITION) {
|
||||
onClick(getProductItem(position))
|
||||
}
|
||||
}
|
||||
}
|
||||
ViewType.LOADING -> LoadingViewHolder(parent.context)
|
||||
ViewType.EMPTY -> EmptyViewHolder(parent.context)
|
||||
@@ -170,7 +184,7 @@ class ProductsAdapter(private val onClick: (ProductItem) -> Unit):
|
||||
}
|
||||
}
|
||||
val enabled = productItem.compatible || productItem.installedVersion.isNotEmpty()
|
||||
sequenceOf(holder.name, holder.status, holder.summary).forEach { it.isEnabled = enabled }
|
||||
sequenceOf(holder.name, holder.status, holder.summary).forEach { view -> view.isEnabled = enabled }
|
||||
}
|
||||
ViewType.LOADING -> {
|
||||
// Do nothing
|
||||
@@ -179,6 +193,25 @@ class ProductsAdapter(private val onClick: (ProductItem) -> Unit):
|
||||
holder as EmptyViewHolder
|
||||
holder.text.text = emptyText
|
||||
}
|
||||
}::class
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
override fun onCursorChanged(oldCursor: Cursor?, newCursor: Cursor?) {
|
||||
val oldSize = oldCursor?.count ?: 0
|
||||
val newSize = newCursor?.count ?: 0
|
||||
|
||||
val oldIsVirtual = oldSize == 0
|
||||
val newIsVirtual = newSize == 0
|
||||
|
||||
if (oldIsVirtual != newIsVirtual) {
|
||||
notifyDataSetChanged()
|
||||
} else if (oldIsVirtual) {
|
||||
if (oldCursor != newCursor) {
|
||||
notifyItemChanged(0)
|
||||
}
|
||||
} else {
|
||||
super.onCursorChanged(oldCursor, newCursor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import android.os.Parcelable
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.os.BundleCompat
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
@@ -90,17 +91,21 @@ class ProductsFragment(): ScreenFragment(), CursorOwner.Callback {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
currentSearchQuery = savedInstanceState?.getString(STATE_CURRENT_SEARCH_QUERY).orEmpty()
|
||||
currentSection = savedInstanceState?.getParcelable(STATE_CURRENT_SECTION) ?: ProductItem.Section.All
|
||||
currentSection = savedInstanceState?.let { bundle ->
|
||||
BundleCompat.getParcelable(bundle, STATE_CURRENT_SECTION, ProductItem.Section::class.java)
|
||||
} ?: ProductItem.Section.All
|
||||
currentOrder = savedInstanceState?.getString(STATE_CURRENT_ORDER)
|
||||
?.let(ProductItem.Order::valueOf) ?: ProductItem.Order.NAME
|
||||
layoutManagerState = savedInstanceState?.getParcelable(STATE_LAYOUT_MANAGER)
|
||||
layoutManagerState = savedInstanceState?.let { bundle ->
|
||||
BundleCompat.getParcelable(bundle, STATE_LAYOUT_MANAGER, Parcelable::class.java)
|
||||
}
|
||||
|
||||
screenActivity.cursorOwner.attach(this, request)
|
||||
repositoriesDisposable = Observable.just(Unit)
|
||||
.concatWith(Database.observable(Database.Subject.Repositories))
|
||||
.observeOn(Schedulers.io())
|
||||
.flatMapSingle { RxUtils.querySingle { Database.RepositoryAdapter.getAll(it) } }
|
||||
.map { it.asSequence().map { Pair(it.id, it) }.toMap() }
|
||||
.flatMapSingle { RxUtils.querySingle { signal -> Database.RepositoryAdapter.getAll(signal) } }
|
||||
.map { result -> result.asSequence().map { Pair(it.id, it) }.toMap() }
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { (recyclerView?.adapter as? ProductsAdapter)?.repositories = it }
|
||||
}
|
||||
|
||||
@@ -36,10 +36,17 @@ class RepositoriesAdapter(private val onClick: (Repository) -> Unit,
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: ViewType): RecyclerView.ViewHolder {
|
||||
return ViewHolder(parent.inflate(R.layout.repository_item)).apply {
|
||||
itemView.setOnClickListener { onClick(getRepository(adapterPosition)) }
|
||||
itemView.setOnClickListener {
|
||||
val position = bindingAdapterPosition
|
||||
if (position != RecyclerView.NO_POSITION) {
|
||||
onClick(getRepository(position))
|
||||
}
|
||||
}
|
||||
enabled.setOnCheckedChangeListener { _, isChecked ->
|
||||
if (listenSwitch) {
|
||||
if (!onSwitch(getRepository(adapterPosition), isChecked)) {
|
||||
val position = bindingAdapterPosition
|
||||
if (position != RecyclerView.NO_POSITION) {
|
||||
if (!onSwitch(getRepository(position), isChecked)) {
|
||||
listenSwitch = false
|
||||
enabled.isChecked = !isChecked
|
||||
listenSwitch = true
|
||||
@@ -48,6 +55,7 @@ class RepositoriesAdapter(private val onClick: (Repository) -> Unit,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
holder as ViewHolder
|
||||
|
||||
@@ -113,14 +113,12 @@ class RepositoryFragment(): ScreenFragment() {
|
||||
layout.addTitleText(R.string.name, repository.name)
|
||||
layout.addTitleText(R.string.description, repository.description.replace('\n', ' '))
|
||||
layout.addTitleText(R.string.last_update, run {
|
||||
val lastUpdated = repository.updated
|
||||
if (lastUpdated > 0L) {
|
||||
repository.updated
|
||||
run {
|
||||
val date = Date(repository.updated)
|
||||
val format = if (DateUtils.isToday(date.time)) DateUtils.FORMAT_SHOW_TIME else
|
||||
DateUtils.FORMAT_SHOW_TIME or DateUtils.FORMAT_SHOW_DATE
|
||||
DateUtils.formatDateTime(layout.context, date.time, format)
|
||||
} else {
|
||||
getString(R.string.unknown)
|
||||
}
|
||||
})
|
||||
if (repository.enabled && (repository.lastModified.isNotEmpty() || repository.entityTag.isNotEmpty())) {
|
||||
@@ -139,7 +137,7 @@ class RepositoryFragment(): ScreenFragment() {
|
||||
}
|
||||
} else {
|
||||
val fingerprint = SpannableStringBuilder(repository.fingerprint.windowed(2, 2, false)
|
||||
.take(32).joinToString(separator = " ") { it.toUpperCase(Locale.US) })
|
||||
.take(32).joinToString(separator = " ") { it.uppercase(Locale.US) })
|
||||
fingerprint.setSpan(TypefaceSpan("monospace"), 0, fingerprint.length,
|
||||
SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
layout.addTitleText(R.string.fingerprint, fingerprint)
|
||||
@@ -158,9 +156,7 @@ class RepositoryFragment(): ScreenFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
internal fun onDeleteConfirm() {
|
||||
if (syncConnection.binder?.deleteRepository(repositoryId) == true) {
|
||||
requireActivity().onBackPressed()
|
||||
}
|
||||
}
|
||||
internal fun onDeleteConfirm() {if (syncConnection.binder?.deleteRepository(repositoryId) == true) {
|
||||
screenActivity.onBackPressedDispatcher.onBackPressed()
|
||||
}}
|
||||
}
|
||||
|
||||
@@ -5,11 +5,13 @@ import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.Parcel
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.Toolbar
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.core.os.BundleCompat
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import nya.kitsunyan.foxydroid.R
|
||||
@@ -72,8 +74,7 @@ abstract class ScreenActivity: FragmentActivity() {
|
||||
setTheme(Preferences[Preferences.Key.Theme].getResId(resources.configuration))
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
window.decorView.systemUiVisibility = window.decorView.systemUiVisibility or View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
addContentView(FrameLayout(this).apply { id = R.id.main_content },
|
||||
ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT))
|
||||
|
||||
@@ -87,14 +88,34 @@ abstract class ScreenActivity: FragmentActivity() {
|
||||
.findFragmentByTag(CursorOwner::class.java.name) as CursorOwner
|
||||
}
|
||||
|
||||
savedInstanceState?.getParcelableArrayList<FragmentStackItem>(STATE_FRAGMENT_STACK)
|
||||
?.let { fragmentStack += it }
|
||||
savedInstanceState?.let {
|
||||
BundleCompat.getParcelableArrayList(it, STATE_FRAGMENT_STACK, FragmentStackItem::class.java)
|
||||
?.let { list -> fragmentStack += list }
|
||||
}
|
||||
if (savedInstanceState == null) {
|
||||
replaceFragment(TabsFragment(), null)
|
||||
if ((intent.flags and Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY) == 0) {
|
||||
handleIntent(intent)
|
||||
}
|
||||
}
|
||||
|
||||
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
val currentFragment = currentFragment
|
||||
if (!(currentFragment is ScreenFragment && currentFragment.onBackPressed())) {
|
||||
hideKeyboard()
|
||||
if (!popFragment()) {
|
||||
isEnabled = false
|
||||
onBackPressedDispatcher.onBackPressed()
|
||||
isEnabled = true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
supportFragmentManager.addFragmentOnAttachListener { _, _ ->
|
||||
hideKeyboard()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
@@ -102,16 +123,6 @@ abstract class ScreenActivity: FragmentActivity() {
|
||||
outState.putParcelableArrayList(STATE_FRAGMENT_STACK, ArrayList(fragmentStack))
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
val currentFragment = currentFragment
|
||||
if (!(currentFragment is ScreenFragment && currentFragment.onBackPressed())) {
|
||||
hideKeyboard()
|
||||
if (!popFragment()) {
|
||||
super.onBackPressed()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun replaceFragment(fragment: Fragment, open: Boolean?) {
|
||||
if (open != null) {
|
||||
currentFragment?.view?.translationZ = (if (open) Int.MIN_VALUE else Int.MAX_VALUE).toFloat()
|
||||
@@ -137,7 +148,7 @@ abstract class ScreenActivity: FragmentActivity() {
|
||||
private fun popFragment(): Boolean {
|
||||
return fragmentStack.isNotEmpty() && run {
|
||||
val stackItem = fragmentStack.removeAt(fragmentStack.size - 1)
|
||||
val fragment = Class.forName(stackItem.className).newInstance() as Fragment
|
||||
val fragment = supportFragmentManager.fragmentFactory.instantiate(classLoader, stackItem.className)
|
||||
stackItem.arguments?.let(fragment::setArguments)
|
||||
stackItem.savedState?.let(fragment::setInitialSavedState)
|
||||
replaceFragment(fragment, false)
|
||||
@@ -150,15 +161,10 @@ abstract class ScreenActivity: FragmentActivity() {
|
||||
?.hideSoftInputFromWindow((currentFocus ?: window.decorView).windowToken, 0)
|
||||
}
|
||||
|
||||
override fun onAttachFragment(fragment: Fragment) {
|
||||
super.onAttachFragment(fragment)
|
||||
hideKeyboard()
|
||||
}
|
||||
|
||||
internal fun onToolbarCreated(toolbar: Toolbar) {
|
||||
if (fragmentStack.isNotEmpty()) {
|
||||
toolbar.navigationIcon = toolbar.context.getDrawableFromAttr(android.R.attr.homeAsUpIndicator)
|
||||
toolbar.setNavigationOnClickListener { onBackPressed() }
|
||||
toolbar.setNavigationOnClickListener { onBackPressedDispatcher.onBackPressed() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -235,10 +241,10 @@ abstract class ScreenActivity: FragmentActivity() {
|
||||
} else {
|
||||
Pair(Uri.fromFile(Cache.getReleaseFile(this, cacheFileName)), 0)
|
||||
}
|
||||
// TODO Handle deprecation
|
||||
@Suppress("DEPRECATION")
|
||||
startActivity(Intent(Intent.ACTION_INSTALL_PACKAGE)
|
||||
.setDataAndType(uri, "application/vnd.android.package-archive").setFlags(flags))
|
||||
|
||||
startActivity(Intent(Intent.ACTION_VIEW)
|
||||
.setDataAndType(uri, "application/vnd.android.package-archive")
|
||||
.addFlags(flags or Intent.FLAG_ACTIVITY_NEW_TASK))
|
||||
}
|
||||
|
||||
internal fun navigateProduct(packageName: String) = pushFragment(ProductFragment(packageName))
|
||||
|
||||
@@ -5,13 +5,16 @@ import android.content.Context
|
||||
import android.graphics.PixelFormat
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.WindowManager
|
||||
import android.widget.ImageView
|
||||
import androidx.core.graphics.ColorUtils
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.viewpager2.widget.MarginPageTransformer
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
@@ -65,10 +68,11 @@ class ScreenshotsFragment(): DialogFragment() {
|
||||
val background = dialog.context.getColorFromAttr(android.R.attr.colorBackground).defaultColor
|
||||
decorView.setBackgroundColor(background.let { ColorUtils.blendARGB(0x00ffffff and it, it, 0.9f) })
|
||||
decorView.setPadding(0, 0, 0, 0)
|
||||
background.let { ColorUtils.blendARGB(0x00ffffff and it, it, 0.8f) }.let {
|
||||
window.statusBarColor = it
|
||||
window.navigationBarColor = it
|
||||
}
|
||||
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
val insetsController = WindowCompat.getInsetsController(window, decorView)
|
||||
insetsController.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||
|
||||
window.attributes = window.attributes.apply {
|
||||
title = ScreenshotsFragment::class.java.name
|
||||
format = PixelFormat.TRANSLUCENT
|
||||
@@ -86,17 +90,16 @@ class ScreenshotsFragment(): DialogFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
val hideFlags = View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or
|
||||
View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_IMMERSIVE
|
||||
decorView.systemUiVisibility = decorView.systemUiVisibility or View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||
val applyHide = Runnable { decorView.systemUiVisibility = decorView.systemUiVisibility or hideFlags }
|
||||
val applyHide = Runnable { insetsController.hide(WindowInsetsCompat.Type.systemBars()) }
|
||||
val handleClick = {
|
||||
decorView.removeCallbacks(applyHide)
|
||||
if ((decorView.systemUiVisibility and hideFlags) == hideFlags) {
|
||||
decorView.systemUiVisibility = decorView.systemUiVisibility and hideFlags.inv()
|
||||
val isVisible = decorView.rootWindowInsets?.let {
|
||||
it.isVisible(WindowInsetsCompat.Type.statusBars()) || it.isVisible(WindowInsetsCompat.Type.navigationBars())
|
||||
} ?: true
|
||||
if (isVisible) {
|
||||
insetsController.hide(WindowInsetsCompat.Type.systemBars())
|
||||
} else {
|
||||
decorView.systemUiVisibility = decorView.systemUiVisibility or hideFlags
|
||||
insetsController.show(WindowInsetsCompat.Type.systemBars())
|
||||
}
|
||||
}
|
||||
decorView.postDelayed(applyHide, 2000L)
|
||||
@@ -112,21 +115,20 @@ class ScreenshotsFragment(): DialogFragment() {
|
||||
ViewGroup.LayoutParams.MATCH_PARENT))
|
||||
this.viewPager = viewPager
|
||||
|
||||
var restored = false
|
||||
val restored = false
|
||||
productDisposable = Observable.just(Unit)
|
||||
.concatWith(Database.observable(Database.Subject.Products))
|
||||
.observeOn(Schedulers.io())
|
||||
.flatMapSingle { RxUtils.querySingle { Database.ProductAdapter.get(packageName, it) } }
|
||||
.map { Pair(it.find { it.repositoryId == repositoryId }, Database.RepositoryAdapter.get(repositoryId)) }
|
||||
.flatMapSingle { RxUtils.querySingle { signal -> Database.ProductAdapter.get(packageName, signal) } }
|
||||
.map { result -> Pair(result.find { it.repositoryId == repositoryId }, Database.RepositoryAdapter.get(repositoryId)) }
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe {
|
||||
val (product, repository) = it
|
||||
.subscribe { result ->
|
||||
val (product, repository) = result
|
||||
val screenshots = product?.screenshots.orEmpty()
|
||||
(viewPager.adapter as Adapter).update(repository, screenshots)
|
||||
if (!restored) {
|
||||
restored = true
|
||||
val identifier = savedInstanceState?.getString(STATE_IDENTIFIER)
|
||||
?: requireArguments().getString(STATE_IDENTIFIER)
|
||||
?: requireArguments().getString(EXTRA_IDENTIFIER)
|
||||
if (identifier != null) {
|
||||
val index = screenshots.indexOfFirst { it.identifier == identifier }
|
||||
if (index >= 0) {
|
||||
@@ -160,7 +162,9 @@ class ScreenshotsFragment(): DialogFragment() {
|
||||
|
||||
private class Adapter(private val packageName: String, private val onClick: () -> Unit):
|
||||
StableRecyclerAdapter<Adapter.ViewType, RecyclerView.ViewHolder>() {
|
||||
enum class ViewType { SCREENSHOT }
|
||||
enum class ViewType {
|
||||
SECTION
|
||||
}
|
||||
|
||||
private class ViewHolder(context: Context): RecyclerView.ViewHolder(ImageView(context)) {
|
||||
val image: ImageView
|
||||
@@ -182,17 +186,28 @@ class ScreenshotsFragment(): DialogFragment() {
|
||||
private var repository: Repository? = null
|
||||
private var screenshots = emptyList<Product.Screenshot>()
|
||||
|
||||
private class ScreenshotDiffCallback(private val oldList: List<Product.Screenshot>,
|
||||
private val newList: List<Product.Screenshot>): DiffUtil.Callback() {
|
||||
override fun getOldListSize(): Int = oldList.size
|
||||
override fun getNewListSize(): Int = newList.size
|
||||
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean =
|
||||
oldList[oldItemPosition].identifier == newList[newItemPosition].identifier
|
||||
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean =
|
||||
oldList[oldItemPosition] == newList[newItemPosition]
|
||||
}
|
||||
|
||||
fun update(repository: Repository?, screenshots: List<Product.Screenshot>) {
|
||||
this.repository = repository
|
||||
val diffResult = DiffUtil.calculateDiff(ScreenshotDiffCallback(this.screenshots, screenshots))
|
||||
this.screenshots = screenshots
|
||||
notifyDataSetChanged()
|
||||
diffResult.dispatchUpdatesTo(this)
|
||||
}
|
||||
|
||||
var size = Pair(0, 0)
|
||||
set(value) {
|
||||
if (field != value) {
|
||||
field = value
|
||||
notifyDataSetChanged()
|
||||
notifyItemRangeChanged(0, itemCount)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -206,7 +221,7 @@ class ScreenshotsFragment(): DialogFragment() {
|
||||
|
||||
override fun getItemCount(): Int = screenshots.size
|
||||
override fun getItemDescriptor(position: Int): String = screenshots[position].identifier
|
||||
override fun getItemEnumViewType(position: Int): ViewType = ViewType.SCREENSHOT
|
||||
override fun getItemEnumViewType(position: Int): ViewType = ViewType.SECTION
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: ViewType): RecyclerView.ViewHolder {
|
||||
return ViewHolder(parent.context).apply {
|
||||
|
||||
@@ -22,7 +22,10 @@ import android.widget.LinearLayout
|
||||
import android.widget.SearchView
|
||||
import android.widget.TextView
|
||||
import android.widget.Toolbar
|
||||
import androidx.core.os.BundleCompat
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
@@ -75,10 +78,10 @@ class TabsFragment: ScreenFragment() {
|
||||
if (field != value) {
|
||||
field = value
|
||||
val layout = layout
|
||||
layout?.tabs?.let { (0 until it.childCount)
|
||||
.forEach { index -> it.getChildAt(index)!!.isEnabled = !value } }
|
||||
layout?.tabs?.let { tabs -> (0 until tabs.childCount)
|
||||
.forEach { index -> tabs.getChildAt(index)!!.isEnabled = !value } }
|
||||
layout?.sectionIcon?.scaleY = if (value) -1f else 1f
|
||||
if ((sectionsList?.parent as? View)?.height ?: 0 > 0) {
|
||||
if (((sectionsList?.parent as? View)?.height ?: 0) > 0) {
|
||||
animateSectionsList()
|
||||
}
|
||||
}
|
||||
@@ -89,8 +92,8 @@ class TabsFragment: ScreenFragment() {
|
||||
private var section: ProductItem.Section = ProductItem.Section.All
|
||||
|
||||
private val syncConnection = Connection(SyncService::class.java, onBind = { _, _ ->
|
||||
viewPager?.let {
|
||||
val source = ProductsFragment.Source.values()[it.currentItem]
|
||||
viewPager?.let { pager ->
|
||||
val source = ProductsFragment.Source.entries[pager.currentItem]
|
||||
updateUpdateNotificationBlocker(source)
|
||||
}
|
||||
})
|
||||
@@ -195,7 +198,7 @@ class TabsFragment: ScreenFragment() {
|
||||
|
||||
layout.tabs.background = TabsBackgroundDrawable(layout.tabs.context,
|
||||
layout.tabs.layoutDirection == View.LAYOUT_DIRECTION_RTL)
|
||||
ProductsFragment.Source.values().forEach {
|
||||
ProductsFragment.Source.entries.forEach { source ->
|
||||
val tab = TextView(layout.tabs.context)
|
||||
val selectedColor = tab.context.getColorFromAttr(android.R.attr.textColorPrimary).defaultColor
|
||||
val normalColor = tab.context.getColorFromAttr(android.R.attr.textColorSecondary).defaultColor
|
||||
@@ -205,25 +208,25 @@ class TabsFragment: ScreenFragment() {
|
||||
intArrayOf(selectedColor, normalColor)))
|
||||
tab.setTextSizeScaled(14)
|
||||
tab.isAllCaps = true
|
||||
tab.text = getString(it.titleResId)
|
||||
tab.text = getString(source.titleResId)
|
||||
tab.background = tab.context.getDrawableFromAttr(android.R.attr.selectableItemBackground)
|
||||
tab.setOnClickListener { _ ->
|
||||
setSelectedTab(it)
|
||||
viewPager!!.setCurrentItem(it.ordinal, Utils.areAnimationsEnabled(tab.context))
|
||||
tab.setOnClickListener {
|
||||
setSelectedTab(source)
|
||||
viewPager!!.setCurrentItem(source.ordinal, Utils.areAnimationsEnabled(tab.context))
|
||||
}
|
||||
layout.tabs.addView(tab, 0, LinearLayout.LayoutParams.MATCH_PARENT)
|
||||
(tab.layoutParams as LinearLayout.LayoutParams).weight = 1f
|
||||
}
|
||||
|
||||
showSections = savedInstanceState?.getByte(STATE_SHOW_SECTIONS)?.toInt() ?: 0 != 0
|
||||
sections = savedInstanceState?.getParcelableArrayList<ProductItem.Section>(STATE_SECTIONS).orEmpty()
|
||||
section = savedInstanceState?.getParcelable(STATE_SECTION) ?: ProductItem.Section.All
|
||||
showSections = (savedInstanceState?.getByte(STATE_SHOW_SECTIONS)?.toInt() ?: 0) != 0
|
||||
sections = savedInstanceState?.let { BundleCompat.getParcelableArrayList(it, STATE_SECTIONS, ProductItem.Section::class.java) }.orEmpty()
|
||||
section = savedInstanceState?.let { BundleCompat.getParcelable(it, STATE_SECTION, ProductItem.Section::class.java) } ?: ProductItem.Section.All
|
||||
layout.sectionChange.setOnClickListener { showSections = sections
|
||||
.any { it !is ProductItem.Section.All } && !showSections }
|
||||
|
||||
updateOrder()
|
||||
sortOrderDisposable = Preferences.observable.subscribe {
|
||||
if (it == Preferences.Key.SortOrder) {
|
||||
sortOrderDisposable = Preferences.observable.subscribe { key ->
|
||||
if (key == Preferences.Key.SortOrder) {
|
||||
updateOrder()
|
||||
}
|
||||
}
|
||||
@@ -233,9 +236,8 @@ class TabsFragment: ScreenFragment() {
|
||||
viewPager = ViewPager2(content.context).apply {
|
||||
id = R.id.fragment_pager
|
||||
adapter = object: FragmentStateAdapter(this@TabsFragment) {
|
||||
override fun getItemCount(): Int = ProductsFragment.Source.values().size
|
||||
override fun createFragment(position: Int): Fragment = ProductsFragment(ProductsFragment
|
||||
.Source.values()[position])
|
||||
override fun getItemCount(): Int = ProductsFragment.Source.entries.size
|
||||
override fun createFragment(position: Int): Fragment = ProductsFragment(ProductsFragment.Source.entries[position])
|
||||
}
|
||||
content.addView(this, FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)
|
||||
registerOnPageChangeCallback(pageChangeCallback)
|
||||
@@ -245,16 +247,17 @@ class TabsFragment: ScreenFragment() {
|
||||
categoriesDisposable = Observable.just(Unit)
|
||||
.concatWith(Database.observable(Database.Subject.Products))
|
||||
.observeOn(Schedulers.io())
|
||||
.flatMapSingle { RxUtils.querySingle { Database.CategoryAdapter.getAll(it) } }
|
||||
.flatMapSingle { RxUtils.querySingle { signal -> Database.CategoryAdapter.getAll(signal) } }
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { setSectionsAndUpdate(it.asSequence().sorted()
|
||||
.subscribe { result -> setSectionsAndUpdate(result.asSequence().sorted()
|
||||
.map(ProductItem.Section::Category).toList(), null) }
|
||||
repositoriesDisposable = Observable.just(Unit)
|
||||
.concatWith(Database.observable(Database.Subject.Repositories))
|
||||
.observeOn(Schedulers.io())
|
||||
.flatMapSingle { RxUtils.querySingle { Database.RepositoryAdapter.getAll(it) } }
|
||||
.flatMapSingle { RxUtils.querySingle { signal -> Database.RepositoryAdapter.getAll(signal) } }
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { setSectionsAndUpdate(null, it.asSequence().filter { it.enabled }
|
||||
.subscribe { result ->
|
||||
setSectionsAndUpdate(null, result.asSequence().filter { it.enabled }
|
||||
.map { ProductItem.Section.Repository(it.id, it.name) }.toList()) }
|
||||
updateSection()
|
||||
|
||||
@@ -264,10 +267,10 @@ class TabsFragment: ScreenFragment() {
|
||||
isMotionEventSplittingEnabled = false
|
||||
isVerticalScrollBarEnabled = false
|
||||
setHasFixedSize(true)
|
||||
val adapter = SectionsAdapter({ sections }) {
|
||||
val adapter = SectionsAdapter({ sections }) { newSection ->
|
||||
if (showSections) {
|
||||
showSections = false
|
||||
section = it
|
||||
section = newSection
|
||||
updateSection()
|
||||
}
|
||||
}
|
||||
@@ -280,13 +283,12 @@ class TabsFragment: ScreenFragment() {
|
||||
}
|
||||
this.sectionsList = sectionsList
|
||||
|
||||
var lastContentHeight = -1
|
||||
val lastContentHeight = -1
|
||||
content.viewTreeObserver.addOnGlobalLayoutListener {
|
||||
if (this.view != null) {
|
||||
val initial = lastContentHeight <= 0
|
||||
val initial = true
|
||||
val contentHeight = content.height
|
||||
if (lastContentHeight != contentHeight) {
|
||||
lastContentHeight = contentHeight
|
||||
if (initial) {
|
||||
sectionsList.layoutParams.height = if (showSections) contentHeight else 0
|
||||
sectionsList.visibility = if (showSections) View.VISIBLE else View.GONE
|
||||
@@ -340,7 +342,9 @@ class TabsFragment: ScreenFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun onAttachFragment(childFragment: Fragment) {
|
||||
@Suppress("DEPRECATION")
|
||||
super.onAttachFragment(childFragment)
|
||||
|
||||
if (view != null && childFragment is ProductsFragment) {
|
||||
@@ -368,7 +372,7 @@ class TabsFragment: ScreenFragment() {
|
||||
|
||||
private fun setSelectedTab(source: ProductsFragment.Source) {
|
||||
val layout = layout!!
|
||||
(0 until layout.tabs.childCount).forEach { layout.tabs.getChildAt(it).isSelected = it == source.ordinal }
|
||||
(0 until layout.tabs.childCount).forEach { index -> layout.tabs.getChildAt(index).isSelected = index == source.ordinal }
|
||||
}
|
||||
|
||||
internal fun selectUpdates() = selectUpdatesInternal(true)
|
||||
@@ -407,10 +411,20 @@ class TabsFragment: ScreenFragment() {
|
||||
val oldCategories = collectOldSections(categories)
|
||||
val oldRepositories = collectOldSections(repositories)
|
||||
if (oldCategories == null || oldRepositories == null) {
|
||||
val oldSections = sections
|
||||
sections = listOf(ProductItem.Section.All) +
|
||||
(categories ?: oldCategories).orEmpty() +
|
||||
(repositories ?: oldRepositories).orEmpty()
|
||||
val diffResult = DiffUtil.calculateDiff(object : DiffUtil.Callback() {
|
||||
override fun getOldListSize(): Int = oldSections.size
|
||||
override fun getNewListSize(): Int = sections.size
|
||||
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean =
|
||||
oldSections[oldItemPosition] == sections[newItemPosition]
|
||||
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean =
|
||||
oldSections[oldItemPosition] == sections[newItemPosition]
|
||||
})
|
||||
updateSection()
|
||||
sectionsList?.adapter?.let { adapter -> diffResult.dispatchUpdatesTo(adapter) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -418,14 +432,19 @@ class TabsFragment: ScreenFragment() {
|
||||
if (section !in sections) {
|
||||
section = ProductItem.Section.All
|
||||
}
|
||||
layout?.sectionName?.text = when (val section = section) {
|
||||
layout?.sectionName?.text = when (val s = section) {
|
||||
is ProductItem.Section.All -> getString(R.string.all_applications)
|
||||
is ProductItem.Section.Category -> section.name
|
||||
is ProductItem.Section.Repository -> section.name
|
||||
is ProductItem.Section.Category -> s.name
|
||||
is ProductItem.Section.Repository -> s.name
|
||||
}
|
||||
layout?.sectionIcon?.visibility = if (sections.any { it !is ProductItem.Section.All }) View.VISIBLE else View.GONE
|
||||
productFragments.forEach { it.setSection(section) }
|
||||
sectionsList?.adapter?.notifyDataSetChanged()
|
||||
sectionsList?.adapter?.let { adapter ->
|
||||
val index = sections.indexOf(section)
|
||||
if (index >= 0) {
|
||||
adapter.notifyItemRangeChanged(0, sections.size)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun animateSectionsList() {
|
||||
@@ -440,16 +459,16 @@ class TabsFragment: ScreenFragment() {
|
||||
sectionsAnimator = ValueAnimator.ofFloat(value, target).apply {
|
||||
duration = (250 * abs(target - value)).toLong()
|
||||
interpolator = if (target >= 1f) AccelerateInterpolator(2f) else DecelerateInterpolator(2f)
|
||||
addUpdateListener {
|
||||
val newValue = animatedValue as Float
|
||||
addUpdateListener { animator ->
|
||||
val newValue = animator.animatedValue as Float
|
||||
sectionsList.apply {
|
||||
val height = ((parent as View).height * newValue).toInt()
|
||||
val visible = height > 0
|
||||
if ((visibility == View.VISIBLE) != visible) {
|
||||
val h = ((parent as View).height * newValue).toInt()
|
||||
val visible = h > 0
|
||||
if ((isVisible) != visible) {
|
||||
visibility = if (visible) View.VISIBLE else View.GONE
|
||||
}
|
||||
if (layoutParams.height != height) {
|
||||
layoutParams.height = height
|
||||
if (layoutParams.height != h) {
|
||||
layoutParams.height = h
|
||||
requestLayout()
|
||||
}
|
||||
}
|
||||
@@ -464,30 +483,30 @@ class TabsFragment: ScreenFragment() {
|
||||
|
||||
private val pageChangeCallback = object: ViewPager2.OnPageChangeCallback() {
|
||||
override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {
|
||||
val layout = layout!!
|
||||
val fromSections = ProductsFragment.Source.values()[position].sections
|
||||
val l = layout!!
|
||||
val fromSections = ProductsFragment.Source.entries[position].sections
|
||||
val toSections = if (positionOffset <= 0f) fromSections else
|
||||
ProductsFragment.Source.values()[position + 1].sections
|
||||
ProductsFragment.Source.entries[position + 1].sections
|
||||
val offset = if (fromSections != toSections) {
|
||||
if (fromSections) 1f - positionOffset else positionOffset
|
||||
} else {
|
||||
if (fromSections) 1f else 0f
|
||||
}
|
||||
(layout.tabs.background as TabsBackgroundDrawable)
|
||||
.update(position + positionOffset, layout.tabs.childCount)
|
||||
assert(layout.sectionLayout.childCount == 1)
|
||||
val child = layout.sectionLayout.getChildAt(0)
|
||||
val height = child.layoutParams.height
|
||||
assert(height > 0)
|
||||
val currentHeight = (offset * height).roundToInt()
|
||||
if (layout.sectionLayout.layoutParams.height != currentHeight) {
|
||||
layout.sectionLayout.layoutParams.height = currentHeight
|
||||
layout.sectionLayout.requestLayout()
|
||||
(l.tabs.background as TabsBackgroundDrawable)
|
||||
.update(position + positionOffset, l.tabs.childCount)
|
||||
assert(l.sectionLayout.childCount == 1)
|
||||
val child = l.sectionLayout.getChildAt(0)
|
||||
val h = child.layoutParams.height
|
||||
assert(h > 0)
|
||||
val currentHeight = (offset * h).roundToInt()
|
||||
if (l.sectionLayout.layoutParams.height != currentHeight) {
|
||||
l.sectionLayout.layoutParams.height = currentHeight
|
||||
l.sectionLayout.requestLayout()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPageSelected(position: Int) {
|
||||
val source = ProductsFragment.Source.values()[position]
|
||||
val source = ProductsFragment.Source.entries[position]
|
||||
updateUpdateNotificationBlocker(source)
|
||||
sortOrderMenu!!.first.isVisible = source.order
|
||||
syncRepositoriesMenuItem!!.setShowAsActionFlags(if (!source.order ||
|
||||
@@ -499,7 +518,7 @@ class TabsFragment: ScreenFragment() {
|
||||
}
|
||||
|
||||
override fun onPageScrollStateChanged(state: Int) {
|
||||
val source = ProductsFragment.Source.values()[viewPager!!.currentItem]
|
||||
val source = ProductsFragment.Source.entries[viewPager!!.currentItem]
|
||||
layout!!.sectionChange.isEnabled = state != ViewPager2.SCROLL_STATE_DRAGGING && source.sections
|
||||
if (state == ViewPager2.SCROLL_STATE_IDLE) {
|
||||
// onPageSelected can be called earlier than fragments created
|
||||
@@ -509,7 +528,7 @@ class TabsFragment: ScreenFragment() {
|
||||
}
|
||||
|
||||
private class TabsBackgroundDrawable(context: Context, private val rtl: Boolean): Drawable() {
|
||||
private val height = context.resources.sizeScaled(2)
|
||||
private val h = context.resources.sizeScaled(2)
|
||||
private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = context.getColorFromAttr(android.R.attr.colorAccent).defaultColor
|
||||
}
|
||||
@@ -525,15 +544,15 @@ class TabsFragment: ScreenFragment() {
|
||||
|
||||
override fun draw(canvas: Canvas) {
|
||||
if (total > 0) {
|
||||
val bounds = bounds
|
||||
val width = bounds.width() / total.toFloat()
|
||||
val x = width * position
|
||||
val b = bounds
|
||||
val w = b.width() / total.toFloat()
|
||||
val x = w * position
|
||||
if (rtl) {
|
||||
canvas.drawRect(bounds.right - width - x, (bounds.bottom - height).toFloat(),
|
||||
bounds.right - x, bounds.bottom.toFloat(), paint)
|
||||
canvas.drawRect(b.right - w - x, (b.bottom - h).toFloat(),
|
||||
b.right - x, b.bottom.toFloat(), paint)
|
||||
} else {
|
||||
canvas.drawRect(bounds.left + x, (bounds.bottom - height).toFloat(),
|
||||
bounds.left + x + width, bounds.bottom.toFloat(), paint)
|
||||
canvas.drawRect(b.left + x, (b.bottom - h).toFloat(),
|
||||
b.left + x + w, b.bottom.toFloat(), paint)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -554,10 +573,10 @@ class TabsFragment: ScreenFragment() {
|
||||
|
||||
init {
|
||||
itemView as TextView
|
||||
itemView.gravity = Gravity.CENTER_VERTICAL
|
||||
(itemView as TextView).gravity = Gravity.CENTER_VERTICAL
|
||||
itemView.resources.sizeScaled(16).let { itemView.setPadding(it, 0, it, 0) }
|
||||
itemView.setTextColor(context.getColorFromAttr(android.R.attr.textColorPrimary))
|
||||
itemView.setTextSizeScaled(16)
|
||||
(itemView as TextView).setTextColor(context.getColorFromAttr(android.R.attr.textColorPrimary))
|
||||
(itemView as TextView).setTextSizeScaled(16)
|
||||
itemView.background = context.getDrawableFromAttr(android.R.attr.selectableItemBackground)
|
||||
itemView.layoutParams = RecyclerView.LayoutParams(RecyclerView.LayoutParams.MATCH_PARENT,
|
||||
itemView.resources.sizeScaled(48))
|
||||
@@ -570,10 +589,15 @@ class TabsFragment: ScreenFragment() {
|
||||
when {
|
||||
nextSection != null && currentSection.javaClass != nextSection.javaClass -> {
|
||||
val padding = context.resources.sizeScaled(16)
|
||||
configuration.set(true, false, padding, padding)
|
||||
configuration.set(
|
||||
needDivider = true,
|
||||
toTop = false,
|
||||
paddingStart = padding,
|
||||
paddingEnd = padding
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
configuration.set(false, false, 0, 0)
|
||||
configuration.set(needDivider = false, toTop = false, paddingStart = 0, paddingEnd = 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -587,7 +611,12 @@ class TabsFragment: ScreenFragment() {
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: ViewType): RecyclerView.ViewHolder {
|
||||
return SectionViewHolder(parent.context).apply {
|
||||
itemView.setOnClickListener { onClick(sections()[adapterPosition]) }
|
||||
itemView.setOnClickListener {
|
||||
val pos = bindingAdapterPosition
|
||||
if (pos != RecyclerView.NO_POSITION) {
|
||||
onClick(sections()[pos])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,9 +6,9 @@ import android.app.PendingIntent
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.view.ContextThemeWrapper
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.ServiceCompat
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import io.reactivex.rxjava3.disposables.Disposable
|
||||
@@ -29,6 +29,7 @@ import java.io.File
|
||||
import java.security.MessageDigest
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.math.*
|
||||
import androidx.core.net.toUri
|
||||
|
||||
class DownloadService: ConnectionService<DownloadService.Binder>() {
|
||||
companion object {
|
||||
@@ -47,14 +48,14 @@ class DownloadService: ConnectionService<DownloadService.Binder>() {
|
||||
action.startsWith("$ACTION_OPEN.") -> {
|
||||
val packageName = action.substring(ACTION_OPEN.length + 1)
|
||||
context.startActivity(Intent(context, MainActivity::class.java)
|
||||
.setAction(Intent.ACTION_VIEW).setData(Uri.parse("package:$packageName"))
|
||||
.setAction(Intent.ACTION_VIEW).setData("package:$packageName".toUri())
|
||||
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
|
||||
}
|
||||
action.startsWith("$ACTION_INSTALL.") -> {
|
||||
val packageName = action.substring(ACTION_INSTALL.length + 1)
|
||||
val cacheFileName = intent.getStringExtra(EXTRA_CACHE_FILE_NAME)
|
||||
context.startActivity(Intent(context, MainActivity::class.java)
|
||||
.setAction(MainActivity.ACTION_INSTALL).setData(Uri.parse("package:$packageName"))
|
||||
.setAction(MainActivity.ACTION_INSTALL).setData("package:$packageName".toUri())
|
||||
.putExtra(MainActivity.EXTRA_CACHE_FILE_NAME, cacheFileName)
|
||||
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
|
||||
}
|
||||
@@ -189,8 +190,10 @@ class DownloadService: ConnectionService<DownloadService.Binder>() {
|
||||
.setSmallIcon(android.R.drawable.stat_sys_warning)
|
||||
.setColor(ContextThemeWrapper(this, R.style.Theme_Main_Light)
|
||||
.getColorFromAttr(android.R.attr.colorAccent).defaultColor)
|
||||
.setContentIntent(PendingIntent.getBroadcast(this, 0, Intent(this, Receiver::class.java)
|
||||
.setAction("$ACTION_OPEN.${task.packageName}"), PendingIntent.FLAG_UPDATE_CURRENT))
|
||||
.setContentIntent(PendingIntent.getActivity(this, 0, Intent(this, MainActivity::class.java)
|
||||
.setAction(Intent.ACTION_VIEW).setData("package:${task.packageName}".toUri())
|
||||
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK), PendingIntent.FLAG_UPDATE_CURRENT or
|
||||
Android.PendingIntent.FLAG_IMMUTABLE))
|
||||
.apply {
|
||||
when (errorType) {
|
||||
is ErrorType.Network -> {
|
||||
@@ -223,9 +226,11 @@ class DownloadService: ConnectionService<DownloadService.Binder>() {
|
||||
.setSmallIcon(android.R.drawable.stat_sys_download_done)
|
||||
.setColor(ContextThemeWrapper(this, R.style.Theme_Main_Light)
|
||||
.getColorFromAttr(android.R.attr.colorAccent).defaultColor)
|
||||
.setContentIntent(PendingIntent.getBroadcast(this, 0, Intent(this, Receiver::class.java)
|
||||
.setAction("$ACTION_INSTALL.${task.packageName}")
|
||||
.putExtra(EXTRA_CACHE_FILE_NAME, task.release.cacheFileName), PendingIntent.FLAG_UPDATE_CURRENT))
|
||||
.setContentIntent(PendingIntent.getActivity(this, 0, Intent(this, MainActivity::class.java)
|
||||
.setAction(MainActivity.ACTION_INSTALL).setData("package:${task.packageName}".toUri())
|
||||
.putExtra(MainActivity.EXTRA_CACHE_FILE_NAME, task.release.cacheFileName)
|
||||
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK), PendingIntent.FLAG_UPDATE_CURRENT or
|
||||
Android.PendingIntent.FLAG_IMMUTABLE))
|
||||
.setContentTitle(getString(R.string.downloaded_FORMAT, task.name))
|
||||
.setContentText(getString(R.string.tap_to_install_DESC))
|
||||
.build())
|
||||
@@ -243,12 +248,12 @@ class DownloadService: ConnectionService<DownloadService.Binder>() {
|
||||
val hash = try {
|
||||
val hashType = task.release.hashType.nullIfEmpty() ?: "SHA256"
|
||||
val digest = MessageDigest.getInstance(hashType)
|
||||
file.inputStream().use {
|
||||
file.inputStream().use { it ->
|
||||
val bytes = ByteArray(8 * 1024)
|
||||
generateSequence { it.read(bytes) }.takeWhile { it >= 0 }.forEach { digest.update(bytes, 0, it) }
|
||||
digest.digest().hex()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
} catch (_: Exception) {
|
||||
""
|
||||
}
|
||||
return if (hash.isEmpty() || hash != task.release.hash) {
|
||||
@@ -287,7 +292,8 @@ class DownloadService: ConnectionService<DownloadService.Binder>() {
|
||||
.setColor(ContextThemeWrapper(this, R.style.Theme_Main_Light)
|
||||
.getColorFromAttr(android.R.attr.colorAccent).defaultColor)
|
||||
.addAction(0, getString(R.string.cancel), PendingIntent.getService(this, 0,
|
||||
Intent(this, this::class.java).setAction(ACTION_CANCEL), PendingIntent.FLAG_UPDATE_CURRENT)) }
|
||||
Intent(this, this::class.java).setAction(ACTION_CANCEL), PendingIntent.FLAG_UPDATE_CURRENT or
|
||||
Android.PendingIntent.FLAG_IMMUTABLE)) }
|
||||
|
||||
private fun publishForegroundState(force: Boolean, state: State) {
|
||||
if (force || currentTask != null) {
|
||||
@@ -313,7 +319,7 @@ class DownloadService: ConnectionService<DownloadService.Binder>() {
|
||||
throw IllegalStateException()
|
||||
}
|
||||
}::class
|
||||
}.build())
|
||||
}.build(), Android.ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC)
|
||||
stateSubject.onNext(state)
|
||||
}
|
||||
}
|
||||
@@ -361,7 +367,7 @@ class DownloadService: ConnectionService<DownloadService.Binder>() {
|
||||
currentTask = CurrentTask(task, disposable, initialState)
|
||||
} else if (started) {
|
||||
started = false
|
||||
stopForeground(true)
|
||||
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
|
||||
stopSelf()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import android.text.SpannableStringBuilder
|
||||
import android.text.style.ForegroundColorSpan
|
||||
import android.view.ContextThemeWrapper
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.ServiceCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
@@ -123,10 +124,6 @@ class SyncService: ConnectionService<SyncService.Binder>() {
|
||||
return true
|
||||
}
|
||||
|
||||
fun isCurrentlySyncing(repositoryId: Long): Boolean {
|
||||
return currentTask?.task?.repositoryId == repositoryId
|
||||
}
|
||||
|
||||
fun deleteRepository(repositoryId: Long): Boolean {
|
||||
val repository = Database.RepositoryAdapter.get(repositoryId)
|
||||
return repository != null && run {
|
||||
@@ -220,7 +217,8 @@ class SyncService: ConnectionService<SyncService.Binder>() {
|
||||
.setColor(ContextThemeWrapper(this, R.style.Theme_Main_Light)
|
||||
.getColorFromAttr(android.R.attr.colorAccent).defaultColor)
|
||||
.addAction(0, getString(R.string.cancel), PendingIntent.getService(this, 0,
|
||||
Intent(this, this::class.java).setAction(ACTION_CANCEL), PendingIntent.FLAG_UPDATE_CURRENT)) }
|
||||
Intent(this, this::class.java).setAction(ACTION_CANCEL), PendingIntent.FLAG_UPDATE_CURRENT or
|
||||
Android.PendingIntent.FLAG_IMMUTABLE)) }
|
||||
|
||||
private fun publishForegroundState(force: Boolean, state: State) {
|
||||
if (force || currentTask?.lastState != state) {
|
||||
@@ -267,7 +265,7 @@ class SyncService: ConnectionService<SyncService.Binder>() {
|
||||
setProgress(0, 0, true)
|
||||
}
|
||||
}::class
|
||||
}.build())
|
||||
}.build(), Android.ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -294,7 +292,7 @@ class SyncService: ConnectionService<SyncService.Binder>() {
|
||||
val unstable = Preferences[Preferences.Key.UpdateUnstable]
|
||||
lateinit var disposable: Disposable
|
||||
disposable = RepositoryUpdater
|
||||
.update(repository, unstable) { stage, progress, total ->
|
||||
.update(this, repository, unstable) { stage, progress, total ->
|
||||
if (!disposable.isDisposed) {
|
||||
stateSubject.onNext(State.Syncing(repository.name, stage, progress, total))
|
||||
}
|
||||
@@ -315,8 +313,16 @@ class SyncService: ConnectionService<SyncService.Binder>() {
|
||||
} else if (started != Started.NO) {
|
||||
if (hasUpdates && Preferences[Preferences.Key.UpdateNotify]) {
|
||||
val disposable = RxUtils
|
||||
.querySingle { Database.ProductAdapter
|
||||
.query(true, true, "", ProductItem.Section.All, ProductItem.Order.NAME, it)
|
||||
.querySingle { it ->
|
||||
Database.ProductAdapter
|
||||
.query(
|
||||
installed = true,
|
||||
updates = true,
|
||||
searchQuery = "",
|
||||
section = ProductItem.Section.All,
|
||||
order = ProductItem.Order.NAME,
|
||||
signal = it
|
||||
)
|
||||
.use { it.asSequence().map(Database.ProductAdapter::transformItem).toList() } }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
@@ -335,7 +341,7 @@ class SyncService: ConnectionService<SyncService.Binder>() {
|
||||
val needStop = started == Started.MANUAL
|
||||
started = Started.NO
|
||||
if (needStop) {
|
||||
stopForeground(true)
|
||||
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
|
||||
stopSelf()
|
||||
}
|
||||
}
|
||||
@@ -355,7 +361,8 @@ class SyncService: ConnectionService<SyncService.Binder>() {
|
||||
.setColor(ContextThemeWrapper(this, R.style.Theme_Main_Light)
|
||||
.getColorFromAttr(android.R.attr.colorAccent).defaultColor)
|
||||
.setContentIntent(PendingIntent.getActivity(this, 0, Intent(this, MainActivity::class.java)
|
||||
.setAction(MainActivity.ACTION_UPDATES), PendingIntent.FLAG_UPDATE_CURRENT))
|
||||
.setAction(MainActivity.ACTION_UPDATES), PendingIntent.FLAG_UPDATE_CURRENT or
|
||||
Android.PendingIntent.FLAG_IMMUTABLE))
|
||||
.setStyle(NotificationCompat.InboxStyle().applyHack {
|
||||
for (productItem in productItems.take(maxUpdates)) {
|
||||
val builder = SpannableStringBuilder(productItem.name)
|
||||
|
||||
@@ -40,7 +40,7 @@ object PackageItemResolver {
|
||||
@Suppress("DEPRECATION")
|
||||
resources.updateConfiguration(context.resources.configuration, null)
|
||||
resources
|
||||
} catch (e: Exception) {
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
resources?.let { localCache.resources[packageName] = it }
|
||||
|
||||
@@ -21,7 +21,7 @@ object RxUtils {
|
||||
}
|
||||
}
|
||||
|
||||
private fun <T, R> managedSingle(create: () -> T, cancel: (T) -> Unit, execute: (T) -> R): Single<R> {
|
||||
private fun <T, R : Any> managedSingle(create: () -> T, cancel: (T) -> Unit, execute: (T) -> R): Single<R> {
|
||||
return Single.create {
|
||||
val task = create()
|
||||
val thread = Thread.currentThread()
|
||||
@@ -52,27 +52,27 @@ object RxUtils {
|
||||
}
|
||||
}
|
||||
|
||||
fun <R> managedSingle(execute: () -> R): Single<R> {
|
||||
return managedSingle({ Unit }, { }, { execute() })
|
||||
fun <R : Any> managedSingle(execute: () -> R): Single<R> {
|
||||
return managedSingle({ }, { }, { execute() })
|
||||
}
|
||||
|
||||
fun callSingle(create: () -> Call): Single<Response> {
|
||||
return managedSingle(create, Call::cancel, Call::execute)
|
||||
}
|
||||
|
||||
fun <T> querySingle(query: (CancellationSignal) -> T): Single<T> {
|
||||
fun <T : Any> querySingle(query: (CancellationSignal?) -> T): Single<T> {
|
||||
return Single.create {
|
||||
val cancellationSignal = CancellationSignal()
|
||||
it.setCancellable {
|
||||
try {
|
||||
cancellationSignal.cancel()
|
||||
} catch (e: OperationCanceledException) {
|
||||
} catch (_: OperationCanceledException) {
|
||||
// Do nothing
|
||||
}
|
||||
}
|
||||
val result = try {
|
||||
query(cancellationSignal)
|
||||
} catch (e: OperationCanceledException) {
|
||||
} catch (_: OperationCanceledException) {
|
||||
null
|
||||
}
|
||||
if (result != null) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package nya.kitsunyan.foxydroid.utility
|
||||
|
||||
import android.animation.ValueAnimator
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.pm.Signature
|
||||
import android.content.res.Configuration
|
||||
@@ -35,14 +36,14 @@ object Utils {
|
||||
return drawable
|
||||
}
|
||||
|
||||
fun calculateHash(signature: Signature): String? {
|
||||
fun calculateHash(signature: Signature): String {
|
||||
return MessageDigest.getInstance("MD5").digest(signature.toCharsString().toByteArray()).hex()
|
||||
}
|
||||
|
||||
fun calculateFingerprint(certificate: Certificate): String {
|
||||
val encoded = try {
|
||||
certificate.encoded
|
||||
} catch (e: CertificateEncodingException) {
|
||||
} catch (_: CertificateEncodingException) {
|
||||
null
|
||||
}
|
||||
return encoded?.let(::calculateFingerprint).orEmpty()
|
||||
@@ -66,6 +67,7 @@ object Utils {
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("SuspiciousIndentation")
|
||||
fun configureLocale(context: Context): Context {
|
||||
val supportedLanguages = BuildConfig.LANGUAGES.toSet()
|
||||
val configuration = context.resources.configuration
|
||||
@@ -78,15 +80,10 @@ object Utils {
|
||||
}
|
||||
val compatibleLocales = currentLocales
|
||||
.filter { it.language in supportedLanguages }
|
||||
.let { if (it.isEmpty()) listOf(Locale.US) else it }
|
||||
.let { it.ifEmpty { listOf(Locale.US) } }
|
||||
Locale.setDefault(compatibleLocales.first())
|
||||
val newConfiguration = Configuration(configuration)
|
||||
if (Android.sdk(24)) {
|
||||
newConfiguration.setLocales(LocaleList(*compatibleLocales.toTypedArray()))
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
newConfiguration.locale = compatibleLocales.first()
|
||||
}
|
||||
return context.createConfigurationContext(newConfiguration)
|
||||
}
|
||||
|
||||
|
||||
@@ -34,8 +34,7 @@ val PackageInfo.singleSignature: Signature?
|
||||
if (signingInfo?.hasMultipleSigners() == false) signingInfo.apkContentsSigners
|
||||
?.let { if (it.size == 1) it[0] else null } else null
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
signatures?.let { if (it.size == 1) it[0] else null }
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,11 +54,20 @@ object Android {
|
||||
return Build.VERSION.SDK_INT >= sdk
|
||||
}
|
||||
|
||||
object PendingIntent {
|
||||
val FLAG_IMMUTABLE: Int
|
||||
get() = if (sdk(23)) android.app.PendingIntent.FLAG_IMMUTABLE else 0
|
||||
|
||||
}
|
||||
|
||||
object PackageManager {
|
||||
// GET_SIGNATURES should always present for getPackageArchiveInfo
|
||||
val signaturesFlag: Int
|
||||
get() = (if (sdk(28)) android.content.pm.PackageManager.GET_SIGNING_CERTIFICATES else 0) or
|
||||
@Suppress("DEPRECATION") android.content.pm.PackageManager.GET_SIGNATURES
|
||||
get() = (if (sdk(28)) android.content.pm.PackageManager.GET_SIGNING_CERTIFICATES else 0)
|
||||
}
|
||||
|
||||
object ServiceInfo {
|
||||
val FOREGROUND_SERVICE_TYPE_DATA_SYNC: Int
|
||||
get() = if (sdk(29)) android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC else 0
|
||||
}
|
||||
|
||||
object Device {
|
||||
@@ -68,7 +76,7 @@ object Android {
|
||||
return try {
|
||||
Class.forName("com.huawei.android.os.BuildEx")
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
} catch (_: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ inline fun JsonParser.forEachKey(callback: JsonParser.(KeyToken) -> Unit) {
|
||||
while (true) {
|
||||
val token = nextToken()
|
||||
if (token == JsonToken.FIELD_NAME) {
|
||||
passKey = currentName
|
||||
passKey = currentName()
|
||||
passToken = nextToken()
|
||||
callback(keyToken)
|
||||
} else if (token == JsonToken.END_OBJECT) {
|
||||
|
||||
@@ -31,7 +31,7 @@ fun Context.getDrawableCompat(resId: Int): Drawable {
|
||||
val drawable = if (!Android.sdk(24)) {
|
||||
val fileName = TypedValue().apply { resources.getValue(resId, this, true) }.string
|
||||
if (fileName.endsWith(".xml")) {
|
||||
resources.getXml(resId).use {
|
||||
resources.getXml(resId).use { it ->
|
||||
val eventType = generateSequence { it.next() }
|
||||
.find { it == XmlPullParser.START_TAG || it == XmlPullParser.END_DOCUMENT }
|
||||
if (eventType == XmlPullParser.START_TAG) {
|
||||
@@ -77,8 +77,7 @@ fun Resources.sizeScaled(size: Int): Int {
|
||||
}
|
||||
|
||||
fun TextView.setTextSizeScaled(size: Int) {
|
||||
val realSize = (size * resources.displayMetrics.scaledDensity).roundToInt()
|
||||
setTextSize(TypedValue.COMPLEX_UNIT_PX, realSize.toFloat())
|
||||
setTextSize(TypedValue.COMPLEX_UNIT_SP, size.toFloat())
|
||||
}
|
||||
|
||||
fun ViewGroup.inflate(layoutResId: Int): View {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package nya.kitsunyan.foxydroid.widget
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.database.Cursor
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
||||
abstract class CursorRecyclerAdapter<VT: Enum<VT>, VH: RecyclerView.ViewHolder>: EnumRecyclerAdapter<VT, VH>() {
|
||||
@@ -13,13 +15,58 @@ abstract class CursorRecyclerAdapter<VT: Enum<VT>, VH: RecyclerView.ViewHolder>:
|
||||
var cursor: Cursor? = null
|
||||
set(value) {
|
||||
if (field != value) {
|
||||
field?.close()
|
||||
val oldCursor = field
|
||||
field = value
|
||||
rowIdIndex = value?.getColumnIndexOrThrow("_id") ?: 0
|
||||
onCursorChanged(oldCursor, value)
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
protected open fun onCursorChanged(oldCursor: Cursor?, newCursor: Cursor?) {
|
||||
val oldSize = oldCursor?.count ?: 0
|
||||
val newSize = newCursor?.count ?: 0
|
||||
|
||||
if (oldCursor == null || newCursor == null) {
|
||||
if (oldSize > 0) notifyItemRangeRemoved(0, oldSize)
|
||||
if (newSize > 0) notifyItemRangeInserted(0, newSize)
|
||||
return
|
||||
}
|
||||
|
||||
// Further reduced threshold to 100 for DiffUtil to avoid any noticeable frame drops on the main thread.
|
||||
// JSON parsing and DB access during diffing are slow.
|
||||
if (oldSize > 100 || newSize > 100) {
|
||||
notifyDataSetChanged()
|
||||
return
|
||||
}
|
||||
|
||||
val oldIdIndex = oldCursor.getColumnIndexOrThrow("_id")
|
||||
val newIdIndex = newCursor.getColumnIndexOrThrow("_id")
|
||||
|
||||
try {
|
||||
val diffResult = DiffUtil.calculateDiff(object : DiffUtil.Callback() {
|
||||
override fun getOldListSize(): Int = oldSize
|
||||
override fun getNewListSize(): Int = newSize
|
||||
|
||||
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
||||
if (!oldCursor.moveToPosition(oldItemPosition) || !newCursor.moveToPosition(newItemPosition)) return false
|
||||
return oldCursor.getLong(oldIdIndex) == newCursor.getLong(newIdIndex)
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
||||
if (!oldCursor.moveToPosition(oldItemPosition) || !newCursor.moveToPosition(newItemPosition)) return false
|
||||
return areContentsTheSame(oldCursor, newCursor)
|
||||
}
|
||||
})
|
||||
diffResult.dispatchUpdatesTo(this)
|
||||
} catch (_: Exception) {
|
||||
// Fallback in case of cursor issues during diffing
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun areContentsTheSame(oldCursor: Cursor, newCursor: Cursor): Boolean = false
|
||||
|
||||
final override fun setHasStableIds(hasStableIds: Boolean) {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package nya.kitsunyan.foxydroid.widget
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Rect
|
||||
import android.os.SystemClock
|
||||
@@ -11,7 +10,6 @@ import androidx.recyclerview.widget.RecyclerView
|
||||
import nya.kitsunyan.foxydroid.utility.extension.resources.*
|
||||
import kotlin.math.*
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
class RecyclerFastScroller(private val recyclerView: RecyclerView) {
|
||||
companion object {
|
||||
private const val TRANSITION_IN = 100L
|
||||
|
||||
|
Before Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 8.6 KiB |
|
Before Width: | Height: | Size: 12 KiB |
@@ -1,8 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<background android:drawable="@color/primary_light" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
|
Before Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 5.1 KiB |
|
After Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 7.9 KiB |
|
After Width: | Height: | Size: 9.3 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 7.1 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 13 KiB |
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#00E4FF</color>
|
||||
</resources>
|
||||
@@ -163,5 +163,6 @@
|
||||
<string name="versions">Versions</string>
|
||||
<string name="waiting_to_start_download">Waiting to start download</string>
|
||||
<string name="website">Website</string>
|
||||
<string name="address_redirect_FORMAT">The repository address was redirected to %s. Do you want to use it instead?</string>
|
||||
|
||||
</resources>
|
||||
|
||||