Compare commits
12 Commits
5645b5ad77
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 43214ecc8a | |||
|
|
334fd2fffd | ||
|
|
3ab3943f1e | ||
|
|
4b937c6fe1 | ||
|
|
be3a03a784 | ||
|
|
19ab89c864 | ||
|
|
66b7c4c260 | ||
|
|
786a107053 | ||
|
|
e176a6ab62 | ||
|
|
c4db5fd7e2 | ||
|
|
b6769f13f5 | ||
|
|
b9de3c9850 |
5
.dockerignore
Normal file
5
.dockerignore
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
.gradle
|
||||||
|
**/build
|
||||||
|
**/downloads
|
||||||
|
downloads
|
||||||
|
local.properties
|
||||||
98
README.md
98
README.md
@@ -53,6 +53,104 @@ Please only download the latest stable releases from one of these sources:
|
|||||||
|
|
||||||
You can also get latest debug builds signed with AOSP test keys for testing latest changes from our [GitLab Package Registry](https://gitlab.com/AuroraOSS/AuroraStore/-/packages/24103616).
|
You can also get latest debug builds signed with AOSP test keys for testing latest changes from our [GitLab Package Registry](https://gitlab.com/AuroraOSS/AuroraStore/-/packages/24103616).
|
||||||
|
|
||||||
|
## Headless APK Downloader
|
||||||
|
|
||||||
|
This repository includes a JVM CLI module for downloading the latest available APK files for a Google Play package name. Build the compiled distribution once:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew :apk-downloader:installDist
|
||||||
|
```
|
||||||
|
|
||||||
|
Then run the generated launcher directly, without Gradle:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./apk-downloader/build/install/apk-downloader/bin/apk-downloader org.mozilla.firefox
|
||||||
|
```
|
||||||
|
|
||||||
|
By default, files are saved under `./downloads/<packageName>/<versionCode>/`. To choose a different output directory:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./apk-downloader/build/install/apk-downloader/bin/apk-downloader org.mozilla.firefox --out ./downloads
|
||||||
|
```
|
||||||
|
|
||||||
|
When Google Play returns multiple APK files, the downloader keeps the verified raw files and also writes a split APK archive next to them:
|
||||||
|
|
||||||
|
```text
|
||||||
|
downloads/<packageName>/<versionCode>/
|
||||||
|
base.apk
|
||||||
|
config_*.apk
|
||||||
|
<packageName>_<versionCode>.apks
|
||||||
|
```
|
||||||
|
|
||||||
|
This `.apks` file is a zip archive of the downloaded base/split APKs for split-aware installers. It is not a true universal APK. Bundletool can create a universal APK from an Android App Bundle (`.aab`) with `build-apks --mode=universal`, but Google Play download delivery provides APK files here, not the original `.aab`, so bundletool cannot merge these split APKs back into one universal APK.
|
||||||
|
|
||||||
|
To create a portable archive:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew :apk-downloader:distZip
|
||||||
|
```
|
||||||
|
|
||||||
|
The archive is written to `apk-downloader/build/distributions/apk-downloader.zip`. Unzip it anywhere with Java 17+ installed and run `bin/apk-downloader`.
|
||||||
|
|
||||||
|
For development, you can still run through Gradle:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew :apk-downloader:run --args="org.mozilla.firefox"
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also build a Docker image for the downloader:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -f apk-downloader/Dockerfile -t aurora-apk-downloader .
|
||||||
|
docker run --rm \
|
||||||
|
-v "$PWD/downloads:/downloads" \
|
||||||
|
-v "$HOME/.config/aurora-apk-downloader:/root/.config/aurora-apk-downloader" \
|
||||||
|
aurora-apk-downloader \
|
||||||
|
com.nestle.tr.nescafe.nescafe3in1loyaltyapp --country TR --out /downloads
|
||||||
|
```
|
||||||
|
|
||||||
|
If your Proton VPN runs in a separate VPN container, run the downloader with that container's network namespace, for example `--network container:<vpn-container-name>`.
|
||||||
|
|
||||||
|
The image also includes an exec-friendly wrapper that takes the country first, then the package name. Start a reusable container and run downloads with `docker exec`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -d --name aurora-apk-downloader \
|
||||||
|
-v "$PWD/downloads:/downloads" \
|
||||||
|
-v "$HOME/.config/aurora-apk-downloader:/root/.config/aurora-apk-downloader" \
|
||||||
|
--entrypoint sleep \
|
||||||
|
aurora-apk-downloader infinity
|
||||||
|
|
||||||
|
docker exec aurora-apk-downloader \
|
||||||
|
download-apk TR com.nestle.tr.nescafe.nescafe3in1loyaltyapp
|
||||||
|
```
|
||||||
|
|
||||||
|
To run Proton VPN inside Docker instead of on the host, use the bundled Compose file. Copy the example environment file, fill in your Proton WireGuard values, then start the reusable downloader stack:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp apk-downloader/proton.env.example apk-downloader/proton.env
|
||||||
|
# edit apk-downloader/proton.env
|
||||||
|
docker compose \
|
||||||
|
--env-file apk-downloader/proton.env \
|
||||||
|
-f apk-downloader/docker-compose.proton.yml \
|
||||||
|
up -d --build
|
||||||
|
|
||||||
|
docker compose \
|
||||||
|
--env-file apk-downloader/proton.env \
|
||||||
|
-f apk-downloader/docker-compose.proton.yml \
|
||||||
|
exec downloader download-apk TR com.nestle.tr.nescafe.nescafe3in1loyaltyapp
|
||||||
|
```
|
||||||
|
|
||||||
|
If `APK_COUNTRY` and `APK_PACKAGE` are set in the env file, you can use those defaults instead:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose \
|
||||||
|
--env-file apk-downloader/proton.env \
|
||||||
|
-f apk-downloader/docker-compose.proton.yml \
|
||||||
|
exec downloader sh -lc 'download-apk "$APK_COUNTRY" "$APK_PACKAGE"'
|
||||||
|
```
|
||||||
|
|
||||||
|
For Proton WireGuard, set `WIREGUARD_PRIVATE_KEY` and `WIREGUARD_ADDRESSES` from a Proton VPN WireGuard config. For OpenVPN, set `VPN_TYPE=openvpn` and use Proton's OpenVPN/IKEv2 username and password in `OPENVPN_USER` and `OPENVPN_PASSWORD`.
|
||||||
|
|
||||||
## Certificate Fingerprints
|
## Certificate Fingerprints
|
||||||
|
|
||||||
- SHA1: 94:42:75:D7:59:8B:C0:3E:48:85:06:06:42:25:A7:19:90:A2:22:02
|
- SHA1: 94:42:75:D7:59:8B:C0:3E:48:85:06:06:42:25:A7:19:90:A2:22:02
|
||||||
|
|||||||
22
apk-downloader/.env
Normal file
22
apk-downloader/.env
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# SPDX-FileCopyrightText: 2026 Aurora OSS
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
SERVER_COUNTRIES=Turkey
|
||||||
|
TZ=Europe/Paris
|
||||||
|
|
||||||
|
# Local output/cache paths. DOWNLOADS_DIR is relative to apk-downloader/ when using
|
||||||
|
# `docker compose -f apk-downloader/docker-compose.proton.yml`.
|
||||||
|
DOWNLOADS_DIR=../downloads
|
||||||
|
AUTH_CACHE_DIR=../.apk-downloader-auth
|
||||||
|
|
||||||
|
# Recommended: Proton WireGuard.
|
||||||
|
# Get these values from a Proton VPN WireGuard config.
|
||||||
|
VPN_TYPE=wireguard
|
||||||
|
WIREGUARD_PRIVATE_KEY=
|
||||||
|
WIREGUARD_ADDRESSES=
|
||||||
|
|
||||||
|
# Alternative: Proton OpenVPN.
|
||||||
|
# Use Proton's OpenVPN/IKEv2 credentials, not your normal Proton account password.
|
||||||
|
# VPN_TYPE=openvpn
|
||||||
|
# OPENVPN_USER=
|
||||||
|
# OPENVPN_PASSWORD=
|
||||||
26
apk-downloader/Dockerfile
Normal file
26
apk-downloader/Dockerfile
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# SPDX-FileCopyrightText: 2026 Aurora OSS
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
FROM eclipse-temurin:17-jdk AS build
|
||||||
|
WORKDIR /workspace
|
||||||
|
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends git \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
RUN ./gradlew --no-daemon :apk-downloader:installDist
|
||||||
|
|
||||||
|
FROM eclipse-temurin:17-jre
|
||||||
|
WORKDIR /work
|
||||||
|
|
||||||
|
COPY --from=build /workspace/apk-downloader/build/install/apk-downloader /opt/apk-downloader
|
||||||
|
COPY apk-downloader/docker/download-apk /usr/local/bin/download-apk
|
||||||
|
|
||||||
|
ENV PATH="/opt/apk-downloader/bin:${PATH}" \
|
||||||
|
APK_DOWNLOADS_DIR="/downloads"
|
||||||
|
|
||||||
|
RUN mkdir -p /downloads \
|
||||||
|
&& chmod +x /usr/local/bin/download-apk
|
||||||
|
|
||||||
|
ENTRYPOINT ["/opt/apk-downloader/bin/apk-downloader"]
|
||||||
79
apk-downloader/build.gradle.kts
Normal file
79
apk-downloader/build.gradle.kts
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2026 Aurora OSS
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
import org.gradle.api.file.DuplicatesStrategy
|
||||||
|
import org.gradle.api.attributes.Category
|
||||||
|
import org.gradle.api.attributes.Usage
|
||||||
|
import org.gradle.api.attributes.java.TargetJvmEnvironment
|
||||||
|
import org.gradle.api.tasks.JavaExec
|
||||||
|
import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType
|
||||||
|
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
alias(libs.plugins.jetbrains.kotlin.jvm)
|
||||||
|
alias(libs.plugins.jetbrains.kotlin.serialization)
|
||||||
|
application
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
jvmToolchain(17)
|
||||||
|
}
|
||||||
|
|
||||||
|
application {
|
||||||
|
mainClass.set("com.aurora.apkdownloader.MainKt")
|
||||||
|
}
|
||||||
|
|
||||||
|
val gplayapiAar by configurations.creating {
|
||||||
|
isCanBeConsumed = false
|
||||||
|
isCanBeResolved = true
|
||||||
|
attributes {
|
||||||
|
attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category.LIBRARY))
|
||||||
|
attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage.JAVA_RUNTIME))
|
||||||
|
attribute(TargetJvmEnvironment.TARGET_JVM_ENVIRONMENT_ATTRIBUTE, objects.named("android"))
|
||||||
|
attribute(KotlinPlatformType.attribute, KotlinPlatformType.androidJvm)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val extractedGplayApiJar = layout.buildDirectory.file("gplayapi/gplayapi.jar")
|
||||||
|
val extractGplayApiJar by tasks.registering(Copy::class) {
|
||||||
|
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
|
||||||
|
from({ gplayapiAar.files.map { zipTree(it) } }) {
|
||||||
|
include("classes.jar")
|
||||||
|
rename { "gplayapi.jar" }
|
||||||
|
}
|
||||||
|
into(layout.buildDirectory.dir("gplayapi"))
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
gplayapiAar(libs.auroraoss.gplayapi)
|
||||||
|
implementation(files(extractedGplayApiJar))
|
||||||
|
implementation("com.google.code.gson:gson:2.14.0")
|
||||||
|
implementation(libs.google.protobuf.javalite)
|
||||||
|
implementation("org.jetbrains.kotlin:kotlin-parcelize-runtime:${libs.versions.kotlin.get()}")
|
||||||
|
implementation(libs.kotlinx.coroutines.core)
|
||||||
|
implementation(libs.kotlinx.serialization.json)
|
||||||
|
implementation(libs.squareup.okhttp)
|
||||||
|
|
||||||
|
testImplementation(libs.junit)
|
||||||
|
testImplementation(libs.google.truth)
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.withType<KotlinCompile>().configureEach {
|
||||||
|
dependsOn(extractGplayApiJar)
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.withType<JavaExec>().configureEach {
|
||||||
|
workingDir = rootProject.projectDir
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.named<JavaExec>("run") {
|
||||||
|
isIgnoreExitValue = true
|
||||||
|
doLast {
|
||||||
|
val exitValue = executionResult.get().exitValue
|
||||||
|
if (exitValue != 0) {
|
||||||
|
logger.lifecycle("apk-downloader exited with code $exitValue")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
48
apk-downloader/docker-compose.proton.yml
Normal file
48
apk-downloader/docker-compose.proton.yml
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
# SPDX-FileCopyrightText: 2026 Aurora OSS
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
services:
|
||||||
|
protonvpn:
|
||||||
|
image: qmcgaw/gluetun:latest
|
||||||
|
container_name: aurora-apk-downloader-protonvpn
|
||||||
|
cap_add:
|
||||||
|
- NET_ADMIN
|
||||||
|
devices:
|
||||||
|
- /dev/net/tun:/dev/net/tun
|
||||||
|
environment:
|
||||||
|
VPN_SERVICE_PROVIDER: protonvpn
|
||||||
|
VPN_TYPE: ${VPN_TYPE:-wireguard}
|
||||||
|
SERVER_COUNTRIES: ${SERVER_COUNTRIES}
|
||||||
|
WIREGUARD_PRIVATE_KEY: ${WIREGUARD_PRIVATE_KEY:-}
|
||||||
|
WIREGUARD_ADDRESSES: ${WIREGUARD_ADDRESSES:-}
|
||||||
|
OPENVPN_USER: ${OPENVPN_USER:-}
|
||||||
|
OPENVPN_PASSWORD: ${OPENVPN_PASSWORD:-}
|
||||||
|
TZ: ${TZ:-Europe/Istanbul}
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "/gluetun-entrypoint", "healthcheck"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 12
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
downloader:
|
||||||
|
container_name: aurora-apk-downloader
|
||||||
|
build:
|
||||||
|
context: ..
|
||||||
|
dockerfile: apk-downloader/Dockerfile
|
||||||
|
image: aurora-apk-downloader:latest
|
||||||
|
entrypoint:
|
||||||
|
- sleep
|
||||||
|
- infinity
|
||||||
|
network_mode: "service:protonvpn"
|
||||||
|
depends_on:
|
||||||
|
protonvpn:
|
||||||
|
condition: service_healthy
|
||||||
|
environment:
|
||||||
|
APK_DOWNLOADS_DIR: /downloads
|
||||||
|
APK_COUNTRY: ${APK_COUNTRY:-TR}
|
||||||
|
APK_PACKAGE: ${APK_PACKAGE:-com.nestle.tr.nescafe.nescafe3in1loyaltyapp}
|
||||||
|
volumes:
|
||||||
|
- ${DOWNLOADS_DIR:-../downloads}:/downloads
|
||||||
|
- ${AUTH_CACHE_DIR:-../.apk-downloader-auth}:/root/.config/aurora-apk-downloader
|
||||||
|
restart: unless-stopped
|
||||||
25
apk-downloader/proton.env.example
Normal file
25
apk-downloader/proton.env.example
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# SPDX-FileCopyrightText: 2026 Aurora OSS
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
# Optional defaults for `docker compose exec downloader sh -lc 'download-apk "$APK_COUNTRY" "$APK_PACKAGE"'`.
|
||||||
|
APK_PACKAGE=com.nestle.tr.nescafe.nescafe3in1loyaltyapp
|
||||||
|
APK_COUNTRY=TR
|
||||||
|
SERVER_COUNTRIES=Turkey
|
||||||
|
TZ=Europe/Istanbul
|
||||||
|
|
||||||
|
# Local output/cache paths. DOWNLOADS_DIR is relative to apk-downloader/ when using
|
||||||
|
# `docker compose -f apk-downloader/docker-compose.proton.yml`.
|
||||||
|
DOWNLOADS_DIR=../downloads
|
||||||
|
AUTH_CACHE_DIR=../.apk-downloader-auth
|
||||||
|
|
||||||
|
# Recommended: Proton WireGuard.
|
||||||
|
# Get these values from a Proton VPN WireGuard config.
|
||||||
|
VPN_TYPE=wireguard
|
||||||
|
WIREGUARD_PRIVATE_KEY=
|
||||||
|
WIREGUARD_ADDRESSES=
|
||||||
|
|
||||||
|
# Alternative: Proton OpenVPN.
|
||||||
|
# Use Proton's OpenVPN/IKEv2 credentials, not your normal Proton account password.
|
||||||
|
# VPN_TYPE=openvpn
|
||||||
|
# OPENVPN_USER=
|
||||||
|
# OPENVPN_PASSWORD=
|
||||||
91
apk-downloader/src/main/java/android/os/Parcel.java
Normal file
91
apk-downloader/src/main/java/android/os/Parcel.java
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2026 Aurora OSS
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package android.os;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class Parcel {
|
||||||
|
private Parcel() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public int readInt() {
|
||||||
|
throw unsupported();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void writeInt(int value) {
|
||||||
|
throw unsupported();
|
||||||
|
}
|
||||||
|
|
||||||
|
public long readLong() {
|
||||||
|
throw unsupported();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void writeLong(long value) {
|
||||||
|
throw unsupported();
|
||||||
|
}
|
||||||
|
|
||||||
|
public float readFloat() {
|
||||||
|
throw unsupported();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void writeFloat(float value) {
|
||||||
|
throw unsupported();
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte readByte() {
|
||||||
|
throw unsupported();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void writeByte(byte value) {
|
||||||
|
throw unsupported();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String readString() {
|
||||||
|
throw unsupported();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void writeString(String value) {
|
||||||
|
throw unsupported();
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T extends Parcelable> T readParcelable(ClassLoader loader) {
|
||||||
|
throw unsupported();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void writeParcelable(Parcelable value, int flags) {
|
||||||
|
throw unsupported();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Serializable readSerializable() {
|
||||||
|
throw unsupported();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void writeSerializable(Serializable value) {
|
||||||
|
throw unsupported();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ArrayList<String> createStringArrayList() {
|
||||||
|
throw unsupported();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void writeStringList(List<String> value) {
|
||||||
|
throw unsupported();
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T extends Parcelable> ArrayList<T> createTypedArrayList(Parcelable.Creator<T> creator) {
|
||||||
|
throw unsupported();
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T extends Parcelable> void writeTypedList(List<T> value) {
|
||||||
|
throw unsupported();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static UnsupportedOperationException unsupported() {
|
||||||
|
return new UnsupportedOperationException("Parcel is not supported in apk-downloader");
|
||||||
|
}
|
||||||
|
}
|
||||||
18
apk-downloader/src/main/java/android/os/Parcelable.java
Normal file
18
apk-downloader/src/main/java/android/os/Parcelable.java
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2026 Aurora OSS
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package android.os;
|
||||||
|
|
||||||
|
public interface Parcelable {
|
||||||
|
int describeContents();
|
||||||
|
|
||||||
|
void writeToParcel(Parcel dest, int flags);
|
||||||
|
|
||||||
|
interface Creator<T> {
|
||||||
|
T createFromParcel(Parcel source);
|
||||||
|
|
||||||
|
T[] newArray(int size);
|
||||||
|
}
|
||||||
|
}
|
||||||
66
apk-downloader/src/main/java/android/util/Base64.java
Normal file
66
apk-downloader/src/main/java/android/util/Base64.java
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2026 Aurora OSS
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package android.util;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
|
public final class Base64 {
|
||||||
|
public static final int DEFAULT = 0;
|
||||||
|
public static final int NO_PADDING = 1;
|
||||||
|
public static final int NO_WRAP = 2;
|
||||||
|
public static final int CRLF = 4;
|
||||||
|
public static final int URL_SAFE = 8;
|
||||||
|
public static final int NO_CLOSE = 16;
|
||||||
|
|
||||||
|
private Base64() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] decode(String str, int flags) {
|
||||||
|
java.util.Base64.Decoder decoder = decoder(flags);
|
||||||
|
return decoder.decode(padIfNeeded(str));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] decode(byte[] input, int flags) {
|
||||||
|
return decode(new String(input, StandardCharsets.UTF_8), flags);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String encodeToString(byte[] input, int flags) {
|
||||||
|
java.util.Base64.Encoder encoder = encoder(flags);
|
||||||
|
return encoder.encodeToString(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] encode(byte[] input, int flags) {
|
||||||
|
return encodeToString(input, flags).getBytes(StandardCharsets.UTF_8);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static java.util.Base64.Decoder decoder(int flags) {
|
||||||
|
if ((flags & URL_SAFE) != 0) {
|
||||||
|
return java.util.Base64.getUrlDecoder();
|
||||||
|
}
|
||||||
|
if ((flags & NO_WRAP) != 0) {
|
||||||
|
return java.util.Base64.getDecoder();
|
||||||
|
}
|
||||||
|
return java.util.Base64.getMimeDecoder();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static java.util.Base64.Encoder encoder(int flags) {
|
||||||
|
java.util.Base64.Encoder encoder = (flags & URL_SAFE) != 0
|
||||||
|
? java.util.Base64.getUrlEncoder()
|
||||||
|
: java.util.Base64.getEncoder();
|
||||||
|
if ((flags & NO_PADDING) != 0) {
|
||||||
|
encoder = encoder.withoutPadding();
|
||||||
|
}
|
||||||
|
return encoder;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String padIfNeeded(String value) {
|
||||||
|
int remainder = value.length() % 4;
|
||||||
|
if (remainder == 0) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
return value + "====".substring(remainder);
|
||||||
|
}
|
||||||
|
}
|
||||||
43
apk-downloader/src/main/java/android/util/Log.java
Normal file
43
apk-downloader/src/main/java/android/util/Log.java
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2026 Aurora OSS
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package android.util;
|
||||||
|
|
||||||
|
public final class Log {
|
||||||
|
private Log() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int d(String tag, String msg) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int d(String tag, String msg, Throwable tr) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int i(String tag, String msg) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int i(String tag, String msg, Throwable tr) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int w(String tag, String msg) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int w(String tag, String msg, Throwable tr) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int e(String tag, String msg) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int e(String tag, String msg, Throwable tr) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2026 Aurora OSS
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.aurora.apkdownloader
|
||||||
|
|
||||||
|
import com.aurora.gplayapi.exceptions.GooglePlayException
|
||||||
|
import java.util.Locale
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
|
||||||
|
class ApkDownloaderRunner(
|
||||||
|
private val httpClient: DesktopHttpClient = DesktopHttpClient(OkHttpClient()),
|
||||||
|
private val authSource: AuthSource? = null,
|
||||||
|
private val appResolver: AppResolver = PlayAppResolver(httpClient),
|
||||||
|
private val downloader: Downloader = FileDownloader(httpClient),
|
||||||
|
private val archiveWriter: ApkArchiveWriter = SplitApkArchiveWriter()
|
||||||
|
) {
|
||||||
|
fun run(options: CliOptions): Int {
|
||||||
|
return try {
|
||||||
|
val authData = (authSource ?: buildAuthSource(options)).getAuthData()
|
||||||
|
val app = appResolver.resolve(options.packageName, authData)
|
||||||
|
val paths = DownloadPaths(options.outDir)
|
||||||
|
val appDir = paths.appVersionDirectory(app.packageName, app.versionCode)
|
||||||
|
val downloadedFiles = mutableListOf<java.nio.file.Path>()
|
||||||
|
|
||||||
|
println("Downloading ${app.displayName} (${app.packageName}) version ${app.versionCode}")
|
||||||
|
app.files.forEachIndexed { index, item ->
|
||||||
|
val target = paths.filePath(app.packageName, app.versionCode, item)
|
||||||
|
println("[${index + 1}/${app.files.size}] ${item.name}")
|
||||||
|
downloader.download(item, target)
|
||||||
|
downloadedFiles.add(target)
|
||||||
|
}
|
||||||
|
|
||||||
|
archiveWriter.write(app.packageName, app.versionCode, appDir, downloadedFiles)?.let {
|
||||||
|
println("Saved split APK archive to $it")
|
||||||
|
}
|
||||||
|
println("Saved APK files to $appDir")
|
||||||
|
ExitCodes.SUCCESS
|
||||||
|
} catch (exception: AuthFailureException) {
|
||||||
|
System.err.println(exception.cleanMessage())
|
||||||
|
ExitCodes.AUTH_FAILURE
|
||||||
|
} catch (exception: UnsupportedPackageException) {
|
||||||
|
System.err.println(exception.cleanMessage())
|
||||||
|
ExitCodes.INVALID_ARGS_OR_UNSUPPORTED
|
||||||
|
} catch (exception: GooglePlayException) {
|
||||||
|
System.err.println(exception.cleanMessage())
|
||||||
|
ExitCodes.INVALID_ARGS_OR_UNSUPPORTED
|
||||||
|
} catch (exception: DownloadException) {
|
||||||
|
System.err.println(exception.cleanMessage())
|
||||||
|
ExitCodes.DOWNLOAD_FAILURE
|
||||||
|
} catch (exception: Exception) {
|
||||||
|
System.err.println(exception.cleanMessage())
|
||||||
|
ExitCodes.DOWNLOAD_FAILURE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildAuthSource(options: CliOptions): AuthSource {
|
||||||
|
val locale = Locale.forLanguageTag(options.geoProfile.localeTag)
|
||||||
|
return AuthSessionManager(
|
||||||
|
httpClient = httpClient,
|
||||||
|
json = JsonFactory.create(),
|
||||||
|
properties = DesktopDeviceProfile.properties(options.geoProfile),
|
||||||
|
locale = locale,
|
||||||
|
cacheFile = AuthSessionManager.defaultCacheFile(options.geoProfile.cacheKey)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Exception.cleanMessage(): String {
|
||||||
|
val message = message ?: this::class.simpleName.orEmpty()
|
||||||
|
return message.filter { it == '\n' || it == '\t' || !it.isISOControl() }.trim()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2026 Aurora OSS
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.aurora.apkdownloader
|
||||||
|
|
||||||
|
import com.aurora.gplayapi.data.models.AuthData
|
||||||
|
import com.aurora.gplayapi.data.models.PlayResponse
|
||||||
|
import com.aurora.gplayapi.helpers.AuthHelper
|
||||||
|
import java.nio.file.Files
|
||||||
|
import java.nio.file.Path
|
||||||
|
import java.util.Locale
|
||||||
|
import java.util.Properties
|
||||||
|
import kotlin.io.path.Path
|
||||||
|
import kotlin.io.path.exists
|
||||||
|
import kotlin.io.path.readText
|
||||||
|
import kotlin.io.path.writeText
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.decodeFromString
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
|
||||||
|
class AuthFailureException(message: String, cause: Throwable? = null) : Exception(message, cause)
|
||||||
|
|
||||||
|
fun interface AuthSource {
|
||||||
|
fun getAuthData(): AuthData
|
||||||
|
}
|
||||||
|
|
||||||
|
class AuthSessionManager(
|
||||||
|
private val httpClient: DesktopHttpClient,
|
||||||
|
private val json: Json,
|
||||||
|
private val properties: Properties,
|
||||||
|
private val locale: Locale = Locale.US,
|
||||||
|
private val dispenserUrl: String = DEFAULT_DISPENSER_URL,
|
||||||
|
private val cacheFile: Path = defaultCacheFile()
|
||||||
|
) : AuthSource {
|
||||||
|
override fun getAuthData(): AuthData {
|
||||||
|
val cached = readCached()
|
||||||
|
if (cached != null && AuthHelper.isValid(cached)) {
|
||||||
|
return cached
|
||||||
|
}
|
||||||
|
|
||||||
|
return buildAnonymousAuthData().also { authData ->
|
||||||
|
Files.createDirectories(cacheFile.parent)
|
||||||
|
cacheFile.writeText(json.encodeToString(authData))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun readCached(): AuthData? {
|
||||||
|
if (!cacheFile.exists()) return null
|
||||||
|
return runCatching {
|
||||||
|
json.decodeFromString<AuthData>(cacheFile.readText())
|
||||||
|
}.getOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildAnonymousAuthData(): AuthData {
|
||||||
|
return try {
|
||||||
|
val requestBody = json.encodeToString(properties).toByteArray()
|
||||||
|
val response = retryTransientDispenserFailures {
|
||||||
|
httpClient.postAuth(dispenserUrl, requestBody)
|
||||||
|
}
|
||||||
|
if (!response.isSuccessful) {
|
||||||
|
throw AuthFailureException(
|
||||||
|
"Dispenser returned HTTP ${response.code}: ${response.errorString}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val dispenserAuth = json.decodeFromString<DispenserAuth>(
|
||||||
|
response.responseBytes.decodeToString()
|
||||||
|
)
|
||||||
|
AuthHelper.build(
|
||||||
|
email = dispenserAuth.email,
|
||||||
|
token = dispenserAuth.auth,
|
||||||
|
tokenType = AuthHelper.Token.AUTH,
|
||||||
|
isAnonymous = true,
|
||||||
|
properties = properties,
|
||||||
|
locale = locale
|
||||||
|
)
|
||||||
|
} catch (exception: AuthFailureException) {
|
||||||
|
throw exception
|
||||||
|
} catch (exception: Exception) {
|
||||||
|
val location = exception.stackTrace.firstOrNull()?.let { " at $it" }.orEmpty()
|
||||||
|
throw AuthFailureException(
|
||||||
|
"Failed to build anonymous auth session: ${exception::class.qualifiedName}: ${exception.message}$location",
|
||||||
|
exception
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun retryTransientDispenserFailures(
|
||||||
|
maxAttempts: Int = 3,
|
||||||
|
request: () -> PlayResponse
|
||||||
|
): PlayResponse {
|
||||||
|
var lastResponse: PlayResponse? = null
|
||||||
|
repeat(maxAttempts) { attempt ->
|
||||||
|
val response = request()
|
||||||
|
if (response.code !in 500..599 || attempt == maxAttempts - 1) {
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
lastResponse = response
|
||||||
|
Thread.sleep(500L * (attempt + 1))
|
||||||
|
}
|
||||||
|
return lastResponse ?: request()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
private data class DispenserAuth(
|
||||||
|
val email: String,
|
||||||
|
@SerialName("authToken") val auth: String
|
||||||
|
)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val DEFAULT_DISPENSER_URL = "https://auroraoss.com/api/auth"
|
||||||
|
|
||||||
|
fun defaultCacheFile(cacheKey: String = "default"): Path {
|
||||||
|
val home = System.getProperty("user.home")
|
||||||
|
?: error("user.home system property is not set")
|
||||||
|
return Path(home, ".config", "aurora-apk-downloader", "auth-$cacheKey.json")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
131
apk-downloader/src/main/kotlin/com/aurora/apkdownloader/Cli.kt
Normal file
131
apk-downloader/src/main/kotlin/com/aurora/apkdownloader/Cli.kt
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2026 Aurora OSS
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.aurora.apkdownloader
|
||||||
|
|
||||||
|
import java.nio.file.Path
|
||||||
|
import kotlin.io.path.Path
|
||||||
|
|
||||||
|
data class CliOptions(
|
||||||
|
val packageName: String,
|
||||||
|
val outDir: Path = Path("downloads"),
|
||||||
|
val geoProfile: GeoProfile = GeoProfile.default()
|
||||||
|
)
|
||||||
|
|
||||||
|
data class GeoProfile(
|
||||||
|
val country: String,
|
||||||
|
val localeTag: String,
|
||||||
|
val timeZone: String,
|
||||||
|
val operator: String
|
||||||
|
) {
|
||||||
|
val cacheKey: String
|
||||||
|
get() = listOf(country, localeTag, timeZone, operator)
|
||||||
|
.joinToString("-")
|
||||||
|
.lowercase()
|
||||||
|
.replace(Regex("[^a-z0-9._-]"), "_")
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun default(): GeoProfile = RegionProfile.US.geoProfile
|
||||||
|
|
||||||
|
fun forCountry(country: String): GeoProfile? = RegionProfile.entries
|
||||||
|
.firstOrNull { it.name.equals(country, ignoreCase = true) }
|
||||||
|
?.geoProfile
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum class RegionProfile(val geoProfile: GeoProfile) {
|
||||||
|
US(
|
||||||
|
GeoProfile(
|
||||||
|
country = "US",
|
||||||
|
localeTag = "en-US",
|
||||||
|
timeZone = "America/New_York",
|
||||||
|
operator = "310260"
|
||||||
|
)
|
||||||
|
),
|
||||||
|
TR(
|
||||||
|
GeoProfile(
|
||||||
|
country = "TR",
|
||||||
|
localeTag = "tr-TR",
|
||||||
|
timeZone = "Europe/Istanbul",
|
||||||
|
operator = "28601"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed interface CliParseResult {
|
||||||
|
data class Valid(val options: CliOptions) : CliParseResult
|
||||||
|
data class Invalid(val message: String) : CliParseResult
|
||||||
|
}
|
||||||
|
|
||||||
|
object CliParser {
|
||||||
|
fun parse(args: Array<String>): CliParseResult {
|
||||||
|
if (args.isEmpty()) return CliParseResult.Invalid("Missing package name")
|
||||||
|
|
||||||
|
var packageName: String? = null
|
||||||
|
var outDir = Path("downloads")
|
||||||
|
var geoProfile = GeoProfile.default()
|
||||||
|
var index = 0
|
||||||
|
|
||||||
|
while (index < args.size) {
|
||||||
|
when (val arg = args[index]) {
|
||||||
|
"--out" -> {
|
||||||
|
val value = args.getOrNull(index + 1)
|
||||||
|
?: return CliParseResult.Invalid("Missing value for --out")
|
||||||
|
outDir = Path(value)
|
||||||
|
index += 2
|
||||||
|
}
|
||||||
|
"--country" -> {
|
||||||
|
val value = args.getOrNull(index + 1)
|
||||||
|
?: return CliParseResult.Invalid("Missing value for --country")
|
||||||
|
geoProfile = GeoProfile.forCountry(value)
|
||||||
|
?: return CliParseResult.Invalid("Unsupported country: $value")
|
||||||
|
index += 2
|
||||||
|
}
|
||||||
|
"--locale" -> {
|
||||||
|
val value = args.getOrNull(index + 1)
|
||||||
|
?: return CliParseResult.Invalid("Missing value for --locale")
|
||||||
|
geoProfile = geoProfile.copy(localeTag = value)
|
||||||
|
index += 2
|
||||||
|
}
|
||||||
|
"--timezone" -> {
|
||||||
|
val value = args.getOrNull(index + 1)
|
||||||
|
?: return CliParseResult.Invalid("Missing value for --timezone")
|
||||||
|
geoProfile = geoProfile.copy(timeZone = value)
|
||||||
|
index += 2
|
||||||
|
}
|
||||||
|
"--operator" -> {
|
||||||
|
val value = args.getOrNull(index + 1)
|
||||||
|
?: return CliParseResult.Invalid("Missing value for --operator")
|
||||||
|
if (!value.matches(Regex("\\d{5,6}"))) {
|
||||||
|
return CliParseResult.Invalid("Invalid operator: $value")
|
||||||
|
}
|
||||||
|
geoProfile = geoProfile.copy(operator = value)
|
||||||
|
index += 2
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
if (arg.startsWith("--")) {
|
||||||
|
return CliParseResult.Invalid("Unknown option: $arg")
|
||||||
|
}
|
||||||
|
if (packageName != null) {
|
||||||
|
return CliParseResult.Invalid("Unexpected argument: $arg")
|
||||||
|
}
|
||||||
|
packageName = arg
|
||||||
|
index++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val parsedPackageName = packageName
|
||||||
|
?: return CliParseResult.Invalid("Missing package name")
|
||||||
|
if (!PACKAGE_NAME_REGEX.matches(parsedPackageName)) {
|
||||||
|
return CliParseResult.Invalid("Invalid package name: $parsedPackageName")
|
||||||
|
}
|
||||||
|
|
||||||
|
return CliParseResult.Valid(CliOptions(parsedPackageName, outDir, geoProfile))
|
||||||
|
}
|
||||||
|
|
||||||
|
private val PACKAGE_NAME_REGEX =
|
||||||
|
Regex("[A-Za-z][A-Za-z0-9_]*(\\.[A-Za-z][A-Za-z0-9_]*)+")
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2026 Aurora OSS
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.aurora.apkdownloader
|
||||||
|
|
||||||
|
import java.util.Properties
|
||||||
|
|
||||||
|
object DesktopDeviceProfile {
|
||||||
|
fun properties(geoProfile: GeoProfile = GeoProfile.default()): Properties = Properties().apply {
|
||||||
|
setProperty("UserReadableName", "Google Pixel 7a")
|
||||||
|
setProperty("Build.BOARD", "lynx")
|
||||||
|
setProperty("Build.BOOTLOADER", "lynx-1.0-11017859")
|
||||||
|
setProperty("Build.BRAND", "google")
|
||||||
|
setProperty("Build.DEVICE", "lynx")
|
||||||
|
setProperty("Build.FINGERPRINT", "google/lynx/lynx:14/AP2A.240805.005/12025142:user/release-keys")
|
||||||
|
setProperty("Build.HARDWARE", "lynx")
|
||||||
|
setProperty("Build.ID", "AP2A.240805.005")
|
||||||
|
setProperty("Build.MANUFACTURER", "Google")
|
||||||
|
setProperty("Build.MODEL", "Pixel 7a")
|
||||||
|
setProperty("Build.PRODUCT", "lynx")
|
||||||
|
setProperty("Build.RADIO", "g5300g-230927-231026-B-11040898")
|
||||||
|
setProperty("Build.TAGS", "release-keys")
|
||||||
|
setProperty("Build.TIME", "1722459600000")
|
||||||
|
setProperty("Build.TYPE", "user")
|
||||||
|
setProperty("Build.USER", "android-build")
|
||||||
|
setProperty("Build.VERSION.CODENAME", "REL")
|
||||||
|
setProperty("Build.VERSION.INCREMENTAL", "12025142")
|
||||||
|
setProperty("Build.VERSION.RELEASE", "15")
|
||||||
|
setProperty("Build.VERSION.SDK_INT", "35")
|
||||||
|
setProperty("CellOperator", geoProfile.operator.take(3))
|
||||||
|
setProperty("Client", "android-google")
|
||||||
|
setProperty("Features", "android.hardware.audio.output,android.hardware.bluetooth,android.hardware.bluetooth_le,android.hardware.camera,android.hardware.camera.autofocus,android.hardware.camera.flash,android.hardware.camera.front,android.hardware.fingerprint,android.hardware.location,android.hardware.location.gps,android.hardware.location.network,android.hardware.microphone,android.hardware.nfc,android.hardware.opengles.aep,android.hardware.ram.normal,android.hardware.screen.landscape,android.hardware.screen.portrait,android.hardware.sensor.accelerometer,android.hardware.sensor.compass,android.hardware.sensor.gyroscope,android.hardware.sensor.light,android.hardware.sensor.proximity,android.hardware.telephony,android.hardware.telephony.gsm,android.hardware.touchscreen,android.hardware.touchscreen.multitouch,android.hardware.usb.accessory,android.hardware.usb.host,android.hardware.wifi,android.hardware.wifi.direct,android.software.app_widgets,android.software.backup,android.software.cts,android.software.device_admin,android.software.file_based_encryption,android.software.home_screen,android.software.input_methods,android.software.live_wallpaper,android.software.managed_users,android.software.print,android.software.secure_lock_screen,android.software.verified_boot,android.software.webview,com.google.android.feature.GOOGLE_BUILD,com.google.android.feature.GOOGLE_EXPERIENCE,com.google.android.feature.PIXEL_EXPERIENCE")
|
||||||
|
setProperty("GL.Extensions", "GL_ANDROID_extension_pack_es31a,GL_EXT_color_buffer_float,GL_EXT_debug_marker,GL_EXT_texture_filter_anisotropic,GL_KHR_debug,GL_OES_EGL_image,GL_OES_EGL_image_external,GL_OES_depth24,GL_OES_depth_texture,GL_OES_element_index_uint,GL_OES_rgb8_rgba8,GL_OES_standard_derivatives,GL_OES_texture_float,GL_OES_texture_npot,GL_OES_vertex_array_object")
|
||||||
|
setProperty("GL.Version", "196610")
|
||||||
|
setProperty("GSF.version", "251333035")
|
||||||
|
setProperty("HasFiveWayNavigation", "false")
|
||||||
|
setProperty("HasHardKeyboard", "false")
|
||||||
|
setProperty("Keyboard", "1")
|
||||||
|
setProperty("Locales", localeList(geoProfile))
|
||||||
|
setProperty("Navigation", "1")
|
||||||
|
setProperty("Platforms", "arm64-v8a")
|
||||||
|
setProperty("Roaming", "mobile-notroaming")
|
||||||
|
setProperty("Screen.Density", "420")
|
||||||
|
setProperty("Screen.Height", "2424")
|
||||||
|
setProperty("Screen.Width", "1080")
|
||||||
|
setProperty("ScreenLayout", "2")
|
||||||
|
setProperty("SharedLibraries", "android.ext.shared,android.hidl.base-V1.0-java,android.hidl.manager-V1.0-java,android.net.ipsec.ike,android.test.base,android.test.mock,android.test.runner,com.android.future.usb.accessory,com.android.location.provider,com.android.media.remotedisplay,com.android.mediadrm.signer,com.android.nfc_extras,com.android.cts.ctsshim.shared_library,com.google.android.gms,javax.obex,org.apache.http.legacy")
|
||||||
|
setProperty("SimOperator", geoProfile.operator)
|
||||||
|
setProperty("TimeZone", geoProfile.timeZone)
|
||||||
|
setProperty("TouchScreen", "3")
|
||||||
|
setProperty("Vending.version", "84651510")
|
||||||
|
setProperty("Vending.versionString", "46.5.15-31 [0] [PR] 777931631")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun localeList(geoProfile: GeoProfile): String {
|
||||||
|
val normalized = geoProfile.localeTag.replace("-", "_")
|
||||||
|
val language = normalized.substringBefore("_")
|
||||||
|
return listOf(
|
||||||
|
language,
|
||||||
|
normalized,
|
||||||
|
"en",
|
||||||
|
"en_US",
|
||||||
|
"fr",
|
||||||
|
"fr_FR",
|
||||||
|
"de",
|
||||||
|
"de_DE",
|
||||||
|
"es",
|
||||||
|
"es_ES"
|
||||||
|
).distinct().joinToString(",")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2026 Aurora OSS
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.aurora.apkdownloader
|
||||||
|
|
||||||
|
import com.aurora.gplayapi.data.models.PlayResponse
|
||||||
|
import com.aurora.gplayapi.network.IHttpClient
|
||||||
|
import java.io.IOException
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import okhttp3.Headers.Companion.toHeaders
|
||||||
|
import okhttp3.HttpUrl
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
|
import okhttp3.Response
|
||||||
|
|
||||||
|
class DesktopHttpClient(private val okHttpClient: OkHttpClient) : IHttpClient {
|
||||||
|
companion object {
|
||||||
|
private const val USER_AGENT = "com.aurora.store-4.8.3-75"
|
||||||
|
}
|
||||||
|
|
||||||
|
private val _responseCode = MutableStateFlow(0)
|
||||||
|
override val responseCode: StateFlow<Int>
|
||||||
|
get() = _responseCode.asStateFlow()
|
||||||
|
|
||||||
|
fun call(url: String, headers: Map<String, String> = emptyMap()): Response {
|
||||||
|
val request = Request.Builder()
|
||||||
|
.url(url)
|
||||||
|
.headers(headers.toHeaders())
|
||||||
|
.get()
|
||||||
|
.build()
|
||||||
|
return okHttpClient.newCall(request).execute()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
override fun post(
|
||||||
|
url: String,
|
||||||
|
headers: Map<String, String>,
|
||||||
|
params: Map<String, String>
|
||||||
|
): PlayResponse {
|
||||||
|
val request = Request.Builder()
|
||||||
|
.url(buildUrl(url, params))
|
||||||
|
.headers(headers.toHeaders())
|
||||||
|
.post(ByteArray(0).toRequestBody())
|
||||||
|
.build()
|
||||||
|
return process(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun postAuth(url: String, body: ByteArray): PlayResponse {
|
||||||
|
val request = Request.Builder()
|
||||||
|
.url(url)
|
||||||
|
.header("User-Agent", USER_AGENT)
|
||||||
|
.post(body.toRequestBody("application/json".toMediaType()))
|
||||||
|
.build()
|
||||||
|
return process(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
override fun post(url: String, headers: Map<String, String>, body: ByteArray): PlayResponse {
|
||||||
|
val request = Request.Builder()
|
||||||
|
.url(url)
|
||||||
|
.headers(headers.toHeaders())
|
||||||
|
.post(body.toRequestBody())
|
||||||
|
.build()
|
||||||
|
return process(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
override fun get(url: String, headers: Map<String, String>): PlayResponse =
|
||||||
|
get(url, headers, emptyMap())
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
override fun get(
|
||||||
|
url: String,
|
||||||
|
headers: Map<String, String>,
|
||||||
|
params: Map<String, String>
|
||||||
|
): PlayResponse {
|
||||||
|
val request = Request.Builder()
|
||||||
|
.url(buildUrl(url, params))
|
||||||
|
.headers(headers.toHeaders())
|
||||||
|
.get()
|
||||||
|
.build()
|
||||||
|
return process(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getAuth(url: String): PlayResponse {
|
||||||
|
val request = Request.Builder()
|
||||||
|
.url(url)
|
||||||
|
.header("User-Agent", USER_AGENT)
|
||||||
|
.get()
|
||||||
|
.build()
|
||||||
|
return process(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
override fun get(url: String, headers: Map<String, String>, paramString: String): PlayResponse {
|
||||||
|
val request = Request.Builder()
|
||||||
|
.url("$url$paramString")
|
||||||
|
.headers(headers.toHeaders())
|
||||||
|
.get()
|
||||||
|
.build()
|
||||||
|
return process(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun process(request: Request): PlayResponse {
|
||||||
|
_responseCode.value = 0
|
||||||
|
okHttpClient.newCall(request).execute().use { response ->
|
||||||
|
val responseBytes = response.body.bytes()
|
||||||
|
_responseCode.value = response.code
|
||||||
|
return PlayResponse(
|
||||||
|
isSuccessful = response.isSuccessful,
|
||||||
|
code = response.code,
|
||||||
|
responseBytes = responseBytes,
|
||||||
|
errorString = if (response.isSuccessful) {
|
||||||
|
""
|
||||||
|
} else {
|
||||||
|
responseBytes.decodeToString().ifBlank { response.message }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildUrl(url: String, params: Map<String, String>): HttpUrl {
|
||||||
|
val builder = url.toHttpUrl().newBuilder()
|
||||||
|
params.forEach { (key, value) -> builder.addQueryParameter(key, value) }
|
||||||
|
return builder.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2026 Aurora OSS
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.aurora.apkdownloader
|
||||||
|
|
||||||
|
data class DownloadItem(
|
||||||
|
val url: String,
|
||||||
|
val name: String,
|
||||||
|
val size: Long,
|
||||||
|
val sha1: String = "",
|
||||||
|
val sha256: String = "",
|
||||||
|
val sharedLibraryPackageName: String? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
data class ResolvedApp(
|
||||||
|
val packageName: String,
|
||||||
|
val displayName: String,
|
||||||
|
val versionCode: Long,
|
||||||
|
val files: List<DownloadItem>
|
||||||
|
)
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2026 Aurora OSS
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.aurora.apkdownloader
|
||||||
|
|
||||||
|
import java.nio.file.Path
|
||||||
|
|
||||||
|
class DownloadPaths(private val outDir: Path) {
|
||||||
|
fun appVersionDirectory(packageName: String, versionCode: Long): Path =
|
||||||
|
outDir.resolve(packageName).resolve(versionCode.toString())
|
||||||
|
|
||||||
|
fun filePath(packageName: String, versionCode: Long, item: DownloadItem): Path {
|
||||||
|
val appDir = appVersionDirectory(packageName, versionCode)
|
||||||
|
val parent = item.sharedLibraryPackageName?.let {
|
||||||
|
appDir.resolve("libraries").resolve(it)
|
||||||
|
} ?: appDir
|
||||||
|
return parent.resolve(item.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2026 Aurora OSS
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.aurora.apkdownloader
|
||||||
|
|
||||||
|
object ExitCodes {
|
||||||
|
const val SUCCESS = 0
|
||||||
|
const val INVALID_ARGS_OR_UNSUPPORTED = 1
|
||||||
|
const val AUTH_FAILURE = 2
|
||||||
|
const val DOWNLOAD_FAILURE = 3
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2026 Aurora OSS
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.aurora.apkdownloader
|
||||||
|
|
||||||
|
import java.net.HttpURLConnection.HTTP_PARTIAL
|
||||||
|
import java.nio.file.Files
|
||||||
|
import java.nio.file.Path
|
||||||
|
import java.nio.file.StandardCopyOption
|
||||||
|
import java.nio.file.StandardOpenOption
|
||||||
|
import kotlin.io.path.deleteIfExists
|
||||||
|
import kotlin.io.path.exists
|
||||||
|
import kotlin.io.path.fileSize
|
||||||
|
import kotlin.io.path.notExists
|
||||||
|
import kotlin.io.path.outputStream
|
||||||
|
import kotlin.io.path.pathString
|
||||||
|
|
||||||
|
class DownloadException(message: String, cause: Throwable? = null) : Exception(message, cause)
|
||||||
|
|
||||||
|
fun interface Downloader {
|
||||||
|
fun download(item: DownloadItem, target: Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
class FileDownloader(private val httpClient: DesktopHttpClient) : Downloader {
|
||||||
|
override fun download(item: DownloadItem, target: Path) {
|
||||||
|
if (target.exists() && verifyCompletedFile(target, item)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Files.createDirectories(target.parent)
|
||||||
|
val tmp = target.resolveSibling("${target.fileName}.tmp")
|
||||||
|
val existingBytes = if (tmp.exists()) tmp.fileSize() else 0L
|
||||||
|
val headers = if (existingBytes > 0) {
|
||||||
|
mapOf("Range" to "bytes=$existingBytes-")
|
||||||
|
} else {
|
||||||
|
emptyMap()
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
httpClient.call(item.url, headers).use { response ->
|
||||||
|
if (!response.isSuccessful) {
|
||||||
|
throw DownloadException("Download failed for ${item.name}: HTTP ${response.code}")
|
||||||
|
}
|
||||||
|
|
||||||
|
val resuming = existingBytes > 0 && response.code == HTTP_PARTIAL
|
||||||
|
if (existingBytes > 0 && !resuming) {
|
||||||
|
tmp.deleteIfExists()
|
||||||
|
}
|
||||||
|
|
||||||
|
response.body.byteStream().use { input ->
|
||||||
|
val options = if (resuming) {
|
||||||
|
arrayOf(StandardOpenOption.CREATE, StandardOpenOption.APPEND)
|
||||||
|
} else {
|
||||||
|
arrayOf(StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)
|
||||||
|
}
|
||||||
|
Files.newOutputStream(tmp, *options).use { output ->
|
||||||
|
input.copyTo(output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Files.move(tmp, target, StandardCopyOption.REPLACE_EXISTING)
|
||||||
|
if (!verifyCompletedFile(target, item)) {
|
||||||
|
target.deleteIfExists()
|
||||||
|
throw DownloadException("Verification failed for ${target.pathString}")
|
||||||
|
}
|
||||||
|
} catch (exception: DownloadException) {
|
||||||
|
throw exception
|
||||||
|
} catch (exception: Exception) {
|
||||||
|
throw DownloadException("Download failed for ${item.name}: ${exception.message}", exception)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun verifyCompletedFile(target: Path, item: DownloadItem): Boolean {
|
||||||
|
if (target.notExists()) return false
|
||||||
|
if (item.size > 0 && target.fileSize() != item.size) return false
|
||||||
|
return FileVerifier.verify(target, item)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2026 Aurora OSS
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.aurora.apkdownloader
|
||||||
|
|
||||||
|
import java.nio.file.Path
|
||||||
|
import java.security.DigestInputStream
|
||||||
|
import java.security.MessageDigest
|
||||||
|
import kotlin.io.path.inputStream
|
||||||
|
import kotlin.io.path.notExists
|
||||||
|
import kotlin.io.path.pathString
|
||||||
|
|
||||||
|
object FileVerifier {
|
||||||
|
fun verify(file: Path, item: DownloadItem): Boolean {
|
||||||
|
if (file.notExists()) return false
|
||||||
|
val algorithm = if (item.sha256.isBlank()) "SHA-1" else "SHA-256"
|
||||||
|
val expected = if (algorithm == "SHA-1") item.sha1 else item.sha256
|
||||||
|
if (expected.isBlank()) return false
|
||||||
|
|
||||||
|
return runCatching {
|
||||||
|
val digest = MessageDigest.getInstance(algorithm)
|
||||||
|
file.inputStream().use { input ->
|
||||||
|
DigestInputStream(input, digest).use { digestInput ->
|
||||||
|
val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
|
||||||
|
while (digestInput.read(buffer) != -1) {
|
||||||
|
/* DigestInputStream updates the digest as bytes are read. */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
digest.digest().toHex() == expected.lowercase()
|
||||||
|
}.getOrElse {
|
||||||
|
System.err.println("Failed to verify ${file.pathString}: ${it.message}")
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ByteArray.toHex(): String = joinToString(separator = "") { byte ->
|
||||||
|
"%02x".format(byte)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2026 Aurora OSS
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.aurora.apkdownloader
|
||||||
|
|
||||||
|
import com.aurora.gplayapi.data.serializers.LocaleSerializer
|
||||||
|
import com.aurora.gplayapi.data.serializers.PropertiesSerializer
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import kotlinx.serialization.modules.SerializersModule
|
||||||
|
import kotlinx.serialization.modules.contextual
|
||||||
|
|
||||||
|
object JsonFactory {
|
||||||
|
fun create(): Json {
|
||||||
|
val module = SerializersModule {
|
||||||
|
contextual(LocaleSerializer)
|
||||||
|
contextual(PropertiesSerializer)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Json {
|
||||||
|
prettyPrint = true
|
||||||
|
ignoreUnknownKeys = true
|
||||||
|
coerceInputValues = true
|
||||||
|
serializersModule = module
|
||||||
|
explicitNulls = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2026 Aurora OSS
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.aurora.apkdownloader
|
||||||
|
|
||||||
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
|
fun main(args: Array<String>) {
|
||||||
|
when (val result = CliParser.parse(args)) {
|
||||||
|
is CliParseResult.Invalid -> {
|
||||||
|
System.err.println(result.message)
|
||||||
|
System.err.println(
|
||||||
|
"Usage: apk-downloader <package.name> [--out <path>] [--country US|TR] " +
|
||||||
|
"[--locale <tag>] [--timezone <zone>] [--operator <mccmnc>]"
|
||||||
|
)
|
||||||
|
exitProcess(ExitCodes.INVALID_ARGS_OR_UNSUPPORTED)
|
||||||
|
}
|
||||||
|
is CliParseResult.Valid -> {
|
||||||
|
exitProcess(ApkDownloaderRunner().run(result.options))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2026 Aurora OSS
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.aurora.apkdownloader
|
||||||
|
|
||||||
|
import com.aurora.gplayapi.data.models.App
|
||||||
|
import com.aurora.gplayapi.data.models.AuthData
|
||||||
|
import com.aurora.gplayapi.data.models.PlayFile
|
||||||
|
import com.aurora.gplayapi.helpers.AppDetailsHelper
|
||||||
|
import com.aurora.gplayapi.helpers.PurchaseHelper
|
||||||
|
|
||||||
|
class UnsupportedPackageException(message: String) : Exception(message)
|
||||||
|
|
||||||
|
fun interface AppResolver {
|
||||||
|
fun resolve(packageName: String, authData: AuthData): ResolvedApp
|
||||||
|
}
|
||||||
|
|
||||||
|
class PlayAppResolver(
|
||||||
|
private val httpClient: DesktopHttpClient
|
||||||
|
) : AppResolver {
|
||||||
|
override fun resolve(packageName: String, authData: AuthData): ResolvedApp {
|
||||||
|
val app = AppDetailsHelper(authData)
|
||||||
|
.using(httpClient)
|
||||||
|
.getAppByPackageName(packageName)
|
||||||
|
|
||||||
|
val purchaseHelper = PurchaseHelper(authData).using(httpClient)
|
||||||
|
val appFiles = app.usableFiles().ifEmpty {
|
||||||
|
purchaseHelper.purchase(app.packageName, app.versionCode, app.offerType)
|
||||||
|
.usableFiles()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (appFiles.isEmpty()) {
|
||||||
|
throw UnsupportedPackageException("No APK files available for ${app.packageName}")
|
||||||
|
}
|
||||||
|
|
||||||
|
val sharedLibraryFiles = app.dependencies.dependentLibraries.flatMap { sharedLibrary ->
|
||||||
|
val files = sharedLibrary.usableFiles(sharedLibrary.packageName).ifEmpty {
|
||||||
|
purchaseHelper.purchase(sharedLibrary.packageName, sharedLibrary.versionCode, 0)
|
||||||
|
.usableFiles(sharedLibrary.packageName)
|
||||||
|
}
|
||||||
|
files
|
||||||
|
}
|
||||||
|
|
||||||
|
return ResolvedApp(
|
||||||
|
packageName = app.packageName,
|
||||||
|
displayName = app.displayName,
|
||||||
|
versionCode = app.versionCode,
|
||||||
|
files = sharedLibraryFiles + appFiles
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun App.usableFiles(sharedLibraryPackageName: String? = null): List<DownloadItem> =
|
||||||
|
fileList.usableFiles(sharedLibraryPackageName)
|
||||||
|
|
||||||
|
private fun List<PlayFile>.usableFiles(
|
||||||
|
sharedLibraryPackageName: String? = null
|
||||||
|
): List<DownloadItem> = mapNotNull { playFile ->
|
||||||
|
playFile.toDownloadItem(sharedLibraryPackageName)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun PlayFile.toDownloadItem(sharedLibraryPackageName: String?): DownloadItem? {
|
||||||
|
if (url.isBlank()) return null
|
||||||
|
return when (type) {
|
||||||
|
PlayFile.Type.BASE,
|
||||||
|
PlayFile.Type.SPLIT -> DownloadItem(
|
||||||
|
url = url,
|
||||||
|
name = name,
|
||||||
|
size = size,
|
||||||
|
sha1 = sha1,
|
||||||
|
sha256 = sha256,
|
||||||
|
sharedLibraryPackageName = sharedLibraryPackageName
|
||||||
|
)
|
||||||
|
PlayFile.Type.OBB,
|
||||||
|
PlayFile.Type.PATCH -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2026 Aurora OSS
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.aurora.apkdownloader
|
||||||
|
|
||||||
|
import java.nio.file.Files
|
||||||
|
import java.nio.file.Path
|
||||||
|
import java.util.zip.Deflater
|
||||||
|
import java.util.zip.ZipEntry
|
||||||
|
import java.util.zip.ZipOutputStream
|
||||||
|
import kotlin.io.path.inputStream
|
||||||
|
import kotlin.io.path.name
|
||||||
|
import kotlin.io.path.outputStream
|
||||||
|
|
||||||
|
fun interface ApkArchiveWriter {
|
||||||
|
fun write(packageName: String, versionCode: Long, appDir: Path, files: List<Path>): Path?
|
||||||
|
}
|
||||||
|
|
||||||
|
class SplitApkArchiveWriter : ApkArchiveWriter {
|
||||||
|
override fun write(
|
||||||
|
packageName: String,
|
||||||
|
versionCode: Long,
|
||||||
|
appDir: Path,
|
||||||
|
files: List<Path>
|
||||||
|
): Path? {
|
||||||
|
if (files.size <= 1) return null
|
||||||
|
|
||||||
|
Files.createDirectories(appDir)
|
||||||
|
val archive = appDir.resolve("${packageName}_$versionCode.apks")
|
||||||
|
val apkFiles = files.filter { it.name.endsWith(".apk") }
|
||||||
|
if (apkFiles.size <= 1) return null
|
||||||
|
|
||||||
|
ZipOutputStream(archive.outputStream()).use { zip ->
|
||||||
|
zip.setLevel(Deflater.NO_COMPRESSION)
|
||||||
|
apkFiles.forEach { file ->
|
||||||
|
val entryName = appDir.relativize(file).toString().replace('\\', '/')
|
||||||
|
zip.putNextEntry(ZipEntry(entryName))
|
||||||
|
file.inputStream().use { input -> input.copyTo(zip) }
|
||||||
|
zip.closeEntry()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return archive
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2026 Aurora OSS
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.aurora.apkdownloader
|
||||||
|
|
||||||
|
import com.aurora.gplayapi.data.models.AuthData
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import java.nio.file.Path
|
||||||
|
import kotlin.io.path.Path
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class ApkDownloaderRunnerTest {
|
||||||
|
@Test
|
||||||
|
fun `download failure exits with code 3`() {
|
||||||
|
val runner = ApkDownloaderRunner(
|
||||||
|
authSource = AuthSource { AuthData("BOGUS") },
|
||||||
|
appResolver = AppResolver { packageName, _ ->
|
||||||
|
ResolvedApp(
|
||||||
|
packageName = packageName,
|
||||||
|
displayName = "Example",
|
||||||
|
versionCode = 1,
|
||||||
|
files = listOf(
|
||||||
|
DownloadItem(
|
||||||
|
url = "https://example.test/base.apk",
|
||||||
|
name = "base.apk",
|
||||||
|
size = 1,
|
||||||
|
sha256 = "00"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
downloader = Downloader { _, _: Path ->
|
||||||
|
throw DownloadException("Verification failed")
|
||||||
|
},
|
||||||
|
archiveWriter = ApkArchiveWriter { _, _, _, _ -> null }
|
||||||
|
)
|
||||||
|
|
||||||
|
val exitCode = runner.run(CliOptions("com.example.app", Path("downloads")))
|
||||||
|
|
||||||
|
assertThat(exitCode).isEqualTo(ExitCodes.DOWNLOAD_FAILURE)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2026 Aurora OSS
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.aurora.apkdownloader
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import kotlin.io.path.Path
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class CliParserTest {
|
||||||
|
@Test
|
||||||
|
fun `valid package only uses default output directory`() {
|
||||||
|
val result = CliParser.parse(arrayOf("org.mozilla.firefox"))
|
||||||
|
|
||||||
|
assertThat(result).isEqualTo(
|
||||||
|
CliParseResult.Valid(
|
||||||
|
CliOptions(
|
||||||
|
packageName = "org.mozilla.firefox",
|
||||||
|
outDir = Path("downloads"),
|
||||||
|
geoProfile = GeoProfile.default()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `valid package with output directory`() {
|
||||||
|
val result = CliParser.parse(arrayOf("org.mozilla.firefox", "--out", "./downloads"))
|
||||||
|
|
||||||
|
assertThat(result).isEqualTo(
|
||||||
|
CliParseResult.Valid(
|
||||||
|
CliOptions(
|
||||||
|
packageName = "org.mozilla.firefox",
|
||||||
|
outDir = Path("./downloads"),
|
||||||
|
geoProfile = GeoProfile.default()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `valid package with country profile`() {
|
||||||
|
val result = CliParser.parse(arrayOf("com.example.app", "--country", "TR"))
|
||||||
|
|
||||||
|
assertThat(result).isEqualTo(
|
||||||
|
CliParseResult.Valid(
|
||||||
|
CliOptions(
|
||||||
|
packageName = "com.example.app",
|
||||||
|
outDir = Path("downloads"),
|
||||||
|
geoProfile = GeoProfile(
|
||||||
|
country = "TR",
|
||||||
|
localeTag = "tr-TR",
|
||||||
|
timeZone = "Europe/Istanbul",
|
||||||
|
operator = "28601"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `missing package is invalid`() {
|
||||||
|
val result = CliParser.parse(emptyArray())
|
||||||
|
|
||||||
|
assertThat(result).isEqualTo(CliParseResult.Invalid("Missing package name"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `unknown option is invalid`() {
|
||||||
|
val result = CliParser.parse(arrayOf("org.mozilla.firefox", "--version-code", "1"))
|
||||||
|
|
||||||
|
assertThat(result).isEqualTo(CliParseResult.Invalid("Unknown option: --version-code"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `unsupported country is invalid`() {
|
||||||
|
val result = CliParser.parse(arrayOf("com.example.app", "--country", "ZZ"))
|
||||||
|
|
||||||
|
assertThat(result).isEqualTo(CliParseResult.Invalid("Unsupported country: ZZ"))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2026 Aurora OSS
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.aurora.apkdownloader
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import kotlin.io.path.Path
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class DownloadPathsTest {
|
||||||
|
@Test
|
||||||
|
fun `base and split APK files go under package version directory`() {
|
||||||
|
val paths = DownloadPaths(Path("downloads"))
|
||||||
|
|
||||||
|
assertThat(
|
||||||
|
paths.filePath(
|
||||||
|
"org.mozilla.firefox",
|
||||||
|
123,
|
||||||
|
DownloadItem("https://example.test/base.apk", "base.apk", 4, sha256 = "abcd")
|
||||||
|
)
|
||||||
|
).isEqualTo(Path("downloads", "org.mozilla.firefox", "123", "base.apk"))
|
||||||
|
|
||||||
|
assertThat(
|
||||||
|
paths.filePath(
|
||||||
|
"org.mozilla.firefox",
|
||||||
|
123,
|
||||||
|
DownloadItem("https://example.test/split.apk", "split_config.en.apk", 4, sha256 = "abcd")
|
||||||
|
)
|
||||||
|
).isEqualTo(Path("downloads", "org.mozilla.firefox", "123", "split_config.en.apk"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `shared library APK files go under libraries package directory`() {
|
||||||
|
val paths = DownloadPaths(Path("downloads"))
|
||||||
|
|
||||||
|
assertThat(
|
||||||
|
paths.filePath(
|
||||||
|
"org.mozilla.firefox",
|
||||||
|
123,
|
||||||
|
DownloadItem(
|
||||||
|
url = "https://example.test/lib.apk",
|
||||||
|
name = "base.apk",
|
||||||
|
size = 4,
|
||||||
|
sha256 = "abcd",
|
||||||
|
sharedLibraryPackageName = "com.google.android.trichromelibrary"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
).isEqualTo(
|
||||||
|
Path(
|
||||||
|
"downloads",
|
||||||
|
"org.mozilla.firefox",
|
||||||
|
"123",
|
||||||
|
"libraries",
|
||||||
|
"com.google.android.trichromelibrary",
|
||||||
|
"base.apk"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2026 Aurora OSS
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.aurora.apkdownloader
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import com.sun.net.httpserver.HttpExchange
|
||||||
|
import com.sun.net.httpserver.HttpServer
|
||||||
|
import java.net.InetSocketAddress
|
||||||
|
import java.nio.file.Files
|
||||||
|
import java.nio.file.Path
|
||||||
|
import java.security.MessageDigest
|
||||||
|
import kotlin.io.path.exists
|
||||||
|
import kotlin.io.path.readBytes
|
||||||
|
import kotlin.io.path.writeBytes
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.rules.TemporaryFolder
|
||||||
|
|
||||||
|
class FileDownloaderTest {
|
||||||
|
@get:Rule
|
||||||
|
val temporaryFolder = TemporaryFolder()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `fresh download writes tmp then final file`() {
|
||||||
|
val bytes = "hello world".toByteArray()
|
||||||
|
TestHttpServer { exchange ->
|
||||||
|
exchange.respond(200, bytes)
|
||||||
|
}.use { server ->
|
||||||
|
val target = targetPath("base.apk")
|
||||||
|
val item = DownloadItem(server.url, "base.apk", bytes.size.toLong(), sha256 = bytes.sha256())
|
||||||
|
|
||||||
|
downloader().download(item, target)
|
||||||
|
|
||||||
|
assertThat(target.readBytes()).isEqualTo(bytes)
|
||||||
|
assertThat(tmpPath(target).exists()).isFalse()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `partial tmp sends range and appends when server honors it`() {
|
||||||
|
val fullBytes = "hello world".toByteArray()
|
||||||
|
var rangeHeader: String? = null
|
||||||
|
TestHttpServer { exchange ->
|
||||||
|
rangeHeader = exchange.requestHeaders.getFirst("Range")
|
||||||
|
exchange.respond(206, "world".toByteArray())
|
||||||
|
}.use { server ->
|
||||||
|
val target = targetPath("base.apk")
|
||||||
|
tmpPath(target).writeBytes("hello ".toByteArray())
|
||||||
|
val item = DownloadItem(server.url, "base.apk", fullBytes.size.toLong(), sha256 = fullBytes.sha256())
|
||||||
|
|
||||||
|
downloader().download(item, target)
|
||||||
|
|
||||||
|
assertThat(rangeHeader).isEqualTo("bytes=6-")
|
||||||
|
assertThat(target.readBytes()).isEqualTo(fullBytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `server ignoring range restarts download cleanly`() {
|
||||||
|
val fullBytes = "hello world".toByteArray()
|
||||||
|
var rangeHeader: String? = null
|
||||||
|
TestHttpServer { exchange ->
|
||||||
|
rangeHeader = exchange.requestHeaders.getFirst("Range")
|
||||||
|
exchange.respond(200, fullBytes)
|
||||||
|
}.use { server ->
|
||||||
|
val target = targetPath("base.apk")
|
||||||
|
tmpPath(target).writeBytes("partial ".toByteArray())
|
||||||
|
val item = DownloadItem(server.url, "base.apk", fullBytes.size.toLong(), sha256 = fullBytes.sha256())
|
||||||
|
|
||||||
|
downloader().download(item, target)
|
||||||
|
|
||||||
|
assertThat(rangeHeader).isEqualTo("bytes=8-")
|
||||||
|
assertThat(target.readBytes()).isEqualTo(fullBytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `corrupt file is rejected`() {
|
||||||
|
val bytes = "hello world".toByteArray()
|
||||||
|
TestHttpServer { exchange ->
|
||||||
|
exchange.respond(200, bytes)
|
||||||
|
}.use { server ->
|
||||||
|
val target = targetPath("base.apk")
|
||||||
|
val item = DownloadItem(server.url, "base.apk", bytes.size.toLong(), sha256 = "00")
|
||||||
|
|
||||||
|
val result = runCatching { downloader().download(item, target) }
|
||||||
|
|
||||||
|
assertThat(result.exceptionOrNull()).isInstanceOf(DownloadException::class.java)
|
||||||
|
assertThat(target.exists()).isFalse()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun downloader(): FileDownloader = FileDownloader(DesktopHttpClient(OkHttpClient()))
|
||||||
|
|
||||||
|
private fun targetPath(name: String): Path = temporaryFolder.newFolder().toPath().resolve(name)
|
||||||
|
|
||||||
|
private fun tmpPath(target: Path): Path = target.resolveSibling("${target.fileName}.tmp")
|
||||||
|
|
||||||
|
private class TestHttpServer(
|
||||||
|
private val handler: (HttpExchange) -> Unit
|
||||||
|
) : AutoCloseable {
|
||||||
|
private val server = HttpServer.create(InetSocketAddress(0), 0)
|
||||||
|
|
||||||
|
val url: String
|
||||||
|
get() = "http://127.0.0.1:${server.address.port}/file"
|
||||||
|
|
||||||
|
init {
|
||||||
|
server.createContext("/file") { exchange -> handler(exchange) }
|
||||||
|
server.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun close() {
|
||||||
|
server.stop(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun HttpExchange.respond(code: Int, bytes: ByteArray) {
|
||||||
|
sendResponseHeaders(code, bytes.size.toLong())
|
||||||
|
responseBody.use { it.write(bytes) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ByteArray.sha256(): String {
|
||||||
|
val digest = MessageDigest.getInstance("SHA-256").digest(this)
|
||||||
|
return digest.joinToString(separator = "") { "%02x".format(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2026 Aurora OSS
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.aurora.apkdownloader
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import java.util.zip.ZipFile
|
||||||
|
import kotlin.io.path.createDirectories
|
||||||
|
import kotlin.io.path.exists
|
||||||
|
import kotlin.io.path.writeText
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.rules.TemporaryFolder
|
||||||
|
|
||||||
|
class SplitApkArchiveWriterTest {
|
||||||
|
@get:Rule
|
||||||
|
val temporaryFolder = TemporaryFolder()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `writes apks archive for multiple apk files`() {
|
||||||
|
val appDir = temporaryFolder.newFolder().toPath()
|
||||||
|
val base = appDir.resolve("base.apk").also { it.writeText("base") }
|
||||||
|
val split = appDir.resolve("config.arm64_v8a.apk").also { it.writeText("split") }
|
||||||
|
|
||||||
|
val archive = SplitApkArchiveWriter().write(
|
||||||
|
packageName = "org.mozilla.firefox",
|
||||||
|
versionCode = 123,
|
||||||
|
appDir = appDir,
|
||||||
|
files = listOf(base, split)
|
||||||
|
)
|
||||||
|
|
||||||
|
assertThat(archive).isNotNull()
|
||||||
|
assertThat(archive!!.exists()).isTrue()
|
||||||
|
ZipFile(archive.toFile()).use { zip ->
|
||||||
|
assertThat(zip.getEntry("base.apk")).isNotNull()
|
||||||
|
assertThat(zip.getEntry("config.arm64_v8a.apk")).isNotNull()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `preserves shared library relative paths`() {
|
||||||
|
val appDir = temporaryFolder.newFolder().toPath()
|
||||||
|
val base = appDir.resolve("base.apk").also { it.writeText("base") }
|
||||||
|
val libDir = appDir.resolve("libraries/com.example.lib").also { it.createDirectories() }
|
||||||
|
val lib = libDir.resolve("base.apk").also { it.writeText("lib") }
|
||||||
|
|
||||||
|
val archive = SplitApkArchiveWriter().write(
|
||||||
|
packageName = "com.example.app",
|
||||||
|
versionCode = 123,
|
||||||
|
appDir = appDir,
|
||||||
|
files = listOf(base, lib)
|
||||||
|
)
|
||||||
|
|
||||||
|
ZipFile(archive!!.toFile()).use { zip ->
|
||||||
|
assertThat(zip.getEntry("base.apk")).isNotNull()
|
||||||
|
assertThat(zip.getEntry("libraries/com.example.lib/base.apk")).isNotNull()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `does not write archive for a single apk file`() {
|
||||||
|
val appDir = temporaryFolder.newFolder().toPath()
|
||||||
|
val base = appDir.resolve("base.apk").also { it.writeText("base") }
|
||||||
|
|
||||||
|
val archive = SplitApkArchiveWriter().write(
|
||||||
|
packageName = "org.mozilla.firefox",
|
||||||
|
versionCode = 123,
|
||||||
|
appDir = appDir,
|
||||||
|
files = listOf(base)
|
||||||
|
)
|
||||||
|
|
||||||
|
assertThat(archive).isNull()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -619,4 +619,20 @@
|
|||||||
<string name="account_switch_failed">تعذّر النظام من تحديث هذا الحساب، لذا لم يتم تغيير الإعداد الافتراضي.</string>
|
<string name="account_switch_failed">تعذّر النظام من تحديث هذا الحساب، لذا لم يتم تغيير الإعداد الافتراضي.</string>
|
||||||
<string name="account_add_failed">تعذّر إضافة هذا الحساب. يُرجى المحاولة مرة أخرى.</string>
|
<string name="account_add_failed">تعذّر إضافة هذا الحساب. يُرجى المحاولة مرة أخرى.</string>
|
||||||
<string name="action_switch_account">بدّل الحساب</string>
|
<string name="action_switch_account">بدّل الحساب</string>
|
||||||
|
<plurals name="account_count">
|
||||||
|
<item quantity="zero">لا حسابات</item>
|
||||||
|
<item quantity="one">إجمالي %1$d حساب</item>
|
||||||
|
<item quantity="two">إجمالي حسابان</item>
|
||||||
|
<item quantity="few">إجمالي %1$d حسابات</item>
|
||||||
|
<item quantity="many">إجمالي %1$d حسابًا</item>
|
||||||
|
<item quantity="other">إجمالي %1$d حساب</item>
|
||||||
|
</plurals>
|
||||||
|
<plurals name="account_refresh_failed">
|
||||||
|
<item quantity="zero">تعذّر تحديث أي حساب</item>
|
||||||
|
<item quantity="one">تعذّر تحديث %1$d حساب</item>
|
||||||
|
<item quantity="two">تعذّر تحديث حسابين</item>
|
||||||
|
<item quantity="few">تعذّر تحديث %1$d حسابات</item>
|
||||||
|
<item quantity="many">تعذّر تحديث %1$d حسابًا</item>
|
||||||
|
<item quantity="other">تعذّر تحديث %1$d حساب</item>
|
||||||
|
</plurals>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -591,4 +591,36 @@
|
|||||||
<item quantity="many">%1$d oblíbených</item>
|
<item quantity="many">%1$d oblíbených</item>
|
||||||
<item quantity="other">%1$d oblíbených</item>
|
<item quantity="other">%1$d oblíbených</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
|
<string name="account_set_default">Nastavit jako výchozí</string>
|
||||||
|
<string name="account_default">Výchozí</string>
|
||||||
|
<string name="account_remove">Odstranit</string>
|
||||||
|
<string name="account_remove_title">Odstranit účet?</string>
|
||||||
|
<string name="account_remove_message">Odstranit účet %1$s z Obchodu Aurora?</string>
|
||||||
|
<string name="account_remove_last_message">Toto je váš jediný účet. Po jeho odstranění budete odhlášeni a nebudete moci použít Obchod Aurora, dokud se znovu nepřihlásíte.</string>
|
||||||
|
<string name="account_add">Přidat účet</string>
|
||||||
|
<string name="account_add_device">Účty zařízení</string>
|
||||||
|
<string name="account_add_google">Přihlášení přes Google</string>
|
||||||
|
<string name="account_add_anonymous">Přidat anonymní účet</string>
|
||||||
|
<string name="account_set_default_title">Přepnout výchozí účet?</string>
|
||||||
|
<string name="account_set_default_message">Obchod Aurora se restartuje pro použití nového výchozího účtu.</string>
|
||||||
|
<string name="account_switching">Přepínání účtu…</string>
|
||||||
|
<string name="account_adding">Přidávání účtu…</string>
|
||||||
|
<string name="account_refresh_all">Aktualizovat všechny účty</string>
|
||||||
|
<string name="account_refreshing">Aktualizace účtů…</string>
|
||||||
|
<string name="account_refresh_done">Všechny účty byly aktualizovány</string>
|
||||||
|
<string name="account_switch_failed">Nepodařilo se aktualizovat daný účet, výchozí hodnota nebyla změněna.</string>
|
||||||
|
<string name="account_add_failed">Nepodařilo se přidat daný účet. Zkuste to prosím znovu.</string>
|
||||||
|
<string name="action_switch_account">Přepnout účet</string>
|
||||||
|
<plurals name="account_count">
|
||||||
|
<item quantity="one">Celkem %1$d účet</item>
|
||||||
|
<item quantity="few">Celkem %1$d účty</item>
|
||||||
|
<item quantity="many">Celkem %1$d účtů</item>
|
||||||
|
<item quantity="other">Celkem %1$d účtů</item>
|
||||||
|
</plurals>
|
||||||
|
<plurals name="account_refresh_failed">
|
||||||
|
<item quantity="one">Nepodařilo se aktualizovat %1$d účet</item>
|
||||||
|
<item quantity="few">Nepodařilo se aktualizovat %1$d účty</item>
|
||||||
|
<item quantity="many">Nepodařilo se aktualizovat %1$d účtů</item>
|
||||||
|
<item quantity="other">Nepodařilo se aktualizovat %1$d účtů</item>
|
||||||
|
</plurals>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
<string name="device_miui_title">MIUI a été détecté !</string>
|
<string name="device_miui_title">MIUI a été détecté !</string>
|
||||||
<string name="menu_terms">Conditions générales d’utilisation</string>
|
<string name="menu_terms">Conditions générales d’utilisation</string>
|
||||||
<string name="menu_license">Licence</string>
|
<string name="menu_license">Licence</string>
|
||||||
<string name="onboarding_welcome_select">Comment allez-vous ?</string>
|
<string name="onboarding_welcome_select">Comment allez-vous?</string>
|
||||||
<string name="onboarding_title_welcome">Bienvenue</string>
|
<string name="onboarding_title_welcome">Bienvenue</string>
|
||||||
<string name="onboarding_title_installer">Programme d’installation</string>
|
<string name="onboarding_title_installer">Programme d’installation</string>
|
||||||
<string name="pref_filter_fdroid_title">Filtrer les applis provenant de F-Droid</string>
|
<string name="pref_filter_fdroid_title">Filtrer les applis provenant de F-Droid</string>
|
||||||
@@ -62,7 +62,7 @@
|
|||||||
<string name="download_completed">Le téléchargement est terminé</string>
|
<string name="download_completed">Le téléchargement est terminé</string>
|
||||||
<string name="download_canceled">Annulé</string>
|
<string name="download_canceled">Annulé</string>
|
||||||
<string name="download_cancel_all">Tout annuler</string>
|
<string name="download_cancel_all">Tout annuler</string>
|
||||||
<string name="details_think_this_app">Que pensez-vous de cette appli ?</string>
|
<string name="details_think_this_app">Que pensez-vous de cette appli?</string>
|
||||||
<string name="details_ratings">Évaluation et avis</string>
|
<string name="details_ratings">Évaluation et avis</string>
|
||||||
<string name="details_privacy">Protection des données personnelles</string>
|
<string name="details_privacy">Protection des données personnelles</string>
|
||||||
<string name="details_permission">Autorisation</string>
|
<string name="details_permission">Autorisation</string>
|
||||||
@@ -84,7 +84,7 @@
|
|||||||
<string name="details_changelog_unavailable">Le journal des changements n’a pas été fourni</string>
|
<string name="details_changelog_unavailable">Le journal des changements n’a pas été fourni</string>
|
||||||
<string name="details_changelog">Journal des changements</string>
|
<string name="details_changelog">Journal des changements</string>
|
||||||
<string name="details_dev_email">Adresse courriel</string>
|
<string name="details_dev_email">Adresse courriel</string>
|
||||||
<string name="details_beta_available">Participer au programme bêta ?</string>
|
<string name="details_beta_available">Participer au programme bêta?</string>
|
||||||
<string name="details_beta_subscribed">Vous essayez la version bêta</string>
|
<string name="details_beta_subscribed">Vous essayez la version bêta</string>
|
||||||
<string name="details_beta">Programme bêta</string>
|
<string name="details_beta">Programme bêta</string>
|
||||||
<string name="action_whitelist">Liste d’inclusion</string>
|
<string name="action_whitelist">Liste d’inclusion</string>
|
||||||
@@ -271,7 +271,7 @@
|
|||||||
<string name="title_search_results">Résultats de la recherche</string>
|
<string name="title_search_results">Résultats de la recherche</string>
|
||||||
<string name="failed_generating_session">Échec de génération de la session. Code d’erreur : <xliff:g id="status_code">%1$d</xliff:g></string>
|
<string name="failed_generating_session">Échec de génération de la session. Code d’erreur : <xliff:g id="status_code">%1$d</xliff:g></string>
|
||||||
<string name="failed_to_fetch_report">Échec de récupération du relevé de confidentialité</string>
|
<string name="failed_to_fetch_report">Échec de récupération du relevé de confidentialité</string>
|
||||||
<string name="access_denied_using_vpn">L’accès a été refusé. Utilisez-vous un RPV/VPN ou Tor ?</string>
|
<string name="access_denied_using_vpn">L’accès a été refusé. Utilisez-vous un RPV/VPN ou Tor?</string>
|
||||||
<string name="bad_request">Erreur interne ! Réessayez plus tard</string>
|
<string name="bad_request">Erreur interne ! Réessayez plus tard</string>
|
||||||
<string name="login_rate_limited">Votre débit est limité</string>
|
<string name="login_rate_limited">Votre débit est limité</string>
|
||||||
<string name="server_unreachable">Impossible d’accéder au serveur</string>
|
<string name="server_unreachable">Impossible d’accéder au serveur</string>
|
||||||
@@ -308,7 +308,7 @@
|
|||||||
<string name="pref_updates_incompatible">Afficher les mises à jour susceptibles d’échouer</string>
|
<string name="pref_updates_incompatible">Afficher les mises à jour susceptibles d’échouer</string>
|
||||||
<string name="pref_updates_incompatible_desc">Afficher les mises à jour pour les applis incompatibles ou désactivées dont l’installation peut échouer.</string>
|
<string name="pref_updates_incompatible_desc">Afficher les mises à jour pour les applis incompatibles ou désactivées dont l’installation peut échouer.</string>
|
||||||
<string name="action_home_screen">Ajouter à l’écran d’accueil</string>
|
<string name="action_home_screen">Ajouter à l’écran d’accueil</string>
|
||||||
<string name="faqs_subtitle">Des questions ? Trouvez les réponses</string>
|
<string name="faqs_subtitle">Des questions? Trouvez les réponses</string>
|
||||||
<string name="source_code_title">Code source</string>
|
<string name="source_code_title">Code source</string>
|
||||||
<string name="privacy_policy_subtitle">Découvrez comment Aurora Store utilise vos données</string>
|
<string name="privacy_policy_subtitle">Découvrez comment Aurora Store utilise vos données</string>
|
||||||
<string name="manual_download_hint">Saisissez le code de la version que vous voulez télécharger</string>
|
<string name="manual_download_hint">Saisissez le code de la version que vous voulez télécharger</string>
|
||||||
@@ -336,18 +336,18 @@
|
|||||||
<string name="add_dispenser_title">Ajouter un distributeur de jetons</string>
|
<string name="add_dispenser_title">Ajouter un distributeur de jetons</string>
|
||||||
<string name="add_dispenser_error">L’URL est invalide</string>
|
<string name="add_dispenser_error">L’URL est invalide</string>
|
||||||
<string name="remove">Supprimer</string>
|
<string name="remove">Supprimer</string>
|
||||||
<string name="remove_dispenser_summary">Voulez-vous supprimer le distributeur « <xliff:g id="url">%1$s</xliff:g> » ?</string>
|
<string name="remove_dispenser_summary">Voulez-vous supprimer le distributeur « <xliff:g id="url">%1$s</xliff:g> »?</string>
|
||||||
<string name="session_login">Connectez-vous à votre compte Google Play</string>
|
<string name="session_login">Connectez-vous à votre compte Google Play</string>
|
||||||
<string name="add_dispenser_summary">Ajouter un distributeur de jetons à Aurora Store. Les distributeurs de jetons fournissent des identifiants de compte à Aurora Store pour les connexions anonymes.</string>
|
<string name="add_dispenser_summary">Ajouter un distributeur de jetons à Aurora Store. Les distributeurs de jetons fournissent des identifiants de compte à Aurora Store pour les connexions anonymes.</string>
|
||||||
<string name="add_dispenser_hint">URL du distributeur de jetons</string>
|
<string name="add_dispenser_hint">URL du distributeur de jetons</string>
|
||||||
<string name="remove_dispenser_title">Supprimer le distributeur de jetons ?</string>
|
<string name="remove_dispenser_title">Supprimer le distributeur de jetons?</string>
|
||||||
<string name="add">Ajouter</string>
|
<string name="add">Ajouter</string>
|
||||||
<string name="pref_dispenser_summary">Consulter et gérer les distributeurs de jetons pour les connexions anonymes</string>
|
<string name="pref_dispenser_summary">Consulter et gérer les distributeurs de jetons pour les connexions anonymes</string>
|
||||||
<string name="no_dispensers_available">Aucun distributeur de jetons n’est proposé</string>
|
<string name="no_dispensers_available">Aucun distributeur de jetons n’est proposé</string>
|
||||||
<string name="about_aurora_store_summary">Aurora Store vous permet de rechercher et de télécharger des applis depuis le Google Play Store. Vous pouvez consulter les descriptions, les captures d\'écran, les mises à jour, les commentaires des autres utilisateurs et télécharger le fichier APK directement depuis Google Play sur votre appareil. Pour utiliser Aurora Store, connectez-vous avec votre compte Google Play lors de la première ouverture et configuration de l\'appli.\n\nContrairement à une boutique d\'applis traditionnelle, Aurora Store ne possède, ne distribue et n\'exploite aucune appli. Toutes les applis, descriptions, captures d\'écran et autres contenus d\'Aurora Store sont directement accessibles, téléchargés et/ou affichés depuis Google Play. Aurora Store fonctionne comme une passerelle, vous permettant de vous connecter à votre compte Google Play et de trouver des applis sur Google Play.\n\nVeuillez noter qu\'Aurora Store n\'est ni approuvé, ni sponsorisé, ni autorisé par Google, Google Play, les applis téléchargées via Aurora Store ou les développeurs d\'applis ; Aurora Store n\'a aucune affiliation, coopération ou lien avec eux.</string>
|
<string name="about_aurora_store_summary">Le magasin Aurora Store vous permet de chercher et de télécharger des applis du magasin officiel Google Play. Vous pouvez consulter la description des applis, les captures d’écran, les mises à jour, les commentaires d’Autres utilisateurs, et télécharger le fichier APK directement de Google Play vers votre appareil. Pour utiliser le magasin Aurora Store, connectez-vous avec votre compte Google Play lors de la première ouverture et de la configuration de l’appli.\n\nContrairement à un magasin d’applis traditionnel, Aurora Store ne détient, ne concède sous licence, ni ne distribue aucune appli. Les applis, descriptions d’applis, captures d’écran et autres contenus du magasin Aurora Store sont accessibles, téléchargeables et affichés directement du magasin Google Play. Le magasin Aurora Store fonctionne comme un portail ou un navigateur ; il vous permet de vous connecter à votre compte Google Play et de trouver des applis sur Google Play.\n\nNotez que le magasin Aurora Store n’a reçu aucune approbation, aucune commandite ni autorisation de la part de Google, de Google Play, des applis téléchargées grâce au magasin Aurora Store, ni de développeurs d’appli. Par ailleurs, le magasin Aurora Store n’a aucun lien d’affiliation, de coopération ou de connexion avec ces entités.</string>
|
||||||
<string name="about_aurora_store_title">À propos d’Aurora Store</string>
|
<string name="about_aurora_store_title">À propos d’Aurora Store</string>
|
||||||
<string name="about_aurora_store_subtitle">En savoir plus sur Aurora Store</string>
|
<string name="about_aurora_store_subtitle">En savoir plus sur Aurora Store</string>
|
||||||
<string name="pref_clear_device_owner_desc">Cela révoquera l\'autorisation accordée au programme d\'installation de session d\'installer l\'appli en mode silencieux. Voulez-vous vraiment supprimer les droits d\'accès à l\'appareil ?</string>
|
<string name="pref_clear_device_owner_desc">Cela révoquera l\'autorisation accordée au programme d\'installation de session d\'installer l\'appli en mode silencieux. Voulez-vous vraiment supprimer les droits d\'accès à l\'appareil?</string>
|
||||||
<string name="pref_clear_device_owner_title">Révoquer le droit de propriété de l’appareil</string>
|
<string name="pref_clear_device_owner_title">Révoquer le droit de propriété de l’appareil</string>
|
||||||
<string name="toast_app_unavailable">L\'appli n\'est pas disponible pour votre appareil</string>
|
<string name="toast_app_unavailable">L\'appli n\'est pas disponible pour votre appareil</string>
|
||||||
<string name="toast_fav_import_failed">Échec d’importation des favoris !</string>
|
<string name="toast_fav_import_failed">Échec d’importation des favoris !</string>
|
||||||
@@ -359,8 +359,8 @@
|
|||||||
<string name="pref_network_proxy_url_message">Saisissez une URL de mandataire valide pour faire transiter toute les données par le mandataire.</string>
|
<string name="pref_network_proxy_url_message">Saisissez une URL de mandataire valide pour faire transiter toute les données par le mandataire.</string>
|
||||||
<string name="title_favourites_manager">Applis favorites</string>
|
<string name="title_favourites_manager">Applis favorites</string>
|
||||||
<string name="toast_fav_export_success">Les favoris ont été exportés !</string>
|
<string name="toast_fav_export_success">Les favoris ont été exportés !</string>
|
||||||
<string name="action_logout_confirmation_title">Vous déconnecter ?</string>
|
<string name="action_logout_confirmation_title">Vous déconnecter?</string>
|
||||||
<string name="action_logout_confirmation_message">Voulez-vous vous déconnecter ?</string>
|
<string name="action_logout_confirmation_message">Voulez-vous vous déconnecter?</string>
|
||||||
<string name="check_updates">Chercher des mises à jour</string>
|
<string name="check_updates">Chercher des mises à jour</string>
|
||||||
<string name="details_data_safety_title">Sécurité des données</string>
|
<string name="details_data_safety_title">Sécurité des données</string>
|
||||||
<string name="onboarding_permission_installer_legacy_desc">Autoriser l’installation d’applis de sources inconnues</string>
|
<string name="onboarding_permission_installer_legacy_desc">Autoriser l’installation d’applis de sources inconnues</string>
|
||||||
@@ -383,7 +383,7 @@
|
|||||||
<string name="status_cancelled">Annulé</string>
|
<string name="status_cancelled">Annulé</string>
|
||||||
<string name="status_failed">Échec</string>
|
<string name="status_failed">Échec</string>
|
||||||
<string name="status_unavailable">Inaccessible</string>
|
<string name="status_unavailable">Inaccessible</string>
|
||||||
<string name="authentication_required_unarchive">Veuillez vous connecter à votre compte Google Play pour désarchiver l\'appli !</string>
|
<string name="authentication_required_unarchive">Connectez-vous à votre compte Google Play pour désarchiver l’appli.</string>
|
||||||
<string name="authentication_required_title">L’authentification est requise</string>
|
<string name="authentication_required_title">L’authentification est requise</string>
|
||||||
<string name="checking_updates">Recherche de mises à jour</string>
|
<string name="checking_updates">Recherche de mises à jour</string>
|
||||||
<string name="made_with_love">Conçu en Inde avec <xliff:g id="heart_emoji">%1$s</xliff:g></string>
|
<string name="made_with_love">Conçu en Inde avec <xliff:g id="heart_emoji">%1$s</xliff:g></string>
|
||||||
@@ -470,7 +470,7 @@
|
|||||||
<string name="loading">En cours de chargement</string>
|
<string name="loading">En cours de chargement</string>
|
||||||
<string name="page">Page <xliff:g id="number">%1$d</xliff:g></string>
|
<string name="page">Page <xliff:g id="number">%1$d</xliff:g></string>
|
||||||
<string name="action_skip">Ignorer</string>
|
<string name="action_skip">Ignorer</string>
|
||||||
<string name="force_restart_snack">Redémarrer pour appliquer les changements ?</string>
|
<string name="force_restart_snack">Redémarrer pour appliquer les changements?</string>
|
||||||
<string name="preparing_to_download">Préparation du téléchargement</string>
|
<string name="preparing_to_download">Préparation du téléchargement</string>
|
||||||
<string name="verifying_downloads">Vérification des téléchargements</string>
|
<string name="verifying_downloads">Vérification des téléchargements</string>
|
||||||
<string name="action_retry">Réessayer</string>
|
<string name="action_retry">Réessayer</string>
|
||||||
@@ -566,15 +566,15 @@
|
|||||||
<string name="details_review_comment_hint">Décrivez votre expérience (facultatif)</string>
|
<string name="details_review_comment_hint">Décrivez votre expérience (facultatif)</string>
|
||||||
<string name="details_review_edit">Modifier votre avis</string>
|
<string name="details_review_edit">Modifier votre avis</string>
|
||||||
<string name="details_review_delete">Supprimer</string>
|
<string name="details_review_delete">Supprimer</string>
|
||||||
<string name="details_review_delete_title">Supprimer l\'avis ?</string>
|
<string name="details_review_delete_title">Supprimer l\'avis?</string>
|
||||||
<string name="details_review_delete_message">Votre note et votre avis seront supprimés de Google Play.</string>
|
<string name="details_review_delete_message">Votre note et votre avis seront supprimés de Google Play.</string>
|
||||||
<string name="details_your_review">Votre avis</string>
|
<string name="details_your_review">Votre avis</string>
|
||||||
<string name="confirm_deeplink_title">Vérifier les liens externes</string>
|
<string name="confirm_deeplink_title">Vérifier les liens externes</string>
|
||||||
<string name="confirm_deeplink_summary">Demandez confirmation avant d\'ouvrir la fiche d\'une appli à partir d\'un lien, afin d\'empêcher les publicités de lancer l\'Aurora Store sans votre consentement.</string>
|
<string name="confirm_deeplink_summary">Demandez confirmation avant d\'ouvrir la fiche d\'une appli à partir d\'un lien, afin d\'empêcher les publicités de lancer l\'Aurora Store sans votre consentement.</string>
|
||||||
<string name="confirm_deeplink_sheet_title">Poursuivre vers le magasin Aurora Store ?</string>
|
<string name="confirm_deeplink_sheet_title">Poursuivre vers le magasin Aurora Store?</string>
|
||||||
<string name="confirm_deeplink_sheet_message">Une appli externe veut ouvrir Aurora Store</string>
|
<string name="confirm_deeplink_sheet_message">Une appli externe veut ouvrir Aurora Store</string>
|
||||||
<string name="confirm_deeplink_sheet_source">Ouvert à partir de %1$s</string>
|
<string name="confirm_deeplink_sheet_source">Ouvert à partir de %1$s</string>
|
||||||
<string name="title_install_favourites">Installer les favoris ?</string>
|
<string name="title_install_favourites">Installer les favoris?</string>
|
||||||
<string name="install_favourites_summary">Cela permettra de télécharger et d\'installer %1$d applis.</string>
|
<string name="install_favourites_summary">Cela permettra de télécharger et d\'installer %1$d applis.</string>
|
||||||
<string name="install_favourites_warning">Il se peut qu\'une invite d\'installation distincte s\'affiche pour chaque appli.</string>
|
<string name="install_favourites_warning">Il se peut qu\'une invite d\'installation distincte s\'affiche pour chaque appli.</string>
|
||||||
<string name="pref_ui_dynamic_color">Couleurs dynamiques</string>
|
<string name="pref_ui_dynamic_color">Couleurs dynamiques</string>
|
||||||
@@ -583,16 +583,16 @@
|
|||||||
<string name="toast_fav_install_none">Rien à installer</string>
|
<string name="toast_fav_install_none">Rien à installer</string>
|
||||||
<string name="toast_fav_install_failed">Impossible de récupérer les informations sur l\'appli</string>
|
<string name="toast_fav_install_failed">Impossible de récupérer les informations sur l\'appli</string>
|
||||||
<plurals name="favourites_count">
|
<plurals name="favourites_count">
|
||||||
<item quantity="one">%1$d favoris</item>
|
<item quantity="one">%1$d favori</item>
|
||||||
<item quantity="many">%1$d favoris</item>
|
<item quantity="many">%1$d de favoris</item>
|
||||||
<item quantity="other">%1$d favoris</item>
|
<item quantity="other">%1$d favori</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<string name="account_set_default">Définir comme compte par défaut</string>
|
<string name="account_set_default">Définir comme compte par défaut</string>
|
||||||
<string name="account_default">Par défaut</string>
|
<string name="account_default">Par défaut</string>
|
||||||
<string name="account_remove">Supprimer</string>
|
<string name="account_remove">Supprimer</string>
|
||||||
<string name="account_remove_title">Supprimer le compte ?</string>
|
<string name="account_remove_title">Supprimer le compte?</string>
|
||||||
<string name="account_remove_message">Supprimer %1$s de Aurora Store ?</string>
|
<string name="account_remove_message">Supprimer %1$s du magasin Aurora Store?</string>
|
||||||
<string name="account_remove_last_message">Il s\'agit de votre seul compte. Si vous le supprimez, vous serez déconnecté et vous ne pourrez plus utiliser Aurora Store tant que vous ne vous serez pas reconnecté.</string>
|
<string name="account_remove_last_message">Il s’agit de votre seul compte. Le supprimer vous déconnectera et vous ne pourrez plus utiliser le magasin Aurora Store avant de vous reconnecter.</string>
|
||||||
<string name="account_add">Ajouter un compte</string>
|
<string name="account_add">Ajouter un compte</string>
|
||||||
<string name="account_add_device">Comptes d’appareil</string>
|
<string name="account_add_device">Comptes d’appareil</string>
|
||||||
<string name="account_add_google">Se connecter avec Google</string>
|
<string name="account_add_google">Se connecter avec Google</string>
|
||||||
@@ -603,9 +603,9 @@
|
|||||||
<string name="account_adding">Ajout d’un compte…</string>
|
<string name="account_adding">Ajout d’un compte…</string>
|
||||||
<string name="account_refresh_all">Actualiser tous les comptes</string>
|
<string name="account_refresh_all">Actualiser tous les comptes</string>
|
||||||
<string name="account_refreshing">Actualisation des comptes…</string>
|
<string name="account_refreshing">Actualisation des comptes…</string>
|
||||||
<string name="account_refresh_done">Tous les comptes ont été mis à jour</string>
|
<string name="account_refresh_done">Les comptes ont été actualisés</string>
|
||||||
<string name="account_switch_failed">Impossible d\'actualiser ce compte ; le paramètre par défaut n\'a donc pas été modifié.</string>
|
<string name="account_switch_failed">Impossible d’actualiser ce compte ; le compte par défaut n’a donc pas été changé.</string>
|
||||||
<string name="account_add_failed">Impossible d\'ajouter ce compte. Veuillez réessayer.</string>
|
<string name="account_add_failed">Impossible d’ajouter ce compte. Réessayez.</string>
|
||||||
<string name="action_switch_account">Changer de compte</string>
|
<string name="action_switch_account">Changer de compte</string>
|
||||||
<plurals name="account_count">
|
<plurals name="account_count">
|
||||||
<item quantity="one">Total %1$d compte</item>
|
<item quantity="one">Total %1$d compte</item>
|
||||||
@@ -613,8 +613,8 @@
|
|||||||
<item quantity="other">Total %1$d comptes</item>
|
<item quantity="other">Total %1$d comptes</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<plurals name="account_refresh_failed">
|
<plurals name="account_refresh_failed">
|
||||||
<item quantity="one">Impossible d\'actualiser le compte %1$d</item>
|
<item quantity="one">Impossible d’actualiser %1$d compte</item>
|
||||||
<item quantity="many">Impossible d\'actualiser %1$d comptes</item>
|
<item quantity="many">Impossible d’actualiser %1$d de comptes</item>
|
||||||
<item quantity="other">Impossible d\'actualiser %1$d comptes</item>
|
<item quantity="other">Impossible d’actualiser %1$d de comptes</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
<string name="details_dev_email">E-mail</string>
|
<string name="details_dev_email">E-mail</string>
|
||||||
<string name="about_bitcoin_bch_summary">Doneer met Bitcoin Cash (BCH)</string>
|
<string name="about_bitcoin_bch_summary">Doneer met Bitcoin Cash (BCH)</string>
|
||||||
<string name="about_bitcoin_btc_summary">Doneer met Bitcoin (BTC)</string>
|
<string name="about_bitcoin_btc_summary">Doneer met Bitcoin (BTC)</string>
|
||||||
<string name="about_bhim_summary">Doneer met UPI</string>
|
<string name="about_bhim_summary">Via UPI doneren</string>
|
||||||
<string name="installer_root_unavailable">Geen root-toegang. Sta toe of verander de installatiemethode.</string>
|
<string name="installer_root_unavailable">Geen root-toegang. Sta toe of verander de installatiemethode.</string>
|
||||||
<string name="about_gitlab_summary">Verkrijg de Aurora Store broncode van Aurora OSS op GitLab.</string>
|
<string name="about_gitlab_summary">Verkrijg de Aurora Store broncode van Aurora OSS op GitLab.</string>
|
||||||
<string name="about_fdroid_summary">Haal de Aurora Store op F-Droid.</string>
|
<string name="about_fdroid_summary">Haal de Aurora Store op F-Droid.</string>
|
||||||
@@ -205,7 +205,7 @@
|
|||||||
<string name="action_cancel">Afbreken</string>
|
<string name="action_cancel">Afbreken</string>
|
||||||
<string name="action_back">Terug</string>
|
<string name="action_back">Terug</string>
|
||||||
<string name="account_logout">Log uit van Aurora</string>
|
<string name="account_logout">Log uit van Aurora</string>
|
||||||
<string name="account_login_using">Login met</string>
|
<string name="account_login_using">Inloggen via</string>
|
||||||
<string name="account_google">Google</string>
|
<string name="account_google">Google</string>
|
||||||
<string name="account_anonymous">Anoniem</string>
|
<string name="account_anonymous">Anoniem</string>
|
||||||
<string name="about_xda_summary">Bekijk de Aurora Store XDA pagina voor discussies of suggesties.</string>
|
<string name="about_xda_summary">Bekijk de Aurora Store XDA pagina voor discussies of suggesties.</string>
|
||||||
@@ -214,7 +214,7 @@
|
|||||||
<string name="about_telegram">Telegram</string>
|
<string name="about_telegram">Telegram</string>
|
||||||
<string name="about_paypal_summary">Doneer met PayPal</string>
|
<string name="about_paypal_summary">Doneer met PayPal</string>
|
||||||
<string name="about_paypal">PayPal</string>
|
<string name="about_paypal">PayPal</string>
|
||||||
<string name="about_libera_summary">Wordt een patron met Liberapay</string>
|
<string name="about_libera_summary">Word een patron met Liberapay</string>
|
||||||
<string name="about_libera">Liberapay</string>
|
<string name="about_libera">Liberapay</string>
|
||||||
<string name="about_gitlab">GitLab</string>
|
<string name="about_gitlab">GitLab</string>
|
||||||
<string name="about_fdroid">F-Droid</string>
|
<string name="about_fdroid">F-Droid</string>
|
||||||
@@ -558,4 +558,47 @@
|
|||||||
<string name="pref_notification_disabled_desc">Tik om meldingen voor Aurora Store in te schakelen</string>
|
<string name="pref_notification_disabled_desc">Tik om meldingen voor Aurora Store in te schakelen</string>
|
||||||
<string name="updates_self_header">Zelfinstallatie</string>
|
<string name="updates_self_header">Zelfinstallatie</string>
|
||||||
<string name="updates_self_desc">Er is een nieuwere versie van Aurora Store beschikbaar</string>
|
<string name="updates_self_desc">Er is een nieuwere versie van Aurora Store beschikbaar</string>
|
||||||
|
<string name="account_set_default">Als standaard instellen</string>
|
||||||
|
<string name="account_default">Standaard</string>
|
||||||
|
<string name="account_remove">Verwijderen</string>
|
||||||
|
<string name="account_remove_title">Account verwijderen?</string>
|
||||||
|
<string name="account_remove_message">%1$s uit Aurora Store verwijderen?</string>
|
||||||
|
<string name="account_remove_last_message">U heeft maar één account. Als u dit verwijdert, wordt u uitgelogd en kunt u Aurora Store niet meer gebruiken totdat u opnieuw inlogt.</string>
|
||||||
|
<string name="account_add">Account toevoegen</string>
|
||||||
|
<string name="account_add_google">Met Google inloggen</string>
|
||||||
|
<string name="account_add_anonymous">Anoniem account toevoegen</string>
|
||||||
|
<string name="account_set_default_title">Standaardaccount omwisselen?</string>
|
||||||
|
<string name="account_set_default_message">Aurora Store wordt opnieuw gestart om het nieuwe standaardaccount te gebruiken.</string>
|
||||||
|
<string name="account_switching">Van account wisselen…</string>
|
||||||
|
<string name="account_adding">Account wordt toegevoegd…</string>
|
||||||
|
<string name="account_refresh_all">Alle accounts bijwerken</string>
|
||||||
|
<string name="account_refreshing">Accounts worden bijgewerkt…</string>
|
||||||
|
<string name="account_refresh_done">Alle accounts zijn bijgewerkt</string>
|
||||||
|
<string name="account_add_failed">Kon gebruikersaccount niet toevoegen. Probeer het opnieuw.</string>
|
||||||
|
<string name="action_buy">Bij %1$s kopen</string>
|
||||||
|
<string name="action_install_all">Alles installeren</string>
|
||||||
|
<string name="action_switch_account">Account wisselen</string>
|
||||||
|
<string name="details_review_comment_hint">Omschrijf uw ervaring (optioneel)</string>
|
||||||
|
<string name="details_review_edit">Uw beoordeling bewerken</string>
|
||||||
|
<string name="details_review_delete">Verwijderen</string>
|
||||||
|
<string name="details_review_delete_title">Beoordeling verwijderen?</string>
|
||||||
|
<string name="details_your_review">Uw beoordeling</string>
|
||||||
|
<string name="confirm_deeplink_sheet_message">Een externe app wilt Aurora Store openen</string>
|
||||||
|
<string name="confirm_deeplink_sheet_source">Geopend via %1$s</string>
|
||||||
|
<string name="title_install_favourites">Favorieten installeren?</string>
|
||||||
|
<string name="pref_ui_dynamic_color">Dynamische kleuren</string>
|
||||||
|
<string name="toast_fav_install_enqueued">%1$d apps worden geïnstalleert</string>
|
||||||
|
<string name="toast_fav_install_none">Niks om te installeren</string>
|
||||||
|
<plurals name="favourites_count">
|
||||||
|
<item quantity="one">%1$d favoriet</item>
|
||||||
|
<item quantity="other">%1$d favorieten</item>
|
||||||
|
</plurals>
|
||||||
|
<plurals name="account_count">
|
||||||
|
<item quantity="one">Totaal %1$d account</item>
|
||||||
|
<item quantity="other">Totaal %1$d accounts</item>
|
||||||
|
</plurals>
|
||||||
|
<plurals name="account_refresh_failed">
|
||||||
|
<item quantity="one">%1$d account kon niet worden bijgewerkt</item>
|
||||||
|
<item quantity="other">%1$d accounts konden niet worden bijgewerkt</item>
|
||||||
|
</plurals>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
<string name="action_post">Wyślij</string>
|
<string name="action_post">Wyślij</string>
|
||||||
<string name="action_pending">Oczekujące</string>
|
<string name="action_pending">Oczekujące</string>
|
||||||
<string name="action_open">Otwórz</string>
|
<string name="action_open">Otwórz</string>
|
||||||
<string name="action_ok">OK</string>
|
<string name="action_ok">Okej</string>
|
||||||
<string name="action_next">Następna</string>
|
<string name="action_next">Następna</string>
|
||||||
<string name="action_logout">Wyloguj się</string>
|
<string name="action_logout">Wyloguj się</string>
|
||||||
<string name="action_leave">Zrezygnuj</string>
|
<string name="action_leave">Zrezygnuj</string>
|
||||||
|
|||||||
@@ -564,4 +564,63 @@
|
|||||||
<string name="installed_filter_installer">Inštalované z</string>
|
<string name="installed_filter_installer">Inštalované z</string>
|
||||||
<string name="installed_filter_all">Všetko</string>
|
<string name="installed_filter_all">Všetko</string>
|
||||||
<string name="installed_updated_at">Aktualizované <xliff:g id="when">%1$s</xliff:g></string>
|
<string name="installed_updated_at">Aktualizované <xliff:g id="when">%1$s</xliff:g></string>
|
||||||
|
<string name="confirm_deeplink_title">Potvrdiť externé odkazy</string>
|
||||||
|
<string name="confirm_deeplink_summary">Pred otvorením záznamu o aplikácii z odkazu sa opýtajte, čím zabránite tomu, aby reklamy spustili Aurora Store bez vášho súhlasu.</string>
|
||||||
|
<string name="confirm_deeplink_sheet_title">Chcete pokračovať do obchodu Aurora?</string>
|
||||||
|
<string name="confirm_deeplink_sheet_message">Externá aplikácia sa snaží otvoriť Aurora Store</string>
|
||||||
|
<string name="confirm_deeplink_sheet_source">Otvorené od %1$s</string>
|
||||||
|
<string name="account_set_default">Nastaviť ako predvolené</string>
|
||||||
|
<string name="account_default">Predvolené</string>
|
||||||
|
<string name="account_remove">Odstrániť</string>
|
||||||
|
<string name="account_remove_title">Odstrániť účet?</string>
|
||||||
|
<string name="account_remove_message">Odstrániť %1$s z obchodu Aurora?</string>
|
||||||
|
<string name="account_remove_last_message">Toto je váš jediný účet. Jeho odstránením sa odhlásite a nebudete môcť používať Aurora Store, kým sa znovu neprihlásite.</string>
|
||||||
|
<string name="account_add">Pridať účet</string>
|
||||||
|
<string name="account_add_device">Účty zariadenIa</string>
|
||||||
|
<string name="account_add_google">Prihlásiť sa cez Google</string>
|
||||||
|
<string name="account_add_anonymous">Pridať anonymný účet</string>
|
||||||
|
<string name="account_set_default_title">Zmeniť predvolený účet?</string>
|
||||||
|
<string name="account_set_default_message">Obchod Aurora sa reštartuje a začne používať nový predvolený účet.</string>
|
||||||
|
<string name="account_switching">Prechod na iný účet…</string>
|
||||||
|
<string name="account_adding">Pridávanie účtu…</string>
|
||||||
|
<string name="account_refresh_all">Obnoviť všetky účty</string>
|
||||||
|
<string name="account_refreshing">Aktualizácia účtov…</string>
|
||||||
|
<string name="account_refresh_done">Všetky účty boli aktualizované</string>
|
||||||
|
<string name="account_switch_failed">Tento účet sa nepodarilo aktualizovať, takže predvolené nastavenie sa nezmenilo.</string>
|
||||||
|
<string name="account_add_failed">Tento účet sa nepodarilo pridať. Skúste to prosím znova.</string>
|
||||||
|
<string name="action_buy">Kúpiť @ %1$s</string>
|
||||||
|
<string name="action_install_all">Nainštalovať všetko</string>
|
||||||
|
<string name="action_switch_account">Prepnúť účet</string>
|
||||||
|
<string name="details_review_comment_hint">Opíšte svoje skúsenosti (nevyžaduje sa)</string>
|
||||||
|
<string name="details_review_edit">Upraviť svoju recenziu</string>
|
||||||
|
<string name="details_review_delete">Odstrániť</string>
|
||||||
|
<string name="details_review_delete_title">Odstrániť recenziu?</string>
|
||||||
|
<string name="details_review_delete_message">Vaše hodnotenie a recenzia budú z Google Play odstránené.</string>
|
||||||
|
<string name="details_your_review">Vaša recenzia</string>
|
||||||
|
<string name="title_install_favourites">Nainštalovať do obľúbených?</string>
|
||||||
|
<string name="install_favourites_summary">Týmto sa stiahne a nainštaluje %1$d aplikácií.</string>
|
||||||
|
<string name="install_favourites_warning">Pre každú aplikáciu sa môže zobraziť samostatná výzva na inštaláciu.</string>
|
||||||
|
<string name="pref_ui_dynamic_color">Živé farby</string>
|
||||||
|
<string name="pref_ui_dynamic_color_desc">Použite farby z vašej tapety. Vypnite pre farebnú paletu značky Aurora.</string>
|
||||||
|
<string name="toast_fav_install_enqueued">Inštalujem %1$d aplikácií</string>
|
||||||
|
<string name="toast_fav_install_none">Nie je potrebné nič inštalovať</string>
|
||||||
|
<string name="toast_fav_install_failed">Nepodarilo sa načítať informácie o aplikácii</string>
|
||||||
|
<plurals name="favourites_count">
|
||||||
|
<item quantity="one">%1$d obľúbený</item>
|
||||||
|
<item quantity="few">%1$d obľúbených</item>
|
||||||
|
<item quantity="many">%1$d obľúbených</item>
|
||||||
|
<item quantity="other">%1$d obľúbených</item>
|
||||||
|
</plurals>
|
||||||
|
<plurals name="account_count">
|
||||||
|
<item quantity="one">Celkový účet %1$d</item>
|
||||||
|
<item quantity="few">Celkových účtov %1$d</item>
|
||||||
|
<item quantity="many">Celkových účtov %1$d</item>
|
||||||
|
<item quantity="other">Celkových účtov %1$d</item>
|
||||||
|
</plurals>
|
||||||
|
<plurals name="account_refresh_failed">
|
||||||
|
<item quantity="one">Nepodarilo sa aktualizovať účet %1$d</item>
|
||||||
|
<item quantity="few">Nepodarilo sa aktualizovať %1$d účtov</item>
|
||||||
|
<item quantity="many">Nepodarilo sa aktualizovať %1$d účtov</item>
|
||||||
|
<item quantity="other">Nepodarilo sa aktualizovať %1$d účtov</item>
|
||||||
|
</plurals>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -568,7 +568,7 @@
|
|||||||
<string name="details_your_review">İncelemeniz</string>
|
<string name="details_your_review">İncelemeniz</string>
|
||||||
<string name="confirm_deeplink_title">Dış bağlantıları onayla</string>
|
<string name="confirm_deeplink_title">Dış bağlantıları onayla</string>
|
||||||
<string name="confirm_deeplink_summary">Bir bağlantıdan uygulama sayfasını açmadan önce sor, böylece reklamların senin iznin olmadan Aurora Store\'u başlatmasını engellersiniz.</string>
|
<string name="confirm_deeplink_summary">Bir bağlantıdan uygulama sayfasını açmadan önce sor, böylece reklamların senin iznin olmadan Aurora Store\'u başlatmasını engellersiniz.</string>
|
||||||
<string name="confirm_deeplink_sheet_title">Aurora Store\'a devam edilsin mi?</string>
|
<string name="confirm_deeplink_sheet_title">Aurora Store\'a sürdürülsün mü?</string>
|
||||||
<string name="confirm_deeplink_sheet_message">Bir dış uygulama Aurora Store\'u açmak istiyor</string>
|
<string name="confirm_deeplink_sheet_message">Bir dış uygulama Aurora Store\'u açmak istiyor</string>
|
||||||
<string name="confirm_deeplink_sheet_source">%1$s aracılığıyla açıldı</string>
|
<string name="confirm_deeplink_sheet_source">%1$s aracılığıyla açıldı</string>
|
||||||
<string name="title_install_favourites">Gözde uygulamalar kurulsun mu?</string>
|
<string name="title_install_favourites">Gözde uygulamalar kurulsun mu?</string>
|
||||||
@@ -583,4 +583,32 @@
|
|||||||
<item quantity="one">%1$d gözde uygulama</item>
|
<item quantity="one">%1$d gözde uygulama</item>
|
||||||
<item quantity="other">%1$d gözde uygulama</item>
|
<item quantity="other">%1$d gözde uygulama</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
|
<string name="account_set_default">Öntanımlı belirle</string>
|
||||||
|
<string name="account_default">Öntanımlı</string>
|
||||||
|
<string name="account_remove">Kaldır</string>
|
||||||
|
<string name="account_remove_title">Hesap kaldırılsın mı?</string>
|
||||||
|
<string name="account_remove_message">%1$s Aurora Store\'dan kaldırılsın mı?</string>
|
||||||
|
<string name="account_remove_last_message">Bu tek hesabınız. Kaldırdığınızda oturumunuz kapanacak ve yeniden oturum açana dek Aurora Store\'u kullanamayacaksınız.</string>
|
||||||
|
<string name="account_add">Hesap ekle</string>
|
||||||
|
<string name="account_add_device">Aygıt hesapları</string>
|
||||||
|
<string name="account_add_google">Google ile oturum aç</string>
|
||||||
|
<string name="account_add_anonymous">Anonim hesap ekle</string>
|
||||||
|
<string name="account_set_default_title">Öntanımlı hesap değiştirilsin mi?</string>
|
||||||
|
<string name="account_set_default_message">Aurora Store, yeni öntanımlı hesabı kullanmak için yeniden başlayacak.</string>
|
||||||
|
<string name="account_switching">Hesap değiştiriliyor…</string>
|
||||||
|
<string name="account_adding">Hesap ekleniyor…</string>
|
||||||
|
<string name="account_refresh_all">Tüm hesapları yenile</string>
|
||||||
|
<string name="account_refreshing">Hesaplar yenileniyor…</string>
|
||||||
|
<string name="account_refresh_done">Tüm hesaplar yenilendi</string>
|
||||||
|
<string name="account_switch_failed">Hesap yenilenemedi, bu nedenle öntanımlı değiştirilemedi.</string>
|
||||||
|
<string name="account_add_failed">Hesap eklenemedi. Lütfen yeniden deneyin.</string>
|
||||||
|
<string name="action_switch_account">Hesap değiştir</string>
|
||||||
|
<plurals name="account_count">
|
||||||
|
<item quantity="one">Toplam %1$d hesap</item>
|
||||||
|
<item quantity="other">Toplam %1$d hesap</item>
|
||||||
|
</plurals>
|
||||||
|
<plurals name="account_refresh_failed">
|
||||||
|
<item quantity="one">%1$d hesap yenilenemedi</item>
|
||||||
|
<item quantity="other">%1$d hesap yenilenemedi</item>
|
||||||
|
</plurals>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -591,4 +591,36 @@
|
|||||||
<item quantity="many">%1$d улюблених</item>
|
<item quantity="many">%1$d улюблених</item>
|
||||||
<item quantity="other">%1$d улюблених</item>
|
<item quantity="other">%1$d улюблених</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
|
<string name="account_set_default">Встановити як типовий</string>
|
||||||
|
<string name="account_default">Типовий</string>
|
||||||
|
<string name="account_remove">Вилучити</string>
|
||||||
|
<string name="account_remove_title">Вилучити обліковий запис?</string>
|
||||||
|
<string name="account_remove_message">Вилучити %1$s з Aurora Store?</string>
|
||||||
|
<string name="account_remove_last_message">Це ваш єдиний обліковий запис. Його видалення призведе до виходу із системи, і ви не зможете користуватися Aurora Store, доки не ввійдете знову.</string>
|
||||||
|
<string name="account_add">Додати обліковий запис</string>
|
||||||
|
<string name="account_add_device">Облікові записи пристрою</string>
|
||||||
|
<string name="account_add_google">Увійти через Google</string>
|
||||||
|
<string name="account_add_anonymous">Додати анонімний обліковий запис</string>
|
||||||
|
<string name="account_set_default_title">Змінити типовий обліковий запис?</string>
|
||||||
|
<string name="account_set_default_message">Aurora Store почне працювати з новим типовим обліковим записом.</string>
|
||||||
|
<string name="account_switching">Зміна облікового запису…</string>
|
||||||
|
<string name="account_adding">Додавання облікового запису…</string>
|
||||||
|
<string name="account_refresh_all">Оновити всі облікові записи</string>
|
||||||
|
<string name="account_refreshing">Оновлення облікових записів…</string>
|
||||||
|
<string name="account_refresh_done">Усі облікові записи оновлено</string>
|
||||||
|
<string name="account_switch_failed">Не вдалося оновити цей обліковий запис, тому типовий не було змінено.</string>
|
||||||
|
<string name="account_add_failed">Не вдалося додати обліковий запис. Спробуйте ще раз.</string>
|
||||||
|
<string name="action_switch_account">Змінити обліковий запис</string>
|
||||||
|
<plurals name="account_count">
|
||||||
|
<item quantity="one">Загалом %1$d обліковий запис</item>
|
||||||
|
<item quantity="few">Загалом %1$d облікові записи</item>
|
||||||
|
<item quantity="many">Загалом %1$d облікових записів</item>
|
||||||
|
<item quantity="other">Загалом %1$d облікових записів</item>
|
||||||
|
</plurals>
|
||||||
|
<plurals name="account_refresh_failed">
|
||||||
|
<item quantity="one">Не вдалося оновити %1$d обліковий запис</item>
|
||||||
|
<item quantity="few">Не вдалося оновити %1$d облікові записи</item>
|
||||||
|
<item quantity="many">Не вдалося оновити %1$d облікових записів</item>
|
||||||
|
<item quantity="other">Не вдалося оновити %1$d облікових записів</item>
|
||||||
|
</plurals>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ buildscript {
|
|||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.android.application) apply false
|
alias(libs.plugins.android.application) apply false
|
||||||
|
alias(libs.plugins.jetbrains.kotlin.jvm) apply false
|
||||||
alias(libs.plugins.jetbrains.kotlin.compose) apply false
|
alias(libs.plugins.jetbrains.kotlin.compose) apply false
|
||||||
alias(libs.plugins.jetbrains.kotlin.parcelize) apply false
|
alias(libs.plugins.jetbrains.kotlin.parcelize) apply false
|
||||||
alias(libs.plugins.jetbrains.kotlin.serialization) apply false
|
alias(libs.plugins.jetbrains.kotlin.serialization) apply false
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ gplayapi = "3.6.3"
|
|||||||
hiddenapibypass = "6.1"
|
hiddenapibypass = "6.1"
|
||||||
hilt = "2.59.2"
|
hilt = "2.59.2"
|
||||||
junit = "4.13.2"
|
junit = "4.13.2"
|
||||||
|
coroutines = "1.10.2"
|
||||||
kotlin = "2.3.21"
|
kotlin = "2.3.21"
|
||||||
ksp = "2.3.7"
|
ksp = "2.3.7"
|
||||||
ktlint = "14.2.0"
|
ktlint = "14.2.0"
|
||||||
@@ -91,6 +92,7 @@ hilt-androidx-compiler = { module = "androidx.hilt:hilt-compiler", version.ref =
|
|||||||
hilt-androidx-work = { module = "androidx.hilt:hilt-work", version.ref = "androidx-hilt" }
|
hilt-androidx-work = { module = "androidx.hilt:hilt-work", version.ref = "androidx-hilt" }
|
||||||
huawei-hms-coreservice = { module = "com.huawei.hms:ag-coreservice", version.ref = "agCoreservice" }
|
huawei-hms-coreservice = { module = "com.huawei.hms:ag-coreservice", version.ref = "agCoreservice" }
|
||||||
junit = { module = "junit:junit", version.ref = "junit" }
|
junit = { module = "junit:junit", version.ref = "junit" }
|
||||||
|
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
|
||||||
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization" }
|
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization" }
|
||||||
lsposed-hiddenapibypass = { module = "org.lsposed.hiddenapibypass:hiddenapibypass", version.ref = "hiddenapibypass" }
|
lsposed-hiddenapibypass = { module = "org.lsposed.hiddenapibypass:hiddenapibypass", version.ref = "hiddenapibypass" }
|
||||||
process-phoenix = { module = "com.jakewharton:process-phoenix", version.ref = "processPhoenix" }
|
process-phoenix = { module = "com.jakewharton:process-phoenix", version.ref = "processPhoenix" }
|
||||||
@@ -107,6 +109,7 @@ androidx-navigation = { id = "androidx.navigation.safeargs.kotlin", version.ref
|
|||||||
google-ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
|
google-ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
|
||||||
hilt-android-plugin = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
|
hilt-android-plugin = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
|
||||||
jetbrains-kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
jetbrains-kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
||||||
|
jetbrains-kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
|
||||||
jetbrains-kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" }
|
jetbrains-kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" }
|
||||||
jetbrains-kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
|
jetbrains-kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
|
||||||
ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlint" }
|
ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlint" }
|
||||||
|
|||||||
@@ -32,4 +32,5 @@ dependencyResolutionManagement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
include(":app")
|
include(":app")
|
||||||
|
include(":apk-downloader")
|
||||||
rootProject.name = "AuroraStore4"
|
rootProject.name = "AuroraStore4"
|
||||||
|
|||||||
Reference in New Issue
Block a user