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