Compare commits

..

12 Commits

Author SHA1 Message Date
43214ecc8a Merge pull request 'Add apk downloader project based on aurora's core' (#2) from nathan/apkDownloader into master
Reviewed-on: #2
2026-07-07 14:26:29 +00:00
Nathan Lecoanet
334fd2fffd Add apk downloader project based on aurora's core
Docker all in one
2026-07-07 16:25:44 +02:00
Rahul Patel
3ab3943f1e Merge branch 'weblate-aurora-store-aurorastore-translations' into 'master'
Translations update from Hosted Weblate

See merge request AuroraOSS/AuroraStore!587
2026-06-13 03:34:34 +05:30
Feike Donia
4b937c6fe1 Translated using Weblate (Dutch)
Currently translated at 96.8% (588 of 607 strings)

Translation: Aurora Store/Android
Translate-URL: https://hosted.weblate.org/projects/aurora-store/aurorastore-translations/nl/
2026-06-12 16:05:33 +02:00
Emin Tufan Çetin
be3a03a784 Translated using Weblate (Turkish)
Currently translated at 100.0% (607 of 607 strings)

Translation: Aurora Store/Android
Translate-URL: https://hosted.weblate.org/projects/aurora-store/aurorastore-translations/tr/
2026-06-12 16:05:33 +02:00
jonnysemon
19ab89c864 Translated using Weblate (Arabic)
Currently translated at 100.0% (607 of 607 strings)

Translation: Aurora Store/Android
Translate-URL: https://hosted.weblate.org/projects/aurora-store/aurorastore-translations/ar/
2026-06-12 16:05:32 +02:00
Dan
66b7c4c260 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (607 of 607 strings)

Translation: Aurora Store/Android
Translate-URL: https://hosted.weblate.org/projects/aurora-store/aurorastore-translations/uk/
2026-06-12 16:05:32 +02:00
Mato
786a107053 Translated using Weblate (Slovak)
Currently translated at 99.0% (601 of 607 strings)

Translation: Aurora Store/Android
Translate-URL: https://hosted.weblate.org/projects/aurora-store/aurorastore-translations/sk/
2026-06-12 16:05:32 +02:00
Mirosław Żylewicz
e176a6ab62 Translated using Weblate (Polish)
Currently translated at 100.0% (607 of 607 strings)

Translation: Aurora Store/Android
Translate-URL: https://hosted.weblate.org/projects/aurora-store/aurorastore-translations/pl/
2026-06-12 16:05:31 +02:00
NooB9496
c4db5fd7e2 Translated using Weblate (Polish)
Currently translated at 100.0% (607 of 607 strings)

Translation: Aurora Store/Android
Translate-URL: https://hosted.weblate.org/projects/aurora-store/aurorastore-translations/pl/
2026-06-12 16:05:31 +02:00
Fjuro
b6769f13f5 Translated using Weblate (Czech)
Currently translated at 100.0% (607 of 607 strings)

Translation: Aurora Store/Android
Translate-URL: https://hosted.weblate.org/projects/aurora-store/aurorastore-translations/cs/
2026-06-12 16:05:31 +02:00
AO yahoe.001
b9de3c9850 Translated using Weblate (French)
Currently translated at 100.0% (607 of 607 strings)

Translation: Aurora Store/Android
Translate-URL: https://hosted.weblate.org/projects/aurora-store/aurorastore-translations/fr/
2026-06-12 16:05:30 +02:00
41 changed files with 2055 additions and 33 deletions

5
.dockerignore Normal file
View File

@@ -0,0 +1,5 @@
.gradle
**/build
**/downloads
downloads
local.properties

View File

@@ -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
View 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
View 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"]

View 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")
}
}
}

View 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

View 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=

View 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");
}
}

View 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);
}
}

View 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);
}
}

View 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;
}
}

View File

@@ -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()
}
}

View File

@@ -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")
}
}
}

View 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_]*)+")
}

View File

@@ -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(",")
}
}

View File

@@ -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()
}
}

View File

@@ -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>
)

View File

@@ -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)
}
}

View File

@@ -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
}

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -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
}
}
}

View File

@@ -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))
}
}
}

View File

@@ -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
}
}
}

View File

@@ -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
}
}

View File

@@ -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)
}
}

View File

@@ -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"))
}
}

View File

@@ -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"
)
)
}
}

View File

@@ -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) }
}
}

View File

@@ -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()
}
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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 dutilisation</string> <string name="menu_terms">Conditions générales dutilisation</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 dinstallation</string> <string name="onboarding_title_installer">Programme dinstallation</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 na pas été fourni</string> <string name="details_changelog_unavailable">Le journal des changements na 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 dinclusion</string> <string name="action_whitelist">Liste dinclusion</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 derreur : <xliff:g id="status_code">%1$d</xliff:g></string> <string name="failed_generating_session">Échec de génération de la session. Code derreur : <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">Laccès a été refusé. Utilisez-vous un RPV/VPN ou Tor?</string> <string name="access_denied_using_vpn">Laccè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 daccéder au serveur</string> <string name="server_unreachable">Impossible daccé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 linstallation peut échouer.</string> <string name="pref_updates_incompatible_desc">Afficher les mises à jour pour les applis incompatibles ou désactivées dont linstallation peut échouer.</string>
<string name="action_home_screen">Ajouter à lécran daccueil</string> <string name="action_home_screen">Ajouter à lécran daccueil</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">LURL est invalide</string> <string name="add_dispenser_error">LURL 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 nest proposé</string> <string name="no_dispensers_available">Aucun distributeur de jetons nest 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 dAutres 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 lappli.\n\nContrairement à un magasin dapplis traditionnel, Aurora Store ne détient, ne concède sous licence, ni ne distribue aucune appli. Les applis, descriptions dapplis, 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 na 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 dappli. Par ailleurs, le magasin Aurora Store na aucun lien daffiliation, de coopération ou de connexion avec ces entités.</string>
<string name="about_aurora_store_title">À propos dAurora Store</string> <string name="about_aurora_store_title">À propos dAurora 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 lappareil</string> <string name="pref_clear_device_owner_title">Révoquer le droit de propriété de lappareil</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 dimportation des favoris!</string> <string name="toast_fav_import_failed">Échec dimportation 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 linstallation dapplis de sources inconnues</string> <string name="onboarding_permission_installer_legacy_desc">Autoriser linstallation dapplis 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 lappli.</string>
<string name="authentication_required_title">Lauthentification est requise</string> <string name="authentication_required_title">Lauthentification 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 sagit 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 dappareil</string> <string name="account_add_device">Comptes dappareil</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 dun compte…</string> <string name="account_adding">Ajout dun 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 dactualiser ce compte; le compte par défaut na 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 dajouter 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 dactualiser %1$d compte</item>
<item quantity="many">Impossible d\'actualiser %1$d comptes</item> <item quantity="many">Impossible dactualiser %1$d de comptes</item>
<item quantity="other">Impossible d\'actualiser %1$d comptes</item> <item quantity="other">Impossible dactualiser %1$d de comptes</item>
</plurals> </plurals>
</resources> </resources>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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

View File

@@ -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" }

View File

@@ -32,4 +32,5 @@ dependencyResolutionManagement {
} }
} }
include(":app") include(":app")
include(":apk-downloader")
rootProject.name = "AuroraStore4" rootProject.name = "AuroraStore4"