├── .github └── workflows │ └── build.yml ├── .gitignore ├── LICENSE ├── README.md ├── app ├── build.gradle.kts ├── proguard-rules.pro └── src │ ├── androidTest │ └── kotlin │ │ └── com │ │ └── apkupdater │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── kotlin │ │ ├── androidx │ │ │ └── compose │ │ │ │ └── material3 │ │ │ │ └── pullrefresh │ │ │ │ ├── PullRefresh.kt │ │ │ │ ├── PullRefreshIndicator.kt │ │ │ │ ├── PullRefreshIndicatorTransform.kt │ │ │ │ └── PullRefreshState.kt │ │ └── com │ │ │ └── apkupdater │ │ │ ├── application │ │ │ └── App.kt │ │ │ ├── data │ │ │ ├── apkmirror │ │ │ │ ├── AppExistsRequest.kt │ │ │ │ ├── AppExistsResponse.kt │ │ │ │ ├── AppExistsResponseApk.kt │ │ │ │ ├── AppExistsResponseApp.kt │ │ │ │ ├── AppExistsResponseData.kt │ │ │ │ ├── AppExistsResponseDeveloper.kt │ │ │ │ ├── AppExistsResponseHeaders.kt │ │ │ │ └── AppExistsResponseRelease.kt │ │ │ ├── apkpure │ │ │ │ ├── AppInfoForUpdate.kt │ │ │ │ ├── AppUpdateResponse.kt │ │ │ │ ├── AppUpdateResponseAsset.kt │ │ │ │ ├── AppUpdateResponseIcon.kt │ │ │ │ ├── AppUpdateResponseIconData.kt │ │ │ │ ├── DeviceHeader.kt │ │ │ │ ├── DeviceInfo.kt │ │ │ │ ├── GetAppUpdate.kt │ │ │ │ ├── GetAppUpdateResponse.kt │ │ │ │ ├── SearchResponse.kt │ │ │ │ ├── SearchResponseApp.kt │ │ │ │ ├── SearchResponseData.kt │ │ │ │ └── SearchResponseItem.kt │ │ │ ├── aptoide │ │ │ │ ├── ApksData.kt │ │ │ │ ├── App.kt │ │ │ │ ├── DataList.kt │ │ │ │ ├── File.kt │ │ │ │ ├── ListAppUpdatesResponse.kt │ │ │ │ ├── ListAppsUpdatesRequest.kt │ │ │ │ ├── ListSearchAppsRequest.kt │ │ │ │ ├── ListSearchAppsResponse.kt │ │ │ │ └── Store.kt │ │ │ ├── fdroid │ │ │ │ ├── FdroidApp.kt │ │ │ │ ├── FdroidData.kt │ │ │ │ ├── FdroidLocalized.kt │ │ │ │ ├── FdroidPackage.kt │ │ │ │ └── FdroidUpdate.kt │ │ │ ├── github │ │ │ │ ├── GitHubApp.kt │ │ │ │ ├── GitHubAuthor.kt │ │ │ │ ├── GitHubRelease.kt │ │ │ │ └── GitHubReleaseAsset.kt │ │ │ ├── gitlab │ │ │ │ ├── GitLabApp.kt │ │ │ │ ├── GitLabAsset.kt │ │ │ │ ├── GitLabAssets.kt │ │ │ │ ├── GitLabAuthor.kt │ │ │ │ ├── GitLabLink.kt │ │ │ │ └── GitLabRelease.kt │ │ │ ├── snack │ │ │ │ └── TextSnack.kt │ │ │ └── ui │ │ │ │ ├── AppInstallProgress.kt │ │ │ │ ├── AppInstallStatus.kt │ │ │ │ ├── AppInstalled.kt │ │ │ │ ├── AppUpdate.kt │ │ │ │ ├── AppsUiState.kt │ │ │ │ ├── Link.kt │ │ │ │ ├── Screen.kt │ │ │ │ ├── SearchUiState.kt │ │ │ │ ├── SettingsUiState.kt │ │ │ │ ├── Source.kt │ │ │ │ └── UpdatesUiState.kt │ │ │ ├── di │ │ │ └── MainModule.kt │ │ │ ├── prefs │ │ │ └── Prefs.kt │ │ │ ├── repository │ │ │ ├── ApkMirrorRepository.kt │ │ │ ├── ApkPureRepository.kt │ │ │ ├── AppsRepository.kt │ │ │ ├── AptoideRepository.kt │ │ │ ├── FdroidRepository.kt │ │ │ ├── GitHubRepository.kt │ │ │ ├── GitLabRepository.kt │ │ │ ├── PlayRepository.kt │ │ │ ├── SearchRepository.kt │ │ │ └── UpdatesRepository.kt │ │ │ ├── service │ │ │ ├── ApkMirrorService.kt │ │ │ ├── ApkPureService.kt │ │ │ ├── AptoideService.kt │ │ │ ├── FdroidService.kt │ │ │ ├── GitHubService.kt │ │ │ └── GitLabService.kt │ │ │ ├── transform │ │ │ └── Transforms.kt │ │ │ ├── ui │ │ │ ├── activity │ │ │ │ └── MainActivity.kt │ │ │ ├── component │ │ │ │ ├── Grid.kt │ │ │ │ ├── Icons.kt │ │ │ │ ├── Image.kt │ │ │ │ ├── Modifier.kt │ │ │ │ ├── Settings.kt │ │ │ │ ├── Text.kt │ │ │ │ ├── TvComponents.kt │ │ │ │ └── UiComponents.kt │ │ │ ├── screen │ │ │ │ ├── AppsScreen.kt │ │ │ │ ├── MainScreen.kt │ │ │ │ ├── SearchScreen.kt │ │ │ │ ├── SettingsScreen.kt │ │ │ │ └── UpdatesScreen.kt │ │ │ └── theme │ │ │ │ └── Theme.kt │ │ │ ├── util │ │ │ ├── Badger.kt │ │ │ ├── Clipboard.kt │ │ │ ├── Downloader.kt │ │ │ ├── Extensions.kt │ │ │ ├── InstallLog.kt │ │ │ ├── SessionInstaller.kt │ │ │ ├── SnackBar.kt │ │ │ ├── Stringer.kt │ │ │ ├── Themer.kt │ │ │ ├── UpdatesNotification.kt │ │ │ └── play │ │ │ │ ├── EglExtensionProvider.kt │ │ │ │ ├── IProxyHttpClient.kt │ │ │ │ ├── NativeDeviceInfoProvider.kt │ │ │ │ ├── PlayHttpClient.kt │ │ │ │ └── ProxyInfo.kt │ │ │ ├── viewmodel │ │ │ ├── AppsViewModel.kt │ │ │ ├── InstallViewModel.kt │ │ │ ├── MainViewModel.kt │ │ │ ├── SearchViewModel.kt │ │ │ ├── SettingsViewModel.kt │ │ │ └── UpdatesViewModel.kt │ │ │ └── worker │ │ │ └── UpdatesWorker.kt │ └── res │ │ ├── drawable │ │ ├── banner.png │ │ ├── ic_alarm.xml │ │ ├── ic_alpha.xml │ │ ├── ic_androidtv.xml │ │ ├── ic_animation.xml │ │ ├── ic_apkmirror.png │ │ ├── ic_apkpure.png │ │ ├── ic_appstore.xml │ │ ├── ic_appstore_off.xml │ │ ├── ic_aptoide.png │ │ ├── ic_beta.xml │ │ ├── ic_copy.xml │ │ ├── ic_disabled.xml │ │ ├── ic_disabled_off.xml │ │ ├── ic_empty.xml │ │ ├── ic_fdroid.xml │ │ ├── ic_frequency.xml │ │ ├── ic_github.xml │ │ ├── ic_gitlab.xml │ │ ├── ic_hour.xml │ │ ├── ic_info.xml │ │ ├── ic_install.xml │ │ ├── ic_izzy.png │ │ ├── ic_landscape.xml │ │ ├── ic_launcher_background.xml │ │ ├── ic_launcher_foreground.xml │ │ ├── ic_play.xml │ │ ├── ic_portrait.xml │ │ ├── ic_pre_release.xml │ │ ├── ic_refresh.xml │ │ ├── ic_root.xml │ │ ├── ic_safe.xml │ │ ├── ic_system.xml │ │ ├── ic_system_off.xml │ │ ├── ic_theme.xml │ │ ├── ic_visible.xml │ │ └── ic_visible_off.xml │ │ ├── mipmap-anydpi-v26 │ │ └── ic_launcher.xml │ │ ├── mipmap-anydpi-v33 │ │ └── ic_launcher.xml │ │ ├── mipmap-hdpi │ │ └── ic_launcher.png │ │ ├── mipmap-mdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxxhdpi │ │ └── ic_launcher.png │ │ ├── values-alb │ │ └── strings.xml │ │ ├── values-ar │ │ └── strings.xml │ │ ├── values-de │ │ └── strings.xml │ │ ├── values-es │ │ └── strings.xml │ │ ├── values-he │ │ └── strings.xml │ │ ├── values-hu │ │ └── strings.xml │ │ ├── values-in │ │ └── strings.xml │ │ ├── values-it │ │ └── strings.xml │ │ ├── values-jp │ │ └── strings.xml │ │ ├── values-ko │ │ └── strings.xml │ │ ├── values-ms │ │ └── strings.xml │ │ ├── values-my-rMM │ │ └── strings.xml │ │ ├── values-nl │ │ └── strings.xml │ │ ├── values-pt │ │ └── strings.xml │ │ ├── values-ro │ │ └── strings.xml │ │ ├── values-ru │ │ └── strings.xml │ │ ├── values-ta │ │ └── strings.xml │ │ ├── values-tk │ │ └── strings.xml │ │ ├── values-tr │ │ └── strings.xml │ │ ├── values-zh-rCN │ │ └── strings.xml │ │ ├── values-zh-rTW │ │ └── strings.xml │ │ ├── values │ │ └── strings.xml │ │ └── xml │ │ ├── backup_rules.xml │ │ └── data_extraction_rules.xml │ └── test │ └── kotlin │ └── com │ └── apkupdater │ └── ExampleUnitTest.kt ├── build.gradle.kts ├── fastlane └── metadata │ └── android │ ├── de │ └── short_description.txt │ └── en-US │ ├── full_description.txt │ ├── images │ ├── icon.png │ └── phoneScreenshots │ │ ├── 01.png │ │ ├── 02.png │ │ ├── 03.png │ │ ├── 04.png │ │ ├── 05.png │ │ └── 06.png │ └── short_description.txt ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle.kts /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Android Build 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v4 10 | 11 | - name: Setup Java 17 12 | uses: actions/setup-java@v4 13 | with: 14 | distribution: 'zulu' 15 | java-version: '17' 16 | 17 | - name: Decode Keystore 18 | env: 19 | KEYSTORE: ${{ secrets.KEYSTORE }} 20 | run: echo $KEYSTORE | base64 --decode > ./app/ci.ks 21 | 22 | - name: Decode Properties 23 | env: 24 | PROPERTIES: ${{ secrets.PROPERTIES }} 25 | run: echo $PROPERTIES | base64 --decode > local.properties 26 | 27 | - name: Build with Gradle 28 | env: 29 | BUILD_TAG: ".ci" 30 | BUILD_NUMBER: ${{ github.run_number }} 31 | run: ./gradlew build 32 | 33 | - name: Get current branch name 34 | run: echo "BRANCH=$(git rev-parse --abbrev-ref HEAD)" >> $GITHUB_ENV 35 | 36 | - name: Create Release 37 | uses: softprops/action-gh-release@v2 38 | if: github.ref == 'refs/heads/3.x' 39 | with: 40 | tag_name: 0.0.${{ github.run_number }}-ci 41 | name: CI-Release-${{ env.BRANCH }}-${{ github.run_number }} 42 | files: ./app/build/outputs/apk/release/com.apkupdater.ci-release.apk 43 | body: ${{ github.event.head_commit.message }} 44 | prerelease: true -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | /local.properties 3 | .DS_Store 4 | /build 5 | /captures 6 | .externalNativeBuild 7 | .cxx 8 | /.idea/ 9 | /app/build/ 10 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | -keepattributes Signature 2 | 3 | # Gson 4 | -keep class com.google.gson.reflect.TypeToken { *; } 5 | -keep class * extends com.google.gson.reflect.TypeToken 6 | 7 | # OkHttp 8 | -keep,allowobfuscation,allowshrinking class okhttp3.RequestBody 9 | -keep,allowobfuscation,allowshrinking class okhttp3.ResponseBody 10 | 11 | #Retrofit 12 | -keep,allowobfuscation,allowshrinking class kotlin.coroutines.Continuation 13 | -keep,allowobfuscation,allowshrinking interface retrofit2.Call 14 | -keep,allowobfuscation,allowshrinking class retrofit2.Response 15 | -if interface * { @retrofit2.http.* public *** *(...); } 16 | -keep,allowoptimization,allowshrinking,allowobfuscation class <3> 17 | 18 | # Models 19 | -keep class com.apkupdater.data.** { *; } 20 | -------------------------------------------------------------------------------- /app/src/androidTest/kotlin/com/apkupdater/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.apkupdater 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.apkupdater", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 27 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /app/src/main/kotlin/androidx/compose/material3/pullrefresh/PullRefreshIndicatorTransform.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package androidx.compose.material3.pullrefresh 18 | 19 | import androidx.compose.animation.core.LinearOutSlowInEasing 20 | import androidx.compose.ui.Modifier 21 | import androidx.compose.ui.draw.drawWithContent 22 | import androidx.compose.ui.graphics.drawscope.clipRect 23 | import androidx.compose.ui.graphics.graphicsLayer 24 | import androidx.compose.ui.platform.debugInspectorInfo 25 | import androidx.compose.ui.platform.inspectable 26 | 27 | /** 28 | * A modifier for translating the position and scaling the size of a pull-to-refresh indicator 29 | * based on the given [PullRefreshState]. 30 | * 31 | * @sample androidx.compose.material.samples.PullRefreshIndicatorTransformSample 32 | * 33 | * @param state The [PullRefreshState] which determines the position of the indicator. 34 | * @param scale A boolean controlling whether the indicator's size scales with pull progress or not. 35 | */ 36 | // TODO: Consider whether the state parameter should be replaced with lambdas. 37 | fun Modifier.pullRefreshIndicatorTransform( 38 | state: PullRefreshState, 39 | scale: Boolean = false, 40 | ) = inspectable(inspectorInfo = debugInspectorInfo { 41 | name = "pullRefreshIndicatorTransform" 42 | properties["state"] = state 43 | properties["scale"] = scale 44 | }) { 45 | Modifier 46 | // Essentially we only want to clip the at the top, so the indicator will not appear when 47 | // the position is 0. It is preferable to clip the indicator as opposed to the layout that 48 | // contains the indicator, as this would also end up clipping shadows drawn by items in a 49 | // list for example - so we leave the clipping to the scrolling container. We use MAX_VALUE 50 | // for the other dimensions to allow for more room for elevation / arbitrary indicators - we 51 | // only ever really want to clip at the top edge. 52 | .drawWithContent { 53 | clipRect( 54 | top = 0f, 55 | left = -Float.MAX_VALUE, 56 | right = Float.MAX_VALUE, 57 | bottom = Float.MAX_VALUE 58 | ) { 59 | this@drawWithContent.drawContent() 60 | } 61 | } 62 | .graphicsLayer { 63 | translationY = state.position - size.height 64 | 65 | if (scale && !state.refreshing) { 66 | val scaleFraction = LinearOutSlowInEasing 67 | .transform(state.position / state.threshold) 68 | .coerceIn(0f, 1f) 69 | scaleX = scaleFraction 70 | scaleY = scaleFraction 71 | } 72 | } 73 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/apkupdater/application/App.kt: -------------------------------------------------------------------------------- 1 | package com.apkupdater.application 2 | 3 | import android.app.Application 4 | import coil.ImageLoader 5 | import coil.ImageLoaderFactory 6 | import com.apkupdater.di.mainModule 7 | import okhttp3.OkHttpClient 8 | import org.koin.android.ext.koin.androidContext 9 | import org.koin.android.ext.koin.androidLogger 10 | import org.koin.core.component.KoinComponent 11 | import org.koin.core.component.get 12 | import org.koin.core.context.startKoin 13 | 14 | class App : Application(), ImageLoaderFactory, KoinComponent { 15 | 16 | override fun onCreate() { 17 | super.onCreate() 18 | 19 | startKoin { 20 | androidLogger() 21 | androidContext(this@App) 22 | modules(mainModule) 23 | } 24 | } 25 | 26 | override fun newImageLoader() = ImageLoader 27 | .Builder(this) 28 | .okHttpClient(get()) 29 | //.logger(DebugLogger()) 30 | .build() 31 | 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/apkupdater/data/apkmirror/AppExistsRequest.kt: -------------------------------------------------------------------------------- 1 | package com.apkupdater.data.apkmirror 2 | 3 | data class AppExistsRequest( 4 | val pnames: List, 5 | val exclude: List = listOf("alpha", "beta") 6 | ) 7 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/apkupdater/data/apkmirror/AppExistsResponse.kt: -------------------------------------------------------------------------------- 1 | package com.apkupdater.data.apkmirror 2 | 3 | data class AppExistsResponse( 4 | val data: List = emptyList(), 5 | val headers: AppExistsResponseHeaders? = null, 6 | val status: Int? = null 7 | ) 8 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/apkupdater/data/apkmirror/AppExistsResponseApk.kt: -------------------------------------------------------------------------------- 1 | package com.apkupdater.data.apkmirror 2 | 3 | import com.apkupdater.data.ui.ApkMirrorSource 4 | import com.apkupdater.data.ui.AppInstalled 5 | import com.apkupdater.data.ui.AppUpdate 6 | import com.apkupdater.data.ui.Link 7 | import com.google.gson.annotations.SerializedName 8 | 9 | data class AppExistsResponseApk( 10 | @SerializedName("version_code") val versionCode: Long = 0, 11 | val link: String = "", 12 | @SerializedName("publish_date") val publishDate: String? = null, 13 | val arches: List = emptyList(), 14 | val dpis: List? = null, 15 | val minapi: String = "0", 16 | val description: String? = null, 17 | val capabilities: List? = null, 18 | @SerializedName("signatures-sha1") 19 | val signaturesSha1: List? = emptyList(), 20 | @SerializedName("signatures-sha256") 21 | val signaturesSha256: List? = emptyList() 22 | ) 23 | 24 | fun AppExistsResponseApk.toAppUpdate(app: AppInstalled, release: AppExistsResponseRelease) = AppUpdate( 25 | app.name, 26 | app.packageName, 27 | release.version, 28 | app.version, 29 | versionCode, 30 | app.versionCode, 31 | ApkMirrorSource, 32 | app.iconUri, 33 | Link.Url("https://www.apkmirror.com$link"), 34 | release.whatsNew.orEmpty() 35 | ) 36 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/apkupdater/data/apkmirror/AppExistsResponseApp.kt: -------------------------------------------------------------------------------- 1 | package com.apkupdater.data.apkmirror 2 | 3 | data class AppExistsResponseApp( 4 | val name: String = "", 5 | val description: String? = null, 6 | val link: String? = null 7 | ) 8 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/apkupdater/data/apkmirror/AppExistsResponseData.kt: -------------------------------------------------------------------------------- 1 | package com.apkupdater.data.apkmirror 2 | 3 | data class AppExistsResponseData( 4 | val pname: String = "", 5 | val exists: Boolean? = null, 6 | val developer: AppExistsResponseDeveloper? = null, 7 | val app: AppExistsResponseApp = AppExistsResponseApp(), 8 | val release: AppExistsResponseRelease = AppExistsResponseRelease(), 9 | val apks: List = emptyList() 10 | ) 11 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/apkupdater/data/apkmirror/AppExistsResponseDeveloper.kt: -------------------------------------------------------------------------------- 1 | package com.apkupdater.data.apkmirror 2 | 3 | data class AppExistsResponseDeveloper( 4 | val name: String? = null, 5 | val link: String? = null 6 | ) 7 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/apkupdater/data/apkmirror/AppExistsResponseHeaders.kt: -------------------------------------------------------------------------------- 1 | package com.apkupdater.data.apkmirror 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | data class AppExistsResponseHeaders(@SerializedName("Allow") val allow: String? = null) 6 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/apkupdater/data/apkmirror/AppExistsResponseRelease.kt: -------------------------------------------------------------------------------- 1 | package com.apkupdater.data.apkmirror 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | data class AppExistsResponseRelease( 6 | val version: String = "", 7 | @SerializedName("publish_date") val publishDate: String? = null, 8 | @SerializedName("whats_new") val whatsNew: String? = null, 9 | val link: String? = null 10 | ) 11 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/apkupdater/data/apkpure/AppInfoForUpdate.kt: -------------------------------------------------------------------------------- 1 | package com.apkupdater.data.apkpure 2 | 3 | 4 | data class AppInfoForUpdate( 5 | val package_name: String, 6 | val version_code: Long, 7 | val is_system: Boolean = false, 8 | val version_id: String = "", 9 | val cached_size: Int = -1 10 | ) 11 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/apkupdater/data/apkpure/AppUpdateResponse.kt: -------------------------------------------------------------------------------- 1 | package com.apkupdater.data.apkpure 2 | 3 | import android.net.Uri 4 | import com.apkupdater.data.ui.ApkPureSource 5 | import com.apkupdater.data.ui.AppInstalled 6 | import com.apkupdater.data.ui.AppUpdate 7 | import com.apkupdater.data.ui.Link 8 | 9 | 10 | data class AppUpdateResponse( 11 | val package_name: String, 12 | val version_code: Long, 13 | val version_name: String, 14 | val sign: List, 15 | val whatsnew: String, 16 | val description_short: String, 17 | val label: String, 18 | val asset: AppUpdateResponseAsset, 19 | val icon: AppUpdateResponseIcon 20 | ) 21 | 22 | fun AppUpdateResponse.toAppUpdate( 23 | app: AppInstalled? 24 | ) = AppUpdate( 25 | label, 26 | package_name, 27 | version_name, 28 | app?.version.orEmpty(), 29 | version_code, 30 | app?.versionCode ?: 0L, 31 | ApkPureSource, 32 | if (app == null) Uri.parse(icon.thumbnail.url) else Uri.EMPTY, 33 | if (asset.url.contains("/XAPK")) Link.Xapk(asset.url.replace("http://", "https://")) else Link.Url(asset.url.replace("http://", "https://")), 34 | if (app == null) description_short else whatsnew 35 | ) 36 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/apkupdater/data/apkpure/AppUpdateResponseAsset.kt: -------------------------------------------------------------------------------- 1 | package com.apkupdater.data.apkpure 2 | 3 | 4 | data class AppUpdateResponseAsset( 5 | val type: String, 6 | val url: String 7 | ) 8 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/apkupdater/data/apkpure/AppUpdateResponseIcon.kt: -------------------------------------------------------------------------------- 1 | package com.apkupdater.data.apkpure 2 | 3 | 4 | data class AppUpdateResponseIcon( 5 | val original: AppUpdateResponseIconData, 6 | val thumbnail: AppUpdateResponseIconData 7 | ) 8 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/apkupdater/data/apkpure/AppUpdateResponseIconData.kt: -------------------------------------------------------------------------------- 1 | package com.apkupdater.data.apkpure 2 | 3 | 4 | data class AppUpdateResponseIconData( 5 | val height: String, 6 | val width: String, 7 | val url: String 8 | ) 9 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/apkupdater/data/apkpure/DeviceHeader.kt: -------------------------------------------------------------------------------- 1 | package com.apkupdater.data.apkpure 2 | 3 | 4 | data class DeviceHeader( 5 | val device_info: DeviceInfo = DeviceInfo() 6 | ) 7 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/apkupdater/data/apkpure/DeviceInfo.kt: -------------------------------------------------------------------------------- 1 | package com.apkupdater.data.apkpure 2 | 3 | import android.content.res.Resources 4 | import android.os.Build 5 | import okhttp3.internal.toHexString 6 | import kotlin.random.Random 7 | 8 | 9 | data class DeviceInfo( 10 | val abis: List = Build.SUPPORTED_ABIS.toList(), 11 | val android_id: String = Random.nextLong().toHexString(), 12 | val os_ver: String = Build.VERSION.SDK_INT.toString(), 13 | val os_ver_name: String = Build.VERSION.RELEASE, 14 | val platform: Int = 1, // TODO: Figure out what is this 15 | val screen_height: Int = Resources.getSystem().displayMetrics.heightPixels, 16 | val screen_width: Int = Resources.getSystem().displayMetrics.widthPixels 17 | ) 18 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/apkupdater/data/apkpure/GetAppUpdate.kt: -------------------------------------------------------------------------------- 1 | package com.apkupdater.data.apkpure 2 | 3 | import okhttp3.internal.toHexString 4 | import kotlin.random.Random 5 | 6 | 7 | data class GetAppUpdate( 8 | val app_info_for_update: List = emptyList(), 9 | val android_id: String = Random.nextLong().toHexString(), 10 | val application_id: String = "com.apkpure.aegon", 11 | val cached_size: Long = -1 12 | ) 13 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/apkupdater/data/apkpure/GetAppUpdateResponse.kt: -------------------------------------------------------------------------------- 1 | package com.apkupdater.data.apkpure 2 | 3 | 4 | data class GetAppUpdateResponse( 5 | val retcode: Int, 6 | val app_update_response: List 7 | ) 8 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/apkupdater/data/apkpure/SearchResponse.kt: -------------------------------------------------------------------------------- 1 | package com.apkupdater.data.apkpure 2 | 3 | 4 | data class SearchResponse( 5 | val ad_priority: Int, 6 | val data: SearchResponseData 7 | ) 8 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/apkupdater/data/apkpure/SearchResponseApp.kt: -------------------------------------------------------------------------------- 1 | package com.apkupdater.data.apkpure 2 | 3 | 4 | data class SearchResponseApp( 5 | val ad: Boolean, 6 | val app_info: AppUpdateResponse 7 | ) 8 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/apkupdater/data/apkpure/SearchResponseData.kt: -------------------------------------------------------------------------------- 1 | package com.apkupdater.data.apkpure 2 | 3 | 4 | data class SearchResponseData( 5 | val data: List 6 | ) 7 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/apkupdater/data/apkpure/SearchResponseItem.kt: -------------------------------------------------------------------------------- 1 | package com.apkupdater.data.apkpure 2 | 3 | 4 | data class SearchResponseItem( 5 | val data: List 6 | ) 7 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/apkupdater/data/aptoide/ApksData.kt: -------------------------------------------------------------------------------- 1 | package com.apkupdater.data.aptoide 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | data class ApksData( 6 | @SerializedName("package") val packageName: String = "", 7 | val vercode: String = "0", 8 | val signature: String?, 9 | val isEnabled: Boolean = true 10 | ) 11 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/apkupdater/data/aptoide/App.kt: -------------------------------------------------------------------------------- 1 | package com.apkupdater.data.aptoide 2 | 3 | import android.net.Uri 4 | import androidx.core.net.toUri 5 | import com.apkupdater.data.ui.AppInstalled 6 | import com.apkupdater.data.ui.AppUpdate 7 | import com.apkupdater.data.ui.AptoideSource 8 | import com.apkupdater.data.ui.Link 9 | import com.google.gson.annotations.SerializedName 10 | 11 | data class App( 12 | val name: String = "", 13 | @SerializedName("package") val packageName:String = "", 14 | val icon: String? = "", 15 | val file: File, 16 | val store: Store 17 | ) 18 | 19 | fun App.toAppUpdate(app: AppInstalled?) = AppUpdate( 20 | name = name, 21 | packageName = packageName, 22 | version = file.vername, 23 | oldVersion = app?.version ?: "?", 24 | versionCode = file.vercode.toLong(), 25 | oldVersionCode = app?.versionCode ?: 0L, 26 | source = AptoideSource, 27 | iconUri = icon?.toUri() ?: Uri.EMPTY, 28 | link = Link.Url(file.path) 29 | ) 30 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/apkupdater/data/aptoide/DataList.kt: -------------------------------------------------------------------------------- 1 | package com.apkupdater.data.aptoide 2 | 3 | data class DataList(val list: List = emptyList()) 4 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/apkupdater/data/aptoide/File.kt: -------------------------------------------------------------------------------- 1 | package com.apkupdater.data.aptoide 2 | 3 | data class File( 4 | val vername: String = "", 5 | val vercode: String = "0", 6 | val path: String = "" 7 | ) 8 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/apkupdater/data/aptoide/ListAppUpdatesResponse.kt: -------------------------------------------------------------------------------- 1 | package com.apkupdater.data.aptoide 2 | 3 | 4 | data class ListAppUpdatesResponse( 5 | val list: List, 6 | val info: Any, 7 | val errors: Any 8 | ) 9 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/apkupdater/data/aptoide/ListAppsUpdatesRequest.kt: -------------------------------------------------------------------------------- 1 | package com.apkupdater.data.aptoide 2 | 3 | 4 | data class ListAppsUpdatesRequest( 5 | val apks_data: List, 6 | val q: String, 7 | val not_apk_tags: String = "alpha,beta", 8 | val store_ids: List? = listOf(15L, 711454L) 9 | ) 10 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/apkupdater/data/aptoide/ListSearchAppsRequest.kt: -------------------------------------------------------------------------------- 1 | package com.apkupdater.data.aptoide 2 | 3 | 4 | data class ListSearchAppsRequest( 5 | val query: String = "", 6 | val limit: String = "10", 7 | val q: String, 8 | val not_apk_tags: String = "alpha,beta", 9 | val store_ids: List? = listOf(15L, 711454L) 10 | ) 11 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/apkupdater/data/aptoide/ListSearchAppsResponse.kt: -------------------------------------------------------------------------------- 1 | package com.apkupdater.data.aptoide 2 | 3 | 4 | data class ListSearchAppsResponse( 5 | val datalist: DataList, 6 | val info: Any, 7 | val errors: Any 8 | ) 9 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/apkupdater/data/aptoide/Store.kt: -------------------------------------------------------------------------------- 1 | package com.apkupdater.data.aptoide 2 | 3 | data class Store(val name: String = "") 4 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/apkupdater/data/fdroid/FdroidApp.kt: -------------------------------------------------------------------------------- 1 | package com.apkupdater.data.fdroid 2 | 3 | data class FdroidApp( 4 | //val description: String = "", 5 | val packageName: String = "", 6 | val name: String = "", 7 | val icon: String = "", 8 | val suggestedVersionCode: String = "", 9 | val suggestedVersionName: String = "", 10 | val added: Long = 0L, 11 | val lastUpdated: Long = 0L, 12 | val allowedAPKSigningKeys: List = emptyList(), 13 | val localized: Map = emptyMap() 14 | ) 15 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/apkupdater/data/fdroid/FdroidData.kt: -------------------------------------------------------------------------------- 1 | package com.apkupdater.data.fdroid 2 | 3 | data class FdroidData( 4 | val packages: Map> = emptyMap(), 5 | val apps: List = emptyList() 6 | ) 7 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/apkupdater/data/fdroid/FdroidLocalized.kt: -------------------------------------------------------------------------------- 1 | package com.apkupdater.data.fdroid 2 | 3 | 4 | data class FdroidLocalized( 5 | val name: String = "", 6 | val summary: String = "", 7 | val whatsNew: String = "" 8 | ) 9 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/apkupdater/data/fdroid/FdroidPackage.kt: -------------------------------------------------------------------------------- 1 | package com.apkupdater.data.fdroid 2 | 3 | data class FdroidPackage( 4 | val apkName: String = "", 5 | val versionCode: Long = 0, 6 | val versionName: String = "", 7 | val minSdkVersion: Int = 0, 8 | val nativecode: List = emptyList(), 9 | val hash: String = "", 10 | val hashType: String = "" 11 | ) 12 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/apkupdater/data/fdroid/FdroidUpdate.kt: -------------------------------------------------------------------------------- 1 | package com.apkupdater.data.fdroid 2 | 3 | import androidx.core.net.toUri 4 | import com.apkupdater.data.ui.AppInstalled 5 | import com.apkupdater.data.ui.AppUpdate 6 | import com.apkupdater.data.ui.Link 7 | import com.apkupdater.data.ui.Source 8 | 9 | data class FdroidUpdate( 10 | val apk: FdroidPackage, 11 | val app: FdroidApp 12 | ) 13 | 14 | fun FdroidUpdate.toAppUpdate(current: AppInstalled?, source: Source, url: String) = AppUpdate( 15 | app.localized["en-US"]?.name ?: app.name, 16 | app.packageName, 17 | apk.versionName, 18 | current?.version ?: "?", 19 | apk.versionCode, 20 | current?.versionCode ?: 0L, 21 | source, 22 | if(app.icon.isEmpty()) 23 | "https://f-droid.org/assets/ic_repo_app_default.png".toUri() 24 | else 25 | "${url}icons-640/${app.icon}".toUri(), 26 | Link.Url("$url${apk.apkName}"), 27 | if (current != null) app.localized["en-US"]?.whatsNew.orEmpty() else app.localized["en-US"]?.summary.orEmpty() 28 | ) 29 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/apkupdater/data/github/GitHubAuthor.kt: -------------------------------------------------------------------------------- 1 | package com.apkupdater.data.github 2 | 3 | data class GitHubAuthor(val avatar_url: String) 4 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/apkupdater/data/github/GitHubRelease.kt: -------------------------------------------------------------------------------- 1 | package com.apkupdater.data.github 2 | 3 | 4 | data class GitHubRelease( 5 | val name: String, 6 | val prerelease: Boolean, 7 | val assets: List, 8 | val tag_name: String, 9 | val author: GitHubAuthor, 10 | val body: String = "" 11 | ) 12 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/apkupdater/data/github/GitHubReleaseAsset.kt: -------------------------------------------------------------------------------- 1 | package com.apkupdater.data.github 2 | 3 | data class GitHubReleaseAsset( 4 | val size: Long, 5 | val browser_download_url: String 6 | ) 7 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/apkupdater/data/gitlab/GitLabApp.kt: -------------------------------------------------------------------------------- 1 | package com.apkupdater.data.gitlab 2 | 3 | data class GitLabApp( 4 | val packageName: String, 5 | val user: String, 6 | val repo: String 7 | ) 8 | 9 | val GitLabApps = listOf( 10 | GitLabApp("com.aurora.store", "AuroraOSS", "AuroraStore") 11 | ) 12 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/apkupdater/data/gitlab/GitLabAsset.kt: -------------------------------------------------------------------------------- 1 | package com.apkupdater.data.gitlab 2 | 3 | 4 | data class GitLabAsset(val format: String, val url: String) 5 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/apkupdater/data/gitlab/GitLabAssets.kt: -------------------------------------------------------------------------------- 1 | package com.apkupdater.data.gitlab 2 | 3 | 4 | data class GitLabAssets( 5 | val sources: List, 6 | val links: List 7 | ) 8 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/apkupdater/data/gitlab/GitLabAuthor.kt: -------------------------------------------------------------------------------- 1 | package com.apkupdater.data.gitlab 2 | 3 | 4 | data class GitLabAuthor(val avatar_url: String) 5 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/apkupdater/data/gitlab/GitLabLink.kt: -------------------------------------------------------------------------------- 1 | package com.apkupdater.data.gitlab 2 | 3 | 4 | data class GitLabLink(val url: String) 5 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/apkupdater/data/gitlab/GitLabRelease.kt: -------------------------------------------------------------------------------- 1 | package com.apkupdater.data.gitlab 2 | 3 | 4 | data class GitLabRelease( 5 | val tag_name: String, 6 | val description: String, 7 | val assets: GitLabAssets, 8 | val author: GitLabAuthor 9 | ) 10 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/apkupdater/data/snack/TextSnack.kt: -------------------------------------------------------------------------------- 1 | package com.apkupdater.data.snack 2 | 3 | import androidx.compose.material3.SnackbarDuration 4 | import androidx.compose.material3.SnackbarVisuals 5 | 6 | 7 | class TextSnack( 8 | override val message: String, 9 | override val actionLabel: String? = null, 10 | override val duration: SnackbarDuration = SnackbarDuration.Short, 11 | override val withDismissAction: Boolean = true 12 | ): SnackbarVisuals 13 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/apkupdater/data/ui/AppInstallProgress.kt: -------------------------------------------------------------------------------- 1 | package com.apkupdater.data.ui 2 | 3 | data class AppInstallProgress(val id: Int, val progress: Long? = null, val total: Long? = null) 4 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/apkupdater/data/ui/AppInstallStatus.kt: -------------------------------------------------------------------------------- 1 | package com.apkupdater.data.ui 2 | 3 | data class AppInstallStatus(val success: Boolean, val id: Int, val snack: Boolean = true) 4 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/apkupdater/data/ui/AppInstalled.kt: -------------------------------------------------------------------------------- 1 | package com.apkupdater.data.ui 2 | 3 | import android.net.Uri 4 | import com.apkupdater.data.aptoide.ApksData 5 | import com.apkupdater.util.toSha1Aptoide 6 | 7 | data class AppInstalled( 8 | val name: String, 9 | val packageName: String, 10 | val version: String, 11 | val versionCode: Long, 12 | val iconUri: Uri = Uri.EMPTY, 13 | val ignored: Boolean = false, 14 | val signature: String = "", 15 | val signatureSha256: String = "" 16 | ) 17 | 18 | fun List.getApp(packageName: String) = find { packageName == it.packageName } 19 | 20 | fun List.getVersionCode(packageName: String) = getApp(packageName) 21 | ?.versionCode 22 | ?: 0L 23 | 24 | fun List.getVersion(packageName: String) = getApp(packageName) 25 | ?.version 26 | ?: "" 27 | 28 | fun List.getSignature(packageName: String) = getApp(packageName) 29 | ?.signature 30 | .orEmpty() 31 | 32 | fun List.getPackageNames() = filter { !it.ignored }.map { it.packageName } 33 | 34 | fun AppInstalled.toApksData() = ApksData( 35 | packageName = packageName, 36 | vercode = versionCode.toString(), 37 | signature = signature.toSha1Aptoide(), 38 | isEnabled = true 39 | ) 40 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/apkupdater/data/ui/AppUpdate.kt: -------------------------------------------------------------------------------- 1 | package com.apkupdater.data.ui 2 | 3 | import android.net.Uri 4 | 5 | data class AppUpdate( 6 | val name: String, 7 | val packageName: String, 8 | val version: String, 9 | val oldVersion: String, 10 | val versionCode: Long, 11 | val oldVersionCode: Long, 12 | val source: Source, 13 | val iconUri: Uri = Uri.EMPTY, 14 | val link: Link = Link.Empty, 15 | val whatsNew: String = "", 16 | val isInstalling: Boolean = false, 17 | val total: Long = 0L, 18 | val progress: Long = 0L, 19 | val id: Int = "${source.name}.$packageName.$versionCode.$version".hashCode() 20 | ) 21 | 22 | fun List.indexOf(id: Int) = indexOfFirst { it.id == id } 23 | 24 | fun MutableList.setIsInstalling(id: Int, b: Boolean): List { 25 | val index = this.indexOf(id) 26 | if (index != -1) { 27 | this[index] = this[index].copy(isInstalling = b) 28 | } 29 | return this 30 | } 31 | 32 | fun MutableList.removeId(id: Int): List { 33 | val index = this.indexOf(id) 34 | if (index != -1) this.removeAt(index) 35 | return this 36 | } 37 | 38 | fun MutableList.setProgress(progress: AppInstallProgress): MutableList { 39 | val index = this.indexOf(progress.id) 40 | if (index != -1) { 41 | progress.progress?.let { this[index] = this[index].copy(progress = it) } 42 | progress.total?.let { this[index] = this[index].copy(total = it) } 43 | } 44 | return this 45 | } 46 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/apkupdater/data/ui/AppsUiState.kt: -------------------------------------------------------------------------------- 1 | package com.apkupdater.data.ui 2 | 3 | 4 | sealed class AppsUiState { 5 | 6 | data class Loading( 7 | val excludeSystem: Boolean, 8 | val excludeAppStore: Boolean, 9 | val excludeDisabled: Boolean 10 | ): AppsUiState() 11 | 12 | data object Error : AppsUiState() 13 | 14 | data class Success( 15 | val apps: List, 16 | val excludeSystem: Boolean, 17 | val excludeAppStore: Boolean, 18 | val excludeDisabled: Boolean 19 | ): AppsUiState() 20 | 21 | inline fun onLoading(block: (Loading) -> Unit): AppsUiState { 22 | if (this is Loading) block(this) 23 | return this 24 | } 25 | 26 | inline fun onError(block: (Error) -> Unit): AppsUiState { 27 | if (this is Error) block(this) 28 | return this 29 | } 30 | 31 | inline fun onSuccess(block: (Success) -> Unit): AppsUiState { 32 | if (this is Success) block(this) 33 | return this 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/apkupdater/data/ui/Link.kt: -------------------------------------------------------------------------------- 1 | package com.apkupdater.data.ui 2 | 3 | import com.aurora.gplayapi.data.models.File 4 | 5 | 6 | sealed class Link { 7 | data object Empty: Link() 8 | data class Url(val link: String, val size: Long = 0L): Link() 9 | data class Xapk(val link: String): Link() 10 | data class Play(val getInstallFiles: () -> List): Link() 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/apkupdater/data/ui/Screen.kt: -------------------------------------------------------------------------------- 1 | package com.apkupdater.data.ui 2 | 3 | import androidx.annotation.StringRes 4 | import androidx.compose.material.icons.Icons 5 | import androidx.compose.material.icons.filled.Home 6 | import androidx.compose.material.icons.filled.Search 7 | import androidx.compose.material.icons.filled.Settings 8 | import androidx.compose.material.icons.filled.ThumbUp 9 | import androidx.compose.material.icons.outlined.Home 10 | import androidx.compose.material.icons.outlined.Search 11 | import androidx.compose.material.icons.outlined.Settings 12 | import androidx.compose.material.icons.outlined.ThumbUp 13 | import androidx.compose.ui.graphics.vector.ImageVector 14 | import com.apkupdater.R 15 | 16 | sealed class Screen( 17 | val route: String, 18 | @StringRes val resourceId: Int, 19 | val icon: ImageVector, 20 | val iconSelected: ImageVector 21 | ) { 22 | data object Apps : Screen("apps", R.string.tab_apps, Icons.Outlined.Home, Icons.Filled.Home) 23 | data object Search : Screen("search", R.string.tab_search, Icons.Outlined.Search, Icons.Filled.Search) 24 | data object Updates : Screen("updates", R.string.tab_updates, Icons.Outlined.ThumbUp, Icons.Filled.ThumbUp) 25 | data object Settings : Screen("settings", R.string.tab_settings, Icons.Outlined.Settings, Icons.Filled.Settings) 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/apkupdater/data/ui/SearchUiState.kt: -------------------------------------------------------------------------------- 1 | package com.apkupdater.data.ui 2 | 3 | 4 | sealed class SearchUiState { 5 | data object Loading: SearchUiState() 6 | data object Error : SearchUiState() 7 | data class Success(val updates: List): SearchUiState() 8 | 9 | inline fun onLoading(block: (Loading) -> Unit): SearchUiState { 10 | if (this is Loading) block(this) 11 | return this 12 | } 13 | 14 | inline fun onError(block: (Error) -> Unit): SearchUiState { 15 | if (this is Error) block(this) 16 | return this 17 | } 18 | 19 | inline fun onSuccess(block: (Success) -> Unit): SearchUiState { 20 | if (this is Success) block(this) 21 | return this 22 | } 23 | 24 | fun mutableUpdates(): MutableList { 25 | if (this is Success) { 26 | return updates.toMutableList() 27 | } 28 | return mutableListOf() 29 | } 30 | 31 | fun updates(): List { 32 | if (this is Success) { 33 | return updates 34 | } 35 | return emptyList() 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/apkupdater/data/ui/SettingsUiState.kt: -------------------------------------------------------------------------------- 1 | package com.apkupdater.data.ui 2 | 3 | 4 | sealed class SettingsUiState { 5 | object Settings : SettingsUiState() 6 | object About : SettingsUiState() 7 | } 8 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/apkupdater/data/ui/Source.kt: -------------------------------------------------------------------------------- 1 | package com.apkupdater.data.ui 2 | 3 | import com.apkupdater.R 4 | 5 | 6 | data class Source( 7 | val name: String, 8 | val resourceId: Int 9 | ) 10 | 11 | val ApkMirrorSource = Source("ApkMirror", R.drawable.ic_apkmirror) 12 | val GitHubSource = Source("GitHub", R.drawable.ic_github) 13 | val FdroidSource = Source("F-Droid (Main)", R.drawable.ic_fdroid) 14 | val IzzySource = Source("F-Droid (Izzy)", R.drawable.ic_izzy) 15 | val AptoideSource = Source("Aptoide", R.drawable.ic_aptoide) 16 | val ApkPureSource = Source("ApkPure", R.drawable.ic_apkpure) 17 | val GitLabSource = Source("GitLab", R.drawable.ic_gitlab) 18 | val PlaySource = Source("Play", R.drawable.ic_play) 19 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/apkupdater/data/ui/UpdatesUiState.kt: -------------------------------------------------------------------------------- 1 | package com.apkupdater.data.ui 2 | 3 | 4 | sealed class UpdatesUiState { 5 | data object Loading: UpdatesUiState() 6 | data object Error : UpdatesUiState() 7 | data class Success(val updates: List): UpdatesUiState() 8 | 9 | inline fun onLoading(block: (Loading) -> Unit): UpdatesUiState { 10 | if (this is Loading) block(this) 11 | return this 12 | } 13 | 14 | inline fun onError(block: (Error) -> Unit): UpdatesUiState { 15 | if (this is Error) block(this) 16 | return this 17 | } 18 | 19 | inline fun onSuccess(block: (Success) -> Unit): UpdatesUiState { 20 | if (this is Success) block(this) 21 | return this 22 | } 23 | 24 | fun mutableUpdates(): MutableList { 25 | if (this is Success) { 26 | return updates.toMutableList() 27 | } 28 | return mutableListOf() 29 | } 30 | 31 | fun updates(): List { 32 | if (this is Success) { 33 | return updates 34 | } 35 | return emptyList() 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/apkupdater/prefs/Prefs.kt: -------------------------------------------------------------------------------- 1 | package com.apkupdater.prefs 2 | 3 | import com.apkupdater.data.ui.Screen 4 | import com.aurora.gplayapi.data.models.AuthData 5 | import com.kryptoprefs.context.KryptoContext 6 | import com.kryptoprefs.gson.json 7 | import com.kryptoprefs.preferences.KryptoPrefs 8 | 9 | 10 | class Prefs( 11 | prefs: KryptoPrefs, 12 | isAndroidTv: Boolean 13 | ): KryptoContext(prefs) { 14 | val ignoredApps = json("ignoredApps", emptyList(), true) 15 | val ignoredVersions = json("ignoredVersions", emptyList(), true) 16 | val excludeSystem = boolean("excludeSystem", defValue = true, backed = true) 17 | val excludeDisabled = boolean("excludeDisabled", defValue = true, backed = true) 18 | val excludeStore = boolean("excludeStore", defValue = false, backed = true) 19 | val portraitColumns = int("portraitColumns", 3, true) 20 | val landscapeColumns = int("landscapeColumns", 6, true) 21 | val playTextAnimations = boolean("playTextAnimations", defValue = true, backed = true) 22 | val ignoreAlpha = boolean("ignoreAlpha", defValue = true, backed = true) 23 | val ignoreBeta = boolean("ignoreBeta", defValue = true, backed = true) 24 | val ignorePreRelease = boolean("ignorePreRelease", defValue = true, backed = true) 25 | val useSafeStores = boolean("useSafeStores", defValue = true, backed = true) 26 | val useApkMirror = boolean("useApkMirror", defValue = false, backed = true) 27 | val useGitHub = boolean("useGitHub", defValue = true, backed = true) 28 | val useGitLab = boolean("useGitLab", defValue = true, backed = true) 29 | val useFdroid = boolean("useFdroid", defValue = true, backed = true) 30 | val useIzzy = boolean("useIzzy", defValue = true, backed = true) 31 | val useAptoide = boolean("useAptoide", defValue = true, backed = true) 32 | val useApkPure = boolean("useApkPure", defValue = true, backed = true) 33 | val usePlay = boolean("usePlay", defValue = true, backed = true) 34 | val enableAlarm = boolean("enableAlarm", defValue = false, backed = true) 35 | val alarmHour = int("alarmHour", defValue = 12, backed = true) 36 | val alarmFrequency = int("alarmFrequency", 0, backed = true) 37 | val androidTvUi = boolean("androidTvUi", defValue = true, backed = true) 38 | val rootInstall = boolean("rootInstall", defValue = false, backed = true) 39 | val theme = int("theme", defValue = 0, backed = true) 40 | val lastTab = string("lastTab", defValue = Screen.Updates.route, backed = true) 41 | val playAuthData = json("playAuthData", AuthData("", ""), true) 42 | val lastPlayCheck = long("lastPlayCheck", 0L, true) 43 | } 44 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/apkupdater/repository/ApkPureRepository.kt: -------------------------------------------------------------------------------- 1 | package com.apkupdater.repository 2 | 3 | import android.util.Log 4 | import com.apkupdater.data.apkpure.AppInfoForUpdate 5 | import com.apkupdater.data.apkpure.AppUpdateResponse 6 | import com.apkupdater.data.apkpure.DeviceHeader 7 | import com.apkupdater.data.apkpure.GetAppUpdate 8 | import com.apkupdater.data.apkpure.toAppUpdate 9 | import com.apkupdater.data.ui.AppInstalled 10 | import com.apkupdater.data.ui.getApp 11 | import com.apkupdater.data.ui.getSignature 12 | import com.apkupdater.prefs.Prefs 13 | import com.apkupdater.service.ApkPureService 14 | import com.google.gson.Gson 15 | import kotlinx.coroutines.flow.catch 16 | import kotlinx.coroutines.flow.flow 17 | 18 | 19 | class ApkPureRepository( 20 | gson: Gson, 21 | private val service: ApkPureService, 22 | private val prefs: Prefs 23 | ) { 24 | 25 | private val header = gson.toJson(DeviceHeader()) 26 | 27 | suspend fun updates(apps: List) = flow { 28 | val info = apps.map { AppInfoForUpdate(it.packageName, it.versionCode) } 29 | val r = service.getAppUpdate(header, GetAppUpdate(info)) 30 | val updates = r.app_update_response 31 | .filter { filterSignature(it.sign, apps.getSignature(it.package_name)) } 32 | .filter { filterAlpha(it) } 33 | .filter { filterBeta(it) } 34 | .map { it.toAppUpdate(apps.getApp(it.package_name)) } 35 | emit(updates) 36 | }.catch { 37 | Log.e("ApkPureRepository", it.message, it) 38 | emit(emptyList()) 39 | } 40 | 41 | suspend fun search(text: String) = flow { 42 | val response = service.search(header, text) 43 | val info = response.data.data.mapNotNull { d -> 44 | d.data.firstOrNull()?.takeIf { !it.ad }?.app_info?.let { 45 | AppInfoForUpdate(it.package_name, 0L, false) 46 | } 47 | } 48 | val r = service.getAppUpdate(header, GetAppUpdate(info)) 49 | val updates = r.app_update_response 50 | .filter { filterAlpha(it) } 51 | .filter { filterBeta(it) } 52 | .map { it.toAppUpdate(null) } 53 | emit(Result.success(updates)) 54 | }.catch { 55 | Log.e("ApkPureRepository", it.message, it) 56 | emit(Result.failure(it)) 57 | } 58 | 59 | private fun filterAlpha(update: AppUpdateResponse) = when { 60 | prefs.ignoreAlpha.get() && update.version_name.contains("alpha", true) -> false 61 | else -> true 62 | } 63 | 64 | private fun filterBeta(update: AppUpdateResponse) = when { 65 | prefs.ignoreBeta.get() && update.version_name.contains("beta", true) -> false 66 | else -> true 67 | } 68 | 69 | private fun filterSignature(signatures: List, signature: String) = when { 70 | signatures.isEmpty() -> true 71 | signatures.contains(signature) -> true 72 | else -> false 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/apkupdater/repository/AppsRepository.kt: -------------------------------------------------------------------------------- 1 | package com.apkupdater.repository 2 | 3 | import android.content.Context 4 | import android.content.pm.ApplicationInfo 5 | import android.content.pm.PackageManager 6 | import android.os.Build 7 | import android.util.Log 8 | import com.apkupdater.prefs.Prefs 9 | import com.apkupdater.transform.toAppInstalled 10 | import com.apkupdater.util.orFalse 11 | import kotlinx.coroutines.flow.catch 12 | import kotlinx.coroutines.flow.flow 13 | 14 | 15 | class AppsRepository( 16 | private val context: Context, 17 | private val prefs: Prefs 18 | ) { 19 | 20 | suspend fun getApps() = flow { 21 | val apps = context.packageManager 22 | .getInstalledPackages(PackageManager.MATCH_ALL + getSignatureFlag()) 23 | .asSequence() 24 | .filter { !excludeSystem() || it.applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM == 0 } 25 | .filter { !excludeSystem() || it.applicationInfo.flags and ApplicationInfo.FLAG_UPDATED_SYSTEM_APP == 0 } 26 | .filter { !excludeDisabled() || it.applicationInfo.enabled } 27 | .filter { !excludeStore() || !isAppStore(getInstallerPackageName(it.packageName)) } 28 | .map { it.toAppInstalled(context, ignoredApps()) } 29 | .sortedBy { it.name } 30 | .sortedBy { it.ignored } 31 | .toList() 32 | emit(Result.success(apps)) 33 | }.catch { 34 | Log.e("AppsRepository", "Error getting apps.", it) 35 | emit(Result.failure(it)) 36 | } 37 | 38 | private fun excludeSystem() = prefs.excludeSystem.get() 39 | private fun excludeDisabled() = prefs.excludeDisabled.get() 40 | private fun excludeStore() = prefs.excludeStore.get() 41 | private fun ignoredApps() = prefs.ignoredApps.get() 42 | 43 | @Suppress("DEPRECATION") 44 | private fun getSignatureFlag(): Int { 45 | return if (Build.VERSION.SDK_INT >= 28) { 46 | PackageManager.GET_SIGNING_CERTIFICATES 47 | } else { 48 | PackageManager.GET_SIGNATURES 49 | } 50 | } 51 | 52 | @Suppress("DEPRECATION") 53 | private fun getInstallerPackageName(packageName: String): String { 54 | return if (Build.VERSION.SDK_INT < 30) { 55 | context.packageManager.getInstallerPackageName(packageName).orEmpty() 56 | } else { 57 | context.packageManager.getInstallSourceInfo(packageName).installingPackageName.orEmpty() 58 | } 59 | } 60 | 61 | // Checks if Play Store or Amazon Store 62 | private fun isAppStore(name: String?) = name?.contains("com.android.vending").orFalse() 63 | || name?.contains("com.amazon").orFalse() 64 | 65 | } 66 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/apkupdater/repository/FdroidRepository.kt: -------------------------------------------------------------------------------- 1 | package com.apkupdater.repository 2 | 3 | import android.os.Build 4 | import android.util.Log 5 | import com.apkupdater.data.fdroid.FdroidApp 6 | import com.apkupdater.data.fdroid.FdroidData 7 | import com.apkupdater.data.fdroid.FdroidUpdate 8 | import com.apkupdater.data.fdroid.toAppUpdate 9 | import com.apkupdater.data.ui.AppInstalled 10 | import com.apkupdater.data.ui.Source 11 | import com.apkupdater.data.ui.getApp 12 | import com.apkupdater.data.ui.getVersionCode 13 | import com.apkupdater.prefs.Prefs 14 | import com.apkupdater.service.FdroidService 15 | import com.google.gson.Gson 16 | import kotlinx.coroutines.flow.catch 17 | import kotlinx.coroutines.flow.flow 18 | import java.io.InputStream 19 | import java.util.jar.JarInputStream 20 | 21 | 22 | class FdroidRepository( 23 | private val service: FdroidService, 24 | private val url: String, 25 | private val source: Source, 26 | private val prefs: Prefs 27 | ) { 28 | private val arch = Build.SUPPORTED_ABIS.toSet() 29 | private val api = Build.VERSION.SDK_INT 30 | 31 | suspend fun updates(apps: List) = flow { 32 | val response = service.getJar("${url}index-v1.jar") 33 | val data = jarToJson(response.byteStream()) 34 | val appNames = apps.map { it.packageName } 35 | val updates = data.apps 36 | .asSequence() 37 | .filter { appNames.contains(it.packageName) } 38 | .filter { filterSignature(apps.getApp(it.packageName)!!, it) } 39 | .map { FdroidUpdate(data.packages[it.packageName]!![0], it) } 40 | .filter { it.apk.versionCode > apps.getVersionCode(it.app.packageName) } 41 | .parseUpdates(apps) 42 | emit(updates) 43 | }.catch { 44 | emit(emptyList()) 45 | Log.e("FdroidRepository", "Error looking for updates.", it) 46 | } 47 | 48 | suspend fun search(text: String) = flow { 49 | val response = service.getJar("${url}index-v1.jar") 50 | val data = jarToJson(response.byteStream()) 51 | val updates = data.apps 52 | .asSequence() 53 | .map { FdroidUpdate(data.packages[it.packageName]!![0], it) } 54 | .filter { it.app.name.contains(text, true) || it.app.packageName.contains(text, true) || it.apk.apkName.contains(text, true) } 55 | .parseUpdates(null) 56 | emit(Result.success(updates)) 57 | }.catch { 58 | emit(Result.failure(it)) 59 | Log.e("FdroidRepository", "Error searching.", it) 60 | } 61 | 62 | private fun Sequence.parseUpdates(apps: List?) = this 63 | .filter { it.apk.minSdkVersion <= api } 64 | .filter { filterArch(it) } 65 | .filter { filterAlpha(it) } 66 | .filter { filterBeta(it) } 67 | .map { it.toAppUpdate(apps?.getApp(it.app.packageName), source, url) } 68 | .toList() 69 | 70 | private fun filterSignature(installed: AppInstalled, update: FdroidApp) = when { 71 | update.allowedAPKSigningKeys.isEmpty() -> true 72 | update.allowedAPKSigningKeys.contains(installed.signatureSha256) -> true 73 | else -> false 74 | } 75 | 76 | private fun filterAlpha(update: FdroidUpdate) = when { 77 | prefs.ignoreAlpha.get() && update.apk.versionName.contains("alpha", true) -> false 78 | else -> true 79 | } 80 | 81 | private fun filterBeta(update: FdroidUpdate) = when { 82 | prefs.ignoreBeta.get() && update.apk.versionName.contains("beta", true) -> false 83 | else -> true 84 | } 85 | 86 | private fun filterArch(update: FdroidUpdate) = when { 87 | update.apk.nativecode.isEmpty() -> true 88 | update.apk.nativecode.intersect(arch).isNotEmpty() -> true 89 | else -> false 90 | } 91 | 92 | private fun jarToJson(stream: InputStream): FdroidData { 93 | val jar = JarInputStream(stream) 94 | var entry = jar.nextJarEntry 95 | while (entry != null) { 96 | if (entry.name == "index-v1.json") { 97 | return Gson().fromJson(jar.reader(), FdroidData::class.java) 98 | } 99 | entry = jar.nextJarEntry 100 | } 101 | return FdroidData() 102 | } 103 | 104 | } 105 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/apkupdater/repository/GitLabRepository.kt: -------------------------------------------------------------------------------- 1 | package com.apkupdater.repository 2 | 3 | import android.net.Uri 4 | import android.util.Log 5 | import com.apkupdater.data.gitlab.GitLabApps 6 | import com.apkupdater.data.gitlab.GitLabRelease 7 | import com.apkupdater.data.ui.AppInstalled 8 | import com.apkupdater.data.ui.AppUpdate 9 | import com.apkupdater.data.ui.GitLabSource 10 | import com.apkupdater.data.ui.Link 11 | import com.apkupdater.data.ui.getApp 12 | import com.apkupdater.prefs.Prefs 13 | import com.apkupdater.service.GitLabService 14 | import com.apkupdater.util.combine 15 | import com.apkupdater.util.filterVersionTag 16 | import io.github.g00fy2.versioncompare.Version 17 | import kotlinx.coroutines.flow.Flow 18 | import kotlinx.coroutines.flow.catch 19 | import kotlinx.coroutines.flow.collect 20 | import kotlinx.coroutines.flow.flow 21 | 22 | 23 | class GitLabRepository( 24 | private val service: GitLabService, 25 | private val prefs: Prefs 26 | ) { 27 | 28 | suspend fun updates(apps: List) = flow { 29 | val checks = mutableListOf>>() 30 | GitLabApps.forEach { app -> 31 | apps.find { it.packageName == app.packageName }?.let { 32 | checks.add(checkApp(apps, app.user, app.repo, app.packageName, it.version, null)) 33 | } 34 | } 35 | if (checks.isEmpty()) { 36 | emit(emptyList()) 37 | } else { 38 | checks.combine { all -> emit(all.flatMap { it }) }.collect() 39 | } 40 | } 41 | 42 | private suspend fun checkApp( 43 | apps: List?, 44 | user: String, 45 | repo: String, 46 | packageName: String, 47 | currentVersion: String, 48 | extra: Regex? 49 | ) = flow { 50 | val releases = service.getReleases(user, repo) 51 | .filter { Version(filterVersionTag(it.tag_name)) > Version(currentVersion) } 52 | 53 | if (releases.isNotEmpty()) { 54 | val app = apps?.getApp(packageName) 55 | emit(listOf( 56 | AppUpdate( 57 | name = repo, 58 | packageName = packageName, 59 | version = releases[0].tag_name, 60 | oldVersion = app?.version ?: "?", 61 | versionCode = 0L, 62 | oldVersionCode = app?.versionCode ?: 0L, 63 | source = GitLabSource, 64 | link = Link.Url(getApkUrl(packageName, releases[0])), 65 | whatsNew = releases[0].description, 66 | iconUri = if (apps == null) Uri.parse(releases[0].author.avatar_url) else Uri.EMPTY 67 | ))) 68 | } else { 69 | emit(emptyList()) 70 | } 71 | }.catch { 72 | emit(emptyList()) 73 | Log.e("GitLabRepository", "Error fetching releases for $packageName.", it) 74 | } 75 | 76 | suspend fun search(text: String) = flow { 77 | val checks = mutableListOf>>() 78 | 79 | GitLabApps.forEach { app -> 80 | if (app.repo.contains(text, true) || app.user.contains(text, true) || app.packageName.contains(text, true)) { 81 | checks.add(checkApp(null, app.user, app.repo, app.packageName, "?", null)) 82 | } 83 | } 84 | 85 | if (checks.isEmpty()) { 86 | emit(Result.success(emptyList())) 87 | } else { 88 | checks.combine { all -> 89 | val r = all.flatMap { it } 90 | emit(Result.success(r)) 91 | }.collect() 92 | } 93 | }.catch { 94 | emit(Result.failure(it)) 95 | Log.e("GitLabRepository", "Error searching.", it) 96 | } 97 | 98 | private fun getApkUrl( 99 | packageName: String, 100 | release: GitLabRelease 101 | ): String { 102 | // TODO: Take into account arch 103 | val source = release.assets.sources.find { it.url.endsWith(".apk", true) } 104 | if (source != null) return source.url 105 | 106 | val link = release.assets.links.find { it.url.endsWith(".apk", true) } 107 | if (link != null) return link.url 108 | 109 | return "" 110 | } 111 | 112 | } 113 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/apkupdater/repository/SearchRepository.kt: -------------------------------------------------------------------------------- 1 | package com.apkupdater.repository 2 | 3 | import android.util.Log 4 | import com.apkupdater.data.ui.AppUpdate 5 | import com.apkupdater.prefs.Prefs 6 | import com.apkupdater.util.combine 7 | import kotlinx.coroutines.flow.Flow 8 | import kotlinx.coroutines.flow.catch 9 | import kotlinx.coroutines.flow.collect 10 | import kotlinx.coroutines.flow.flow 11 | 12 | class SearchRepository( 13 | private val apkMirrorRepository: ApkMirrorRepository, 14 | private val fdroidRepository: FdroidRepository, 15 | private val izzyRepository: FdroidRepository, 16 | private val aptoideRepository: AptoideRepository, 17 | private val gitHubRepository: GitHubRepository, 18 | private val apkPureRepository: ApkPureRepository, 19 | private val gitLabRepository: GitLabRepository, 20 | private val playRepository: PlayRepository, 21 | private val prefs: Prefs 22 | ) { 23 | 24 | fun search(text: String) = flow { 25 | val sources = mutableListOf>>>() 26 | if (prefs.useApkMirror.get()) sources.add(apkMirrorRepository.search(text)) 27 | if (prefs.useFdroid.get()) sources.add(fdroidRepository.search(text)) 28 | if (prefs.useIzzy.get()) sources.add(izzyRepository.search(text)) 29 | if (prefs.useAptoide.get()) sources.add(aptoideRepository.search(text)) 30 | if (prefs.useGitHub.get()) sources.add(gitHubRepository.search(text)) 31 | if (prefs.useApkPure.get()) sources.add(apkPureRepository.search(text)) 32 | if (prefs.useGitLab.get()) sources.add(gitLabRepository.search(text)) 33 | if (prefs.usePlay.get()) sources.add(playRepository.search(text)) 34 | 35 | if (sources.isNotEmpty()) { 36 | sources.combine { updates -> 37 | val result = updates.filter { it.isSuccess }.mapNotNull { it.getOrNull() } 38 | emit(Result.success(result.flatten().sortedBy { it.name })) 39 | }.collect() 40 | } else { 41 | emit(Result.success(emptyList())) 42 | } 43 | }.catch { 44 | emit(Result.failure(it)) 45 | Log.e("SearchRepository", "Error searching.", it) 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/apkupdater/repository/UpdatesRepository.kt: -------------------------------------------------------------------------------- 1 | package com.apkupdater.repository 2 | 3 | import android.util.Log 4 | import com.apkupdater.data.ui.AppUpdate 5 | import com.apkupdater.prefs.Prefs 6 | import com.apkupdater.util.combine 7 | import kotlinx.coroutines.flow.Flow 8 | import kotlinx.coroutines.flow.catch 9 | import kotlinx.coroutines.flow.collect 10 | import kotlinx.coroutines.flow.flow 11 | 12 | 13 | class UpdatesRepository( 14 | private val appsRepository: AppsRepository, 15 | private val apkMirrorRepository: ApkMirrorRepository, 16 | private val gitHubRepository: GitHubRepository, 17 | private val fdroidRepository: FdroidRepository, 18 | private val izzyRepository: FdroidRepository, 19 | private val aptoideRepository: AptoideRepository, 20 | private val apkPureRepository: ApkPureRepository, 21 | private val gitLabRepository: GitLabRepository, 22 | private val playRepository: PlayRepository, 23 | private val prefs: Prefs 24 | ) { 25 | 26 | fun updates() = flow> { 27 | appsRepository.getApps().collect { result -> 28 | result.onSuccess { apps -> 29 | val filtered = apps.filter { !it.ignored } 30 | val sources = mutableListOf>>() 31 | if (prefs.useApkMirror.get()) sources.add(apkMirrorRepository.updates(filtered)) 32 | if (prefs.useGitHub.get()) sources.add(gitHubRepository.updates(filtered)) 33 | if (prefs.useFdroid.get()) sources.add(fdroidRepository.updates(filtered)) 34 | if (prefs.useIzzy.get()) sources.add(izzyRepository.updates(filtered)) 35 | if (prefs.useAptoide.get()) sources.add(aptoideRepository.updates(filtered)) 36 | if (prefs.useApkPure.get()) sources.add(apkPureRepository.updates(filtered)) 37 | if (prefs.useGitLab.get()) sources.add(gitLabRepository.updates(filtered)) 38 | if (prefs.usePlay.get()) sources.add(playRepository.updates(filtered)) 39 | 40 | if (sources.isNotEmpty()) { 41 | sources 42 | .combine { updates -> emit(updates.flatMap { it }) } 43 | .collect() 44 | } else { 45 | emit(emptyList()) 46 | } 47 | }.onFailure { 48 | Log.e("UpdatesRepository", "Error getting apps", it) 49 | } 50 | } 51 | }.catch { 52 | Log.e("UpdatesRepository", "Error getting updates", it) 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/apkupdater/service/ApkMirrorService.kt: -------------------------------------------------------------------------------- 1 | package com.apkupdater.service 2 | 3 | import com.apkupdater.data.apkmirror.AppExistsRequest 4 | import com.apkupdater.data.apkmirror.AppExistsResponse 5 | import com.apkupdater.BuildConfig 6 | import retrofit2.http.Body 7 | import retrofit2.http.Headers 8 | import retrofit2.http.POST 9 | 10 | 11 | interface ApkMirrorService { 12 | 13 | @Headers( 14 | "User-Agent: APKUpdater-v" + BuildConfig.VERSION_NAME, 15 | "Authorization: Basic YXBpLWFwa3VwZGF0ZXI6cm01cmNmcnVVakt5MDRzTXB5TVBKWFc4", 16 | "Content-Type: application/json" 17 | ) 18 | @POST("/wp-json/apkm/v1/app_exists/") 19 | suspend fun appExists( 20 | @Body request: AppExistsRequest 21 | ): AppExistsResponse 22 | 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/apkupdater/service/ApkPureService.kt: -------------------------------------------------------------------------------- 1 | package com.apkupdater.service 2 | 3 | import com.apkupdater.data.apkpure.GetAppUpdate 4 | import com.apkupdater.data.apkpure.GetAppUpdateResponse 5 | import com.apkupdater.data.apkpure.SearchResponse 6 | import retrofit2.http.Body 7 | import retrofit2.http.GET 8 | import retrofit2.http.Header 9 | import retrofit2.http.Headers 10 | import retrofit2.http.POST 11 | import retrofit2.http.Query 12 | 13 | 14 | interface ApkPureService { 15 | 16 | @Headers( 17 | "content-type: application/json", 18 | "ual-access-businessid: projecta" 19 | ) 20 | @POST("v3/get_app_update") 21 | suspend fun getAppUpdate( 22 | @Header("ual-access-projecta") header: String, 23 | @Body request: GetAppUpdate 24 | ): GetAppUpdateResponse 25 | 26 | @Headers( 27 | "ual-access-businessid: projecta", 28 | ) 29 | @GET("v3/search_query_new") 30 | suspend fun search( 31 | @Header("ual-access-projecta") header: String, 32 | @Query("key") key: String 33 | ): SearchResponse 34 | 35 | } 36 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/apkupdater/service/AptoideService.kt: -------------------------------------------------------------------------------- 1 | package com.apkupdater.service 2 | 3 | import com.apkupdater.data.aptoide.ListAppUpdatesResponse 4 | import com.apkupdater.data.aptoide.ListAppsUpdatesRequest 5 | import com.apkupdater.data.aptoide.ListSearchAppsRequest 6 | import com.apkupdater.data.aptoide.ListSearchAppsResponse 7 | import retrofit2.http.Body 8 | import retrofit2.http.Header 9 | import retrofit2.http.POST 10 | import retrofit2.http.Query 11 | 12 | interface AptoideService { 13 | 14 | @POST("listSearchApps") 15 | suspend fun searchApps(@Body request: ListSearchAppsRequest): ListSearchAppsResponse 16 | 17 | @POST("listAppsUpdates") 18 | suspend fun findUpdates( 19 | @Body request: ListAppsUpdatesRequest, 20 | @Header("X-Bypass-Cache") bypassCache: Boolean = true, 21 | @Query("aab") showAabs: Boolean = false 22 | ): ListAppUpdatesResponse 23 | 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/apkupdater/service/FdroidService.kt: -------------------------------------------------------------------------------- 1 | package com.apkupdater.service 2 | 3 | import okhttp3.ResponseBody 4 | import retrofit2.http.GET 5 | import retrofit2.http.Streaming 6 | import retrofit2.http.Url 7 | 8 | interface FdroidService { 9 | 10 | @Streaming 11 | @GET 12 | suspend fun getJar(@Url url: String): ResponseBody 13 | 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/apkupdater/service/GitHubService.kt: -------------------------------------------------------------------------------- 1 | package com.apkupdater.service 2 | 3 | import com.apkupdater.data.github.GitHubRelease 4 | import retrofit2.http.GET 5 | import retrofit2.http.Path 6 | 7 | interface GitHubService { 8 | 9 | @GET("/repos/{user}/{repo}/releases") 10 | suspend fun getReleases( 11 | @Path("user") user: String = "rumboalla", 12 | @Path("repo") repo: String = "apkupdater" 13 | ): List 14 | 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/apkupdater/service/GitLabService.kt: -------------------------------------------------------------------------------- 1 | package com.apkupdater.service 2 | 3 | import com.apkupdater.data.gitlab.GitLabRelease 4 | import retrofit2.http.GET 5 | import retrofit2.http.Path 6 | 7 | 8 | interface GitLabService { 9 | 10 | @GET("api/v4/projects/{user}%2F{repo}/releases") 11 | suspend fun getReleases( 12 | @Path("user") user: String, 13 | @Path("repo") repo: String 14 | ): List 15 | 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/apkupdater/transform/Transforms.kt: -------------------------------------------------------------------------------- 1 | package com.apkupdater.transform 2 | 3 | import android.content.Context 4 | import android.content.pm.PackageInfo 5 | import android.net.Uri 6 | import android.os.Build 7 | import com.apkupdater.data.ui.AppInstalled 8 | import com.apkupdater.util.getSignatureSha1 9 | import com.apkupdater.util.getSignatureSha256 10 | import com.apkupdater.util.name 11 | 12 | 13 | @Suppress("DEPRECATION") 14 | fun PackageInfo.toAppInstalled(context: Context, ignored: List) = AppInstalled( 15 | name(context), 16 | packageName, 17 | versionName.orEmpty(), 18 | if (Build.VERSION.SDK_INT >= 28) longVersionCode else versionCode.toLong(), 19 | iconUri(packageName, applicationInfo.icon), 20 | ignored.contains(packageName), 21 | getSignatureSha1(), 22 | getSignatureSha256() 23 | ) 24 | 25 | fun iconUri(packageName: String, id: Int): Uri = Uri.parse("android.resource://$packageName/$id") 26 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/apkupdater/ui/activity/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.apkupdater.ui.activity 2 | 3 | import android.os.Bundle 4 | import androidx.activity.ComponentActivity 5 | import androidx.activity.compose.setContent 6 | import com.apkupdater.ui.screen.MainScreen 7 | 8 | 9 | class MainActivity : ComponentActivity() { 10 | override fun onCreate(savedInstanceState: Bundle?) { 11 | super.onCreate(savedInstanceState) 12 | setContent { MainScreen() } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/apkupdater/ui/component/Grid.kt: -------------------------------------------------------------------------------- 1 | package com.apkupdater.ui.component 2 | 3 | import android.content.res.Configuration 4 | import androidx.compose.foundation.layout.Arrangement 5 | import androidx.compose.foundation.layout.Box 6 | import androidx.compose.foundation.layout.PaddingValues 7 | import androidx.compose.foundation.layout.fillMaxSize 8 | import androidx.compose.foundation.layout.height 9 | import androidx.compose.foundation.lazy.LazyColumn 10 | import androidx.compose.foundation.lazy.grid.GridCells 11 | import androidx.compose.foundation.lazy.grid.LazyGridScope 12 | import androidx.compose.foundation.lazy.grid.LazyVerticalGrid 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.ui.Alignment 15 | import androidx.compose.ui.Modifier 16 | import androidx.compose.ui.platform.LocalConfiguration 17 | import androidx.compose.ui.unit.dp 18 | import androidx.tv.foundation.lazy.grid.TvGridCells 19 | import androidx.tv.foundation.lazy.grid.TvLazyGridScope 20 | import androidx.tv.foundation.lazy.grid.TvLazyVerticalGrid 21 | import com.apkupdater.prefs.Prefs 22 | import org.koin.androidx.compose.get 23 | 24 | @Composable 25 | fun LoadingGrid() { 26 | if (get().androidTvUi.get()) { 27 | TvShimmeringGrid() 28 | } else { 29 | ShimmeringGrid() 30 | } 31 | } 32 | 33 | @Composable 34 | fun ShimmeringGrid() = InstalledGrid(false) { 35 | items(16) { 36 | Box(Modifier.height(170.dp).shimmering(true)) 37 | } 38 | } 39 | 40 | @Composable 41 | fun TvShimmeringGrid() = TvInstalledGrid(false) { 42 | items(16) { 43 | Box(Modifier.height(155.dp).shimmering(true)) 44 | } 45 | } 46 | 47 | @Composable 48 | fun EmptyGrid( 49 | text: String = "" 50 | ) = Box(Modifier.fillMaxSize()) { 51 | if (text.isNotEmpty()) { 52 | MediumTitle(text, Modifier.align(Alignment.Center)) 53 | } 54 | LazyColumn(Modifier.fillMaxSize()) {} 55 | } 56 | 57 | @Composable 58 | fun InstalledGrid( 59 | scroll: Boolean = true, 60 | content: LazyGridScope.() -> Unit 61 | ) = LazyVerticalGrid( 62 | columns = GridCells.Fixed(getNumColumns(LocalConfiguration.current.orientation)), 63 | contentPadding = PaddingValues(horizontal = 8.dp, vertical = 8.dp), 64 | verticalArrangement = Arrangement.spacedBy(8.dp), 65 | horizontalArrangement = Arrangement.spacedBy(8.dp), 66 | content = content, 67 | userScrollEnabled = scroll, 68 | modifier = Modifier.fillMaxSize() 69 | ) 70 | 71 | @Composable 72 | fun TvInstalledGrid(scroll: Boolean = true, content: TvLazyGridScope.() -> Unit) = TvLazyVerticalGrid( 73 | columns = TvGridCells.Fixed(getTvNumColumns()), 74 | contentPadding = PaddingValues(horizontal = 8.dp, vertical = 8.dp), 75 | verticalArrangement = Arrangement.spacedBy(8.dp), 76 | horizontalArrangement = Arrangement.spacedBy(8.dp), 77 | content = content, 78 | userScrollEnabled = scroll, 79 | modifier = Modifier.fillMaxSize() 80 | ) 81 | 82 | @Composable 83 | fun getNumColumns(orientation: Int): Int { 84 | val prefs = get() 85 | return if(orientation == Configuration.ORIENTATION_PORTRAIT) 86 | prefs.portraitColumns.get() 87 | else 88 | prefs.landscapeColumns.get() 89 | } 90 | 91 | @Composable 92 | fun getTvNumColumns(): Int { 93 | return if(LocalConfiguration.current.orientation == Configuration.ORIENTATION_PORTRAIT) 94 | 1 95 | else 96 | 2 97 | } 98 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/apkupdater/ui/component/Image.kt: -------------------------------------------------------------------------------- 1 | package com.apkupdater.ui.component 2 | 3 | import android.net.Uri 4 | import androidx.compose.foundation.background 5 | import androidx.compose.foundation.layout.fillMaxSize 6 | import androidx.compose.foundation.layout.height 7 | import androidx.compose.foundation.layout.padding 8 | import androidx.compose.foundation.shape.RoundedCornerShape 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.draw.clip 12 | import androidx.compose.ui.graphics.Color 13 | import androidx.compose.ui.layout.ContentScale 14 | import androidx.compose.ui.platform.LocalContext 15 | import androidx.compose.ui.res.painterResource 16 | import androidx.compose.ui.res.stringResource 17 | import androidx.compose.ui.unit.dp 18 | import coil.compose.AsyncImage 19 | import coil.request.ImageRequest 20 | import com.apkupdater.R 21 | import com.apkupdater.util.getAppIcon 22 | 23 | @Composable 24 | private fun BaseLoadingImage( 25 | request: ImageRequest, 26 | modifier: Modifier, 27 | color: Color = Color.Transparent 28 | ) = AsyncImage( 29 | model = request, 30 | contentDescription = stringResource(R.string.app_cd), 31 | modifier = modifier 32 | .padding(10.dp) 33 | .clip(RoundedCornerShape(8.dp)) 34 | .background(color), 35 | contentScale = ContentScale.Fit, 36 | error = painterResource(R.drawable.ic_root), 37 | placeholder = painterResource(R.drawable.ic_empty) 38 | ) 39 | 40 | @Composable 41 | fun LoadingImage( 42 | uri: Uri, 43 | modifier: Modifier = Modifier.height(120.dp).fillMaxSize(), 44 | crossfade: Boolean = true, 45 | color: Color = Color.Transparent 46 | ) = BaseLoadingImage( 47 | ImageRequest.Builder(LocalContext.current).data(uri).crossfade(crossfade).build(), 48 | modifier, 49 | color 50 | ) 51 | 52 | @Composable 53 | fun LoadingImageApp( 54 | packageName: String, 55 | modifier: Modifier = Modifier.height(120.dp).fillMaxSize(), 56 | crossfade: Boolean = false, 57 | color: Color = Color.Transparent 58 | ) = BaseLoadingImage( 59 | ImageRequest.Builder(LocalContext.current).data(LocalContext.current.getAppIcon(packageName)).crossfade(crossfade).build(), 60 | modifier, 61 | color 62 | ) 63 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/apkupdater/ui/component/Modifier.kt: -------------------------------------------------------------------------------- 1 | package com.apkupdater.ui.component 2 | 3 | import androidx.compose.animation.core.animateFloat 4 | import androidx.compose.animation.core.infiniteRepeatable 5 | import androidx.compose.animation.core.rememberInfiniteTransition 6 | import androidx.compose.animation.core.tween 7 | import androidx.compose.foundation.background 8 | import androidx.compose.material3.MaterialTheme 9 | import androidx.compose.runtime.getValue 10 | import androidx.compose.runtime.mutableStateOf 11 | import androidx.compose.runtime.remember 12 | import androidx.compose.runtime.setValue 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.composed 15 | import androidx.compose.ui.geometry.Offset 16 | import androidx.compose.ui.graphics.Brush 17 | import androidx.compose.ui.layout.onGloballyPositioned 18 | import androidx.compose.ui.unit.IntSize 19 | import com.apkupdater.ui.theme.statusBarColor 20 | 21 | 22 | fun Modifier.shimmering(enabled: Boolean): Modifier = composed { 23 | if (enabled) { 24 | var size by remember { mutableStateOf(IntSize.Zero) } 25 | val transition = rememberInfiniteTransition("shimmering") 26 | val color = MaterialTheme.colorScheme.statusBarColor() 27 | val startOffsetX by transition.animateFloat( 28 | label = "shimmering", 29 | initialValue = -2 * size.width.toFloat(), 30 | targetValue = 2 * size.width.toFloat(), 31 | animationSpec = infiniteRepeatable(animation = tween(1000)) 32 | ) 33 | background( 34 | brush = Brush.linearGradient( 35 | colors = listOf( 36 | color.copy(alpha = 0.9f), 37 | color.copy(alpha = 0.3f), 38 | color.copy(alpha = 0.9f) 39 | ), 40 | start = Offset(startOffsetX, 0f), 41 | end = Offset(startOffsetX + size.width.toFloat(), size.height.toFloat()) 42 | ) 43 | ).onGloballyPositioned { 44 | size = it.size 45 | } 46 | } else { 47 | Modifier 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/apkupdater/ui/component/UiComponents.kt: -------------------------------------------------------------------------------- 1 | package com.apkupdater.ui.component 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.fillMaxSize 6 | import androidx.compose.foundation.layout.padding 7 | import androidx.compose.foundation.layout.size 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.Alignment 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.draw.alpha 12 | import androidx.compose.ui.platform.LocalContext 13 | import androidx.compose.ui.res.stringResource 14 | import androidx.compose.ui.unit.dp 15 | import com.apkupdater.R 16 | import com.apkupdater.data.ui.AppInstalled 17 | import com.apkupdater.data.ui.AppUpdate 18 | import com.apkupdater.data.ui.Link 19 | import com.apkupdater.util.getAppName 20 | 21 | 22 | @Composable 23 | fun AppImage(app: AppInstalled, onIgnore: (String) -> Unit = {}) = Box { 24 | LoadingImageApp(app.packageName) 25 | TextBubble(app.versionCode, Modifier.align(Alignment.BottomStart)) 26 | IgnoreIcon( 27 | app.ignored, 28 | { onIgnore(app.packageName) }, 29 | Modifier.align(Alignment.TopEnd).padding(4.dp) 30 | ) 31 | } 32 | 33 | @Composable 34 | fun UpdateImage(app: AppUpdate, onInstall: (Link) -> Unit = {}) = Box { 35 | LoadingImageApp(app.packageName) 36 | TextBubble(app.versionCode, Modifier.align(Alignment.BottomStart)) 37 | InstallProgressIcon(app.isInstalling) { onInstall(app.link) } 38 | SourceIcon( 39 | app.source, 40 | Modifier.align(Alignment.TopStart).padding(4.dp).size(28.dp) 41 | ) 42 | } 43 | 44 | 45 | @Composable 46 | fun SearchImage(app: AppUpdate, onInstall: (Link) -> Unit = {}) = Box { 47 | LoadingImage(app.iconUri) 48 | TextBubble(app.versionCode, Modifier.align(Alignment.BottomStart)) 49 | InstallProgressIcon(app.isInstalling) { onInstall(app.link) } 50 | SourceIcon( 51 | app.source, 52 | Modifier.align(Alignment.TopStart).padding(4.dp).size(28.dp) 53 | ) 54 | } 55 | 56 | @Composable 57 | fun InstalledItem(app: AppInstalled, onIgnore: (String) -> Unit = {}) = Column( 58 | modifier = Modifier.alpha(if (app.ignored) 0.5f else 1f) 59 | ) { 60 | AppImage(app, onIgnore) 61 | Column(Modifier.padding(top = 4.dp)) { 62 | ScrollableText { SmallText(app.packageName) } 63 | MediumTitle(app.name) 64 | } 65 | } 66 | 67 | @Composable 68 | fun UpdateItem(app: AppUpdate, onInstall: (Link) -> Unit = {}) = Column { 69 | UpdateImage(app, onInstall) 70 | Column(Modifier.padding(top = 4.dp)) { 71 | ScrollableText { SmallText(app.packageName) } 72 | MediumTitle(app.name.ifEmpty { LocalContext.current.getAppName(app.packageName) }) 73 | } 74 | } 75 | 76 | @Composable 77 | fun SearchItem(app: AppUpdate, onInstall: (Link) -> Unit = {}) = Column { 78 | SearchImage(app, onInstall) 79 | Column(Modifier.padding(top = 4.dp)) { 80 | ScrollableText { SmallText(app.packageName) } 81 | MediumTitle(app.name) 82 | } 83 | } 84 | 85 | @Composable 86 | fun DefaultErrorScreen() = Box(Modifier.fillMaxSize()) { 87 | HugeText( 88 | stringResource(R.string.something_went_wrong), 89 | Modifier.align(Alignment.Center), 90 | 2 91 | ) 92 | } 93 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/apkupdater/ui/screen/AppsScreen.kt: -------------------------------------------------------------------------------- 1 | package com.apkupdater.ui.screen 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.size 6 | import androidx.compose.foundation.lazy.grid.items 7 | import androidx.compose.material.icons.Icons 8 | import androidx.compose.material.icons.filled.Home 9 | import androidx.compose.material3.ExperimentalMaterial3Api 10 | import androidx.compose.material3.Icon 11 | import androidx.compose.material3.IconButton 12 | import androidx.compose.material3.MaterialTheme 13 | import androidx.compose.material3.Text 14 | import androidx.compose.material3.TopAppBar 15 | import androidx.compose.material3.TopAppBarDefaults 16 | import androidx.compose.material3.minimumInteractiveComponentSize 17 | import androidx.compose.runtime.Composable 18 | import androidx.compose.ui.Alignment 19 | import androidx.compose.ui.Modifier 20 | import androidx.compose.ui.res.stringResource 21 | import androidx.compose.ui.unit.dp 22 | import androidx.lifecycle.compose.collectAsStateWithLifecycle 23 | import androidx.tv.foundation.lazy.grid.items 24 | import com.apkupdater.R 25 | import com.apkupdater.data.ui.AppsUiState 26 | import com.apkupdater.prefs.Prefs 27 | import com.apkupdater.ui.component.DefaultErrorScreen 28 | import com.apkupdater.ui.component.ExcludeAppStoreIcon 29 | import com.apkupdater.ui.component.ExcludeDisabledIcon 30 | import com.apkupdater.ui.component.ExcludeSystemIcon 31 | import com.apkupdater.ui.component.InstalledGrid 32 | import com.apkupdater.ui.component.InstalledItem 33 | import com.apkupdater.ui.component.LoadingGrid 34 | import com.apkupdater.ui.component.TvInstalledGrid 35 | import com.apkupdater.ui.component.TvInstalledItem 36 | import com.apkupdater.ui.theme.statusBarColor 37 | import com.apkupdater.viewmodel.AppsViewModel 38 | import org.koin.androidx.compose.get 39 | import org.koin.androidx.compose.koinViewModel 40 | 41 | 42 | @Composable 43 | fun AppsScreen( 44 | viewModel: AppsViewModel = koinViewModel() 45 | ) { 46 | viewModel.state().collectAsStateWithLifecycle().value.onLoading { 47 | AppsScreenLoading(viewModel, it) 48 | }.onError { 49 | AppsScreenError() 50 | }.onSuccess { 51 | AppsScreenSuccess(viewModel, it) 52 | } 53 | } 54 | 55 | @Composable 56 | fun AppsScreenSuccess(viewModel: AppsViewModel, state: AppsUiState.Success) = Column { 57 | AppsTopBar(viewModel, state.excludeSystem, state.excludeAppStore, state.excludeDisabled) 58 | if (get().androidTvUi.get()) { 59 | TvInstalledGrid { 60 | items(state.apps) { 61 | TvInstalledItem(it) { app -> viewModel.ignore(app) } 62 | } 63 | } 64 | } else { 65 | InstalledGrid { 66 | items(state.apps) { 67 | InstalledItem(it) { app -> viewModel.ignore(app) } 68 | } 69 | } 70 | } 71 | } 72 | 73 | @Composable 74 | fun AppsScreenLoading(viewModel: AppsViewModel, state: AppsUiState.Loading) = Column { 75 | AppsTopBar(viewModel, state.excludeSystem, state.excludeAppStore, state.excludeDisabled) 76 | LoadingGrid() 77 | } 78 | 79 | @OptIn(ExperimentalMaterial3Api::class) 80 | @Composable 81 | fun AppsTopBar( 82 | viewModel: AppsViewModel, 83 | excludeSystem: Boolean, 84 | excludeAppStore: Boolean, 85 | excludeDisabled: Boolean 86 | ) = TopAppBar( 87 | title = { Text(stringResource(R.string.tab_apps)) }, 88 | colors = TopAppBarDefaults.topAppBarColors(containerColor = MaterialTheme.colorScheme.statusBarColor()), 89 | actions = { 90 | IconButton(onClick = { viewModel.onSystemClick() }) { 91 | ExcludeSystemIcon(excludeSystem) 92 | } 93 | IconButton(onClick = { viewModel.onAppStoreClick() }) { 94 | ExcludeAppStoreIcon(excludeAppStore) 95 | } 96 | IconButton(onClick = { viewModel.onDisabledClick() }) { 97 | ExcludeDisabledIcon(excludeDisabled) 98 | } 99 | }, 100 | navigationIcon = { 101 | Box(Modifier.minimumInteractiveComponentSize().size(40.dp), Alignment.Center) { 102 | Icon(Icons.Filled.Home, "Tab Icon") 103 | } 104 | } 105 | ) 106 | 107 | @Composable 108 | fun AppsScreenError() = DefaultErrorScreen() 109 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/apkupdater/ui/screen/UpdatesScreen.kt: -------------------------------------------------------------------------------- 1 | package com.apkupdater.ui.screen 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.size 6 | import androidx.compose.foundation.lazy.grid.items 7 | import androidx.compose.material.icons.Icons 8 | import androidx.compose.material.icons.filled.ThumbUp 9 | import androidx.compose.material3.ExperimentalMaterial3Api 10 | import androidx.compose.material3.Icon 11 | import androidx.compose.material3.IconButton 12 | import androidx.compose.material3.MaterialTheme 13 | import androidx.compose.material3.Text 14 | import androidx.compose.material3.TopAppBar 15 | import androidx.compose.material3.TopAppBarDefaults 16 | import androidx.compose.material3.minimumInteractiveComponentSize 17 | import androidx.compose.runtime.Composable 18 | import androidx.compose.ui.Alignment 19 | import androidx.compose.ui.Modifier 20 | import androidx.compose.ui.platform.LocalUriHandler 21 | import androidx.compose.ui.platform.UriHandler 22 | import androidx.compose.ui.res.stringResource 23 | import androidx.compose.ui.unit.dp 24 | import androidx.lifecycle.compose.collectAsStateWithLifecycle 25 | import androidx.tv.foundation.lazy.grid.items 26 | import com.apkupdater.R 27 | import com.apkupdater.data.ui.AppUpdate 28 | import com.apkupdater.prefs.Prefs 29 | import com.apkupdater.ui.component.DefaultErrorScreen 30 | import com.apkupdater.ui.component.EmptyGrid 31 | import com.apkupdater.ui.component.InstalledGrid 32 | import com.apkupdater.ui.component.LoadingGrid 33 | import com.apkupdater.ui.component.RefreshIcon 34 | import com.apkupdater.ui.component.TvInstalledGrid 35 | import com.apkupdater.ui.component.TvUpdateItem 36 | import com.apkupdater.ui.component.UpdateItem 37 | import com.apkupdater.ui.theme.statusBarColor 38 | import com.apkupdater.viewmodel.UpdatesViewModel 39 | import org.koin.androidx.compose.get 40 | 41 | 42 | @Composable 43 | fun UpdatesScreen(viewModel: UpdatesViewModel) { 44 | viewModel.state().collectAsStateWithLifecycle().value.onLoading { 45 | UpdatesScreenLoading(viewModel) 46 | }.onError { 47 | UpdatesScreenError() 48 | }.onSuccess { 49 | UpdatesScreenSuccess(viewModel, it.updates) 50 | } 51 | } 52 | 53 | @OptIn(ExperimentalMaterial3Api::class) 54 | @Composable 55 | fun UpdatesTopBar(viewModel: UpdatesViewModel) = TopAppBar( 56 | title = { 57 | Text(stringResource(R.string.tab_updates)) 58 | }, 59 | colors = TopAppBarDefaults.topAppBarColors(containerColor = MaterialTheme.colorScheme.statusBarColor()), 60 | actions = { 61 | IconButton(onClick = { viewModel.refresh() }) { 62 | RefreshIcon(stringResource(R.string.refresh_updates)) 63 | } 64 | }, 65 | navigationIcon = { 66 | Box(Modifier.minimumInteractiveComponentSize().size(40.dp), Alignment.Center) { 67 | Icon(Icons.Filled.ThumbUp, "Tab Icon") 68 | } 69 | } 70 | ) 71 | 72 | @Composable 73 | fun UpdatesScreenLoading(viewModel: UpdatesViewModel) = Column { 74 | UpdatesTopBar(viewModel) 75 | LoadingGrid() 76 | } 77 | 78 | @Composable 79 | fun UpdatesScreenError() = DefaultErrorScreen() 80 | 81 | @Composable 82 | fun UpdatesScreenSuccess( 83 | viewModel: UpdatesViewModel, 84 | updates: List 85 | ) = Column { 86 | val handler = LocalUriHandler.current 87 | val tv = get().androidTvUi.get() 88 | 89 | UpdatesTopBar(viewModel) 90 | 91 | when { 92 | updates.isEmpty() -> EmptyGrid() 93 | tv -> TvGrid(viewModel, updates, handler) 94 | !tv -> Grid(viewModel, updates, handler) 95 | } 96 | } 97 | 98 | @Composable 99 | fun TvGrid( 100 | viewModel: UpdatesViewModel, 101 | updates: List, 102 | handler: UriHandler 103 | ) = TvInstalledGrid { 104 | items(updates) { update -> 105 | TvUpdateItem( 106 | update, 107 | { viewModel.install(update, handler) }, 108 | { viewModel.ignoreVersion(update.id)} 109 | ) 110 | } 111 | } 112 | 113 | @Composable 114 | fun Grid( 115 | viewModel: UpdatesViewModel, 116 | updates: List, 117 | handler: UriHandler 118 | ) = InstalledGrid { 119 | items(updates) { update -> 120 | UpdateItem(update) { 121 | viewModel.install(update, handler) 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/apkupdater/ui/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package com.apkupdater.ui.theme 2 | 3 | import android.app.Activity 4 | import android.os.Build 5 | import androidx.compose.material3.ColorScheme 6 | import androidx.compose.material3.MaterialTheme 7 | import androidx.compose.material3.darkColorScheme 8 | import androidx.compose.material3.dynamicDarkColorScheme 9 | import androidx.compose.material3.dynamicLightColorScheme 10 | import androidx.compose.material3.lightColorScheme 11 | import androidx.compose.material3.surfaceColorAtElevation 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.runtime.SideEffect 14 | import androidx.compose.ui.graphics.toArgb 15 | import androidx.compose.ui.platform.LocalContext 16 | import androidx.compose.ui.platform.LocalView 17 | import androidx.compose.ui.unit.dp 18 | import androidx.core.view.WindowCompat 19 | import com.apkupdater.util.isDark 20 | 21 | 22 | @Composable 23 | fun AppTheme( 24 | darkTheme: Boolean, 25 | dynamicColor: Boolean = true, 26 | content: @Composable () -> Unit 27 | ) { 28 | val colorScheme = when { 29 | dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { 30 | val context = LocalContext.current 31 | if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) 32 | } 33 | darkTheme -> darkColorScheme() 34 | else -> lightColorScheme() 35 | } 36 | 37 | val view = LocalView.current 38 | if (!view.isInEditMode) { 39 | SideEffect { 40 | val activity = view.context as Activity 41 | 42 | // Set Navigation Bar color 43 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 44 | activity.window.navigationBarColor = colorScheme.statusBarColor().toArgb() 45 | WindowCompat.getInsetsController( 46 | activity.window, 47 | view 48 | ).isAppearanceLightNavigationBars = !darkTheme 49 | } 50 | 51 | // Set Status Bar color 52 | activity.window.statusBarColor = colorScheme.statusBarColor().toArgb() 53 | WindowCompat.getInsetsController( 54 | activity.window, 55 | view 56 | ).isAppearanceLightStatusBars = !darkTheme 57 | } 58 | } 59 | 60 | MaterialTheme(colorScheme = colorScheme, content = content) 61 | } 62 | 63 | fun ColorScheme.statusBarColor() = surfaceColorAtElevation(3.dp) 64 | 65 | fun isDarkTheme(theme: Int): Boolean { 66 | if (theme == 1) return true 67 | if (theme == 2) return false 68 | return isDark() 69 | } 70 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/apkupdater/util/Badger.kt: -------------------------------------------------------------------------------- 1 | package com.apkupdater.util 2 | 3 | import com.apkupdater.data.ui.Screen 4 | import kotlinx.coroutines.flow.MutableStateFlow 5 | 6 | 7 | class Badger { 8 | 9 | private val badges = MutableStateFlow(mapOf( 10 | Screen.Apps.route to "", 11 | Screen.Search.route to "", 12 | Screen.Updates.route to "", 13 | Screen.Settings.route to "" 14 | )) 15 | 16 | fun flow() = badges 17 | 18 | fun changeSearchBadge(number: String) = changeBadge(Screen.Search.route, number) 19 | 20 | fun changeAppsBadge(number: String) = changeBadge(Screen.Apps.route, number) 21 | 22 | fun changeUpdatesBadge(number: String) = changeBadge(Screen.Updates.route, number) 23 | 24 | private fun changeBadge(route: String, number: String) { 25 | val finalNumber = if (number.toIntOrNull() == 0) "" else number 26 | val newBadges = badges.value.toMutableMap() 27 | newBadges[route] = finalNumber 28 | badges.value = newBadges 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/apkupdater/util/Clipboard.kt: -------------------------------------------------------------------------------- 1 | package com.apkupdater.util 2 | 3 | import android.content.ClipData 4 | import android.content.Context 5 | 6 | 7 | class Clipboard(private val context: Context) { 8 | 9 | fun copy(text: String, label: String = "Copied Text") { 10 | val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as android.content.ClipboardManager 11 | clipboard.setPrimaryClip(ClipData.newPlainText(label, text)) 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/apkupdater/util/Downloader.kt: -------------------------------------------------------------------------------- 1 | package com.apkupdater.util 2 | 3 | import android.util.Log 4 | import okhttp3.OkHttpClient 5 | import okhttp3.Request 6 | import java.io.File 7 | import java.io.InputStream 8 | 9 | 10 | class Downloader( 11 | private val client: OkHttpClient, 12 | private val apkPureClient: OkHttpClient, 13 | private val auroraClient: OkHttpClient, 14 | private val dir: File 15 | ) { 16 | 17 | fun download(url: String): File { 18 | val file = File(dir, randomUUID()) 19 | client.newCall(downloadRequest(url)).execute().use { 20 | if (it.isSuccessful) { 21 | it.body?.byteStream()?.copyTo(file.outputStream()) 22 | } 23 | } 24 | return file 25 | } 26 | 27 | fun downloadStream(url: String): InputStream? = runCatching { 28 | val c = when { 29 | url.contains("apkpure") -> apkPureClient 30 | url.contains("aurora") -> auroraClient 31 | else -> client 32 | } 33 | val response = c.newCall(downloadRequest(url)).execute() 34 | if (response.isSuccessful) { 35 | response.body?.let { 36 | return it.byteStream() 37 | } 38 | } else { 39 | response.close() 40 | Log.e("Downloader", "Download failed with error code: ${response.code}") 41 | } 42 | return null 43 | }.getOrElse { 44 | Log.e("Downloader", "Error downloading", it) 45 | null 46 | } 47 | 48 | private fun downloadRequest(url: String) = Request.Builder().url(url).build() 49 | 50 | } 51 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/apkupdater/util/InstallLog.kt: -------------------------------------------------------------------------------- 1 | package com.apkupdater.util 2 | 3 | import com.apkupdater.data.ui.AppInstallProgress 4 | import com.apkupdater.data.ui.AppInstallStatus 5 | import kotlinx.coroutines.flow.MutableSharedFlow 6 | import kotlinx.coroutines.flow.asSharedFlow 7 | 8 | 9 | class InstallLog { 10 | 11 | private val status = MutableSharedFlow(100) 12 | private val progress = MutableSharedFlow(100) 13 | private var currentInstallLog: Int = 0 14 | 15 | fun status() = status.asSharedFlow() 16 | fun progress() = progress.asSharedFlow() 17 | 18 | fun cancelCurrentInstall() = status.tryEmit(AppInstallStatus(false, currentInstallLog, false)) 19 | fun emitStatus(newStatus: AppInstallStatus) = status.tryEmit(newStatus) 20 | fun emitProgress(newProgress: AppInstallProgress) = progress.tryEmit(newProgress) 21 | 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/apkupdater/util/SnackBar.kt: -------------------------------------------------------------------------------- 1 | package com.apkupdater.util 2 | 3 | import androidx.compose.material3.SnackbarVisuals 4 | import kotlinx.coroutines.CoroutineScope 5 | import kotlinx.coroutines.Dispatchers 6 | import kotlinx.coroutines.flow.MutableSharedFlow 7 | import kotlinx.coroutines.flow.SharedFlow 8 | import kotlinx.coroutines.launch 9 | 10 | class SnackBar { 11 | 12 | private val snackBars = MutableSharedFlow() 13 | 14 | fun flow(): SharedFlow = snackBars 15 | 16 | fun snackBar( 17 | scope: CoroutineScope = CoroutineScope(Dispatchers.IO), 18 | message: SnackbarVisuals 19 | ) = scope.launch { snackBars.emit(message) } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/apkupdater/util/Stringer.kt: -------------------------------------------------------------------------------- 1 | package com.apkupdater.util 2 | 3 | import android.content.Context 4 | 5 | class Stringer(val context: Context) { 6 | 7 | fun get(id: Int) = context.getString(id) 8 | fun get(id: Int, vararg params: Any?) = context.getString(id, *params) 9 | 10 | } 11 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/apkupdater/util/Themer.kt: -------------------------------------------------------------------------------- 1 | package com.apkupdater.util 2 | 3 | import com.apkupdater.prefs.Prefs 4 | import com.apkupdater.ui.theme.isDarkTheme 5 | import kotlinx.coroutines.flow.MutableStateFlow 6 | 7 | class Themer(prefs: Prefs) { 8 | 9 | private val theme = MutableStateFlow(isDarkTheme(prefs.theme.get())) 10 | 11 | fun flow() = theme 12 | 13 | fun setTheme(v: Boolean) { theme.value = v } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/apkupdater/util/UpdatesNotification.kt: -------------------------------------------------------------------------------- 1 | package com.apkupdater.util 2 | 3 | import android.Manifest 4 | import android.annotation.SuppressLint 5 | import android.app.NotificationChannel 6 | import android.app.NotificationManager 7 | import android.app.PendingIntent 8 | import android.content.Context 9 | import android.content.Intent 10 | import android.os.Build 11 | import androidx.activity.compose.ManagedActivityResultLauncher 12 | import androidx.core.app.NotificationCompat 13 | import androidx.core.app.NotificationManagerCompat 14 | import com.apkupdater.R 15 | import com.apkupdater.ui.activity.MainActivity 16 | 17 | class UpdatesNotification(private val context: Context) { 18 | 19 | companion object { 20 | const val UpdateAction = "updateAction" 21 | } 22 | 23 | private val notificationManager get() = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager 24 | private val channelId = context.getString(R.string.notification_channel_id) 25 | private val channelName = context.getString(R.string.notification_channel_name) 26 | private val updateTitle = context.getString(R.string.notification_update_title) 27 | private val updateId = 42 28 | 29 | @SuppressLint("MissingPermission") 30 | fun showUpdateNotification(num: Int) { 31 | // Intent for the notification click 32 | val intent = Intent(context, MainActivity::class.java).apply { 33 | flags = Intent.FLAG_ACTIVITY_REORDER_TO_FRONT 34 | action = UpdateAction 35 | } 36 | 37 | val builder = NotificationCompat.Builder(context, channelId) 38 | .setSmallIcon(R.drawable.ic_install) 39 | .setContentTitle(updateTitle) 40 | .setContentText(context.resources.getQuantityString(R.plurals.notification_update_description, num, num)) 41 | .setPriority(NotificationCompat.PRIORITY_DEFAULT) 42 | .setContentIntent(PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT + PendingIntent.FLAG_IMMUTABLE)) 43 | .setAutoCancel(true) 44 | 45 | createNotificationChannel() 46 | if (areNotificationsEnabled()) { 47 | NotificationManagerCompat.from(context).notify(updateId, builder.build()) 48 | } 49 | } 50 | 51 | fun checkNotificationPermission(launcher: ManagedActivityResultLauncher) { 52 | if (Build.VERSION.SDK_INT >= 33) { 53 | if (!areNotificationsEnabled()) { 54 | launcher.launch(Manifest.permission.POST_NOTIFICATIONS) 55 | } 56 | } 57 | } 58 | 59 | private fun areNotificationsEnabled() = NotificationManagerCompat 60 | .from(context) 61 | .areNotificationsEnabled() 62 | 63 | private fun createNotificationChannel() { 64 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 65 | val channel = NotificationChannel(channelId, channelName, NotificationManager.IMPORTANCE_DEFAULT).apply { 66 | description = context.getString(R.string.notification_channel_description) 67 | } 68 | notificationManager.createNotificationChannel(channel) 69 | } 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/apkupdater/util/play/EglExtensionProvider.kt: -------------------------------------------------------------------------------- 1 | package com.apkupdater.util.play 2 | 3 | import android.opengl.GLES10 4 | import android.text.TextUtils 5 | import javax.microedition.khronos.egl.EGL10 6 | import javax.microedition.khronos.egl.EGLConfig 7 | import javax.microedition.khronos.egl.EGLContext 8 | import javax.microedition.khronos.egl.EGLDisplay 9 | 10 | 11 | object EglExtensionProvider { 12 | @JvmStatic 13 | val eglExtensions: List 14 | get() { 15 | val glExtensions: MutableSet = HashSet() 16 | val egl10 = EGLContext.getEGL() as EGL10 17 | val display = egl10.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY) 18 | egl10.eglInitialize(display, IntArray(2)) 19 | val cf = IntArray(1) 20 | if (egl10.eglGetConfigs(display, null, 0, cf)) { 21 | val configs = arrayOfNulls(cf[0]) 22 | if (egl10.eglGetConfigs(display, configs, cf[0], cf)) { 23 | val a1 = intArrayOf(EGL10.EGL_WIDTH, EGL10.EGL_PBUFFER_BIT, EGL10.EGL_HEIGHT, EGL10.EGL_PBUFFER_BIT, EGL10.EGL_NONE) 24 | val a2 = intArrayOf(12440, EGL10.EGL_PIXMAP_BIT, EGL10.EGL_NONE) 25 | val a3 = IntArray(1) 26 | for (i in 0 until cf[0]) { 27 | egl10.eglGetConfigAttrib(display, configs[i], EGL10.EGL_CONFIG_CAVEAT, a3) 28 | if (a3[0] != EGL10.EGL_SLOW_CONFIG) { 29 | egl10.eglGetConfigAttrib(display, configs[i], EGL10.EGL_SURFACE_TYPE, a3) 30 | if (1 and a3[0] != 0) { 31 | egl10.eglGetConfigAttrib(display, configs[i], EGL10.EGL_RENDERABLE_TYPE, a3) 32 | if (1 and a3[0] != 0) { 33 | addExtensionsForConfig(egl10, display, configs[i], a1, null, glExtensions) 34 | } 35 | if (4 and a3[0] != 0) { 36 | addExtensionsForConfig(egl10, display, configs[i], a1, a2, glExtensions) 37 | } 38 | } 39 | } 40 | } 41 | } 42 | } 43 | egl10.eglTerminate(display) 44 | return ArrayList(glExtensions).sorted() 45 | } 46 | 47 | private fun addExtensionsForConfig( 48 | egl10: EGL10, 49 | eglDisplay: EGLDisplay, 50 | eglConfig: EGLConfig?, 51 | ai: IntArray, 52 | ai1: IntArray?, 53 | set: MutableSet 54 | ) { 55 | val eglContext = egl10.eglCreateContext(eglDisplay, eglConfig, EGL10.EGL_NO_CONTEXT, ai1) 56 | if (eglContext === EGL10.EGL_NO_CONTEXT) { 57 | return 58 | } 59 | val eglSurface = egl10.eglCreatePbufferSurface(eglDisplay, eglConfig, ai) 60 | if (eglSurface === EGL10.EGL_NO_SURFACE) { 61 | egl10.eglDestroyContext(eglDisplay, eglContext) 62 | } else { 63 | egl10.eglMakeCurrent(eglDisplay, eglSurface, eglSurface, eglContext) 64 | val s = GLES10.glGetString(7939) 65 | if (!TextUtils.isEmpty(s)) { 66 | val split = s.split(" ".toRegex()).toTypedArray() 67 | val i = split.size 68 | set.addAll(listOf(*split).subList(0, i)) 69 | } 70 | egl10.eglMakeCurrent( 71 | eglDisplay, 72 | EGL10.EGL_NO_SURFACE, 73 | EGL10.EGL_NO_SURFACE, 74 | EGL10.EGL_NO_CONTEXT 75 | ) 76 | egl10.eglDestroySurface(eglDisplay, eglSurface) 77 | egl10.eglDestroyContext(eglDisplay, eglContext) 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/apkupdater/util/play/IProxyHttpClient.kt: -------------------------------------------------------------------------------- 1 | package com.apkupdater.util.play 2 | 3 | import com.aurora.gplayapi.network.IHttpClient 4 | 5 | 6 | interface IProxyHttpClient : IHttpClient { 7 | @Throws(UnsupportedOperationException::class) 8 | fun setProxy(proxyInfo: ProxyInfo): IHttpClient 9 | } 10 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/apkupdater/util/play/ProxyInfo.kt: -------------------------------------------------------------------------------- 1 | package com.apkupdater.util.play 2 | 3 | 4 | data class ProxyInfo( 5 | var protocol: String, 6 | var host: String, 7 | var port: Int, 8 | var proxyUser: String?, 9 | var proxyPassword: String? 10 | ) 11 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/apkupdater/viewmodel/AppsViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.apkupdater.viewmodel 2 | 3 | import android.util.Log 4 | import androidx.lifecycle.ViewModel 5 | import androidx.lifecycle.viewModelScope 6 | import com.apkupdater.data.ui.AppsUiState 7 | import com.apkupdater.prefs.Prefs 8 | import com.apkupdater.repository.AppsRepository 9 | import com.apkupdater.util.Badger 10 | import com.apkupdater.util.launchWithMutex 11 | import kotlinx.coroutines.Dispatchers 12 | import kotlinx.coroutines.flow.MutableStateFlow 13 | import kotlinx.coroutines.flow.StateFlow 14 | import kotlinx.coroutines.sync.Mutex 15 | 16 | class AppsViewModel( 17 | private val repository: AppsRepository, 18 | private val prefs: Prefs, 19 | private val badger: Badger 20 | ) : ViewModel() { 21 | 22 | private val mutex = Mutex() 23 | private val state = MutableStateFlow(buildLoadingState()) 24 | 25 | fun state(): StateFlow = state 26 | 27 | fun refresh(load: Boolean = true) = viewModelScope.launchWithMutex(mutex, Dispatchers.IO) { 28 | if (load) state.value = buildLoadingState() 29 | badger.changeAppsBadge("") 30 | repository.getApps().collect { 31 | it.onSuccess { apps -> 32 | state.value = AppsUiState.Success( 33 | apps, 34 | prefs.excludeSystem.get(), 35 | prefs.excludeStore.get(), 36 | prefs.excludeDisabled.get() 37 | ) 38 | badger.changeAppsBadge(apps.size.toString()) 39 | }.onFailure { ex -> 40 | state.value = AppsUiState.Error 41 | badger.changeAppsBadge("!") 42 | Log.e("InstalledViewModel", "Error getting apps.", ex) 43 | } 44 | } 45 | } 46 | 47 | fun onSystemClick() = viewModelScope.launchWithMutex(mutex, Dispatchers.Default) { 48 | prefs.excludeSystem.put(!prefs.excludeSystem.get()) 49 | refresh(false) 50 | } 51 | 52 | fun onAppStoreClick() = viewModelScope.launchWithMutex(mutex, Dispatchers.Default) { 53 | prefs.excludeStore.put(!prefs.excludeStore.get()) 54 | refresh(false) 55 | } 56 | 57 | fun onDisabledClick() = viewModelScope.launchWithMutex(mutex, Dispatchers.Default) { 58 | prefs.excludeDisabled.put(!prefs.excludeDisabled.get()) 59 | refresh(false) 60 | } 61 | 62 | fun ignore(packageName: String) = viewModelScope.launchWithMutex(mutex, Dispatchers.Default) { 63 | val ignored = prefs.ignoredApps.get().toMutableList() 64 | if (ignored.contains(packageName)) { 65 | ignored.remove(packageName) 66 | } else { 67 | ignored.add(packageName) 68 | } 69 | prefs.ignoredApps.put(ignored) 70 | refresh(false) 71 | } 72 | 73 | private fun buildLoadingState() = AppsUiState.Loading( 74 | prefs.excludeSystem.get(), 75 | prefs.excludeStore.get(), 76 | prefs.excludeDisabled.get() 77 | ) 78 | 79 | } 80 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/apkupdater/viewmodel/MainViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.apkupdater.viewmodel 2 | 3 | import android.content.Intent 4 | import android.content.pm.PackageInstaller 5 | import android.util.Log 6 | import androidx.activity.compose.ManagedActivityResultLauncher 7 | import androidx.activity.result.ActivityResult 8 | import androidx.lifecycle.ViewModel 9 | import androidx.lifecycle.viewModelScope 10 | import androidx.navigation.NavController 11 | import androidx.navigation.NavGraph.Companion.findStartDestination 12 | import com.apkupdater.data.ui.AppInstallStatus 13 | import com.apkupdater.data.ui.Screen 14 | import com.apkupdater.prefs.Prefs 15 | import com.apkupdater.util.InstallLog 16 | import com.apkupdater.util.SessionInstaller 17 | import com.apkupdater.util.UpdatesNotification 18 | import com.apkupdater.util.getAppId 19 | import com.apkupdater.util.getIntentExtra 20 | import com.apkupdater.util.orFalse 21 | import kotlinx.coroutines.Dispatchers 22 | import kotlinx.coroutines.flow.MutableStateFlow 23 | import kotlinx.coroutines.launch 24 | 25 | 26 | class MainViewModel( 27 | private val prefs: Prefs, 28 | private val installLog: InstallLog 29 | ) : ViewModel() { 30 | 31 | val screens = listOf(Screen.Apps, Screen.Search, Screen.Updates, Screen.Settings) 32 | 33 | val isRefreshing = MutableStateFlow(false) 34 | 35 | private var currentInstallId = 0 36 | 37 | fun refresh( 38 | appsViewModel: AppsViewModel, 39 | updatesViewModel: UpdatesViewModel 40 | ) = viewModelScope.launch { 41 | isRefreshing.value = true 42 | appsViewModel.refresh(false) 43 | updatesViewModel.refresh(false).invokeOnCompletion { 44 | isRefreshing.value = false 45 | } 46 | } 47 | 48 | fun processIntent( 49 | intent: Intent, 50 | launcher: ManagedActivityResultLauncher, 51 | updatesViewModel: UpdatesViewModel, 52 | navController: NavController 53 | ) { 54 | when { 55 | intent.action == UpdatesNotification.UpdateAction -> processUpdateIntent(navController, updatesViewModel) 56 | intent.action?.contains(SessionInstaller.INSTALL_ACTION).orFalse() -> processInstallIntent(intent, launcher) 57 | else -> {} 58 | } 59 | } 60 | 61 | fun navigateTo(navController: NavController, route: String) = navController.navigate(route) { 62 | popUpTo(navController.graph.findStartDestination().id) { saveState = true } 63 | launchSingleTop = true 64 | restoreState = true 65 | prefs.lastTab.put(route) 66 | } 67 | 68 | fun getLastRoute() = prefs.lastTab.get() 69 | 70 | private fun processInstallIntent( 71 | intent: Intent, 72 | launcher: ManagedActivityResultLauncher 73 | ) = viewModelScope.launch(Dispatchers.IO) { 74 | when (intent.extras?.getInt(PackageInstaller.EXTRA_STATUS)) { 75 | PackageInstaller.STATUS_PENDING_USER_ACTION -> { 76 | currentInstallId = intent.getAppId() ?: 0 77 | // Launch intent to confirm install 78 | intent.getIntentExtra()?.let { 79 | it.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP) 80 | launcher.launch(it) 81 | } 82 | } 83 | PackageInstaller.STATUS_SUCCESS -> { 84 | intent.getAppId()?.let { 85 | installLog.emitStatus(AppInstallStatus(true, it)) 86 | } 87 | } 88 | else -> { 89 | // We assume error and cancel the install 90 | intent.getAppId()?.let { 91 | installLog.emitStatus(AppInstallStatus(false, it)) 92 | } 93 | val message = intent.extras?.getString(PackageInstaller.EXTRA_STATUS_MESSAGE) 94 | Log.e("MainViewModel", "Failed to install app: $message $intent") 95 | } 96 | } 97 | } 98 | 99 | private fun processUpdateIntent( 100 | navController: NavController, 101 | updatesViewModel: UpdatesViewModel 102 | ) { 103 | navigateTo(navController, Screen.Updates.route) 104 | updatesViewModel.refresh() 105 | } 106 | 107 | } 108 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/apkupdater/viewmodel/SearchViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.apkupdater.viewmodel 2 | 3 | import androidx.lifecycle.viewModelScope 4 | import com.apkupdater.data.ui.AppUpdate 5 | import com.apkupdater.data.ui.SearchUiState 6 | import com.apkupdater.data.ui.removeId 7 | import com.apkupdater.data.ui.setIsInstalling 8 | import com.apkupdater.data.ui.setProgress 9 | import com.apkupdater.prefs.Prefs 10 | import com.apkupdater.repository.SearchRepository 11 | import com.apkupdater.util.Badger 12 | import com.apkupdater.util.Downloader 13 | import com.apkupdater.util.InstallLog 14 | import com.apkupdater.util.SessionInstaller 15 | import com.apkupdater.util.SnackBar 16 | import com.apkupdater.util.Stringer 17 | import com.apkupdater.util.launchWithMutex 18 | import kotlinx.coroutines.Dispatchers 19 | import kotlinx.coroutines.Job 20 | import kotlinx.coroutines.flow.MutableStateFlow 21 | import kotlinx.coroutines.flow.StateFlow 22 | import kotlinx.coroutines.launch 23 | import kotlinx.coroutines.sync.Mutex 24 | 25 | 26 | class SearchViewModel( 27 | private val searchRepository: SearchRepository, 28 | private val installer: SessionInstaller, 29 | private val badger: Badger, 30 | downloader: Downloader, 31 | prefs: Prefs, 32 | snackBar: SnackBar, 33 | stringer: Stringer, 34 | installLog: InstallLog 35 | ) : InstallViewModel(downloader, installer, prefs, snackBar, stringer, installLog) { 36 | 37 | private val mutex = Mutex() 38 | private val state = MutableStateFlow(SearchUiState.Success(emptyList())) 39 | private var job: Job? = null 40 | 41 | init { 42 | subscribeToInstallStatus(state.value.updates()) 43 | subscribeToInstallProgress { progress -> 44 | state.value = SearchUiState.Success(state.value.mutableUpdates().setProgress(progress)) 45 | } 46 | } 47 | 48 | fun state(): StateFlow = state 49 | 50 | fun search(text: String) { 51 | job?.cancel() 52 | job = searchJob(text) 53 | } 54 | 55 | private fun searchJob(text: String) = viewModelScope.launchWithMutex(mutex, Dispatchers.IO) { 56 | state.value = SearchUiState.Loading 57 | badger.changeSearchBadge("") 58 | searchRepository.search(text).collect { 59 | it.onSuccess { apps -> 60 | state.value = SearchUiState.Success(apps) 61 | badger.changeSearchBadge(apps.size.toString()) 62 | }.onFailure { 63 | badger.changeSearchBadge("!") 64 | state.value = SearchUiState.Error 65 | } 66 | } 67 | } 68 | 69 | override fun cancelInstall(id: Int) = viewModelScope.launchWithMutex(mutex, Dispatchers.IO) { 70 | state.value = SearchUiState.Success(state.value.mutableUpdates().setIsInstalling(id, false)) 71 | installer.finish() 72 | } 73 | 74 | override fun finishInstall(id: Int) = viewModelScope.launchWithMutex(mutex, Dispatchers.IO) { 75 | val updates = state.value.mutableUpdates().removeId(id) 76 | state.value = SearchUiState.Success(updates) 77 | badger.changeSearchBadge(updates.size.toString()) 78 | installer.finish() 79 | } 80 | 81 | override fun downloadAndRootInstall(update: AppUpdate) = viewModelScope.launch(Dispatchers.IO) { 82 | state.value = SearchUiState.Success(state.value.mutableUpdates().setIsInstalling(update.id, true)) 83 | downloadAndRootInstall(update.id, update.link) 84 | } 85 | 86 | override fun downloadAndInstall(update: AppUpdate) = viewModelScope.launch(Dispatchers.IO) { 87 | if(installer.checkPermission()) { 88 | state.value = SearchUiState.Success(state.value.mutableUpdates().setIsInstalling(update.id, true)) 89 | downloadAndInstall(update.id, update.packageName, update.link) 90 | } 91 | } 92 | 93 | } 94 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/apkupdater/viewmodel/UpdatesViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.apkupdater.viewmodel 2 | 3 | import androidx.lifecycle.viewModelScope 4 | import com.apkupdater.data.ui.AppUpdate 5 | import com.apkupdater.data.ui.UpdatesUiState 6 | import com.apkupdater.data.ui.removeId 7 | import com.apkupdater.data.ui.setIsInstalling 8 | import com.apkupdater.data.ui.setProgress 9 | import com.apkupdater.prefs.Prefs 10 | import com.apkupdater.repository.UpdatesRepository 11 | import com.apkupdater.util.Badger 12 | import com.apkupdater.util.Downloader 13 | import com.apkupdater.util.InstallLog 14 | import com.apkupdater.util.SessionInstaller 15 | import com.apkupdater.util.SnackBar 16 | import com.apkupdater.util.Stringer 17 | import com.apkupdater.util.launchWithMutex 18 | import kotlinx.coroutines.Dispatchers 19 | import kotlinx.coroutines.flow.MutableStateFlow 20 | import kotlinx.coroutines.flow.StateFlow 21 | import kotlinx.coroutines.launch 22 | import kotlinx.coroutines.sync.Mutex 23 | 24 | 25 | class UpdatesViewModel( 26 | private val updatesRepository: UpdatesRepository, 27 | private val installer: SessionInstaller, 28 | private val prefs: Prefs, 29 | private val badger: Badger, 30 | downloader: Downloader, 31 | snackBar: SnackBar, 32 | stringer: Stringer, 33 | installLog: InstallLog 34 | ) : InstallViewModel(downloader, installer, prefs, snackBar, stringer, installLog) { 35 | 36 | private val mutex = Mutex() 37 | private val state = MutableStateFlow(UpdatesUiState.Loading) 38 | 39 | init { 40 | subscribeToInstallStatus(state.value.updates()) 41 | subscribeToInstallProgress { progress -> 42 | state.value = UpdatesUiState.Success(state.value.mutableUpdates().setProgress(progress)) 43 | } 44 | } 45 | 46 | fun state(): StateFlow = state 47 | 48 | fun refresh(load: Boolean = true) = viewModelScope.launchWithMutex(mutex, Dispatchers.IO) { 49 | if (load) state.value = UpdatesUiState.Loading 50 | badger.changeUpdatesBadge("") 51 | updatesRepository.updates().collect { 52 | setSuccess(it) 53 | } 54 | } 55 | 56 | fun ignoreVersion(id: Int) = viewModelScope.launchWithMutex(mutex, Dispatchers.IO) { 57 | val ignored = prefs.ignoredVersions.get().toMutableList() 58 | if (ignored.contains(id)) ignored.remove(id) else ignored.add(id) 59 | prefs.ignoredVersions.put(ignored) 60 | setSuccess(state.value.mutableUpdates()) 61 | } 62 | 63 | override fun cancelInstall(id: Int) = viewModelScope.launchWithMutex(mutex, Dispatchers.IO) { 64 | state.value = UpdatesUiState.Success(state.value.mutableUpdates().setIsInstalling(id, false)) 65 | installer.finish() 66 | } 67 | 68 | override fun finishInstall(id: Int) = viewModelScope.launchWithMutex(mutex, Dispatchers.IO) { 69 | setSuccess(state.value.mutableUpdates().removeId(id)) 70 | installer.finish() 71 | } 72 | 73 | override fun downloadAndRootInstall(update: AppUpdate) = viewModelScope.launch(Dispatchers.IO) { 74 | state.value = UpdatesUiState.Success(state.value.mutableUpdates().setIsInstalling(update.id, true)) 75 | downloadAndRootInstall(update.id, update.link) 76 | } 77 | 78 | override fun downloadAndInstall(update: AppUpdate) = viewModelScope.launch(Dispatchers.IO) { 79 | if(installer.checkPermission()) { 80 | state.value = UpdatesUiState.Success(state.value.mutableUpdates().setIsInstalling(update.id, true)) 81 | downloadAndInstall(update.id, update.packageName, update.link) 82 | } 83 | } 84 | 85 | private fun List.filterIgnoredVersions(ignoredVersions: List) = this 86 | .filter { !ignoredVersions.contains(it.id) } 87 | 88 | private fun setSuccess(updates: List) = updates 89 | .filterIgnoredVersions(prefs.ignoredVersions.get()) 90 | .let { 91 | state.value = UpdatesUiState.Success(it) 92 | badger.changeUpdatesBadge(it.size.toString()) 93 | } 94 | 95 | } 96 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/apkupdater/worker/UpdatesWorker.kt: -------------------------------------------------------------------------------- 1 | package com.apkupdater.worker 2 | 3 | import android.content.Context 4 | import androidx.work.CoroutineWorker 5 | import androidx.work.ExistingPeriodicWorkPolicy 6 | import androidx.work.PeriodicWorkRequestBuilder 7 | import androidx.work.WorkManager 8 | import androidx.work.WorkerParameters 9 | import com.apkupdater.prefs.Prefs 10 | import com.apkupdater.repository.UpdatesRepository 11 | import com.apkupdater.util.UpdatesNotification 12 | import com.apkupdater.util.millisUntilHour 13 | import org.koin.core.component.KoinComponent 14 | import org.koin.core.component.inject 15 | import java.util.concurrent.TimeUnit 16 | import kotlin.random.Random 17 | 18 | 19 | class UpdatesWorker( 20 | context: Context, 21 | workerParams: WorkerParameters 22 | ): CoroutineWorker(context, workerParams), KoinComponent { 23 | 24 | companion object: KoinComponent { 25 | private const val TAG = "UpdatesWorker" 26 | private val prefs: Prefs by inject() 27 | 28 | fun cancel(workManager: WorkManager) = workManager.cancelUniqueWork(TAG) 29 | 30 | fun launch(workManager: WorkManager) { 31 | val request = PeriodicWorkRequestBuilder(getDays(), TimeUnit.DAYS) 32 | .setInitialDelay( 33 | millisUntilHour(prefs.alarmHour.get()) + randomDelay(), 34 | TimeUnit.MILLISECONDS 35 | ).build() 36 | workManager.enqueueUniquePeriodicWork(TAG, ExistingPeriodicWorkPolicy.UPDATE, request) 37 | } 38 | 39 | private fun randomDelay() = if (prefs.useApkMirror.get()) 40 | Random.nextLong(0, 59 * 60 * 1_000) 41 | else 42 | Random.nextLong(-5 * 60 * 1_000, 5 * 60 * 1_000) 43 | 44 | private fun getDays() = when(prefs.alarmFrequency.get()) { 45 | 0 -> 1L 46 | 1 -> 3L 47 | 2 -> 7L 48 | else -> 1L 49 | } 50 | } 51 | 52 | private val updatesRepository: UpdatesRepository by inject() 53 | private val notification: UpdatesNotification by inject() 54 | 55 | override suspend fun doWork(): Result { 56 | updatesRepository.updates().collect { 57 | if (it.isNotEmpty()) { 58 | notification.showUpdateNotification(it.size) 59 | } 60 | } 61 | return Result.success() 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumboalla/apkupdater/c55d9175cddfa50d377a1dd313873510e36517a2/app/src/main/res/drawable/banner.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_alarm.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_alpha.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_androidtv.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_animation.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_apkmirror.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumboalla/apkupdater/c55d9175cddfa50d377a1dd313873510e36517a2/app/src/main/res/drawable/ic_apkmirror.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_apkpure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumboalla/apkupdater/c55d9175cddfa50d377a1dd313873510e36517a2/app/src/main/res/drawable/ic_apkpure.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_appstore.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_appstore_off.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_aptoide.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumboalla/apkupdater/c55d9175cddfa50d377a1dd313873510e36517a2/app/src/main/res/drawable/ic_aptoide.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_beta.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_copy.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_disabled.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_disabled_off.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_empty.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_frequency.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_github.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_gitlab.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 14 | 18 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_hour.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_info.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_install.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_izzy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumboalla/apkupdater/c55d9175cddfa50d377a1dd313873510e36517a2/app/src/main/res/drawable/ic_izzy.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_landscape.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 9 | 12 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_play.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 14 | 18 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_portrait.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_pre_release.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_refresh.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_root.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_safe.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_system.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_system_off.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_theme.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_visible.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_visible_off.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v33/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumboalla/apkupdater/c55d9175cddfa50d377a1dd313873510e36517a2/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumboalla/apkupdater/c55d9175cddfa50d377a1dd313873510e36517a2/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumboalla/apkupdater/c55d9175cddfa50d377a1dd313873510e36517a2/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumboalla/apkupdater/c55d9175cddfa50d377a1dd313873510e36517a2/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumboalla/apkupdater/c55d9175cddfa50d377a1dd313873510e36517a2/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/values-alb/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | APKUpdater 4 | Aplikacionet 5 | Kërko 6 | Përditësime 7 | Cilësimet 8 | "Diçka shkoi keq\n\uD83E\uDD26" 9 | Ignoro Versionin 10 | Ignoro Aplikacionin 11 | Mos e Ignoro Aplikacionin 12 | Kërko për Përditësime 13 | Përjashto Aplikacionet e Sistemit 14 | Përfshij Aplikacionet e Sistemit 15 | Përjashto Dyqanin e Aplikacioneve 16 | Përfshij Dyqanin e Aplikacioneve 17 | Përjashto Aplikacionet e Çaktivizuara 18 | Përfshij Aplikacionet e Çaktivizuara 19 | Instalo Aplikacionin 20 | Ikona e Aplikacionit 21 | %1$s u instalua. 22 | %1$s dështoi të instalohet. 23 | 24 | // Settings 25 | Kolonat në Portret 26 | Kolonat në Peizazh 27 | Burimet 28 | UI 29 | Alarmi 30 | Opsionet 31 | Utilitetet 32 | Ora e Alarmit 33 | Çdo Ditë 34 | Çdo 3 Ditë 35 | Çdo Javë 36 | UI i Android TV 37 | Ignoro Alpha 38 | Ignoro Beta 39 | Ignoro Para-lirim 40 | Përdor Dyqane të Sigurta (Aptoide) 41 | Instalim me Root 42 | ApkMirror 43 | F-Droid Kryesor 44 | F-Droid Izzy 45 | Aptoide 46 | GitHub 47 | GitLab 48 | APKPure 49 | Rreth 50 | Frekuenca 51 | Tema 52 | Sistemi 53 | Errët 54 | Dritë 55 | Merr kodin burim, raporto gabimet, kërko karakteristika të reja dhe shto përkthimet. 56 | Nëse ju pëlqen aplikacioni, mendoni për dhurimin për një shkak të vlefshëm. 57 | Kopjo në të kujtesë 58 | Kopjo Listën e Aplikacioneve 59 | 60 | // Notifications 61 | Përditësime 62 | Kanali për njoftimet e përditësimeve. 63 | updateChannel 64 | Përditësime 65 | 66 | Asnjë përditësim i gjetur. 67 | Gjeti %1$d përditësim. 68 | Gjeti %1$d përditësime. 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /app/src/main/res/values-de/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Apps 5 | Suche 6 | Updates 7 | Setup 8 | "Etwas ist schiefgelaufen\n\uD83E\uDD26" 9 | Ausblenden 10 | Ignorieren 11 | Überprüfen 12 | Suche nach Updates 13 | System-Apps ignorieren 14 | System-Apps überprüfen 15 | App-Store ignorieren 16 | App-Store überprüfen 17 | Deaktivierte Apps ignorieren 18 | Deaktivierte Apps überprüfen 19 | Installieren 20 | App-Icon 21 | %1$s installiert. 22 | %1$s wurde nicht installiert. 23 | Root-Installation dieser App wird nicht unterstützt. 24 | 25 | // Settings 26 | Anzahl der Spalten 27 | Anzahl der Zeilen 28 | Textanimation verwenden 29 | Quellen 30 | Benutzeroberfläche 31 | Alarm 32 | Optionen 33 | Hilfsmittel 34 | Alarmzeit 35 | Täglich 36 | 3-Tägig 37 | Wöchentlich 38 | Alphas ignorieren 39 | Betas ignorieren 40 | Pre-Releases ignorieren 41 | Sichere Stores nutzen (Aptoide) 42 | Root-Installation 43 | Über 44 | Häufigkeit 45 | Thema 46 | System 47 | Dunkel 48 | Hell 49 | Hol dir den Quellcode, melde Fehler und schlage Funktionen / Übersetzungen vor. 50 | Wenn dir die App gefällt solltest du hier eine Spende in Betracht ziehen. 51 | Kopiere in Zwischenablage 52 | App-Liste kopieren 53 | App-Logs kopieren 54 | 55 | // Notifications 56 | Updates 57 | Kanal für Update-Benachrichtigungen. 58 | Updates 59 | 60 | Keine Updates gefunden. 61 | %1$d Update gefunden. 62 | %1$d Updates gefunden. 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /app/src/main/res/values-es/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | APKUpdater 4 | Aplicaciones 5 | Búsqueda 6 | Actualizaciones 7 | Configuración 8 | Algo ha fallado\\n\\uD83E\\uDD26 9 | Ignorar versión 10 | Ignorar Apliación 11 | No Ignorar Aplicación 12 | Buscar Actualizaciones 13 | Excluir aplicaciones del sistema 14 | Incluir aplicaciones del sistema 15 | Excluir aplicaciones de App Stores 16 | Incluir aplicaciones de App Stores 17 | Excluir aplicaciones deshabilitadas 18 | Incluir aplicaciones deshabilitadas 19 | Instalar Aplicación 20 | Icono de la Applicación 21 | %1$s instalado. 22 | %1$s fallo en la instalación. 23 | 24 | // Settings 25 | Columnas en modo Retrato 26 | Columnas en modo Apaisado 27 | Fuentes 28 | Interfaz de Usuario 29 | Alarma 30 | Opciones 31 | Hora de Alarma 32 | Diaria 33 | A cada 3 días 34 | Semanal 35 | Interfaz de Android TV 36 | Ignorar Alpha 37 | Ignorar Beta 38 | Ignorar Prelanzamiento 39 | Utilizar tiendas seguras (Aptoide) 40 | Instalación root 41 | ApkMirror 42 | F-Droid Main 43 | F-Droid Izzy 44 | Aptoide 45 | GitHub 46 | APKPure (Beta) 47 | Sobre 48 | Frecuencia 49 | Tema 50 | Sistema 51 | Oscura 52 | Claro 53 | Obtenga el código fuente, informe errores, solicite nuevas funciones y agregue traducciones. 54 | Si te gusta la aplicación, considera hacer una donación a una buena causa. 55 | Copiar a la portapapeles 56 | Copiar lista de aplicaciones 57 | 58 | // Notifications 59 | Actualizaciones 60 | Canal para las notificaciones de las actualizaciones. 61 | updateChannel 62 | Actualizaciones 63 | 64 | No se encontraron actualizaciones. 65 | Se encontró %1$d actualización. 66 | Se encontraron %1$d actualizaciones. 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /app/src/main/res/values-he/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | APKUpdater 4 | ישומים 5 | חיפוש 6 | עדכונים 7 | הגדרות 8 | "קרתה שגיאה\n\uD83E\uDD26" 9 | התעלם מגירסה 10 | התעלמות מישומון 11 | בטל התעלמות מישומון 12 | חפש עדכונים 13 | החרג ישומוני מערכת 14 | כלול ישומוני מערכת 15 | החרג ישומוני חנות 16 | כלול ישומוני חנות 17 | החרג ישומונים מבוטלים 18 | כלול ישומונים מבוטלים 19 | התקן ישומון 20 | צלמית ישומון 21 | %1$s ההתקנה הצליחה. 22 | %1$s ההתקנה נכשלה. 23 | 24 | // Settings 25 | עמודות לרוחב 26 | עמודות לאורך 27 | מקורות 28 | ממשק משתמש 29 | התרעה 30 | אפשרויות 31 | כלים 32 | שעת התרעה 33 | יומי 34 | תלת-יומי 35 | שבועי 36 | Android TV ממשק משתמש עבור 37 | התעלם מאלפא 38 | התעלם מביטא 39 | התעלם משחרור מוקדם 40 | השתמש בחנות בטוחה (Aptoide) 41 | התקנת שורש 42 | ApkMirror 43 | F-Droid Main 44 | F-Droid Izzy 45 | Aptoide 46 | GitHub 47 | GitLab 48 | APKPure 49 | אודות 50 | תדירות 51 | ערכת נושא 52 | מערכת 53 | כהה 54 | בהיר 55 | לקוד מקור, דיווח שגיאות, בקשה לתכוניות חדשות ותרגומים. 56 | אם אהבת את הישומון, שקול לתרום למטרה הנעלה. 57 | העתק לזיכרון 58 | העתק רשימת ישומונים 59 | 60 | // Notifications 61 | עדכונים 62 | ערוץ עבור הודעות עדכונים. 63 | עדכן ערוץ 64 | עדכונים 65 | 66 | לא נמצאו עדכונים. 67 | נמצאו %1$d עדכונים. 68 | נמצאו %1$d עדכונים. 69 | נמצאו %1$d עדכונים. 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /app/src/main/res/values-it/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | APKUpdater 4 | Applicazioni 5 | Cerca 6 | Aggiornamenti 7 | Impostazioni 8 | Qualcosa è andato storto\n\uD83E\uDD26 9 | Salta questa versione 10 | Ignora applicazioni 11 | Includi applicazioni 12 | Cerca aggiornamenti 13 | Escludi applicazioni di sistema 14 | Includi applicazioni di sistema 15 | Escludi applicazioni store 16 | Includi applicazioni store 17 | Escludi applicazioni disattivate 18 | Includi applicazioni disattivate 19 | Installa applicazioni 20 | Icona dell\'applicazione 21 | %1$s installate. 22 | %1$s installazioni fallite. 23 | 24 | // Settings 25 | Colonne verticali 26 | Colonne orizzontali 27 | Fonti 28 | Interfaccia grafica 29 | Notifiche 30 | Opzioni 31 | Utilità 32 | Orario di notifica 33 | Giornaliera 34 | Ogni 3 giorni 35 | Settimanale 36 | Interfaccia grafica del Android TV 37 | Ignora Alpha 38 | Ignora Beta 39 | Ignora pre-rilascio 40 | Utilizzare store sicuri (Aptoide) 41 | Installa con root 42 | ApkMirror 43 | F-Droid Main 44 | F-Droid Izzy 45 | Aptoide 46 | GitHub 47 | APKPure (Beta) 48 | Informazioni 49 | Frequenza 50 | Tema 51 | Sistema 52 | Scuro 53 | Chiaro 54 | Ottieni il codice sorgente, segnala bug, richiedi nuove funzionalità e aggiungi traduzioni. 55 | Se ti piace l\'applicazione, considera l\'idea di fare una donazione per una buona causa. 56 | Copia negli appunti 57 | Copia lista app 58 | 59 | // Notifications 60 | Aggiornamenti 61 | Canale per notifiche di aggiornamento. 62 | updateChannel 63 | Aggiornamenti 64 | 65 | Nessun aggiornamento trovato. 66 | Trovato %1$d aggiornamento. 67 | Trovato %1$d aggiornamenti. 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /app/src/main/res/values-jp/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | APKUpdater 4 | アプリ 5 | 検索 6 | アップデート 7 | 設定 8 | "失敗しました\n\uD83E\uDD26" 9 | バージョンを無視する 10 | アプリを除外する 11 | アプリの無視を解除する 12 | アップデート一覧を更新する 13 | システムアプリを除外する 14 | システムアプリをリストに含める 15 | アプリストアを除外する 16 | アプリストアをリストに含める 17 | 無効にしたアプリを除外する 18 | 無効にしたアプリをリストに含める 19 | アプリをインストールする 20 | アプリアイコン 21 | %1$sをインストールしました。 22 | %1$sのインストールに失敗しました。 23 | このアプリのrootインストールはサポートされていません。 24 | 25 | // Settings 26 | ポートレートモードでの列数 27 | ランドスケープモードでの列数 28 | テキストのアニメーションを再生する 29 | ソース 30 | UI 31 | アラーム 32 | オプション 33 | ユーティリティ 34 | アラームを鳴らす時刻 35 | 毎日 36 | 3日ごと 37 | 毎週 38 | Android TVのUI 39 | アルファ版を無視する 40 | ベータ版を無視する 41 | プレリリースを無視する 42 | 安全なストアを使用する(Aptoide) 43 | rootインストール 44 | ApkMirror 45 | F-Droid Main 46 | F-Droid Izzy 47 | Aptoide 48 | GitHub 49 | GitLab 50 | APKPure 51 | Play 52 | このアプリについて 53 | 頻度 54 | テーマ 55 | システムに合わせる 56 | ダークテーマ 57 | ライトテーマ 58 | ソースコードの取得、バグの報告、新機能のリクエスト、翻訳の追加ができます。 59 | このアプリが気に入ったら、寄付をお願いします。 60 | クリップボードにコピー 61 | アプリ一覧をコピー 62 | アプリのログをコピー 63 | 64 | // Notifications 65 | Updates 66 | アップデートの通知を受け取るチャンネルです。 67 | アップデートチャンネル 68 | アップデート 69 | 70 | アップデートはありません。 71 | %1$d件のアップデートがあります。 72 | %1$d件のアップデートがあります。 73 | 74 | 75 | -------------------------------------------------------------------------------- /app/src/main/res/values-ko/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | APKUpdater 4 | 모든 앱 5 | 검색 6 | 업데이트 7 | 설정 8 | "문제가 발생했습니다.\n\uD83E\uDD26" 9 | 버전 무시 10 | 앱 무시 11 | 앱 무시 해제 12 | 업데이트 찾기 13 | 시스템 앱 제외 14 | 시스템 앱 포함 15 | 앱스토어 제외 16 | 앱스토어 포함 17 | 비활성화된 앱 제외 18 | 비활성화된 앱 포함 19 | 앱 설치 20 | 앱 아이콘 21 | %1$s 설치됨. 22 | %1$s 설치 실패됨. 23 | 24 | // Settings 25 | 세로 열 26 | 가로 열 27 | 텍스트 애니메이션 재생 28 | 출처 29 | UI 30 | 알람 31 | 옵션 32 | 유틸리티 33 | 알람 시간 34 | 일일 35 | 3일 36 | 주간 37 | 안드로이드TV UI 38 | 알파 무시 39 | 베타 무시 40 | 시험판 무시 41 | 안전한 상점 이용(Aptoide) 42 | 루트 설치 43 | ApkMirror 44 | F-Droid Main 45 | F-Droid Izzy 46 | Aptoide 47 | GitHub 48 | GitLab 49 | APKPure 50 | 도움말 51 | 빈도 52 | 테마 53 | 시스템 54 | 어둡게 55 | 밝게 56 | 소스 코드, 버그 보고, 새로운 기능 요청, 번역 추가 해주세요. 57 | 앱이 마음에 든다면 기부 해주세요 :) . 58 | 클립보드에 복사 59 | 앱 목록 복사 60 | 61 | // Notifications 62 | 업데이트 63 | 업데이트 알림 채널. 64 | 업데이트 채널 65 | 업데이트 66 | 67 | 업데이트를 찾을 수 없어요. 68 | %1$d 업데이트를 찾았어요. 69 | %1$d개의 업데이트를 찾았어요. 70 | 71 | 72 | -------------------------------------------------------------------------------- /app/src/main/res/values-my-rMM/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | APKUpdater 4 | Appများ 5 | ရှာဖွေရန် 6 | ဗားရှင်းအသစ်များ 7 | ဆက်တင်များ 8 | "တစ်ခုခုမှားယွင်းနေပါသည်\n\uD83E\uDD26" 9 | ငြင်းပယ်ထားသောAppများ 10 | မငြင်းပယ်ထားသောAppများ 11 | ဗားရှင်းအသစ်များရှာဖွေခြင်း 12 | System Appsများမပါဝင် 13 | System Appsများပါအပါဝင် 14 | App Storeမပါဝင် 15 | App Storeအပါအဝင် 16 | အသုံးပြုမရသော Apps များမပါဝင် 17 | အသုံးပြုမရသော Apps များပါအပါအဝင် 18 | ထည့်သွင်းထားသောအပ်ပလီကေးရှင်း 19 | အပ်ပလီကေးရှင်းရုပ်ပုံ 20 | %1$s ထည့်သွင်းပြီး။ 21 | %1$s ထည့်သွင်းမှုမအောင်မြင်ပါ။ 22 | 23 | // Settings 24 | အလျားလိုက်အကန့် 25 | ဒေါင်လိုက်အကန့် 26 | အရင်းမြစ်များ 27 | ပြသပုံ 28 | နှိုးစက်အသိပေးချက် 29 | ရွေးချယ်စရာများ 30 | အခြားလုပ်ဆောင်ချက်များ 31 | သတိပေးချိန် နာရီ 32 | နေ့စဥ် 33 | သုံးရက်တိုင်း 34 | အပတ်စဥ် 35 | Android TV UI 36 | အယ်ဖာ(အစမ်း)များမပါဝင် 37 | ဘယ်တာ(အကြို)များမပါဝင် 38 | ကြိုတင်စမ်းသပ်Appများမပါဝင် 39 | လုံခြုံမှု(Aptoide)စတိုးအသုံးပြုရန် 40 | Rootချပြီးထည့်သွင်းမှု 41 | ApkMirror 42 | F-Droid Main 43 | F-Droid Izzy 44 | Aptoide 45 | GitHub 46 | APKPure (Beta) 47 | ကျွန်ုပ်တို့အကြောင်း 48 | ကြိမ်ရေ 49 | တင်ပြပုံ 50 | စက်အတိုင်း 51 | အမှောင် 52 | အလင်း 53 | ပင်ရင်းကုဒ်တွင် ဘာသာစကားများ၊ လိုအပ်ချက်များ၊ ပါဝင်မှုအသစ်များ ထည့်သွင်းရန်။ 54 | ယခုအပ်ပလီကေးရှင်းကိုနှစ်သက်ပါက ကူညီလှူတန်းရန်။ 55 | ကော်ပီကူးယူသည် 56 | Appလိပ်စာကိုကူးယူသည် 57 | 58 | // Notifications 59 | ဗားရှင်းအသစ်များ 60 | ဗားရှင်းအသစ်များအတွက်အသိပေးချက်ရယူရန်။ 61 | updateChannel 62 | ဗားရှင်းအသစ်များ 63 | 64 | ဗားရှင်းအသစ် မတွေ့ပါ။ 65 | ဗားရှင်းအသစ် %1$d ခု။ 66 | ဗားရှင်းအသစ် %1$d ခု။ 67 | 68 | 69 | -------------------------------------------------------------------------------- /app/src/main/res/values-nl/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Applicaties 4 | Zoeken 5 | Updates 6 | Instellingen 7 | Er is iets misgegaan\\n\\uD83E\\uDD26 8 | App Negeren 9 | App niet Negeren 10 | Zoeken naar Updates 11 | Systeemapps Uitsluiten 12 | Systeemapps Toestaan 13 | App Store Uitsluiten 14 | App Store Toestaan 15 | Uitgeschakelde Apps Uitsluiten 16 | Uitgeschakelde Apps Toestaan 17 | App Installeren 18 | App Icoon 19 | %1$s geïnstalleerd. 20 | %1$s niet geïnstalleerd. 21 | 22 | // Settings 23 | Staande Kolommen 24 | Liggende Kolommen 25 | Bronnen 26 | UI 27 | Alarm 28 | Opties 29 | Hulpmiddelen 30 | Alarm In Uren 31 | Dagelijks 32 | 3-Daags 33 | Wekelijks 34 | Alpha Negeren 35 | Bèta Negeren 36 | Pre-release negeren 37 | Gebruik veilige App Stores (Aptoide) 38 | Root Installatie 39 | Over 40 | Frequentie 41 | Thema 42 | Systeem 43 | Donker 44 | Licht 45 | Verkrijg de broncode, rapporteer bugs, vraag nieuwe functies aan en voeg vertalingen toe. 46 | Als je de app leuk vindt, overweeg dan om te doneren aan een goed doel. 47 | Kopieer naar klembord 48 | Kopieer applicatielijst 49 | 50 | // Notifications 51 | Updates 52 | Kanaal voor update notificaties 53 | Updates 54 | 55 | %1$d Update gevonden. 56 | %1$d Updates gevonden. 57 | %1$d Updates gevonden. 58 | 59 | 60 | -------------------------------------------------------------------------------- /app/src/main/res/values-pt/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | APKUpdater 4 | "Aplicações" 5 | "Procurar" 6 | "Atualizações" 7 | "Configurações" 8 | "Aconteceu algo de errado\n\uD83E\uDD26" 9 | Ignorar a versão 10 | "Ignorar a aplicação" 11 | "Não ignorar a aplicação" 12 | "Procurar atualizações" 13 | "Excluir a Loja de Aplicações" 14 | "Incluir aplicações do sistema" 15 | "Excluir a Loja de Aplicações" 16 | "Incluir a Loja de Aplicações" 17 | "Excluir aplicações desativadas" 18 | "Incluir aplicações desativadas" 19 | "Instalar aplicação" 20 | "Ícone da aplicação" 21 | "%1$s foi instalado com sucesso." 22 | A instalação de "%1$s falhou." 23 | 24 | // Settings 25 | "Colunas em modo retrato" 26 | "Colunas em modo paisagem" 27 | "Fontes" 28 | "Interface" 29 | "Alarme" 30 | "Opções" 31 | "Intervalo de horas" 32 | "Diário" 33 | "A cada 3 dias" 34 | "Semanalmente" 35 | Interface do Android TV 36 | "Ignorar alpha" 37 | "Ignorar beta" 38 | "Ignorar pré-lançamento" 39 | "Utilizar lojas seguras (Aptoide)" 40 | "Instalação root" 41 | ApkMirror 42 | F-Droid (Principal) 43 | F-Droid (Izzy) 44 | Aptoide 45 | GitHub 46 | APKPure (Beta) 47 | "Sobre" 48 | "Frequência" 49 | "Tema" 50 | "Sistema" 51 | "Escuro" 52 | "Claro" 53 | "Obter código fonte, reportar bugs, requisitar novas funcionalidades e traduções." 54 | "Se gostas da app, considera fazer um donativo para uma causa justa." 55 | Copiar para a área de transferência 56 | Copiar lista da app 57 | 58 | // Notifications 59 | Atualizações 60 | Canal para notificações de atualizações. 61 | updateChannel 62 | Atualizações 63 | 64 | Nenhuma atualização encontrada. 65 | Foi encontrada %1$d atualização. 66 | Foram encontradas %1$d atualizações. 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /app/src/main/res/values-ro/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | APKUpdater 4 | Aplicații 5 | Caută 6 | Actualizări 7 | Setări 8 | "Ceva nu a mers bine\n\uD83E\uDD26" 9 | Ignoră Aplicația 10 | Nu mai ignora Aplicația 11 | Caută actualizări 12 | Exclude Aplicațiile de Sistem 13 | Include Aplicațiile de Sistem 14 | Exclude Magazinul de Aplicații 15 | Include Magazinul de Aplicații 16 | Exclude Aplicațiile Dezactivate 17 | Include Aplicațiile Dezactivate 18 | Instalează Aplicația 19 | Iconița Aplicației 20 | 21 | // Settings 22 | Coloane Portret 23 | Coloane Peisaj 24 | Surse 25 | UI 26 | Alarmă 27 | Opțiuni 28 | Ora Alarmei 29 | Zilnic 30 | În fiecare 3 zile 31 | Săptămânal 32 | Android TV UI 33 | Ignoră Aplicațiile Alpha 34 | Ignoră Aplicațiile Beta 35 | Instalează Automat Folosind ROOT 36 | ApkMirror 37 | F-Droid 38 | Aptoide 39 | GitHub 40 | Despre 41 | Frecvență 42 | 43 | // Notifications 44 | Actualizări 45 | Canalul pentru notificări despre actualizări. 46 | updateChannel 47 | Actualizări 48 | 49 | Au fost găsite %1$d actualizări. 50 | Nicio actualizare găsită. 51 | A fost găsită %1$d actualizare. 52 | Au fost găsite %1$d actualizări. 53 | 54 | 55 | -------------------------------------------------------------------------------- /app/src/main/res/values-tr/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | APKUpdater 3 | Uygulamalar 4 | Ara 5 | Güncellemeler 6 | Ayarlar 7 | "Bir şeyler ters gitti\n\uD83E\uDD26" 8 | Sürümü Yoksay 9 | Uygulamayı Yoksay 10 | Uygulamayı Yoksayılanlardan Çıkar 11 | Güncellemeleri Denetle 12 | Sistem Uygulamalarını Hariç Tut 13 | Sistem Uygulamalarını Dahil Et 14 | App Storeyi hariç tut 15 | App Storeyi dahil et 16 | Devre Dışı Uygulamaları Hariç Tut 17 | Devre Dışı Uygulamaları Dahil Et 18 | Uygulamayı Yükle 19 | Uygulama Simgesi 20 | %1$s yüklendi. 21 | %1$s yüklenemedi. 22 | Dikey Sütunlar 23 | Yatay Sütunlar 24 | Metin Animasyonlarını Oynat 25 | Kaynaklar 26 | UI 27 | Alarm 28 | Seçenekler 29 | ARAÇLAR 30 | Alarm Saati 31 | Günlük 32 | 3 Gün 33 | Haftalık 34 | Android TV Kullanıcı Arayüzü 35 | Alfayı yoksay 36 | Betayı Yoksay 37 | Ön sürümü yoksay 38 | Güvenli Mağazaları Kullan (Aptoide) 39 | Root Kurulumu 40 | ApkMirror 41 | F-Droid (Ana) 42 | F-Droid (Izzy) 43 | Aptoide 44 | GitHub 45 | GitLab 46 | APKPure 47 | Hakkında 48 | Sıklık 49 | Tema 50 | Sistem 51 | Koyu 52 | Açık 53 | Kaynak kodunu inceleyin, hataları bildirin, yeni özellikler isteyin ve çeviriler ekleyin. 54 | Uygulamayı beğendiyseniz bağış yapmayı düşünün. 55 | Panoya kopyala 56 | Uygulama Listesini Kopyala 57 | Güncellemeler 58 | Güncelleme bildirimleri için kanal. 59 | updateChannel 60 | Güncellemeler 61 | 62 | Güncelleme bulunamadı. 63 | %1$d güncelleme bulundu. 64 | %1$d güncelleme bulundu. 65 | 66 | -------------------------------------------------------------------------------- /app/src/main/res/values-zh-rCN/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 应用 3 | 搜索 4 | 更新 5 | 设置 6 | "出了点问题\n\uD83E\uDD26" 7 | 忽略该版本 8 | 忽略应用 9 | 取消忽略应用 10 | 查找更新 11 | 排除系统应用 12 | 包括系统应用 13 | 排除应用商店 14 | 包括应用商店 15 | 排除已禁用应用 16 | 包括已禁用应用 17 | 安装应用 18 | 应用图标 19 | %1$s 已安装 20 | %1$s 安装失败 21 | 22 | 23 | 不支持用Root安装这个app 24 | 竖屏列数 25 | 横屏列数 26 | 播放文字动画 27 | 来源 28 | 用户界面 29 | 提醒 30 | 选项 31 | 工具 32 | 提醒小时 33 | 每日 34 | 3天 35 | 每周 36 | Android TV 界面 37 | 忽略Alpha版本 38 | 忽略Beta版本 39 | 忽略预发布版本 40 | 使用安全商店(Aptoide) 41 | Root安装 42 | 关于 43 | 频率 44 | 主题 45 | 系统 46 | 深色 47 | 浅色 48 | 49 | 50 | 获取源代码、报告错误、请求新功能以及添加翻译。 51 | 如果你喜欢这个应用程序,请考虑捐赠。 52 | 复制到剪贴板 53 | 复制应用列表 54 | 复制应用日志 55 | 更新 56 | 用于更新通知的通道。 57 | 更新 58 | 59 | 没有发现更新。 60 | 发现 %1$d 个更新。 61 | 发现 %1$d 个更新。 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /app/src/main/res/values-zh-rTW/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | APKUpdater 4 | 應用程式 5 | 搜尋 6 | 更新 7 | 設定 8 | "發生錯誤\n\uD83E\uDD26" 9 | 忽略版本 10 | 忽略應用程式 11 | 取消忽略應用程式 12 | 尋找更新 13 | 排除系統應用程式 14 | 包含系統應用程式 15 | 排除應用程式商店 16 | 包含應用程式商店 17 | 排除已停用的應用程式 18 | 包含已停用的應用程式 19 | 安裝應用程式 20 | 應用程式圖示 21 | %1$s 已安裝。 22 | %1$s 安裝失敗。 23 | 不支援此應用程式的 Root 安裝。 24 | 25 | // Settings 26 | 直向欄數 27 | 橫向欄數 28 | 播放文字動畫 29 | 來源 30 | 使用者介面 31 | 鬧鐘 32 | 選項 33 | 工具 34 | 鬧鐘時間 35 | 每日 36 | 每三天 37 | 每週 38 | Android TV UI 39 | 忽略 Alpha 版 40 | 忽略 Beta 版 41 | 忽略預發布版 42 | 使用安全商店 (Aptoide) 43 | Root 安裝 44 | ApkMirror 45 | F-Droid Main 46 | F-Droid Izzy 47 | Aptoide 48 | GitHub 49 | GitLab 50 | APKPure 51 | Play 52 | 關於 53 | 頻率 54 | 主題 55 | 系統 56 | 深色 57 | 淺色 58 | 取得原始碼、回報錯誤、請求新功能並添加翻譯。 59 | 如果你喜歡這個應用程式,請考慮捐贈。 60 | 複製到剪貼簿 61 | 複製應用程式列表 62 | 複製應用程式日誌 63 | 64 | // Notifications 65 | 更新 66 | 更新通知的頻道。 67 | updateChannel 68 | 更新 69 | 70 | 沒有找到更新。 71 | 找到 %1$d 個更新。 72 | 找到 %1$d 個更新。 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | APKUpdater 4 | Apps 5 | Search 6 | Updates 7 | Settings 8 | "Something went wrong\n\uD83E\uDD26" 9 | Ignore Version 10 | Ignore App 11 | Unignore App 12 | Look for Updates 13 | Exclude System Apps 14 | Include System Apps 15 | Exclude App Store 16 | Include App Store 17 | Exclude Disabled Apps 18 | Include Disabled Apps 19 | Install App 20 | App Icon 21 | %1$s installed. 22 | %1$s failed to install. 23 | Root install of this app is not supported. 24 | 25 | // Settings 26 | Portrait Columns 27 | Landscape Columns 28 | Play Text Animations 29 | Sources 30 | UI 31 | Alarm 32 | Options 33 | Utils 34 | Alarm Hour 35 | Daily 36 | 3-Day 37 | Weekly 38 | Android TV UI 39 | Ignore Alpha 40 | Ignore Beta 41 | Ignore Pre-release 42 | Use Safe Stores (Aptoide) 43 | Root Install 44 | ApkMirror 45 | F-Droid Main 46 | F-Droid Izzy 47 | Aptoide 48 | GitHub 49 | GitLab 50 | APKPure 51 | Play 52 | About 53 | Frequency 54 | Theme 55 | System 56 | Dark 57 | Light 58 | Get the source code, report bugs, request new features and add translations. 59 | If you like the app, consider donating to a worthy cause. 60 | Copy to clipboard 61 | Copy App List 62 | Copy App Logs 63 | 64 | // Notifications 65 | Updates 66 | Channel for update notifications. 67 | updateChannel 68 | Updates 69 | 70 | No updates found. 71 | Found %1$d update. 72 | Found %1$d updates. 73 | 74 | 75 | -------------------------------------------------------------------------------- /app/src/main/res/xml/backup_rules.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/xml/data_extraction_rules.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 12 | 13 | 19 | -------------------------------------------------------------------------------- /app/src/test/kotlin/com/apkupdater/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.apkupdater 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.application") version "8.3.1" apply false 3 | id("org.jetbrains.kotlin.android") version "1.9.0" apply false 4 | } 5 | -------------------------------------------------------------------------------- /fastlane/metadata/android/de/short_description.txt: -------------------------------------------------------------------------------- 1 | Updates von verschiedenen Quellen beziehen -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/full_description.txt: -------------------------------------------------------------------------------- 1 | # APKUpdater [![](https://github.com/rumboalla/apkupdater/workflows/Android%20Build/badge.svg)](https://github.com/rumboalla/apkupdater/actions?query=workflow%3A%22Android+Build%22) 2 | **APKUpdater** is an open source tool that simplifies the process of **finding updates** for your installed apps. 3 | It provides similar functionality to an app store, but instead of depending on a single source, it aggregates the results from **APKMirror**, **Aptoide**, **F-Droid** and **GitHub**. 4 | 5 | The 3.x branch is a full rewrite using modern technologies like **Jetpack Compose**, **Flow** and **WorkManager**. 6 | 7 | # Features 8 | * **Update Sources**: Find updates from **APKMirror**, **Aptoide**, **F-Droid** and **GitHub**. 9 | * **Search Sources**: Find new apps to install from **APKMirror**, **Aptoide**, **F-Droid** and **GitHub**. 10 | * Schedule **background update checks** and receive a **notification** when updates are found. 11 | * Supports **Android 5** (**21**) to **Android 14** (**34**). 12 | * Supports **Android TV**. 13 | * **Material Design 3** with **Dark**, **Light** and **System** theme support. 14 | * Supports **Material You** on Android 12+. 15 | * **Direct install** of updates for sources that support it. 16 | * Supports **installs without user interaction** on Android 12+. 17 | * **Root install** of updates. 18 | * No ads. No tracking. 19 | * **Languages**: English, Spanish, Dutch, German, Traditional Chinese, Simplified Chinese, Romanian, Italian, Portuguese, Russian, Turkish. 20 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumboalla/apkupdater/c55d9175cddfa50d377a1dd313873510e36517a2/fastlane/metadata/android/en-US/images/icon.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumboalla/apkupdater/c55d9175cddfa50d377a1dd313873510e36517a2/fastlane/metadata/android/en-US/images/phoneScreenshots/01.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumboalla/apkupdater/c55d9175cddfa50d377a1dd313873510e36517a2/fastlane/metadata/android/en-US/images/phoneScreenshots/02.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumboalla/apkupdater/c55d9175cddfa50d377a1dd313873510e36517a2/fastlane/metadata/android/en-US/images/phoneScreenshots/03.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumboalla/apkupdater/c55d9175cddfa50d377a1dd313873510e36517a2/fastlane/metadata/android/en-US/images/phoneScreenshots/04.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumboalla/apkupdater/c55d9175cddfa50d377a1dd313873510e36517a2/fastlane/metadata/android/en-US/images/phoneScreenshots/05.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/06.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumboalla/apkupdater/c55d9175cddfa50d377a1dd313873510e36517a2/fastlane/metadata/android/en-US/images/phoneScreenshots/06.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/short_description.txt: -------------------------------------------------------------------------------- 1 | Get updates from multiple sources -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx4096m -Dfile.encoding=UTF-8 2 | android.useAndroidX=true 3 | kotlin.code.style=official 4 | android.nonTransitiveRClass=true 5 | android.nonFinalResIds=false 6 | android.suppressUnsupportedCompileSdk=34 7 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumboalla/apkupdater/c55d9175cddfa50d377a1dd313873510e36517a2/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip 3 | distributionPath=wrapper/dists 4 | zipStorePath=wrapper/dists 5 | zipStoreBase=GRADLE_USER_HOME 6 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | gradlePluginPortal() 6 | } 7 | } 8 | 9 | dependencyResolutionManagement { 10 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 11 | repositories { 12 | google() 13 | mavenCentral() 14 | maven("https://jitpack.io") 15 | maven("https://gitlab.com/api/v4/projects/18497829/packages/maven") 16 | } 17 | } 18 | 19 | rootProject.name = "apkupdater" 20 | include(":app") 21 | --------------------------------------------------------------------------------