From fa52c5fff23ec4caa499ffe48be7a69595226d2c Mon Sep 17 00:00:00 2001 From: Marvin W Date: Mon, 14 Oct 2024 10:35:21 +0200 Subject: [PATCH 001/132] Location: Clean up database migration code --- .../gms/location/network/LocationDatabase.kt | 150 ++++++++++++------ .../location/network/wifi/MovingWifiHelper.kt | 2 + 2 files changed, 105 insertions(+), 47 deletions(-) diff --git a/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/LocationDatabase.kt b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/LocationDatabase.kt index aa2e1b85c2..07c1c0ce52 100644 --- a/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/LocationDatabase.kt +++ b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/LocationDatabase.kt @@ -7,10 +7,10 @@ package org.microg.gms.location.network import android.content.ContentValues import android.content.Context -import android.content.Intent import android.database.Cursor import android.database.DatabaseUtils import android.database.sqlite.SQLiteDatabase +import android.database.sqlite.SQLiteException import android.database.sqlite.SQLiteOpenHelper import android.location.Location import android.net.Uri @@ -39,7 +39,101 @@ import kotlin.math.abs import kotlin.math.max import kotlin.math.min -internal class LocationDatabase(private val context: Context) : SQLiteOpenHelper(context, "geocache.db", null, 8) { +private const val CURRENT_VERSION = 8 + +internal class LocationDatabase(private val context: Context) : SQLiteOpenHelper(context, "geocache.db", null, CURRENT_VERSION) { + private data class Migration(val apply: String?, val revert: String?, val allowApplyFailure: Boolean, val allowRevertFailure: Boolean) + private val migrations: Map> + + init { + val migrations = mutableMapOf>() + fun declare(version: Int, apply: String, revert: String? = null, allowFailure: Boolean = false, allowApplyFailure: Boolean = allowFailure, allowRevertFailure: Boolean = allowFailure) { + if (!migrations.containsKey(version)) + migrations[version] = arrayListOf() + migrations[version]!!.add(Migration(apply, revert, allowApplyFailure, allowRevertFailure)) + } + + declare(3, "CREATE TABLE $TABLE_CELLS($FIELD_MCC INTEGER NOT NULL, $FIELD_MNC INTEGER NOT NULL, $FIELD_TYPE INTEGER NOT NULL, $FIELD_LAC_TAC INTEGER NOT NULL, $FIELD_CID INTEGER NOT NULL, $FIELD_PSC INTEGER NOT NULL, $FIELD_LATITUDE REAL NOT NULL, $FIELD_LONGITUDE REAL NOT NULL, $FIELD_ACCURACY REAL NOT NULL, $FIELD_TIME INTEGER NOT NULL, $FIELD_PRECISION REAL NOT NULL);") + declare(3, "CREATE TABLE $TABLE_CELLS_PRE($FIELD_MCC INTEGER NOT NULL, $FIELD_MNC INTEGER NOT NULL, $FIELD_TIME INTEGER NOT NULL);") + declare(3, "CREATE TABLE $TABLE_WIFIS($FIELD_MAC BLOB, $FIELD_LATITUDE REAL NOT NULL, $FIELD_LONGITUDE REAL NOT NULL, $FIELD_ACCURACY REAL NOT NULL, $FIELD_TIME INTEGER NOT NULL, $FIELD_PRECISION REAL NOT NULL);") + declare(3, "CREATE TABLE $TABLE_CELLS_LEARN($FIELD_MCC INTEGER NOT NULL, $FIELD_MNC INTEGER NOT NULL, $FIELD_TYPE INTEGER NOT NULL, $FIELD_LAC_TAC INTEGER NOT NULL, $FIELD_CID INTEGER NOT NULL, $FIELD_PSC INTEGER NOT NULL, $FIELD_LATITUDE_HIGH REAL NOT NULL, $FIELD_LATITUDE_LOW REAL NOT NULL, $FIELD_LONGITUDE_HIGH REAL NOT NULL, $FIELD_LONGITUDE_LOW REAL NOT NULL, $FIELD_TIME INTEGER NOT NULL, $FIELD_BAD_TIME INTEGER);") + declare(3, "CREATE TABLE $TABLE_WIFI_LEARN($FIELD_MAC BLOB, $FIELD_LATITUDE_HIGH REAL NOT NULL, $FIELD_LATITUDE_LOW REAL NOT NULL, $FIELD_LONGITUDE_HIGH REAL NOT NULL, $FIELD_LONGITUDE_LOW REAL NOT NULL, $FIELD_TIME INTEGER NOT NULL, $FIELD_BAD_TIME INTEGER);") + declare(3, "CREATE UNIQUE INDEX ${TABLE_CELLS}_index ON $TABLE_CELLS($FIELD_MCC, $FIELD_MNC, $FIELD_TYPE, $FIELD_LAC_TAC, $FIELD_CID, $FIELD_PSC);") + declare(3, "CREATE UNIQUE INDEX ${TABLE_CELLS_PRE}_index ON $TABLE_CELLS_PRE($FIELD_MCC, $FIELD_MNC);") + declare(3, "CREATE UNIQUE INDEX ${TABLE_WIFIS}_index ON $TABLE_WIFIS($FIELD_MAC);") + declare(3, "CREATE UNIQUE INDEX ${TABLE_CELLS_LEARN}_index ON $TABLE_CELLS_LEARN($FIELD_MCC, $FIELD_MNC, $FIELD_TYPE, $FIELD_LAC_TAC, $FIELD_CID, $FIELD_PSC);") + declare(3, "CREATE UNIQUE INDEX ${TABLE_WIFI_LEARN}_index ON $TABLE_WIFI_LEARN($FIELD_MAC);") + declare(3, "CREATE INDEX ${TABLE_CELLS}_time_index ON $TABLE_CELLS($FIELD_TIME);") + declare(3, "CREATE INDEX ${TABLE_CELLS_PRE}_time_index ON $TABLE_CELLS_PRE($FIELD_TIME);") + declare(3, "CREATE INDEX ${TABLE_WIFIS}_time_index ON $TABLE_WIFIS($FIELD_TIME);") + declare(3, "CREATE INDEX ${TABLE_CELLS_LEARN}_time_index ON $TABLE_CELLS_LEARN($FIELD_TIME);") + declare(3, "CREATE INDEX ${TABLE_WIFI_LEARN}_time_index ON $TABLE_WIFI_LEARN($FIELD_TIME);") + declare(3, "DROP TABLE IF EXISTS $TABLE_WIFI_SCANS;", allowFailure = true) + + declare(4, "ALTER TABLE $TABLE_CELLS_LEARN ADD COLUMN $FIELD_LEARN_RECORD_COUNT INTEGER;") + declare(4, "ALTER TABLE $TABLE_WIFI_LEARN ADD COLUMN $FIELD_LEARN_RECORD_COUNT INTEGER;") + + declare(5, "ALTER TABLE $TABLE_CELLS ADD COLUMN $FIELD_ALTITUDE REAL;") + declare(5, "ALTER TABLE $TABLE_CELLS ADD COLUMN $FIELD_ALTITUDE_ACCURACY REAL;") + declare(5, "ALTER TABLE $TABLE_WIFIS ADD COLUMN $FIELD_ALTITUDE REAL;") + declare(5, "ALTER TABLE $TABLE_WIFIS ADD COLUMN $FIELD_ALTITUDE_ACCURACY REAL;") + + declare(6, "ALTER TABLE $TABLE_CELLS_LEARN ADD COLUMN $FIELD_ALTITUDE_HIGH REAL;") + declare(6, "ALTER TABLE $TABLE_CELLS_LEARN ADD COLUMN $FIELD_ALTITUDE_LOW REAL;") + declare(6, "ALTER TABLE $TABLE_WIFI_LEARN ADD COLUMN $FIELD_ALTITUDE_HIGH REAL;") + declare(6, "ALTER TABLE $TABLE_WIFI_LEARN ADD COLUMN $FIELD_ALTITUDE_LOW REAL;") + + declare(8, "DELETE FROM $TABLE_WIFIS WHERE $FIELD_ACCURACY = 0.0 AND ($FIELD_LATITUDE != 0.0 OR $FIELD_LONGITUDE != 0.0);", allowRevertFailure = true) + declare(8, "DELETE FROM $TABLE_CELLS WHERE $FIELD_ACCURACY = 0.0 AND ($FIELD_LATITUDE != 0.0 OR $FIELD_LONGITUDE != 0.0);", allowRevertFailure = true) + declare(8, "UPDATE $TABLE_CELLS_LEARN SET $FIELD_ALTITUDE_LOW = NULL WHERE $FIELD_ALTITUDE_LOW = 0.0;", allowRevertFailure = true) + declare(8, "UPDATE $TABLE_CELLS_LEARN SET $FIELD_ALTITUDE_HIGH = NULL WHERE $FIELD_ALTITUDE_HIGH = 0.0;", allowRevertFailure = true) + declare(8, "UPDATE $TABLE_WIFI_LEARN SET $FIELD_ALTITUDE_LOW = NULL WHERE $FIELD_ALTITUDE_LOW = 0.0;", allowRevertFailure = true) + declare(8, "UPDATE $TABLE_WIFI_LEARN SET $FIELD_ALTITUDE_HIGH = NULL WHERE $FIELD_ALTITUDE_HIGH = 0.0;", allowRevertFailure = true) + + this.migrations = migrations + } + + private fun migrate(db: SQLiteDatabase, oldVersion: Int, newVersion: Int, allowFailure: Boolean = false) { + var currentVersion = oldVersion + while (currentVersion < newVersion) { + val nextVersion = currentVersion + 1 + val migrations = this.migrations[nextVersion].orEmpty() + for (migration in migrations) { + if (migration.apply == null && !migration.allowApplyFailure && !allowFailure) + throw SQLiteException("Incomplete migration from $currentVersion to $nextVersion") + try { + db.execSQL(migration.apply) + Log.d(TAG, "Applied migration from version $currentVersion to $nextVersion: ${migration.apply}") + } catch (e: Exception) { + Log.w(TAG, "Error while applying migration from version $currentVersion to $nextVersion: ${migration.apply}", e) + if (!migration.allowApplyFailure && !allowFailure) + throw e + } + } + + currentVersion = nextVersion + } + while (currentVersion > newVersion) { + val nextVersion = currentVersion - 1 + val migrations = this.migrations[currentVersion].orEmpty() + for (migration in migrations.asReversed()) { + if (migration.revert == null && !migration.allowRevertFailure && !allowFailure) + throw SQLiteException("Incomplete migration from $currentVersion to $nextVersion") + try { + db.execSQL(migration.revert) + Log.d(TAG, "Reverted migration from version $currentVersion to $nextVersion: ${migration.revert}") + } catch (e: Exception) { + Log.w(TAG, "Error while reverting migration from version $currentVersion to $nextVersion: ${migration.revert}", e) + if (!migration.allowRevertFailure && !allowFailure) + throw e + } + } + + currentVersion = nextVersion + } + Log.i(TAG, "Migrated from $oldVersion to $newVersion") + } + private fun SQLiteDatabase.query(table: String, columns: Array, selection: String? = null, selectionArgs: Array? = null) = query(table, columns, selection, selectionArgs, null, null, null) @@ -286,22 +380,7 @@ internal class LocationDatabase(private val context: Context) : SQLiteOpenHelper } override fun onCreate(db: SQLiteDatabase) { - db.execSQL("CREATE TABLE IF NOT EXISTS $TABLE_CELLS($FIELD_MCC INTEGER NOT NULL, $FIELD_MNC INTEGER NOT NULL, $FIELD_TYPE INTEGER NOT NULL, $FIELD_LAC_TAC INTEGER NOT NULL, $FIELD_CID INTEGER NOT NULL, $FIELD_PSC INTEGER NOT NULL, $FIELD_LATITUDE REAL NOT NULL, $FIELD_LONGITUDE REAL NOT NULL, $FIELD_ACCURACY REAL NOT NULL, $FIELD_ALTITUDE REAL, $FIELD_ALTITUDE_ACCURACY REAL, $FIELD_TIME INTEGER NOT NULL, $FIELD_PRECISION REAL NOT NULL);") - db.execSQL("CREATE TABLE IF NOT EXISTS $TABLE_CELLS_PRE($FIELD_MCC INTEGER NOT NULL, $FIELD_MNC INTEGER NOT NULL, $FIELD_TIME INTEGER NOT NULL);") - db.execSQL("CREATE TABLE IF NOT EXISTS $TABLE_WIFIS($FIELD_MAC BLOB, $FIELD_LATITUDE REAL NOT NULL, $FIELD_LONGITUDE REAL NOT NULL, $FIELD_ACCURACY REAL NOT NULL, $FIELD_ALTITUDE REAL, $FIELD_ALTITUDE_ACCURACY REAL, $FIELD_TIME INTEGER NOT NULL, $FIELD_PRECISION REAL NOT NULL);") - db.execSQL("CREATE TABLE IF NOT EXISTS $TABLE_CELLS_LEARN($FIELD_MCC INTEGER NOT NULL, $FIELD_MNC INTEGER NOT NULL, $FIELD_TYPE INTEGER NOT NULL, $FIELD_LAC_TAC INTEGER NOT NULL, $FIELD_CID INTEGER NOT NULL, $FIELD_PSC INTEGER NOT NULL, $FIELD_LATITUDE_HIGH REAL NOT NULL, $FIELD_LATITUDE_LOW REAL NOT NULL, $FIELD_LONGITUDE_HIGH REAL NOT NULL, $FIELD_LONGITUDE_LOW REAL NOT NULL, $FIELD_ALTITUDE_HIGH REAL, $FIELD_ALTITUDE_LOW REAL, $FIELD_TIME INTEGER NOT NULL, $FIELD_BAD_TIME INTEGER, $FIELD_LEARN_RECORD_COUNT INTEGER);") - db.execSQL("CREATE TABLE IF NOT EXISTS $TABLE_WIFI_LEARN($FIELD_MAC BLOB, $FIELD_LATITUDE_HIGH REAL NOT NULL, $FIELD_LATITUDE_LOW REAL NOT NULL, $FIELD_LONGITUDE_HIGH REAL NOT NULL, $FIELD_LONGITUDE_LOW REAL NOT NULL, $FIELD_ALTITUDE_HIGH REAL, $FIELD_ALTITUDE_LOW REAL, $FIELD_TIME INTEGER NOT NULL, $FIELD_BAD_TIME INTEGER, $FIELD_LEARN_RECORD_COUNT INTEGER);") - db.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS ${TABLE_CELLS}_index ON $TABLE_CELLS($FIELD_MCC, $FIELD_MNC, $FIELD_TYPE, $FIELD_LAC_TAC, $FIELD_CID, $FIELD_PSC);") - db.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS ${TABLE_CELLS_PRE}_index ON $TABLE_CELLS_PRE($FIELD_MCC, $FIELD_MNC);") - db.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS ${TABLE_WIFIS}_index ON $TABLE_WIFIS($FIELD_MAC);") - db.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS ${TABLE_CELLS_LEARN}_index ON $TABLE_CELLS_LEARN($FIELD_MCC, $FIELD_MNC, $FIELD_TYPE, $FIELD_LAC_TAC, $FIELD_CID, $FIELD_PSC);") - db.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS ${TABLE_WIFI_LEARN}_index ON $TABLE_WIFI_LEARN($FIELD_MAC);") - db.execSQL("CREATE INDEX IF NOT EXISTS ${TABLE_CELLS}_time_index ON $TABLE_CELLS($FIELD_TIME);") - db.execSQL("CREATE INDEX IF NOT EXISTS ${TABLE_CELLS_PRE}_time_index ON $TABLE_CELLS_PRE($FIELD_TIME);") - db.execSQL("CREATE INDEX IF NOT EXISTS ${TABLE_WIFIS}_time_index ON $TABLE_WIFIS($FIELD_TIME);") - db.execSQL("CREATE INDEX IF NOT EXISTS ${TABLE_CELLS_LEARN}_time_index ON $TABLE_CELLS_LEARN($FIELD_TIME);") - db.execSQL("CREATE INDEX IF NOT EXISTS ${TABLE_WIFI_LEARN}_time_index ON $TABLE_WIFI_LEARN($FIELD_TIME);") - db.execSQL("DROP TABLE IF EXISTS $TABLE_WIFI_SCANS;") + migrate(db, 0, CURRENT_VERSION) } fun cleanup(db: SQLiteDatabase) { @@ -320,34 +399,11 @@ internal class LocationDatabase(private val context: Context) : SQLiteOpenHelper } override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { - if (oldVersion < 3) { - onCreate(db) - } else { - if (oldVersion < 4) { - db.execSQL("ALTER TABLE $TABLE_CELLS_LEARN ADD COLUMN $FIELD_LEARN_RECORD_COUNT INTEGER;") - db.execSQL("ALTER TABLE $TABLE_WIFI_LEARN ADD COLUMN $FIELD_LEARN_RECORD_COUNT INTEGER;") - } - if (oldVersion < 5) { - db.execSQL("ALTER TABLE $TABLE_CELLS ADD COLUMN $FIELD_ALTITUDE REAL;") - db.execSQL("ALTER TABLE $TABLE_CELLS ADD COLUMN $FIELD_ALTITUDE_ACCURACY REAL;") - db.execSQL("ALTER TABLE $TABLE_WIFIS ADD COLUMN $FIELD_ALTITUDE REAL;") - db.execSQL("ALTER TABLE $TABLE_WIFIS ADD COLUMN $FIELD_ALTITUDE_ACCURACY REAL;") - } - if (oldVersion < 6) { - db.execSQL("ALTER TABLE $TABLE_CELLS_LEARN ADD COLUMN $FIELD_ALTITUDE_HIGH REAL;") - db.execSQL("ALTER TABLE $TABLE_CELLS_LEARN ADD COLUMN $FIELD_ALTITUDE_LOW REAL;") - db.execSQL("ALTER TABLE $TABLE_WIFI_LEARN ADD COLUMN $FIELD_ALTITUDE_HIGH REAL;") - db.execSQL("ALTER TABLE $TABLE_WIFI_LEARN ADD COLUMN $FIELD_ALTITUDE_LOW REAL;") - } - if (oldVersion < 8) { - db.execSQL("DELETE FROM $TABLE_WIFIS WHERE $FIELD_ACCURACY = 0.0 AND ($FIELD_LATITUDE != 0.0 OR $FIELD_LONGITUDE != 0.0);") - db.execSQL("DELETE FROM $TABLE_CELLS WHERE $FIELD_ACCURACY = 0.0 AND ($FIELD_LATITUDE != 0.0 OR $FIELD_LONGITUDE != 0.0);") - db.execSQL("UPDATE $TABLE_CELLS_LEARN SET $FIELD_ALTITUDE_LOW = NULL WHERE $FIELD_ALTITUDE_LOW = 0.0;") - db.execSQL("UPDATE $TABLE_CELLS_LEARN SET $FIELD_ALTITUDE_HIGH = NULL WHERE $FIELD_ALTITUDE_HIGH = 0.0;") - db.execSQL("UPDATE $TABLE_WIFI_LEARN SET $FIELD_ALTITUDE_LOW = NULL WHERE $FIELD_ALTITUDE_LOW = 0.0;") - db.execSQL("UPDATE $TABLE_WIFI_LEARN SET $FIELD_ALTITUDE_HIGH = NULL WHERE $FIELD_ALTITUDE_HIGH = 0.0;") - } - } + migrate(db, oldVersion, newVersion) + } + + override fun onDowngrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { + migrate(db, oldVersion, newVersion) } fun dump(writer: PrintWriter) { @@ -452,7 +508,7 @@ private fun getCellPreSelectionArgs(cell: CellDetails): Array { private fun Cursor.getSingleLocation(maxAge: Long): Location? { return try { if (moveToNext()) { - getLocation(MAX_CACHE_AGE) + getLocation(maxAge) } else { null } diff --git a/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/wifi/MovingWifiHelper.kt b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/wifi/MovingWifiHelper.kt index b986f5c409..c4d50000fa 100644 --- a/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/wifi/MovingWifiHelper.kt +++ b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/wifi/MovingWifiHelper.kt @@ -53,6 +53,7 @@ private val MOVING_WIFI_HOTSPOTS = setOf( "Telekom_FlyNet", "Vestische WLAN", "agilis-Wifi", + "freeWIFIahead!", // Greece "AegeanWiFi", // Hong Kong @@ -71,6 +72,7 @@ private val MOVING_WIFI_HOTSPOTS = setOf( "saswifi", // Switzerland "SBB-Free", + "SBB-FREE", "SWISS Connect", "Edelweiss Entertainment", // United Kingdom From de192e3bcd82501fa4b811f752931803ed189e97 Mon Sep 17 00:00:00 2001 From: Fynn Godau Date: Thu, 4 Apr 2024 00:59:03 +0200 Subject: [PATCH 002/132] Initial function for sync keys request Sync keys request is required for Google Workspace-enabled accounts that mandate a screenlock to be enabled. Note that with this commit, a working method is provided, but not yet called, and it also doesn't contain the correct "screenlock enabled" value yet. --- .../gms/common/DeviceConfiguration.java | 5 + .../com/google/android/gms/common/Scopes.java | 5 + .../src/main/proto/cryptauth/LICENSE | 27 ++ ...auth_better_together_device_metadata.proto | 69 ++++ ...uth_better_together_feature_metadata.proto | 37 ++ .../cryptauth_client_app_metadata.proto | 139 ++++++++ .../proto/cryptauth/cryptauth_common.proto | 164 +++++++++ .../cryptauth/cryptauth_devicesync.proto | 318 ++++++++++++++++++ .../proto/cryptauth/cryptauth_directive.proto | 113 +++++++ .../cryptauth/cryptauth_enrollment.proto | 298 ++++++++++++++++ .../microg/gms/auth/appcert/AppCertManager.kt | 6 +- .../microg/gms/cryptauth/SyncKeysRequest.kt | 164 +++++++++ 12 files changed, 1342 insertions(+), 3 deletions(-) create mode 100644 play-services-core-proto/src/main/proto/cryptauth/LICENSE create mode 100644 play-services-core-proto/src/main/proto/cryptauth/cryptauth_better_together_device_metadata.proto create mode 100644 play-services-core-proto/src/main/proto/cryptauth/cryptauth_better_together_feature_metadata.proto create mode 100644 play-services-core-proto/src/main/proto/cryptauth/cryptauth_client_app_metadata.proto create mode 100644 play-services-core-proto/src/main/proto/cryptauth/cryptauth_common.proto create mode 100644 play-services-core-proto/src/main/proto/cryptauth/cryptauth_devicesync.proto create mode 100644 play-services-core-proto/src/main/proto/cryptauth/cryptauth_directive.proto create mode 100644 play-services-core-proto/src/main/proto/cryptauth/cryptauth_enrollment.proto create mode 100644 play-services-core/src/main/kotlin/org/microg/gms/cryptauth/SyncKeysRequest.kt diff --git a/play-services-base/core/src/main/java/org/microg/gms/common/DeviceConfiguration.java b/play-services-base/core/src/main/java/org/microg/gms/common/DeviceConfiguration.java index 05473ca559..906567bb7d 100644 --- a/play-services-base/core/src/main/java/org/microg/gms/common/DeviceConfiguration.java +++ b/play-services-base/core/src/main/java/org/microg/gms/common/DeviceConfiguration.java @@ -45,6 +45,7 @@ public class DeviceConfiguration { public List availableFeatures; public int densityDpi; + public double diagonalInch; public int glEsVersion; public List glExtensions; public boolean hasFiveWayNavigation; @@ -92,6 +93,10 @@ public DeviceConfiguration(Context context) { this.nativePlatforms = getNativePlatforms(); widthPixels = displayMetrics.widthPixels; heightPixels = displayMetrics.heightPixels; + diagonalInch = Math.sqrt( + Math.pow(widthPixels / displayMetrics.xdpi, 2) + + Math.pow(heightPixels / displayMetrics.ydpi, 2) + ); locales = getLocales(context); Set glExtensions = new HashSet(); addEglExtensions(glExtensions); diff --git a/play-services-basement/src/main/java/com/google/android/gms/common/Scopes.java b/play-services-basement/src/main/java/com/google/android/gms/common/Scopes.java index a359d6b710..eb01fe2602 100644 --- a/play-services-basement/src/main/java/com/google/android/gms/common/Scopes.java +++ b/play-services-basement/src/main/java/com/google/android/gms/common/Scopes.java @@ -98,4 +98,9 @@ public class Scopes { public static final String USER_BIRTHDAY_READ = "https://www.googleapis.com/auth/user.birthday.read"; @Hide public static final String GMAIL_READONLY = "https://www.googleapis.com/auth/gmail.readonly"; + /** + * Scope for cryptauthenrollment.googleapis.com (required for certain Google Workspace accounts) + */ + @Hide + public static final String CRYPTAUTH = "https://www.googleapis.com/auth/cryptauth"; } diff --git a/play-services-core-proto/src/main/proto/cryptauth/LICENSE b/play-services-core-proto/src/main/proto/cryptauth/LICENSE new file mode 100644 index 0000000000..2249a28657 --- /dev/null +++ b/play-services-core-proto/src/main/proto/cryptauth/LICENSE @@ -0,0 +1,27 @@ +// Copyright 2015 The Chromium Authors +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google LLC nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/play-services-core-proto/src/main/proto/cryptauth/cryptauth_better_together_device_metadata.proto b/play-services-core-proto/src/main/proto/cryptauth/cryptauth_better_together_device_metadata.proto new file mode 100644 index 0000000000..2876477c78 --- /dev/null +++ b/play-services-core-proto/src/main/proto/cryptauth/cryptauth_better_together_device_metadata.proto @@ -0,0 +1,69 @@ +/* SPDX-FileCopyrightText: 2014 The Chromium Authors + * SPDX-License-Identifier: BSD-3-Clause + */ + +syntax = "proto3"; + +package cryptauthv2; + +option optimize_for = LITE_RUNTIME; + +//--------------------- ATTENTION ------------------------ +// If you chamge this file please change +// j/c/g/android/gms/auth_proximity/proto/cryptauth_better_together_feature_metadata.proto +// as well. + + +// A seed used to feed an EID BLE advertisement for some time period. +// Next ID: 4 +message BeaconSeed { + // The beacon seed bytes. + bytes data = 1; + + // The time at which this key becomes active. + int64 start_time_millis = 2; + + // The time at which this key becomes inactive. + int64 end_time_millis = 3; +} + +// Data required to verify the remote device. +// Next ID: 3 +message AttestationData { + enum Type { + UNKNOWN = 0; + + // A Chrome OS "soft-bind" certificate chain. + // The |certificates| field holds a PEM encoded X.509 certificate chain + // ordered from leaf to root. + CROS_SOFT_BIND_CERT_CHAIN = 1; + } + + Type type = 1; + + // The certificate data as specified by |type|. + repeated bytes certificates = 2; +} + +// Device metadata relevant to the suite of multi-device (Better Together) +// features. This data is sent to and received from CryptAuth--using end-to-end +// encryption--as part of DeviceSync v2. +// Next ID: 5 +message BetterTogetherDeviceMetadata { + // A cryptographic public key associated with the device. + // The format of this key is a serialized SecureMessage.GenericPublicKey. + bytes public_key = 1; + + // A device model name that's guaranteed not to contain any PII. + string no_pii_device_name = 2; + + // A list of seeds for EID BLE advertisements targeting this device. + repeated BeaconSeed beacon_seeds = 3; + + // Bluetooth public address, formatted as a hex string with colons and capital + // letters. Example: "01:23:45:67:89:AB" + string bluetooth_public_address = 4; + + // Attestation data associated with this device. + AttestationData attestation_data = 5; +} \ No newline at end of file diff --git a/play-services-core-proto/src/main/proto/cryptauth/cryptauth_better_together_feature_metadata.proto b/play-services-core-proto/src/main/proto/cryptauth/cryptauth_better_together_feature_metadata.proto new file mode 100644 index 0000000000..35eb7fe297 --- /dev/null +++ b/play-services-core-proto/src/main/proto/cryptauth/cryptauth_better_together_feature_metadata.proto @@ -0,0 +1,37 @@ +/* SPDX-FileCopyrightText: 2014 The Chromium Authors + * SPDX-License-Identifier: BSD-3-Clause + */ + +// This message fills the |metadata| bytes field of the FeatureMetadata message +// (found in the file cryptauth_client_app_metadata.proto) when |feature_type| +// is FeatureMetadata::Feature::BETTER_TOGETHER. +syntax = "proto3"; + +package cryptauthv2; + +option optimize_for = LITE_RUNTIME; + +message BetterTogetherFeatureMetadata { + enum FeatureName { + UNKNOWN_FEATURE = 0; + BETTER_TOGETHER_HOST = 1; + BETTER_TOGETHER_CLIENT = 2; + EASY_UNLOCK_HOST = 3; + EASY_UNLOCK_CLIENT = 4; + MAGIC_TETHER_HOST = 5; + MAGIC_TETHER_CLIENT = 6; + SMS_CONNECT_HOST = 7; + SMS_CONNECT_CLIENT = 8; + PHONE_HUB_HOST = 9; + PHONE_HUB_CLIENT = 10; + WIFI_SYNC_HOST = 11; + WIFI_SYNC_CLIENT = 12; + ECHE_HOST = 13; + ECHE_CLIENT = 14; + PHONE_HUB_CAMERA_ROLL_HOST = 15; + PHONE_HUB_CAMERA_ROLL_CLIENT = 16; + } + + repeated FeatureName supported_features = 1; + repeated FeatureName enabled_features = 2; +} diff --git a/play-services-core-proto/src/main/proto/cryptauth/cryptauth_client_app_metadata.proto b/play-services-core-proto/src/main/proto/cryptauth/cryptauth_client_app_metadata.proto new file mode 100644 index 0000000000..1b44a7d3f2 --- /dev/null +++ b/play-services-core-proto/src/main/proto/cryptauth/cryptauth_client_app_metadata.proto @@ -0,0 +1,139 @@ +/* SPDX-FileCopyrightText: 2014 The Chromium Authors + * SPDX-License-Identifier: BSD-3-Clause + */ + +// Client-specific metadata used in the CryptAuth v2 Enrollment protocol, which +// is serialized and held in |client_app_metadata| of SyncKeysRequest (in file +// cryptauth_enrollment.proto). +syntax = "proto3"; + +package cryptauthv2; + +option optimize_for = LITE_RUNTIME; + +// Client specific metadata contained in SyncKeysRequest.client_app_metadata. +// Next id: 31 +message ClientAppMetadata { + // App specific metadata from the device. On Android, these should be common + // for all the features as they come from GmsCore, however, on IOS and other + // devices, there could be multiple apps with the feature. + repeated ApplicationSpecificMetadata application_specific_metadata = 1; + + // Subgrouping of device identifiers. + // Instance ID: See more info at go/gcm-in-gmscore and + // https://g3doc.corp.google.com/java/com/google/wireless/android/iid/g3doc/index.md?cl=head + string instance_id = 2; + // Token to authenticate the instance ID. + string instance_id_token = 3; + // Checkin android id of the device. + fixed64 android_device_id = 4; + // Chrome and iOS use a UUID in the form xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + // Where x is a lowercase hex digit. + // For iOS, this UUID is the IDFV. + // For backward compatibility with some Chrome long_device_ids, this can also + // be something else. + // For the same reason, this field is case sensitive, even with valid UUIDs. + string long_device_id = 5; + + // Subgrouping of device features field. These help in targeting specific + // class of devices, for ex: Tablets vs phones etc. + // Locale of the device. + string locale = 6; + // The Operating System version. + string device_os_version = 7; + // The Operating System version number on the device. + int64 device_os_version_code = 8; + // The Operating system release on the device. + string device_os_release = 9; + // The Operating system codename on the device. + string device_os_codename = 10; + // Size of the display in thousandths of an inch (e.g. 7000 mils = 7 in) + int32 device_display_diagonal_mils = 11; + // Device's model name (e.g., an android.os.Build.MODEL) + string device_model = 12; + // The device manufacturer name. + string device_manufacturer = 13; + // The type of device this is. + enum DeviceType { + UNKNOWN = 0; + ANDROID = 1; + CHROME = 2; + IOS = 3; + BROWSER = 4; + OSX = 5; + } + DeviceType device_type = 14; + + // Subgrouping of lock screen related fields. Used by many identity features. + // Is this device using a secure screenlock (e.g., a pattern or pin unlock). + bool using_secure_screenlock = 15; + // Is auto-unlocking the screenlock supported ? + bool auto_unlock_screenlock_supported = 16; + // Is auto-unlocking the screenlock (e.g., when at "home") enabled ? + bool auto_unlock_screenlock_enabled = 17; + + // Subgrouping of bluetooth state related fields on the device. Used by many + // features. + // Does the device have a Bluetooth (classic) radio? + bool bluetooth_radio_supported = 18; + // Is the Bluetooth (classic) radio on? + bool bluetooth_radio_enabled = 19; + // Does the device have a ble radio? + bool ble_radio_supported = 20; + + // Does the device hardware support a mobile data connection? + bool mobile_data_supported = 21; + // Does the device support tethering ? + bool tethering_supported = 22; + // If a feature wants to upload some metadata common to all its keys. + repeated FeatureMetadata feature_metadata = 23; + + // Bluetooth address for EasyUnlock. + string bluetooth_address = 24; + + // Is the device a "Pixel Experience" Android device? + bool pixel_experience = 25; + // Is the device running in the ARC++ container on a chromebook? + bool arc_plus_plus = 26; + // Does the device support user presence that is backed by hardware + // (unspoofable by malware)? + bool hardware_user_presence_supported = 27; + // Does the device support user verification (E.g., passcode, biometrics)? + bool user_verification_supported = 28; + // Does the device support creating a key in trusted execution environment? + bool trusted_execution_environment_supported = 29; + // Does the device support creating a key in a dedicated secure element + // hardware? + bool dedicated_secure_element_supported = 30; + + // The response blob generated by the DroidGuard client on the device. + string droid_guard_response = 31; +} + +// Metadata that's app specific. +// Next id: 6 +message ApplicationSpecificMetadata { + // Used for device_address of DeviceInfo field 2, but for GCM capable devices. + bytes gcm_registration_id = 1; + // Does the user have notifications enabled for the given device address. + bool notification_enabled = 2; + // The software version running on the device. + string device_software_version = 3; + // The software version number running on the device. + int64 device_software_version_code = 4; + // Software package information if applicable. + string device_software_package = 5; + // Whether the user has Bluetooth enabled for the given device address. + bool bluetooth_enabled = 6; +} + +// Metadata which is same for different keys belonging to a particular feature. +message FeatureMetadata { + enum Feature { + UNKNOWN = 0; + AUTHZEN = 1; + BETTER_TOGETHER = 2; + } + Feature feature_type = 1; + bytes metadata = 2; +} diff --git a/play-services-core-proto/src/main/proto/cryptauth/cryptauth_common.proto b/play-services-core-proto/src/main/proto/cryptauth/cryptauth_common.proto new file mode 100644 index 0000000000..fe9f8a5f88 --- /dev/null +++ b/play-services-core-proto/src/main/proto/cryptauth/cryptauth_common.proto @@ -0,0 +1,164 @@ +/* SPDX-FileCopyrightText: 2014 The Chromium Authors + * SPDX-License-Identifier: BSD-3-Clause + */ + +// Contains messages and data types used by request, response, and directive +// messages in the CryptAuth v2 Enrollment protocol. +syntax = "proto3"; + +package cryptauthv2; + +option optimize_for = LITE_RUNTIME; + +// The types of cryptographic keys that are supported. +enum KeyType { + // Default value. Don't use! + KEY_TYPE_UNSPECIFIED = 0; + + // 16-byte random byte string + RAW128 = 1; + // 32-byte random byte string + RAW256 = 2; + // Curve25519 + CURVE25519 = 3; + // P256 + P256 = 4; + + // The key will be provided by the application. + CUSTOM = 127; +} + +// The generic format for public-key certificates. +message Certificate { + // The identifier bound to the cert, e.g., an email address or phone number. + string common_name = 1; + // The raw bytes of the public key. + bytes public_key = 2; + // The UNIX timestamp when the cert will expire. + int64 expire_time_millis = 3; + + // A restriction imposed on the applications using this key. + // Claims are validated along with the signature, when this key is used. + message Claim { + // Claim name. + string name = 1; + // Whether this claim is critical in the certificate. If it is critical, + // the client must fail the validation of the certificate if the client does + // not recognize the name of the claim. + bool critical = 2; + // Claim value. + bytes value = 3; + } + // All claims associated with the use of this key. + repeated Claim claims = 4; + + // The signature over all of the above. + bytes signature = 5; +} + +// Uniquely identifies a server-side policy instance, which is associated with a +// key or a client. Subset of this policy is communicated to the client and +// referenced using this message. +// A set of related policies are identified by a name. Every time the policy +// changes, it gets a new unique version number to distinguish it from the +// policy instance it is based on. Together, following fields uniquely identify +// a policy instance. +message PolicyReference { + // The name of the policy. + string name = 1; + + // The version of the policy. + int64 version = 2; +} + +// The client-specific metadata contained in SyncKeysRequest. +// +// Note: This message is encoded as query parameters for some requests. If any +// field or subfield of this proto changes, update the files +// cryptauth_proto_to_query_parameters_util.{h,cc}. +message ClientMetadata { + // The counter for how many times the request has been retried. + int64 retry_count = 1; + + // The reason why the request has been invoked. + enum InvocationReason { + // Unspecified invocation reason. + INVOCATION_REASON_UNSPECIFIED = 0; + + // First run of the software package invoking this call. + INITIALIZATION = 1; + // Ordinary periodic actions (e.g., monthly key rotation). + PERIODIC = 2; + // Slow-cycle periodic action (e.g., yearly keypair rotation). + SLOW_PERIODIC = 3; + // Fast-cycle periodic action (e.g., daily sync for Smart Lock users). + FAST_PERIODIC = 4; + + // Expired state (e.g., expired credentials, or cached entries) was + // detected. + EXPIRATION = 5; + // An unexpected protocol failure occurred (so attempting to repair state). + FAILURE_RECOVERY = 6; + + // A new account has been added to the device. + NEW_ACCOUNT = 7; + // An existing account on the device has been changed. + CHANGED_ACCOUNT = 8; + + // The user toggled the state of a feature (e.g., Smart Lock enabled via + // bluetooth). + FEATURE_TOGGLED = 9; + // A "push" from the server caused this action (e.g., a sync tickle). + SERVER_INITIATED = 10; + + // A local address change triggered this (e.g., GCM registration id + // changed). + ADDRESS_CHANGE = 11; + // A software update has triggered this. + SOFTWARE_UPDATE = 12; + + // A manual action by the user triggered this (e.g., commands sent via adb). + MANUAL = 13; + + // A custom key has been invalidated on the device (e.g. screen lock is + // disabled). + CUSTOM_KEY_INVALIDATION = 14; + + // Periodic action triggered by auth_proximity + PROXIMITY_PERIODIC = 15; + } + // Reason for invocation. + InvocationReason invocation_reason = 2; + + // Whether the platform has hardware supports for certain algorithms. + message CryptoHardware { + // AES-128 + bool aes128 = 1; + // ASE-256 + bool aes256 = 2; + // Carryless multiplication + bool clmul = 3; + // Curve25519 + bool curve25519 = 4; + // P256 + bool p256 = 5; + } + // Crypto hardware available on the client. + CryptoHardware crypto_hardware = 3; + + // If the request is issued as a direct result, or a follow-up for a + // notification/tickle, the session_id from that notification. + string session_id = 4; +} + +// Identifies Cryptauth services. +enum TargetService { + // Unspecified Cryptauth service. + TARGET_SERVICE_UNSPECIFIED = 0; + + // Cryptauth Enrollment. + ENROLLMENT = 1; + + // Cryptauth DeviceSync. + DEVICE_SYNC = 2; +} diff --git a/play-services-core-proto/src/main/proto/cryptauth/cryptauth_devicesync.proto b/play-services-core-proto/src/main/proto/cryptauth/cryptauth_devicesync.proto new file mode 100644 index 0000000000..b38ffd6b52 --- /dev/null +++ b/play-services-core-proto/src/main/proto/cryptauth/cryptauth_devicesync.proto @@ -0,0 +1,318 @@ +/* SPDX-FileCopyrightText: 2014 The Chromium Authors + * SPDX-License-Identifier: BSD-3-Clause + */ + +// Contains the request and response messages used in the CryptAuth v2 +// DeviceSync protocol. Specifically, +// +// -- SyncMetadata -- +// * Request: The device sends its metadata to CryptAuth, some of it encrypted +// with the the device's version of the group public key. This version of +// the group public key is also sent in the request. +// * Response: If CryptAuth can confirm that the device is in possession of the +// correct group public key, then CryptAuth returns metadata for the user's +// devices in the DeviceSync group. The group public key and/or the +// encrypted group private key might also be returned. +// +// -- ShareGroupPrivateKey -- +// * Request: The device shares the group private key by encrypting it with the +// public key of the user's other devices. These encrypted group private +// keys are persisted by CryptAuth for the other devices to retrieve as +// necessary. +// * Response: Trivial +// +// -- BatchNotifyGroupDevices -- +// * Request: The client sends a list of the devices that it wants to tickle via +// a GCM message. +// * Response: Trivial +// +// -- BatchGetFeatureStatuses -- +// * Request: The client queries CryptAuth for the state of features on the +// user's devices, for example, whether or not Magic Tether is enabled on +// any of the user's phones. +// * Response: The query results. +// +// -- BatchSetFeatureStatuses -- +// * Request: The client requests CryptAuth to set the state of various features +// for the user's devices. Optionally, features can be enabled exclusively, +// meaning enabled on one device and disabled on all others. +// * Response: Trivial +// +// -- GetDevicesActivityStatus -- +// * Request: Trivial +// * Response: The state of activity for the user's devices. This includes if +// the device is currently online and the time the device was last active. +syntax = "proto3"; + +package cryptauthv2; + +option optimize_for = LITE_RUNTIME; + +import "cryptauth/cryptauth_common.proto"; +import "cryptauth/cryptauth_directive.proto"; + +// A common context for requests. +// +// Note: This message is encoded as query parameters for some requests. If any +// field or subfield of this proto changes, update the files +// cryptauth_proto_to_query_parameters_util.{h,cc}. +message RequestContext { + // The group name. + // A device can join multiple groups. Each group will be identified by a + // unique name. + // The device should have previously enrolled a public key with this name with + // the server, using the Enrollment API. + // The key material associated with the key of this name is used by other + // devices in the group to communicate securely with this device. + string group = 1; + + // Common metadata about this request. + ClientMetadata client_metadata = 2; + + // Device identifier. + string device_id = 3; + + // Used to authenticate device_id. + string device_id_token = 4; +} + +// Requests from a client to sync its metadata and receive encrypted copy of +// other metadata updated since its last sync. +message SyncMetadataRequest { + // The context of this request. + RequestContext context = 1; + + // Public key material intended for group use. Will either be created by the + // device upon first joining, or will be existing group public key in its + // possession. + // May be discarded if device is joining an existing group that already has a + // group key pair. + bytes group_public_key = 3; + + // Device's metadata encrypted with the given group public key. + bytes encrypted_metadata = 4; + + // Set to true if the device needs the group's private key. + bool need_group_private_key = 5; + + // Used to obtain only updated metadata, since the last request. + // If not set, all metadata will be sent. + bytes freshness_token = 6; +} + +// One device's metadata, containing an identifier for the particular device, +// along with its encrypted metadata. +message DeviceMetadataPacket { + // Device identifier. + string device_id = 1; + + // This device's metadata, encrypted with the group's private key. + bytes encrypted_metadata = 2; + + // Indicates that the associated device needs the group's private key. + bool need_group_private_key = 3; + + // The associated device's public key, to be used with + // EncryptedGroupPrivateKey below. + bytes device_public_key = 4; + + // A name known to the server or which was assigned by the user to the device. + string device_name = 5; +} + +// Response from server with any new devices' encrypted metadata and public key. +message SyncMetadataResponse { + // Collection of encrypted metadata from devices that have been updated since + // last provided refresh_token. All metadata if none was provided. + repeated DeviceMetadataPacket encrypted_metadata = 1; + + // Public key associated with the group, used to encrypt all metadata. + // May be different than key received in the request. + // If not set, the server is indicating a new group key pair must be created + // by this device. + bytes group_public_key = 2; + + // An encrypted group private key that contains device public key used for + // encryption. Encrypted with the public key of the device. + EncryptedGroupPrivateKey encrypted_group_private_key = 3; + + // Updated freshness token from the server. + // Use this value in subsequent requests, to obtain only data updated since + // the last request. + bytes freshness_token = 4; + + // The updated client-specific directives. + ClientDirective client_directive = 5; +} + +// Encrypted group private key, including identifiers for both sender and +// recipient devices. +message EncryptedGroupPrivateKey { + // Identifier for device receiving group private key. + string recipient_device_id = 1; + + // Identifier for device sending group private key. + string sender_device_id = 2; + + // Group private key, encrypted with the public key of the recipient device. + bytes encrypted_private_key = 3; + + // Hash of the group public key used to encrypt this device’s metadata. + int64 group_public_key_hash = 7; +} + +// Share the group's private key with another device. +message ShareGroupPrivateKeyRequest { + // The context of this request. + RequestContext context = 1; + + // A collection of encrypted group private keys, each identified with the + // device public key of both the sender and the recipient. + repeated EncryptedGroupPrivateKey encrypted_group_private_keys = 2; +} + +// The server's response to sharing the group's private key. +message ShareGroupPrivateKeyResponse {} + +// Allows a device, which is part of the group, notify another group device. +// This allows setup to work, e.g. by letting the other device know it needs to +// turn on the bluetooth radio. +// +// Note: This request is encoded as query parameters in a GET request. If any +// field or subfield of this proto changes, update the files +// cryptauth_proto_to_query_parameters_util.{h,cc}. +message BatchNotifyGroupDevicesRequest { + // The context of this request. + RequestContext context = 1; + + // Group devices to notify. + repeated string notify_device_ids = 2; + + // Target service & feature type to specify in the notification. + TargetService target_service = 3; + // Feature type. + string feature_type = 4; +} + +// Response to BatchNotifyGroupDevices. +message BatchNotifyGroupDevicesResponse {} + +// Requests feature enabled/disabled statuses per device in the group. +// +// Note: This request is encoded as query parameters in a GET request. If any +// field or subfield of this proto changes, update the files +// cryptauth_proto_to_query_parameters_util.{h,cc}. +message BatchGetFeatureStatusesRequest { + // The context of this request. + RequestContext context = 1; + + // Which devices to query. + // Leave unset if all group devices should be queried. + repeated string device_ids = 2; + + // Which feature types to query. + repeated string feature_types = 3; +} + +// Enabled/disabled status of a single device. +message DeviceFeatureStatus { + // Enabled/disabled status of a named feature. + message FeatureStatus { + // Feature type name. + string feature_type = 1; + + // If the feature is enabled. + bool enabled = 2; + + // The last time the feature was set. Only set for BatchGetFeatureStatuses. + int64 last_modified_time_millis = 3; + + // Set to true to disable all group devices other than the specified device. + // This can only be used when enabling features. Only set for + // BatchSetFeatureStatuses. + bool enable_exclusively = 4; + } + + // Device identifier. + string device_id = 1; + + // The status of features associated with this device. + repeated FeatureStatus feature_statuses = 2; +} + +// Response to BatchGetFeatureStatuses. +message BatchGetFeatureStatusesResponse { + // The status of all queried group devices. + // Only the requested devices and requested features will be populated. + repeated DeviceFeatureStatus device_feature_statuses = 1; +} + +// Sets feature types enable/disable statuses per device in the group. +message BatchSetFeatureStatusesRequest { + // The context of this request. + RequestContext context = 1; + + // The feature statuses to set for specific devices. + repeated DeviceFeatureStatus device_feature_statuses = 2; + + // Set to true to disable all group devices other than the specified device. + // This can only be used with a single device and only when enabling features. + bool enable_exclusively = 3 [deprecated = true]; +} + +// Response to BatchSetFeatureStatuses. +message BatchSetFeatureStatusesResponse {} + +// Request for GetDevicesActivityStatus. +message GetDevicesActivityStatusRequest { + // The context of this request. + RequestContext context = 1; +} + +// Describes the device's network reachability. +enum ConnectivityStatus { + // Default value. + UNKNOWN_CONNECTIVITY = 0; + // The device appears to be unreachable. + OFFLINE = 1; + // The device appears to be online and reachable. + ONLINE = 2; +} + +// Copied from +// https://cs.chromium.org/chromium/src/third_party/protobuf/src/google/protobuf/timestamp.proto?rcl=b51864c7aae4372308052b9fd5c1913ceeee3884 +message Timestamp { + // Represents seconds of UTC time since Unix epoch + // 1970-01-01T00:00:00Z. Must be from 0001-01-01T00:00:00Z to + // 9999-12-31T23:59:59Z inclusive. + int64 seconds = 1; + + // Non-negative fractions of a second at nanosecond resolution. Negative + // second values with fractions must still have non-negative nanos values + // that count forward in time. Must be from 0 to 999,999,999 + // inclusive. + int32 nanos = 2; +} + +// Activity status of a single device. +message DeviceActivityStatus { + // Device identifier. + string device_id = 1; + + // The last time this device was active as retrieved from Bond. Freshness + // is on the order of minutes. + int64 last_activity_time_sec = 2; + + // Online status of the device as inferred by reachability via FCM. + ConnectivityStatus connectivity_status = 3; + + // When the device last enrolled its DeviceSync key or when its metadata was + // last updated, the most recent of these two timestamps. + Timestamp last_update_time = 4; +} + +// Response for GetDevicesActivityStatus. +message GetDevicesActivityStatusResponse { + // The status of all group devices. + repeated DeviceActivityStatus device_activity_statuses = 1; +} diff --git a/play-services-core-proto/src/main/proto/cryptauth/cryptauth_directive.proto b/play-services-core-proto/src/main/proto/cryptauth/cryptauth_directive.proto new file mode 100644 index 0000000000..466b425ceb --- /dev/null +++ b/play-services-core-proto/src/main/proto/cryptauth/cryptauth_directive.proto @@ -0,0 +1,113 @@ +/* SPDX-FileCopyrightText: 2014 The Chromium Authors + * SPDX-License-Identifier: BSD-3-Clause + */ + +// Contains messages sent from CryptAuth to clients who registered keys using +// the v2 Enrollment protocol. The messages provide instructions to clients such +// as how frequently to check in with CryptAuth via a SyncKeysRequest, how long +// to wait between failed enrollment attempts, and what other keys are required +// to cross-sign for a particular key. +syntax = "proto3"; + +package cryptauthv2; + +option optimize_for = LITE_RUNTIME; + +import "cryptauth/cryptauth_common.proto"; + +// The policy to be handed down to the party which registered the public key +// with the server. It is produced on the fly from current ServerSidePolicy +// and PolicyConfig (PC). +message KeyDirective { + // The specific policy which was used to generate this message. + PolicyReference policy_reference = 1; + + // When rotating the current key, 'crossproof_key_name' keys should be used + // to cross sign. This is retrieved from PolicyConfig.crossproof_key_name . + repeated string crossproof_key_names = 2; + + // The time when the key was enrolled/rotated (as observed by the server). + // This should be the same as ServerSidePolicy.enroll_time_millis . + int64 enroll_time_millis = 3; +} + +// This contains the directives handed down to the party which registered the +// public key with the server. These directives are aggregated from across all +// the policies of the keys that have been registered by this first party. +message ClientDirective { + // The specific policy which was used to generate this message. + PolicyReference policy_reference = 1; + + // The first party should check in with the server after this period. + // The server may require the client (first party) to rotate the key + // (based on PolicyConfig.rotate_delay_millis from across all the policies + // of the registered keys). + // For each policy of a registered key a value is randomly drawn from + // [PC.checkin_delay_millis - PC.checkin_delay_millis_interval, + // PC.checkin_delay_millis + PC.checkin_delay_millis_interval]. + // The minimum value from across all these values is used. + // + // Whenever such a time period is coming due, the client should check in + // all its keys with the server. The server tells the client which of those + // keys need to be rotated and the rotation process proceeds for all these + // keys (bundled together). + int64 checkin_delay_millis = 2; + + // In case any call to CryptAuth v2 failed, the first party should retry + // at most these many times right away, without the need to wait at all. + // Passed in from PC.retry_attempts. + // For example, a value of 1 means one original request, and if failed, a + // single retry should follow. + int32 retry_attempts = 3; + + // In case any call to CryptAuth v2 failed retry_attempts + 1 times, the first + // party should retry the call again after this time period. If this latter + // retry fails, the first party should wait this time period again then retry + // and repeat until the request succeeds. + // For each policy of a registered key a value is randomly drawn from + // [PC.retry_period_millis - PC.retry_period_millis_interval, + // PC.retry_period_millis + PC.retry_period_millis_interval]. + // The maximum value from across all these values is used. + int64 retry_period_millis = 4; + + // The timestamp when this policy was minted. + // This can help the client sync with the server's time. + // checkin_delay_millis and retry_period_millis are relative to this time. + // Without this timestamp, the client should act right away with regard to + // the *_millis fields (ie, schedule something at NOW + *_millis). + // With this timestamp (considering the times of both server and client are + // in sync), the client would have all the required information for + // a later action. + int64 create_time_millis = 5; + + // Which other services should be invoked after this interaction is complete. + repeated InvokeNext invoke_next = 6; +} + +// Instructing the client to invoke a specific service. +message InvokeNext { + // Target service to be involved next. + TargetService service = 1; + + // Key name to be processed for target service. + string key_name = 2; +} + +// The policy to be handed down to a third party along with the corresponding +// public key of the device it asked for. It is produced on the fly from current +// PolicyConfig and ServerSidePolicy (defined in +// java/com/google/security/cryptauth/v2/backend/common/policy/policy.proto). +message ThirdPartyKeyDirective { + // The specific policy which was used to generate this message. + PolicyReference policy_reference = 1; + + // The third party should not use this key after this timestamp. + // It should sync with CryptAuth for getting a fresh one after this timestamp. + // This should be consistent with what the latest first party directive states + // (in its create_time_millis field), combined with + // PolicyConfig.rotate_delay_millis . + int64 expire_time_millis = 2; + + // The timestamp when this policy was distributed to the third party. + int64 distribute_time_millis = 3; +} diff --git a/play-services-core-proto/src/main/proto/cryptauth/cryptauth_enrollment.proto b/play-services-core-proto/src/main/proto/cryptauth/cryptauth_enrollment.proto new file mode 100644 index 0000000000..ddf19e9e2b --- /dev/null +++ b/play-services-core-proto/src/main/proto/cryptauth/cryptauth_enrollment.proto @@ -0,0 +1,298 @@ +/* SPDX-FileCopyrightText: 2014 The Chromium Authors + * SPDX-License-Identifier: BSD-3-Clause + */ + +// Contains the request and response messages used in the CryptAuth v2 +// Enrollment protocol. Specifically, +// (1) SyncKeysRequest: The client submits information about their current set +// of keys to the CryptAuth server. The client can update their +// client-specific or key-specific metadata at this time as well. +// (2) SyncKeysResponse: The CryptAuth server responds with instructions such +// as what existing keys to (de)active, what new keys to create, and the +// time of the next check-in. +// (3) EnrollKeysRequest: If new keys were requested in the SyncKeysResponse, +// the client sends the new key information to CryptAuth in this request. +// (4) EnrollKeysResponse: If a certificate was generated, it will be provided +// here; otherwise, this can simply be a signal of a successful +// enrollment. +syntax = "proto3"; + +package cryptauthv2; + +option optimize_for = LITE_RUNTIME; + +import "cryptauth/cryptauth_common.proto"; +import "cryptauth/cryptauth_directive.proto"; + +// The first request in the enrollment protocol. The request contains the +// information including the keys currently held by the client, the latest +// policies received from the server, and the metadata associated with the +// client and keys. +message SyncKeysRequest { + // The unique name of the application. + string application_name = 1; + // The version of the CryptAuth client library. + string client_version = 2; + + // The request to enroll a key or update the info related to a key. + message SyncSingleKeyRequest { + // The purpose/application of the key. + string key_name = 1; + // Identifiers of keys currently held by the client. + repeated bytes key_handles = 2; + + // The policy_reference received in the last KeyDirective. + PolicyReference policy_reference = 3; + // Key-specific metadata. + KeyMetadata key_metadata = 4; + // A key-specific opaque blob provided by the application. + bytes key_app_metadata = 5; + } + // Per key sync data. + repeated SyncSingleKeyRequest sync_single_key_requests = 3; + + // The policy_reference received in the last ClientDirective. + PolicyReference policy_reference = 4; + // Client-specific metadata. + ClientMetadata client_metadata = 5; + // A client-specific opaque blob provided by the application. + // In the GmsCore case, this is a protobuf of type `ClientAppMetadata`. + bytes client_app_metadata = 6; +} + +// The response to SyncKeysRequest. The response instructs how the client should +// manage existing keys and whether to create a new key. +message SyncKeysResponse { + // The session indentifer generated by the server, which must be + // cryptographically random. + bytes random_session_id = 1; + // The ephemeral DH public key generated by the server. + bytes server_ephemeral_dh = 2; + + // The response corresponding to the SyncSingleKeyRequest message. + message SyncSingleKeyResponse { + // The actions corresponding to the key handles in SyncKeysRequest. + enum KeyAction { + // Default value. A client receiving this should treat it as a noop. + // (-- But, be wary of b/119886258. --) + KEY_ACTION_UNSPECIFIED = 0; + + // Keep the key and make it the "active" key. + ACTIVATE = 1; + // Keep the key. When enrollment is complete, ensure the key is not + // "active". + // (-- But, be wary of b/119886258 and a noop on iOS. --) + DEACTIVATE = 2; + // Delete the key. + DELETE = 3; + } + // Key actions with one entry per key handle and in the same order as in the + // request. + repeated KeyAction key_actions = 1; + + // The instruction for the client to create a new key. + enum KeyCreation { + // Do not create a new key. + NONE = 0; + // Create a new key, and then use it as the "active" key. + ACTIVE = 1; + // Create a new key, but do not use it as the "active" key. + // (-- Beware of b/119889101. This doesn't work on Android or iOS. --) + INACTIVE = 2; + } + // Instruction for key creation. + KeyCreation key_creation = 2; + + // The type of the cryptographic key. + KeyType key_type = 3; + // The updated key-specific directives. + KeyDirective key_directive = 4; + // A key-specific opaque blob given to the application. + bytes key_app_directive = 5; + + // The storage level where the key is created and stored. + enum KeyStorageLevel { + // Default value. The client is free to decide where to create the key. + KEY_STORAGE_LEVEL_UNSPECIFIED = 0; + + // The key should be created and stored in software store. E.g. the + // client may create a key using a crypto library and store it in a + // file. + SOFTWARE = 1; + + // The key should be created in a Trusted Execution Environment (TEE). + // E.g., TrustZone from ARM chips. + TRUSTED_EXECUTION_ENVIRONMENT = 2; + + // The key should be created in a dedicated hardware that is separate from + // the main processor. E.g., StrongBox chips in Android devices and Secure + // Enclave in iOS devices. + DEDICATED_SECURE_ELEMENT = 3; + }; + // The storage level to create the key. + KeyStorageLevel key_storage_level = 6; + // The newly created key should require hardware backed user presence when + // using the key. + bool hardware_user_presence_required = 7; + // The newly created key should require user verification when using the + // key. + bool user_verification_required = 8; + } + // Per key sync response. + repeated SyncSingleKeyResponse sync_single_key_responses = 3; + + // The updated client-specific directives. + ClientDirective client_directive = 4; + // A client-specific opaque blob given to the application. + bytes client_app_directive = 5; + + // The state of the server. + enum ServerStatus { + // The server is fine; the rest of SyncKeysResponse should be processed. + SERVER_OK = 0; + // The server is overloaded; client_directive should be followed. + SERVER_OVERLOADED = 1; + } + // The status of the server. + ServerStatus server_status = 6; +} + +// The second request in the enrollment protocol. The second request is +// necessary if the client wants to enroll a new key. The request contains the +// information such as the material of the new key, and necessary proofs for +// verifying the key. +message EnrollKeysRequest { + // The session identifier copied from the SyncKeysResponse message. + bytes random_session_id = 1; + // The ephemeral DH public key generated by the client. + bytes client_ephemeral_dh = 2; + + // The request to enroll a key, e.g., create a new key or rotate an old one. + message EnrollSingleKeyRequest { + // The key_name copied from SyncKeysRequest. + string key_name = 1; + // The identifier of the new key. + bytes new_key_handle = 2; + // The raw bytes of the new public key or custom data. + bytes key_material = 3; + // The public-key signature or MAC tag that shows the client indeed + // possesses the private or secret key. + bytes key_proof = 4; + + // Cross-signatures or MAC tags by other keys. + message KeyCrossproof { + // The key_name of the cross-signing key. + string other_key_name = 1; + // The computed cross-signatures or MAC tags. + bytes other_key_proof = 2; + } + // Cross proofs. + repeated KeyCrossproof key_crossproofs = 5; + + // Subject to certify. + repeated CertificateRequest certificate_requests = 6; + + // Attestation of the key. + message KeyAttestation { + // The type of the key attestation. + enum KeyAttestationType { + // Default value. + KEY_ATTESTATION_TYPE_UNSPECIFIED = 0; + + // Attestation generated by Android KeyStore API. + // See + // https://developer.android.com/training/articles/security-key-attestation + // The payload should be the concatenation of the X.509 + // certificates returned by KeyStore attestation API encoded in ASN.1 + // DER. + ANDROID_KEYSTORE_ATTESTATION = 1; + } + // The attestation type. + KeyAttestationType type = 1; + + // The payload of the key attestation. The content of the payload is + // dependent on the attestation type. + bytes payload = 2; + } + // The attestation of the key if the key supports one. + KeyAttestation key_attestation = 7; + } + // Per key enroll data. + repeated EnrollSingleKeyRequest enroll_single_key_requests = 3; +} + +// The response to EnrollKeysRequest. The response can contain a public-key +// certificate for the client to perform offline authentications. +message EnrollKeysResponse { + // The response corresponding to the EnrollSingleKeyRequest message. + message EnrollSingleKeyResponse { + // The server may produce a certificate and send it to the client. + repeated Certificate certificate = 1; + } + // Per key enroll response. + repeated EnrollSingleKeyResponse enroll_single_key_responses = 1; +} + +// Subject to certify. +message CertificateRequest { + // The type of subject to certify. + enum CommonNameType { + // Reserved. + UNKNOWN_COMMON_NAME_TYPE = 0; + // Indicates a phone number needs to be signed. + PHONE_NUMBER = 1; + } + // Type of content to be signed. + CommonNameType type = 1; + // Raw data of the content. + bytes data = 2; + // Bytes used to verify the validation of data. + bytes token = 3; + // Additional data used to help verify data. (e.g. audience) + bytes additional_data = 4; +} + +// The key-specific metadata contained in SyncKeysRequest. +message KeyMetadata {} + +// This generic rpc is used by MagicShare, BetterTogether and possibly other +// features in the future to obtain enrollment information from the server. +// This method’s behavior shall be based on the key_name which is supplied. +// The client and server shall set and expect specific data in +// request_key_metadata and response_key_metadata, based on the application_name +// and key_name. +message GetMetadataRequest { + // The unique name of the application + string application_name = 1; + // The version of the CryptAuth client library + string client_version = 2; + + // The request to get key metadata related to a key name. + message GetSingleKeyMetadataRequest { + // The purpose/application of the key. + string key_name = 1; + // key specific metadata + bytes request_key_metadata = 2; + } + // Per key request + repeated GetSingleKeyMetadataRequest get_single_key_metadata_request = 3; + + // InvocationReason, retry count, etc. (same as SyncKeys). + ClientMetadata client_metadata = 4; + + // A client-specific opaque blob provided by the application. + bytes app_metadata = 5; +} + +// The response to GetMetadataRequest. The response contains key metadata based +// on the application name_and key_name in GetMetadataRequest. +message GetMetadataResponse { + // The response of GetKeyMetadataRequest. + message GetSingleKeyMetadataResponse { + // Key specific response metadtata. + bytes response_key_metadata = 1; + } + + // A response for every key_metadata_request above. + repeated GetSingleKeyMetadataResponse get_single_skey_metadata_response = 1; +} diff --git a/play-services-core/src/main/kotlin/org/microg/gms/auth/appcert/AppCertManager.kt b/play-services-core/src/main/kotlin/org/microg/gms/auth/appcert/AppCertManager.kt index bcb85ef285..b4e36dc050 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/auth/appcert/AppCertManager.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/auth/appcert/AppCertManager.kt @@ -85,8 +85,8 @@ class AppCertManager(private val context: Context) { .checkin(lastCheckinInfo) .app("com.google.android.gms", Constants.GMS_PACKAGE_SIGNATURE_SHA1, BuildConfig.VERSION_CODE) .sender(REGISTER_SENDER) - .extraParam("subscription", REGISTER_SUBSCIPTION) - .extraParam("X-subscription", REGISTER_SUBSCIPTION) + .extraParam("subscription", REGISTER_SUBSCRIPTION) + .extraParam("X-subscription", REGISTER_SUBSCRIPTION) .extraParam("subtype", REGISTER_SUBTYPE) .extraParam("X-subtype", REGISTER_SUBTYPE) .extraParam("scope", REGISTER_SCOPE)) @@ -187,7 +187,7 @@ class AppCertManager(private val context: Context) { private const val DEVICE_KEY_TIMEOUT = 60 * 60 * 1000L private const val REGISTER_SENDER = "745476177629" private const val REGISTER_SUBTYPE = "745476177629" - private const val REGISTER_SUBSCIPTION = "745476177629" + private const val REGISTER_SUBSCRIPTION = "745476177629" private const val REGISTER_SCOPE = "DeviceKeyRequest" private val deviceKeyLock = Mutex() private var deviceKey: DeviceKey? = null diff --git a/play-services-core/src/main/kotlin/org/microg/gms/cryptauth/SyncKeysRequest.kt b/play-services-core/src/main/kotlin/org/microg/gms/cryptauth/SyncKeysRequest.kt new file mode 100644 index 0000000000..de871a7699 --- /dev/null +++ b/play-services-core/src/main/kotlin/org/microg/gms/cryptauth/SyncKeysRequest.kt @@ -0,0 +1,164 @@ +package org.microg.gms.cryptauth + +import android.accounts.Account +import android.content.Context +import android.util.Log +import com.google.android.gms.BuildConfig +import com.google.android.gms.common.Scopes +import cryptauthv2.ApplicationSpecificMetadata +import cryptauthv2.ClientAppMetadata +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.withContext +import org.json.JSONArray +import org.json.JSONObject +import org.microg.gms.auth.AuthConstants +import org.microg.gms.auth.AuthManager +import org.microg.gms.checkin.LastCheckinInfo +import org.microg.gms.common.Constants +import org.microg.gms.common.DeviceConfiguration +import org.microg.gms.common.Utils +import org.microg.gms.gcm.GcmConstants +import org.microg.gms.gcm.GcmDatabase +import org.microg.gms.gcm.RegisterRequest +import org.microg.gms.gcm.completeRegisterRequest +import org.microg.gms.profile.Build +import java.io.IOException +import java.net.HttpURLConnection +import java.net.URL +import kotlin.math.roundToInt + +private const val SYNC_KEY_URL = "https://cryptauthenrollment.googleapis.com/v1:syncKeys" +private const val API_KEY = "AIzaSyAP-gfH3qvi6vgHZbSYwQ_XHqV_mXHhzIk" +private const val CERTIFICATE = "58E1C4133F7441EC3D2C270270A14802DA47BA0E" +private const val TAG = "SyncKeysRequest" + +private const val GCM_REGISTER_SENDER = "745476177629" +private const val GCM_REGISTER_SUBTYPE = "745476177629" +private const val GCM_REGISTER_SUBSCIPTION = "745476177629" +private const val GCM_REGISTER_SCOPE = "GCM" + +private const val AFTER_REQUEST_DELAY = 500L + +suspend fun request(context: Context, account: Account): JSONObject? = request(context, account.name) +suspend fun request(context: Context, accountName: String): JSONObject? { + + val lastCheckinInfo = LastCheckinInfo.read(context) + + // Instance ID token for use in CryptAuth query + val instanceToken = completeRegisterRequest(context, GcmDatabase(context), RegisterRequest().build(context) + .checkin(lastCheckinInfo) + .app("com.google.android.gms", Constants.GMS_PACKAGE_SIGNATURE_SHA1, BuildConfig.VERSION_CODE) + .sender(GCM_REGISTER_SENDER) + .extraParam("subscription", GCM_REGISTER_SUBSCIPTION) + .extraParam("X-subscription", GCM_REGISTER_SUBSCIPTION) + .extraParam("subtype", GCM_REGISTER_SUBTYPE) + .extraParam("X-subtype", GCM_REGISTER_SUBTYPE) + .extraParam("scope", GCM_REGISTER_SCOPE) + ).let { + if (!it.containsKey(GcmConstants.EXTRA_REGISTRATION_ID)) { + return null + } + it.getString(GcmConstants.EXTRA_REGISTRATION_ID)!! + } + val instanceId = instanceToken.split(':')[0] + + // CryptAuth sync request tells server whether or not screenlock is enabled + val cryptauthService = AuthConstants.SCOPE_OAUTH2 + Scopes.CRYPTAUTH + val authManager = AuthManager(context, accountName, Constants.GMS_PACKAGE_NAME, cryptauthService) + val authToken = withContext(Dispatchers.IO) { authManager.requestAuth(false).auth } + + val deviceConfig = DeviceConfiguration(context) + val clientAppMetadata = ClientAppMetadata( + application_specific_metadata = listOf( + ApplicationSpecificMetadata( + notification_enabled = true, + device_software_version = "%09d".format(BuildConfig.VERSION_CODE).let { + "${it.substring(0, 2)}.${it.substring(2, 4)}.${it.substring(4, 6)} (190800-{{cl}})" + }, + device_software_version_code = BuildConfig.VERSION_CODE.toLong(), + device_software_package = Constants.GMS_PACKAGE_NAME + ) + ), + instance_id = instanceId, + instance_id_token = instanceToken, + android_device_id = lastCheckinInfo.androidId, + locale = Utils.getLocale(context).toString().replace("_", "-"), + device_os_version = Build.DISPLAY ?: "", + device_os_version_code = Build.VERSION.SDK_INT.toLong(), + device_os_release = Build.VERSION.CODENAME?: "", + device_display_diagonal_mils = (deviceConfig.diagonalInch / 1000).roundToInt(), + device_model = Build.MODEL ?: "", + device_manufacturer = Build.MANUFACTURER ?: "", + device_type = ClientAppMetadata.DeviceType.ANDROID, + using_secure_screenlock = true, // TODO actual value + bluetooth_radio_supported = true, // TODO actual value? doesn't seem relevant + // bluetooth_radio_enabled = false, + ble_radio_supported = true, // TODO: actual value? doesn't seem relevant + mobile_data_supported = true, // TODO: actual value? doesn't seem relevant + // droid_guard_response = "…" + ) + .encodeByteString() + .base64Url() + + val jsonBody = jsonObjectOf( + "applicationName" to Constants.GMS_PACKAGE_NAME, + "clientVersion" to "1.0.0", + "syncSingleKeyRequests" to jsonArrayOf( + jsonObjectOf( + "keyName" to "PublicKey", + "keyHandles" to "ZGV2aWNlX2tleQo=" // base64 for `device_key`< + ) + ), + "clientMetadata" to jsonObjectOf( + "invocationReason" to "NEW_ACCOUNT" + ), + "clientAppMetadata" to clientAppMetadata, + ) + + return withContext(Dispatchers.IO) { + val connection = (URL(SYNC_KEY_URL).openConnection() as HttpURLConnection).apply { + setRequestMethod("POST") + setDoInput(true) + setDoOutput(true) + setRequestProperty("x-goog-api-key", API_KEY) + setRequestProperty("x-android-package", Constants.GMS_PACKAGE_NAME) + setRequestProperty("x-android-cert", CERTIFICATE) + setRequestProperty("Authorization", "Bearer $authToken") + setRequestProperty("Content-Type", "application/json") + } + Log.d(TAG, "-- Request --\n$jsonBody") + + val os = connection.outputStream + os.write(jsonBody.toString().toByteArray()) + os.close() + + if (connection.getResponseCode() != 200) { + var error = connection.getResponseMessage() + try { + error = String(Utils.readStreamToEnd(connection.errorStream)) + } catch (e: IOException) { + // Ignore + } + throw IOException(error) + } + + val result = String(Utils.readStreamToEnd(connection.inputStream)) + Log.d(TAG, "-- Response --\n$result") + try { + JSONObject(result) + .also { + /* Give Google server some time to process the new information. + * This leads to higher success rate compared to sending + * the next query immediately after this one. + */ + delay(AFTER_REQUEST_DELAY) + } + } catch (e: Exception) { + null + } + } +} + +fun jsonObjectOf(vararg pairs: Pair): JSONObject = JSONObject(mapOf(*pairs)) +inline fun jsonArrayOf(vararg elements: T): JSONArray = JSONArray(arrayOf(*elements)) \ No newline at end of file From e103c65ade8ced0a82c00feb11a9ee1fd207c293 Mon Sep 17 00:00:00 2001 From: Fynn Godau Date: Thu, 4 Apr 2024 01:00:45 +0200 Subject: [PATCH 003/132] Call sync keys request before opening account settings For manual testing only. Remove this commit --- .../org/microg/gms/accountsettings/ui/WebViewHelper.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/WebViewHelper.kt b/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/WebViewHelper.kt index a65b7a0de1..1ff76e14cc 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/WebViewHelper.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/WebViewHelper.kt @@ -23,6 +23,7 @@ import kotlinx.coroutines.withContext import org.microg.gms.auth.AuthManager import org.microg.gms.common.Constants.GMS_PACKAGE_NAME import org.microg.gms.common.PackageUtils +import org.microg.gms.cryptauth.request import java.net.URLEncoder import java.util.* @@ -99,8 +100,10 @@ class WebViewHelper(private val activity: AppCompatActivity, private val webView } } - private fun openWebWithAccount(accountName: String, url: String?) { + private suspend fun openWebWithAccount(accountName: String, url: String?) { try { + request(activity, accountName) + val service = "weblogin:continue=" + URLEncoder.encode(url, "utf-8") val authManager = AuthManager(activity, accountName, GMS_PACKAGE_NAME, service) val authUrl = authManager.requestAuth(false)?.auth From c5554623babb580d235e6782f893a2c671d5f2dc Mon Sep 17 00:00:00 2001 From: Fynn Godau Date: Sat, 6 Apr 2024 13:23:48 +0200 Subject: [PATCH 004/132] Add skeleton for error resolution functions --- .../microg/gms/accountaction/ErrorResolver.kt | 47 +++++++++++++++++++ .../microg/gms/accountaction/Resolution.kt | 31 ++++++++++++ .../gms/accountsettings/ui/WebViewHelper.kt | 4 +- .../microg/gms/cryptauth/SyncKeysRequest.kt | 5 +- 4 files changed, 83 insertions(+), 4 deletions(-) create mode 100644 play-services-core/src/main/kotlin/org/microg/gms/accountaction/ErrorResolver.kt create mode 100644 play-services-core/src/main/kotlin/org/microg/gms/accountaction/Resolution.kt diff --git a/play-services-core/src/main/kotlin/org/microg/gms/accountaction/ErrorResolver.kt b/play-services-core/src/main/kotlin/org/microg/gms/accountaction/ErrorResolver.kt new file mode 100644 index 0000000000..3cfa4da0c0 --- /dev/null +++ b/play-services-core/src/main/kotlin/org/microg/gms/accountaction/ErrorResolver.kt @@ -0,0 +1,47 @@ +package org.microg.gms.accountaction + +import android.accounts.Account +import android.content.Context +import androidx.appcompat.app.AppCompatActivity +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.microg.gms.cryptauth.syncCryptAuthKeys + +/** + * @return `null` if it is unknown how to resolve the problem, an + * appropriate `Resolution` otherwise + */ +fun fromErrorMessage(s: String): Resolution? = if (s.startsWith("Error=")) { + fromErrorMessage(s.drop("Error=".length)) +} else when (s) { + "DeviceManagementScreenlockRequired" -> { + if (true) { + CryptAuthSyncKeys + } else { + UserIntervention( + setOf( + UserAction.ENABLE_CHECKIN, UserAction.ENABLE_GCM, UserAction.ENABLE_LOCKSCREEN + ) + ) + } + } + + "DeviceManagementRequired" -> NoResolution + "DeviceManagementAdminPendingApproval" -> NoResolution + else -> null +} + + +suspend fun Resolution.initiateBackground(context: Context, account: Account, retryFunction: () -> Any) { + when (this) { + CryptAuthSyncKeys -> { + withContext(Dispatchers.IO) { + syncCryptAuthKeys(context, account) + } + retryFunction() + } + NoResolution -> TODO() + is UserIntervention -> TODO() + } +} \ No newline at end of file diff --git a/play-services-core/src/main/kotlin/org/microg/gms/accountaction/Resolution.kt b/play-services-core/src/main/kotlin/org/microg/gms/accountaction/Resolution.kt new file mode 100644 index 0000000000..59e128d63c --- /dev/null +++ b/play-services-core/src/main/kotlin/org/microg/gms/accountaction/Resolution.kt @@ -0,0 +1,31 @@ +package org.microg.gms.accountaction + +sealed class Resolution + +/** + * In this situation, sending a CryptAuth "sync keys" query is sufficient + * to resolve the problem. This is the case for enterprise accounts that + * mandate device screenlocks after users enabled checkin, GCM, and + * configured a lockscreen. + */ +data object CryptAuthSyncKeys : Resolution() + +/** + * Represents a situation in which user actions are required to fix + * the problem. + */ +data class UserIntervention(val actions: Set) : Resolution() + +/** + * Represents a situation that is known to be unsupported by microG. + * Advise the user to remove the account. + */ +data object NoResolution : Resolution() + +enum class UserAction { + ENABLE_CHECKIN, + ENABLE_GCM, + ALLOW_MICROG_GCM, + ENABLE_LOCKSCREEN, + REAUTHENTICATE +} \ No newline at end of file diff --git a/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/WebViewHelper.kt b/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/WebViewHelper.kt index 1ff76e14cc..4534818ea1 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/WebViewHelper.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/WebViewHelper.kt @@ -23,7 +23,7 @@ import kotlinx.coroutines.withContext import org.microg.gms.auth.AuthManager import org.microg.gms.common.Constants.GMS_PACKAGE_NAME import org.microg.gms.common.PackageUtils -import org.microg.gms.cryptauth.request +import org.microg.gms.cryptauth.syncCryptAuthKeys import java.net.URLEncoder import java.util.* @@ -102,7 +102,7 @@ class WebViewHelper(private val activity: AppCompatActivity, private val webView private suspend fun openWebWithAccount(accountName: String, url: String?) { try { - request(activity, accountName) + syncCryptAuthKeys(activity, accountName) val service = "weblogin:continue=" + URLEncoder.encode(url, "utf-8") val authManager = AuthManager(activity, accountName, GMS_PACKAGE_NAME, service) diff --git a/play-services-core/src/main/kotlin/org/microg/gms/cryptauth/SyncKeysRequest.kt b/play-services-core/src/main/kotlin/org/microg/gms/cryptauth/SyncKeysRequest.kt index de871a7699..ac496bd024 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/cryptauth/SyncKeysRequest.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/cryptauth/SyncKeysRequest.kt @@ -40,8 +40,8 @@ private const val GCM_REGISTER_SCOPE = "GCM" private const val AFTER_REQUEST_DELAY = 500L -suspend fun request(context: Context, account: Account): JSONObject? = request(context, account.name) -suspend fun request(context: Context, accountName: String): JSONObject? { +suspend fun syncCryptAuthKeys(context: Context, account: Account): JSONObject? = syncCryptAuthKeys(context, account.name) +suspend fun syncCryptAuthKeys(context: Context, accountName: String): JSONObject? { val lastCheckinInfo = LastCheckinInfo.read(context) @@ -57,6 +57,7 @@ suspend fun request(context: Context, accountName: String): JSONObject? { .extraParam("scope", GCM_REGISTER_SCOPE) ).let { if (!it.containsKey(GcmConstants.EXTRA_REGISTRATION_ID)) { + Log.d(TAG, "No instance ID was gathered. Is GCM enabled?") return null } it.getString(GcmConstants.EXTRA_REGISTRATION_ID)!! From 138c151b9bc30c8214e3602b7aa91ef6834d68b2 Mon Sep 17 00:00:00 2001 From: Fynn Godau Date: Sat, 6 Apr 2024 14:00:01 +0200 Subject: [PATCH 005/132] Skeleton for background account problem resolution --- .../org/microg/gms/common/HttpFormClient.java | 2 +- .../microg/gms/common/NotOkayException.java | 20 +++++++++++++ .../gms/auth/AskPermissionActivity.java | 2 +- .../java/org/microg/gms/auth/AuthManager.java | 28 +++++++++++++++++++ .../gms/auth/AuthManagerServiceImpl.java | 2 +- .../loginservice/AccountAuthenticator.java | 2 +- .../org/microg/gms/people/PeopleManager.java | 2 +- .../microg/gms/accountaction/ErrorResolver.kt | 13 +++++++-- .../org/microg/gms/games/GamesService.kt | 2 +- .../kotlin/org/microg/gms/games/extensions.kt | 4 +-- 10 files changed, 66 insertions(+), 11 deletions(-) create mode 100644 play-services-base/core/src/main/java/org/microg/gms/common/NotOkayException.java diff --git a/play-services-base/core/src/main/java/org/microg/gms/common/HttpFormClient.java b/play-services-base/core/src/main/java/org/microg/gms/common/HttpFormClient.java index fdef586ecc..07e71af1ef 100644 --- a/play-services-base/core/src/main/java/org/microg/gms/common/HttpFormClient.java +++ b/play-services-base/core/src/main/java/org/microg/gms/common/HttpFormClient.java @@ -93,7 +93,7 @@ public static T request(String url, Request request, Class tClass) throws } catch (IOException e) { // Ignore } - throw new IOException(error); + throw new NotOkayException(error); } String result = new String(Utils.readStreamToEnd(connection.getInputStream())); diff --git a/play-services-base/core/src/main/java/org/microg/gms/common/NotOkayException.java b/play-services-base/core/src/main/java/org/microg/gms/common/NotOkayException.java new file mode 100644 index 0000000000..556bc9b898 --- /dev/null +++ b/play-services-base/core/src/main/java/org/microg/gms/common/NotOkayException.java @@ -0,0 +1,20 @@ +package org.microg.gms.common; + +import java.io.IOException; + +public class NotOkayException extends IOException { + public NotOkayException() { + } + + public NotOkayException(String message) { + super(message); + } + + public NotOkayException(String message, Throwable cause) { + super(message, cause); + } + + public NotOkayException(Throwable cause) { + super(cause); + } +} diff --git a/play-services-core/src/main/java/org/microg/gms/auth/AskPermissionActivity.java b/play-services-core/src/main/java/org/microg/gms/auth/AskPermissionActivity.java index b78ffcac5b..d7701784a4 100644 --- a/play-services-core/src/main/java/org/microg/gms/auth/AskPermissionActivity.java +++ b/play-services-core/src/main/java/org/microg/gms/auth/AskPermissionActivity.java @@ -170,7 +170,7 @@ public void onAllow() { findViewById(R.id.no_progress_bar).setVisibility(GONE); new Thread(() -> { try { - AuthResponse response = authManager.requestAuth(data.fromAccountManager); + AuthResponse response = authManager.requestAuthWithBackgroundResolution(data.fromAccountManager); Bundle result = new Bundle(); result.putString(KEY_AUTHTOKEN, response.auth); result.putString(KEY_ACCOUNT_NAME, data.accountName); diff --git a/play-services-core/src/main/java/org/microg/gms/auth/AuthManager.java b/play-services-core/src/main/java/org/microg/gms/auth/AuthManager.java index 8b5d8518c3..99b8117801 100644 --- a/play-services-core/src/main/java/org/microg/gms/auth/AuthManager.java +++ b/play-services-core/src/main/java/org/microg/gms/auth/AuthManager.java @@ -13,6 +13,11 @@ import android.net.Uri; import android.util.Log; +import androidx.annotation.RequiresPermission; + +import org.microg.gms.accountaction.ErrorResolverKt; +import org.microg.gms.accountaction.Resolution; +import org.microg.gms.common.NotOkayException; import org.microg.gms.common.PackageUtils; import org.microg.gms.settings.SettingsContract; @@ -251,6 +256,29 @@ private boolean isSystemApp() { } } + public AuthResponse requestAuthWithBackgroundResolution(boolean legacy) throws IOException { + try { + return requestAuth(legacy); + } catch (NotOkayException e) { + if (e.getMessage() != null) { + Resolution errorResolution = ErrorResolverKt.fromErrorMessage(e.getMessage()); + if (errorResolution != null) { + return ErrorResolverKt.initiateFromBackgroundBlocking( + errorResolution, + context, + getAccount(), + // infinite loop is prevented + () -> requestAuth(legacy) + ); + } else { + throw new IOException(e); + } + } else { + throw new IOException(e); + } + } + } + public AuthResponse requestAuth(boolean legacy) throws IOException { if (service.equals(AuthConstants.SCOPE_GET_ACCOUNT_ID)) { AuthResponse response = new AuthResponse(); diff --git a/play-services-core/src/main/java/org/microg/gms/auth/AuthManagerServiceImpl.java b/play-services-core/src/main/java/org/microg/gms/auth/AuthManagerServiceImpl.java index 36a0eafb26..f514e1f44d 100644 --- a/play-services-core/src/main/java/org/microg/gms/auth/AuthManagerServiceImpl.java +++ b/play-services-core/src/main/java/org/microg/gms/auth/AuthManagerServiceImpl.java @@ -144,7 +144,7 @@ public Bundle getTokenWithAccount(Account account, String scope, Bundle extras) return result; } try { - AuthResponse res = authManager.requestAuth(false); + AuthResponse res = authManager.requestAuthWithBackgroundResolution(false); if (res.auth != null) { if (!AuthConstants.SCOPE_GET_ACCOUNT_ID.equals(scope)) Log.d(TAG, "getToken: " + res); diff --git a/play-services-core/src/main/java/org/microg/gms/auth/loginservice/AccountAuthenticator.java b/play-services-core/src/main/java/org/microg/gms/auth/loginservice/AccountAuthenticator.java index ba3048eb7a..8dc989990b 100644 --- a/play-services-core/src/main/java/org/microg/gms/auth/loginservice/AccountAuthenticator.java +++ b/play-services-core/src/main/java/org/microg/gms/auth/loginservice/AccountAuthenticator.java @@ -131,7 +131,7 @@ public Bundle getAuthToken(AccountAuthenticatorResponse response, Account accoun authManager = new AuthManager(context, account.name, app, authTokenType); } try { - AuthResponse res = authManager.requestAuth(true); + AuthResponse res = authManager.requestAuthWithBackgroundResolution(true); if (res.auth != null) { Log.d(TAG, "getAuthToken: " + res.auth); Bundle result = new Bundle(); diff --git a/play-services-core/src/main/java/org/microg/gms/people/PeopleManager.java b/play-services-core/src/main/java/org/microg/gms/people/PeopleManager.java index 87a96da17d..e6ea7dbf80 100644 --- a/play-services-core/src/main/java/org/microg/gms/people/PeopleManager.java +++ b/play-services-core/src/main/java/org/microg/gms/people/PeopleManager.java @@ -148,7 +148,7 @@ public static String getUserInfoAuthKey(Context context, Account account) { String result = authManager.getAuthToken(); if (result == null) { try { - AuthResponse response = authManager.requestAuth(false); + AuthResponse response = authManager.requestAuthWithBackgroundResolution(false); result = response.auth; } catch (IOException e) { Log.w(TAG, e); diff --git a/play-services-core/src/main/kotlin/org/microg/gms/accountaction/ErrorResolver.kt b/play-services-core/src/main/kotlin/org/microg/gms/accountaction/ErrorResolver.kt index 3cfa4da0c0..dc11bb3fc9 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/accountaction/ErrorResolver.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/accountaction/ErrorResolver.kt @@ -5,8 +5,10 @@ import android.content.Context import androidx.appcompat.app.AppCompatActivity import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext import org.microg.gms.cryptauth.syncCryptAuthKeys +import java.io.IOException /** * @return `null` if it is unknown how to resolve the problem, an @@ -33,15 +35,20 @@ fun fromErrorMessage(s: String): Resolution? = if (s.startsWith("Error=")) { } -suspend fun Resolution.initiateBackground(context: Context, account: Account, retryFunction: () -> Any) { +fun Resolution.initiateFromBackgroundBlocking(context: Context, account: Account, retryFunction: RetryFunction): T? { when (this) { CryptAuthSyncKeys -> { - withContext(Dispatchers.IO) { + runBlocking { syncCryptAuthKeys(context, account) } - retryFunction() + return retryFunction.run() } NoResolution -> TODO() is UserIntervention -> TODO() } +} + +interface RetryFunction { + @Throws(IOException::class) + fun run(): T } \ No newline at end of file diff --git a/play-services-core/src/main/kotlin/org/microg/gms/games/GamesService.kt b/play-services-core/src/main/kotlin/org/microg/gms/games/GamesService.kt index e4d4cad46a..59195112dd 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/games/GamesService.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/games/GamesService.kt @@ -558,7 +558,7 @@ class GamesServiceImpl(val context: Context, override val lifecycle: Lifecycle, serverAuthTokenManager.setItCaveatTypes("2") serverAuthTokenManager.isPermitted = true serverAuthTokenManager.invalidateAuthToken() - serverAuthTokenManager.requestAuth(true) + serverAuthTokenManager.requestAuthWithBackgroundResolution(true) } if (serverAuthTokenResponse.auth != null) { callbacks.onServerAuthCode(Status(CommonStatusCodes.SUCCESS), serverAuthTokenResponse.auth) diff --git a/play-services-core/src/main/kotlin/org/microg/gms/games/extensions.kt b/play-services-core/src/main/kotlin/org/microg/gms/games/extensions.kt index 32e581f495..1d2d171bbf 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/games/extensions.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/games/extensions.kt @@ -156,7 +156,7 @@ fun JSONObject.toPlayer() = PlayerEntity( suspend fun registerForGames(context: Context, account: Account, queue: RequestQueue = singleInstanceOf { Volley.newRequestQueue(context.applicationContext) }) { val authManager = AuthManager(context, account.name, Constants.GMS_PACKAGE_NAME, "oauth2:${Scopes.GAMES_FIRSTPARTY}") authManager.setOauth2Foreground("1") - val authToken = withContext(Dispatchers.IO) { authManager.requestAuth(false).auth } + val authToken = withContext(Dispatchers.IO) { authManager.requestAuthWithBackgroundResolution(false).auth } val androidId = getSettings(context, CheckIn.getContentUri(context), arrayOf(CheckIn.ANDROID_ID)) { cursor: Cursor -> cursor.getLong(0) } val result = suspendCoroutine { continuation -> queue.add( @@ -216,7 +216,7 @@ suspend fun performGamesSignIn( val authManager = AuthManager(context, account.name, packageName, "oauth2:${realScopes.joinToString(" ")}") if (realScopes.size == 1) authManager.setItCaveatTypes("2") if (permitted) authManager.isPermitted = true - val authResponse = withContext(Dispatchers.IO) { authManager.requestAuth(true) } + val authResponse = withContext(Dispatchers.IO) { authManager.requestAuthWithBackgroundResolution(true) } if (authResponse.auth == null) return false if (authResponse.issueAdvice != "stored" || GamesConfigurationService.getPlayer(context, packageName, account) == null) { suspend fun fetchSelfPlayer() = suspendCoroutine { continuation -> From d9f332e37fb189c4e0b1b648b1a4637ff5d97381 Mon Sep 17 00:00:00 2001 From: Fynn Godau Date: Sat, 6 Apr 2024 16:50:31 +0200 Subject: [PATCH 006/132] Automatic account error diagnosis Logs which of the dependencies for setting up enterprise accounts are missing: * Checkin enabled * GCM enabled * microG allowed to use GCM * Lockscreen enabled --- .../java/org/microg/gms/auth/AuthManager.java | 4 +- .../microg/gms/accountaction/ErrorResolver.kt | 102 ++++++++++++++---- .../microg/gms/accountaction/Resolution.kt | 16 +-- .../microg/gms/cryptauth/SyncKeysRequest.kt | 14 ++- 4 files changed, 107 insertions(+), 29 deletions(-) diff --git a/play-services-core/src/main/java/org/microg/gms/auth/AuthManager.java b/play-services-core/src/main/java/org/microg/gms/auth/AuthManager.java index 99b8117801..492a8e551c 100644 --- a/play-services-core/src/main/java/org/microg/gms/auth/AuthManager.java +++ b/play-services-core/src/main/java/org/microg/gms/auth/AuthManager.java @@ -13,8 +13,6 @@ import android.net.Uri; import android.util.Log; -import androidx.annotation.RequiresPermission; - import org.microg.gms.accountaction.ErrorResolverKt; import org.microg.gms.accountaction.Resolution; import org.microg.gms.common.NotOkayException; @@ -261,7 +259,7 @@ public AuthResponse requestAuthWithBackgroundResolution(boolean legacy) throws I return requestAuth(legacy); } catch (NotOkayException e) { if (e.getMessage() != null) { - Resolution errorResolution = ErrorResolverKt.fromErrorMessage(e.getMessage()); + Resolution errorResolution = ErrorResolverKt.resolveAuthErrorMessage(context, e.getMessage()); if (errorResolution != null) { return ErrorResolverKt.initiateFromBackgroundBlocking( errorResolution, diff --git a/play-services-core/src/main/kotlin/org/microg/gms/accountaction/ErrorResolver.kt b/play-services-core/src/main/kotlin/org/microg/gms/accountaction/ErrorResolver.kt index dc11bb3fc9..023c63c6be 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/accountaction/ErrorResolver.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/accountaction/ErrorResolver.kt @@ -2,49 +2,115 @@ package org.microg.gms.accountaction import android.accounts.Account import android.content.Context -import androidx.appcompat.app.AppCompatActivity -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers +import android.util.Log import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.withContext +import org.microg.gms.common.Constants +import org.microg.gms.cryptauth.isLockscreenConfigured import org.microg.gms.cryptauth.syncCryptAuthKeys +import org.microg.gms.gcm.GcmDatabase +import org.microg.gms.gcm.GcmPrefs +import org.microg.gms.settings.SettingsContract import java.io.IOException + +/** + * High-level resolution: tell server that user has configured a lock screen + */ +const val DEVICE_MANAGEMENT_SCREENLOCK_REQUIRED = "DeviceManagementScreenlockRequired" + +/** + * Indicates that the user is using an enterprise account that is set up to use Advanced + * device management features, for which it is required to install a device manager. + * This is not supported by microG. + */ +const val DEVICE_MANAGEMENT_REQUIRED = "DeviceManagementRequired" + +/** + * Indicates that the user is using an enterprise account that is set up to use Advanced + * device management features, for which it is required to install a device manager, + * and that the device also needs manual admin approval. + * This is not supported by microG. + */ +const val DEVICE_MANAGEMENT_ADMIN_PENDING_APPROVAL = "DeviceManagementAdminPendingApproval" + +const val TAG = "GmsAccountErrorResolve" + /** * @return `null` if it is unknown how to resolve the problem, an * appropriate `Resolution` otherwise */ -fun fromErrorMessage(s: String): Resolution? = if (s.startsWith("Error=")) { - fromErrorMessage(s.drop("Error=".length)) +fun Context.resolveAuthErrorMessage(s: String): Resolution? = if (s.startsWith("Error=")) { + resolveAuthErrorMessage(s.drop("Error=".length)) } else when (s) { - "DeviceManagementScreenlockRequired" -> { - if (true) { + DEVICE_MANAGEMENT_SCREENLOCK_REQUIRED -> { + + val actions = mutableSetOf() + + + + val settingsProjection = arrayOf( + SettingsContract.CheckIn.ENABLED, + SettingsContract.CheckIn.LAST_CHECK_IN + ) + SettingsContract.getSettings(this, SettingsContract.CheckIn.getContentUri(this), settingsProjection) { cursor -> + val checkInEnabled = cursor.getInt(0) != 0 + val lastCheckIn = cursor.getLong(1) + + if (lastCheckIn <= 0 && !checkInEnabled) { + actions += UserAction.ENABLE_CHECKIN + } + } + + val gcmPrefs = GcmPrefs.get(this) + if (!gcmPrefs.isEnabled) { + actions += UserAction.ENABLE_GCM + } + + val gcmDatabaseEntry = GcmDatabase(this).use { + it.getApp(Constants.GMS_PACKAGE_NAME) + } + if (gcmDatabaseEntry != null && + !gcmDatabaseEntry.allowRegister || + gcmDatabaseEntry == null && + gcmPrefs.confirmNewApps + ) { + actions += UserAction.ALLOW_MICROG_GCM + } + + if (!isLockscreenConfigured()) { + actions += UserAction.ENABLE_LOCKSCREEN + } + + if (actions.isEmpty()) { CryptAuthSyncKeys } else { - UserIntervention( - setOf( - UserAction.ENABLE_CHECKIN, UserAction.ENABLE_GCM, UserAction.ENABLE_LOCKSCREEN - ) - ) + UserIntervention(actions) } } - "DeviceManagementRequired" -> NoResolution - "DeviceManagementAdminPendingApproval" -> NoResolution + DEVICE_MANAGEMENT_ADMIN_PENDING_APPROVAL, DEVICE_MANAGEMENT_REQUIRED -> + NoResolution(NoResolutionReason.ADVANCED_DEVICE_MANAGEMENT_NOT_SUPPORTED) else -> null -} +}.also { Log.d(TAG, "Error was: $s. Diagnosis: $it.") } fun Resolution.initiateFromBackgroundBlocking(context: Context, account: Account, retryFunction: RetryFunction): T? { when (this) { CryptAuthSyncKeys -> { + Log.d(TAG, "Resolving account error by performing cryptauth sync keys call.") runBlocking { syncCryptAuthKeys(context, account) } return retryFunction.run() } - NoResolution -> TODO() - is UserIntervention -> TODO() + is NoResolution -> { + Log.w(TAG, "This account cannot be used with microG due to $reason") + return null + } + is UserIntervention -> { + Log.w(TAG, "User intervention required! You need to ${actions.joinToString(", ")}.") + return null + } } } diff --git a/play-services-core/src/main/kotlin/org/microg/gms/accountaction/Resolution.kt b/play-services-core/src/main/kotlin/org/microg/gms/accountaction/Resolution.kt index 59e128d63c..befa6a4b7a 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/accountaction/Resolution.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/accountaction/Resolution.kt @@ -16,16 +16,20 @@ data object CryptAuthSyncKeys : Resolution() */ data class UserIntervention(val actions: Set) : Resolution() -/** - * Represents a situation that is known to be unsupported by microG. - * Advise the user to remove the account. - */ -data object NoResolution : Resolution() - enum class UserAction { ENABLE_CHECKIN, ENABLE_GCM, ALLOW_MICROG_GCM, ENABLE_LOCKSCREEN, REAUTHENTICATE +} + +/** + * Represents a situation that is known to be unsupported by microG. + * Advise the user to remove the account. + */ +data class NoResolution(val reason: NoResolutionReason) : Resolution() + +enum class NoResolutionReason { + ADVANCED_DEVICE_MANAGEMENT_NOT_SUPPORTED } \ No newline at end of file diff --git a/play-services-core/src/main/kotlin/org/microg/gms/cryptauth/SyncKeysRequest.kt b/play-services-core/src/main/kotlin/org/microg/gms/cryptauth/SyncKeysRequest.kt index ac496bd024..b233b9bab0 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/cryptauth/SyncKeysRequest.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/cryptauth/SyncKeysRequest.kt @@ -1,6 +1,7 @@ package org.microg.gms.cryptauth import android.accounts.Account +import android.app.KeyguardManager import android.content.Context import android.util.Log import com.google.android.gms.BuildConfig @@ -57,7 +58,7 @@ suspend fun syncCryptAuthKeys(context: Context, accountName: String): JSONObject .extraParam("scope", GCM_REGISTER_SCOPE) ).let { if (!it.containsKey(GcmConstants.EXTRA_REGISTRATION_ID)) { - Log.d(TAG, "No instance ID was gathered. Is GCM enabled?") + Log.d(TAG, "No instance ID was gathered. Is GCM enabled, has there been a checkin?") return null } it.getString(GcmConstants.EXTRA_REGISTRATION_ID)!! @@ -92,7 +93,7 @@ suspend fun syncCryptAuthKeys(context: Context, accountName: String): JSONObject device_model = Build.MODEL ?: "", device_manufacturer = Build.MANUFACTURER ?: "", device_type = ClientAppMetadata.DeviceType.ANDROID, - using_secure_screenlock = true, // TODO actual value + using_secure_screenlock = context.isLockscreenConfigured(), bluetooth_radio_supported = true, // TODO actual value? doesn't seem relevant // bluetooth_radio_enabled = false, ble_radio_supported = true, // TODO: actual value? doesn't seem relevant @@ -161,5 +162,14 @@ suspend fun syncCryptAuthKeys(context: Context, accountName: String): JSONObject } } +fun Context.isLockscreenConfigured(): Boolean { + val service: KeyguardManager = getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { + return service.isDeviceSecure + } else { + return service.isKeyguardSecure + } +} + fun jsonObjectOf(vararg pairs: Pair): JSONObject = JSONObject(mapOf(*pairs)) inline fun jsonArrayOf(vararg elements: T): JSONArray = JSONArray(arrayOf(*elements)) \ No newline at end of file From c066191dd10d25fadcaeb65fa806989555bd6ca6 Mon Sep 17 00:00:00 2001 From: Fynn Godau Date: Mon, 8 Apr 2024 16:24:51 +0200 Subject: [PATCH 007/132] Add notification and UI when account action needed --- build.gradle | 2 +- play-services-core/build.gradle | 14 ++- .../src/main/AndroidManifest.xml | 26 ++++ .../org/microg/gms/ui/SelfCheckFragment.java | 5 + .../accountaction/AccountActionActivity.kt | 80 +++++++++++++ .../gms/accountaction/AccountNotification.kt | 75 ++++++++++++ .../microg/gms/accountaction/ErrorResolver.kt | 5 +- .../UserInterventionComponents.kt | 111 ++++++++++++++++++ .../src/main/res/values/strings.xml | 19 +++ 9 files changed, 332 insertions(+), 5 deletions(-) create mode 100644 play-services-core/src/main/kotlin/org/microg/gms/accountaction/AccountActionActivity.kt create mode 100644 play-services-core/src/main/kotlin/org/microg/gms/accountaction/AccountNotification.kt create mode 100644 play-services-core/src/main/kotlin/org/microg/gms/accountaction/UserInterventionComponents.kt diff --git a/build.gradle b/build.gradle index 92339b13f2..6e97a8d053 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ buildscript { ext.cronetVersion = '102.5005.125' ext.wearableVersion = '0.1.1' - ext.kotlinVersion = '1.9.22' + ext.kotlinVersion = '1.9.23' ext.coroutineVersion = '1.7.3' ext.annotationVersion = '1.7.1' diff --git a/play-services-core/build.gradle b/play-services-core/build.gradle index 41b0b197c7..13f734b1ee 100644 --- a/play-services-core/build.gradle +++ b/play-services-core/build.gradle @@ -82,6 +82,14 @@ dependencies { // Material Components implementation "com.google.android.material:material:$materialVersion" + // Compose + def composeBom = platform('androidx.compose:compose-bom:2024.04.00') + implementation composeBom + implementation 'androidx.compose.material3:material3' + implementation 'androidx.compose.ui:ui-tooling-preview' + debugImplementation 'androidx.compose.ui:ui-tooling' + implementation 'androidx.activity:activity-compose:1.8.2' + // Navigation implementation "androidx.navigation:navigation-fragment-ktx:$navigationVersion" implementation "androidx.navigation:navigation-ui-ktx:$navigationVersion" @@ -100,6 +108,8 @@ android { buildFeatures { buildConfig = true + dataBinding = true + compose true } defaultConfig { @@ -127,8 +137,8 @@ android { } } - buildFeatures { - dataBinding = true + composeOptions { + kotlinCompilerExtensionVersion = "1.5.11" } sourceSets { diff --git a/play-services-core/src/main/AndroidManifest.xml b/play-services-core/src/main/AndroidManifest.xml index 2e6c2a634a..0b8ddb22f7 100644 --- a/play-services-core/src/main/AndroidManifest.xml +++ b/play-services-core/src/main/AndroidManifest.xml @@ -137,6 +137,7 @@ + @@ -158,6 +159,26 @@ android:name="android.permission.UPDATE_APP_OPS_STATS" tools:ignore="ProtectedPermissions" /> + + + checks permissions.add(READ_EXTERNAL_STORAGE); permissions.add(WRITE_EXTERNAL_STORAGE); permissions.add(GET_ACCOUNTS); + if (SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + permissions.add(POST_NOTIFICATIONS); + } permissions.add(READ_PHONE_STATE); permissions.add(RECEIVE_SMS); checks.add(new PermissionCheckGroup(permissions.toArray(new String[0])) { diff --git a/play-services-core/src/main/kotlin/org/microg/gms/accountaction/AccountActionActivity.kt b/play-services-core/src/main/kotlin/org/microg/gms/accountaction/AccountActionActivity.kt new file mode 100644 index 0000000000..2d5c259fa5 --- /dev/null +++ b/play-services-core/src/main/kotlin/org/microg/gms/accountaction/AccountActionActivity.kt @@ -0,0 +1,80 @@ +package org.microg.gms.accountaction + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.annotation.RequiresApi +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.google.android.gms.R + +@RequiresApi(21) +class AccountActionActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + org.microg.gms.accountaction.Preview() + } + } +} + +@Composable +fun Content(userTasks: UserIntervention, finish: () -> Unit) { + Column { + Column { + Text( + text = stringResource(id = R.string.auth_action_activity_header), + modifier = Modifier.padding(top = 32.dp, start = 16.dp, end = 16.dp), + style = MaterialTheme.typography.headlineLarge + ) + Text( + text = stringResource(id = R.string.auth_action_activity_explanation, "admin@fynngodau.de"), + modifier = Modifier.padding(16.dp), + style = MaterialTheme.typography.bodyMedium + ) + HorizontalDivider() + } + Surface(Modifier.fillMaxHeight()) { + Column(Modifier.verticalScroll(rememberScrollState())) { + userTasks.UiComponents() + Button( + onClick = finish, + enabled = false, + colors = ButtonDefaults.buttonColors( + containerColor = colorResource( + id = R.color.login_blue_theme_primary + ) + ), + modifier = Modifier + .align(Alignment.End) + .padding(16.dp) + ) { + Text(text = stringResource(id = R.string.auth_action_activity_finish)) + } + } + } + } +} + +@Preview +@Composable +fun Preview() { + Content(UserIntervention(setOf(UserAction.ENABLE_CHECKIN, UserAction.ENABLE_GCM, UserAction.ALLOW_MICROG_GCM, UserAction.ENABLE_LOCKSCREEN))) {} +} \ No newline at end of file diff --git a/play-services-core/src/main/kotlin/org/microg/gms/accountaction/AccountNotification.kt b/play-services-core/src/main/kotlin/org/microg/gms/accountaction/AccountNotification.kt new file mode 100644 index 0000000000..db96cc159b --- /dev/null +++ b/play-services-core/src/main/kotlin/org/microg/gms/accountaction/AccountNotification.kt @@ -0,0 +1,75 @@ +package org.microg.gms.accountaction + +import android.Manifest +import android.accounts.Account +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.os.Build +import androidx.annotation.RequiresApi +import androidx.core.app.ActivityCompat +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import com.google.android.gms.R + +private const val CHANNEL_ID = "AccountNotification" + +internal const val INTENT_KEY_USER_ACTION = "userAction" +internal const val INTENT_KEY_ACCOUNT_NAME = "accountName" + +@RequiresApi(21) +fun Context.sendAccountActionNotification(account: Account, action: UserAction) { + + registerAccountNotificationChannel() + + val intent: PendingIntent =Intent(this, AccountActionActivity::class.java).apply { + putExtra(INTENT_KEY_USER_ACTION, action) + putExtra(INTENT_KEY_ACCOUNT_NAME, account.name) + }.let { + PendingIntent.getActivity( + this, + account.hashCode(), + it, + PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_CANCEL_CURRENT + ) + } + + val notification: Notification = + NotificationCompat.Builder(this, CHANNEL_ID) + .setSmallIcon(R.drawable.ic_manage_accounts) + .setSound(null) + .setOnlyAlertOnce(true) + .setContentTitle(getString(R.string.auth_action_notification_title)) + .setContentText(getString(R.string.auth_action_notification_content)) + .setSubText(account.name) + .setOnlyAlertOnce(true) + .setContentIntent(intent) + .setAutoCancel(true) + .build() + + if (ActivityCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) == + PackageManager.PERMISSION_GRANTED + ) { + NotificationManagerCompat.from(this).notify(account.hashCode(), notification) + } + +} + +fun Context.registerAccountNotificationChannel() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val channel = NotificationChannel( + CHANNEL_ID, + getString(R.string.auth_action_notification_channel_name), + NotificationManager.IMPORTANCE_HIGH + ).apply { + description = getString(R.string.auth_action_notification_channel_description) + setSound(null, null) + } + getSystemService(NotificationManager::class.java) + .createNotificationChannel(channel) + } +} \ No newline at end of file diff --git a/play-services-core/src/main/kotlin/org/microg/gms/accountaction/ErrorResolver.kt b/play-services-core/src/main/kotlin/org/microg/gms/accountaction/ErrorResolver.kt index 023c63c6be..8f9e403647 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/accountaction/ErrorResolver.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/accountaction/ErrorResolver.kt @@ -53,10 +53,11 @@ fun Context.resolveAuthErrorMessage(s: String): Resolution? = if (s.startsWith(" SettingsContract.CheckIn.LAST_CHECK_IN ) SettingsContract.getSettings(this, SettingsContract.CheckIn.getContentUri(this), settingsProjection) { cursor -> - val checkInEnabled = cursor.getInt(0) != 0 + //val checkInEnabled = cursor.getInt(0) != 0 val lastCheckIn = cursor.getLong(1) - if (lastCheckIn <= 0 && !checkInEnabled) { + if (lastCheckIn <= 0) { + // user is also asked to enable checkin if there had never been a successful checkin (network errors?) actions += UserAction.ENABLE_CHECKIN } } diff --git a/play-services-core/src/main/kotlin/org/microg/gms/accountaction/UserInterventionComponents.kt b/play-services-core/src/main/kotlin/org/microg/gms/accountaction/UserInterventionComponents.kt new file mode 100644 index 0000000000..b3e5a774e9 --- /dev/null +++ b/play-services-core/src/main/kotlin/org/microg/gms/accountaction/UserInterventionComponents.kt @@ -0,0 +1,111 @@ +package org.microg.gms.accountaction + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight +import androidx.compose.material3.Icon +import androidx.compose.material3.LocalTextStyle +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.google.android.gms.R +import org.microg.gms.accountaction.UserAction.* + +@Composable +fun UserIntervention.UiComponents() { + for ((index, action) in actions.withIndex()) { + when (action) { + ENABLE_CHECKIN -> UserInterventionCommonComponent( + title = stringResource(id = R.string.auth_action_step_enable_checkin), + description = stringResource(id = R.string.auth_action_step_enable_checkin_description), + sequenceNumber = index + 1 + ) + ENABLE_GCM -> UserInterventionCommonComponent( + title = stringResource(id = R.string.auth_action_step_enable_gcm), + description = stringResource(id = R.string.auth_action_step_enable_gcm_description), + sequenceNumber = index + 1 + ) + ALLOW_MICROG_GCM -> UserInterventionCommonComponent( + title = stringResource(id = R.string.auth_action_step_allow_microg_gcm), + description = stringResource(id = R.string.auth_action_step_allow_microg_gcm_description), + sequenceNumber = index + 1 + ) + ENABLE_LOCKSCREEN -> UserInterventionCommonComponent( + title = stringResource(id = R.string.auth_action_step_enable_lockscreen), + description = stringResource(id = R.string.auth_action_step_enable_lockscreen_description), + sequenceNumber = index + 1 + ) + REAUTHENTICATE -> TODO() + } + } +} + +@Composable +fun UserInterventionCommonComponent(title: String, description: String, sequenceNumber: Int?) { + Surface(onClick = { /*TODO*/ }) { + + val circleColor = colorResource(id = R.color.login_blue_theme_primary) + Column( + Modifier + .padding(top = 16.dp, start = 16.dp, end = 16.dp, bottom = 16.dp) + ) { + + Row(Modifier.padding(bottom = 16.dp)) { + Box(Modifier.size(32.dp)) { + Canvas(modifier = Modifier.fillMaxSize()) { + drawCircle( + color = circleColor + ) + } + + sequenceNumber?.let { + Text( + text = it.toString(), + modifier = Modifier.align(Alignment.Center), + style = LocalTextStyle.current.copy(color = Color.White) + ) + } + } + Spacer(Modifier.width(16.dp)) + Text( + text = title, + modifier = Modifier + .align(Alignment.CenterVertically) + .weight(1f), + style = MaterialTheme.typography.labelLarge + ) + Spacer(Modifier.width(16.dp)) + Icon( + imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, + contentDescription = stringResource(id = R.string.auth_action_step_perform_content_description), + modifier = Modifier.align(Alignment.CenterVertically) + ) + } + Text(text = description) + } + } +} + +@Preview +@Composable +fun PreviewInterventionComponent() { + Column { + UserIntervention(setOf(ENABLE_CHECKIN, ENABLE_GCM, ALLOW_MICROG_GCM, ENABLE_LOCKSCREEN)).UiComponents() + } +} \ No newline at end of file diff --git a/play-services-core/src/main/res/values/strings.xml b/play-services-core/src/main/res/values/strings.xml index c8d3a89d47..a95f9ece3d 100644 --- a/play-services-core/src/main/res/values/strings.xml +++ b/play-services-core/src/main/res/values/strings.xml @@ -38,6 +38,25 @@ This can take a couple of minutes." %1$s requires your authorization to access your Google account. "Allow %1$s privileged access to %2$s?" "%1$s wants to access your account as if it was %2$s by %3$s. This might grant it privileged access to your account." + Google account alerts + Notifies when one of your Google accounts requires additional setup before it can be used or when an account is incompatible with microG. + Account action required + Your Google account needs additional setup. + + Finish setting up your Google account + Complete the following steps to be able to use your Google account %s on this device. + Enable device registration + "Your device needs to register to Google at least once. + +You can disable Google device registration after account setup is complete. + Enable Cloud Messaging + You can disable Cloud Messaging after account setup is complete. + Allow Cloud Messaging for microG + According to your preferences, microG needs permission from you before it can register itself for Cloud Messaging. + Configure secure screen lock + Your Google account is managed by your workplace or educational institution. Your administrator decided that devices need to set up a secure screen lock before they can access account data. + Click to perform step + Finish Choose an account to continue to %1$s From 7c47d8747edeb93eb01fad0b9f337fdc6916059f Mon Sep 17 00:00:00 2001 From: Fynn Godau Date: Mon, 8 Apr 2024 16:58:58 +0200 Subject: [PATCH 008/132] Successful user action completion UI --- .../accountaction/AccountActionActivity.kt | 13 +++- .../UserInterventionComponents.kt | 70 +++++++++++++------ .../src/main/res/values/colors.xml | 1 + .../src/main/res/values/strings.xml | 5 +- 4 files changed, 64 insertions(+), 25 deletions(-) diff --git a/play-services-core/src/main/kotlin/org/microg/gms/accountaction/AccountActionActivity.kt b/play-services-core/src/main/kotlin/org/microg/gms/accountaction/AccountActionActivity.kt index 2d5c259fa5..571dd842c6 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/accountaction/AccountActionActivity.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/accountaction/AccountActionActivity.kt @@ -36,7 +36,7 @@ class AccountActionActivity : ComponentActivity() { } @Composable -fun Content(userTasks: UserIntervention, finish: () -> Unit) { +fun Content(userTasks: Map, finish: () -> Unit) { Column { Column { Text( @@ -53,7 +53,7 @@ fun Content(userTasks: UserIntervention, finish: () -> Unit) { } Surface(Modifier.fillMaxHeight()) { Column(Modifier.verticalScroll(rememberScrollState())) { - userTasks.UiComponents() + UserInterventionComponents(userActions = userTasks) Button( onClick = finish, enabled = false, @@ -76,5 +76,12 @@ fun Content(userTasks: UserIntervention, finish: () -> Unit) { @Preview @Composable fun Preview() { - Content(UserIntervention(setOf(UserAction.ENABLE_CHECKIN, UserAction.ENABLE_GCM, UserAction.ALLOW_MICROG_GCM, UserAction.ENABLE_LOCKSCREEN))) {} + Content( + mapOf( + UserAction.ENABLE_CHECKIN to true, + UserAction.ENABLE_GCM to true, + UserAction.ALLOW_MICROG_GCM to false, + UserAction.ENABLE_LOCKSCREEN to false + ) + ) {} } \ No newline at end of file diff --git a/play-services-core/src/main/kotlin/org/microg/gms/accountaction/UserInterventionComponents.kt b/play-services-core/src/main/kotlin/org/microg/gms/accountaction/UserInterventionComponents.kt index b3e5a774e9..a5b4177869 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/accountaction/UserInterventionComponents.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/accountaction/UserInterventionComponents.kt @@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight +import androidx.compose.material.icons.filled.Check import androidx.compose.material3.Icon import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme @@ -28,28 +29,32 @@ import com.google.android.gms.R import org.microg.gms.accountaction.UserAction.* @Composable -fun UserIntervention.UiComponents() { - for ((index, action) in actions.withIndex()) { - when (action) { +fun UserInterventionComponents(userActions: Map) { + for ((index, action) in userActions.entries.withIndex()) { + when (action.component1()) { ENABLE_CHECKIN -> UserInterventionCommonComponent( title = stringResource(id = R.string.auth_action_step_enable_checkin), description = stringResource(id = R.string.auth_action_step_enable_checkin_description), - sequenceNumber = index + 1 + sequenceNumber = index + 1, + completed = action.component2() ) ENABLE_GCM -> UserInterventionCommonComponent( title = stringResource(id = R.string.auth_action_step_enable_gcm), description = stringResource(id = R.string.auth_action_step_enable_gcm_description), - sequenceNumber = index + 1 + sequenceNumber = index + 1, + completed = action.component2() ) ALLOW_MICROG_GCM -> UserInterventionCommonComponent( title = stringResource(id = R.string.auth_action_step_allow_microg_gcm), description = stringResource(id = R.string.auth_action_step_allow_microg_gcm_description), - sequenceNumber = index + 1 + sequenceNumber = index + 1, + completed = action.component2() ) ENABLE_LOCKSCREEN -> UserInterventionCommonComponent( title = stringResource(id = R.string.auth_action_step_enable_lockscreen), description = stringResource(id = R.string.auth_action_step_enable_lockscreen_description), - sequenceNumber = index + 1 + sequenceNumber = index + 1, + completed = action.component2() ) REAUTHENTICATE -> TODO() } @@ -57,29 +62,43 @@ fun UserIntervention.UiComponents() { } @Composable -fun UserInterventionCommonComponent(title: String, description: String, sequenceNumber: Int?) { +fun UserInterventionCommonComponent(title: String, description: String, sequenceNumber: Int?, completed: Boolean) { Surface(onClick = { /*TODO*/ }) { - val circleColor = colorResource(id = R.color.login_blue_theme_primary) + val color = if (completed) { + colorResource(id = R.color.material_success) + } else { + colorResource(id = R.color.login_blue_theme_primary) + } + Column( Modifier .padding(top = 16.dp, start = 16.dp, end = 16.dp, bottom = 16.dp) ) { - Row(Modifier.padding(bottom = 16.dp)) { + Row { Box(Modifier.size(32.dp)) { Canvas(modifier = Modifier.fillMaxSize()) { drawCircle( - color = circleColor + color = color ) } - sequenceNumber?.let { - Text( - text = it.toString(), + if (completed) { + Icon( + imageVector = Icons.Default.Check, + contentDescription = stringResource(R.string.auth_action_step_completed_content_description), modifier = Modifier.align(Alignment.Center), - style = LocalTextStyle.current.copy(color = Color.White) + tint = Color.White ) + } else { + sequenceNumber?.let { + Text( + text = it.toString(), + modifier = Modifier.align(Alignment.Center), + style = LocalTextStyle.current.copy(color = Color.White) + ) + } } } Spacer(Modifier.width(16.dp)) @@ -91,13 +110,20 @@ fun UserInterventionCommonComponent(title: String, description: String, sequence style = MaterialTheme.typography.labelLarge ) Spacer(Modifier.width(16.dp)) - Icon( - imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, - contentDescription = stringResource(id = R.string.auth_action_step_perform_content_description), - modifier = Modifier.align(Alignment.CenterVertically) + if (!completed) { + Icon( + imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, + contentDescription = stringResource(id = R.string.auth_action_step_perform_content_description), + modifier = Modifier.align(Alignment.CenterVertically) + ) + } + } + if (!completed) { + Text( + text = description, + modifier = Modifier.padding(top = 16.dp) ) } - Text(text = description) } } } @@ -106,6 +132,8 @@ fun UserInterventionCommonComponent(title: String, description: String, sequence @Composable fun PreviewInterventionComponent() { Column { - UserIntervention(setOf(ENABLE_CHECKIN, ENABLE_GCM, ALLOW_MICROG_GCM, ENABLE_LOCKSCREEN)).UiComponents() + UserInterventionComponents(userActions = + mapOf(ENABLE_CHECKIN to true, ENABLE_GCM to true, ALLOW_MICROG_GCM to false, ENABLE_LOCKSCREEN to false) + ) } } \ No newline at end of file diff --git a/play-services-core/src/main/res/values/colors.xml b/play-services-core/src/main/res/values/colors.xml index 5f3632ccd5..c396eefafe 100644 --- a/play-services-core/src/main/res/values/colors.xml +++ b/play-services-core/src/main/res/values/colors.xml @@ -21,6 +21,7 @@ #88CCCCCC #fff44336 + #34a853 #ff410e0b #ffffffff diff --git a/play-services-core/src/main/res/values/strings.xml b/play-services-core/src/main/res/values/strings.xml index a95f9ece3d..062aa8d1e7 100644 --- a/play-services-core/src/main/res/values/strings.xml +++ b/play-services-core/src/main/res/values/strings.xml @@ -54,8 +54,11 @@ You can disable Google device registration after account setup is complete.Allow Cloud Messaging for microG According to your preferences, microG needs permission from you before it can register itself for Cloud Messaging. Configure secure screen lock - Your Google account is managed by your workplace or educational institution. Your administrator decided that devices need to set up a secure screen lock before they can access account data. + "Your Google account is managed by your workplace or educational institution. Your administrator decided that devices need a secure screen lock before they can access account data. + +Please set up a password, PIN, or pattern lock screen." Click to perform step + Step completed Finish Choose an account From 527906dcc4cc0b3885d0cdad8e9681e85d63e779 Mon Sep 17 00:00:00 2001 From: Fynn Godau Date: Mon, 8 Apr 2024 17:00:57 +0200 Subject: [PATCH 009/132] Resolve incompatible version problem --- build.gradle | 2 +- play-services-core/build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 6e97a8d053..92339b13f2 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ buildscript { ext.cronetVersion = '102.5005.125' ext.wearableVersion = '0.1.1' - ext.kotlinVersion = '1.9.23' + ext.kotlinVersion = '1.9.22' ext.coroutineVersion = '1.7.3' ext.annotationVersion = '1.7.1' diff --git a/play-services-core/build.gradle b/play-services-core/build.gradle index 13f734b1ee..b776816c2b 100644 --- a/play-services-core/build.gradle +++ b/play-services-core/build.gradle @@ -138,7 +138,7 @@ android { } composeOptions { - kotlinCompilerExtensionVersion = "1.5.11" + kotlinCompilerExtensionVersion = "1.5.10" } sourceSets { From 3df2d1bbb6e4d27c2683cdaed5706e85fa496418 Mon Sep 17 00:00:00 2001 From: Fynn Godau Date: Tue, 9 Apr 2024 12:56:53 +0200 Subject: [PATCH 010/132] Handle clicks on user intervention actions --- .../src/main/AndroidManifest.xml | 3 ++ .../org/microg/gms/ui/AskPushPermission.java | 24 +++------- .../microg/gms/accountaction/ErrorResolver.kt | 4 +- .../UserInterventionComponents.kt | 46 ++++++++++++++++--- .../src/main/res/layout/ask_gcm.xml | 5 +- .../src/main/res/navigation/nav_settings.xml | 9 +++- 6 files changed, 64 insertions(+), 27 deletions(-) diff --git a/play-services-core/src/main/AndroidManifest.xml b/play-services-core/src/main/AndroidManifest.xml index 0b8ddb22f7..d444183602 100644 --- a/play-services-core/src/main/AndroidManifest.xml +++ b/play-services-core/src/main/AndroidManifest.xml @@ -649,6 +649,9 @@ + + + diff --git a/play-services-core/src/main/java/org/microg/gms/ui/AskPushPermission.java b/play-services-core/src/main/java/org/microg/gms/ui/AskPushPermission.java index da42fdd07e..2c69fc1144 100644 --- a/play-services-core/src/main/java/org/microg/gms/ui/AskPushPermission.java +++ b/play-services-core/src/main/java/org/microg/gms/ui/AskPushPermission.java @@ -1,38 +1,27 @@ package org.microg.gms.ui; import android.app.Activity; -import android.content.DialogInterface; -import android.content.Intent; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.graphics.Typeface; import android.os.Bundle; import android.os.ResultReceiver; -import android.text.Html; import android.text.Spannable; import android.text.SpannableString; -import android.text.SpannableStringBuilder; -import android.text.Spanned; -import android.text.SpannedString; import android.text.style.StyleSpan; import android.view.View; import android.widget.TextView; -import androidx.appcompat.app.AlertDialog; import androidx.fragment.app.FragmentActivity; import com.google.android.gms.R; import org.microg.gms.gcm.GcmDatabase; -import org.microg.gms.gcm.PushRegisterService; - -import static org.microg.gms.gcm.GcmConstants.EXTRA_APP; -import static org.microg.gms.gcm.GcmConstants.EXTRA_KID; -import static org.microg.gms.gcm.GcmConstants.EXTRA_PENDING_INTENT; public class AskPushPermission extends FragmentActivity { public static final String EXTRA_REQUESTED_PACKAGE = "package"; public static final String EXTRA_RESULT_RECEIVER = "receiver"; + public static final String EXTRA_FORCE_ASK = "force"; public static final String EXTRA_EXPLICIT = "explicit"; private GcmDatabase database; @@ -49,13 +38,14 @@ public void onCreate(Bundle savedInstanceState) { packageName = getIntent().getStringExtra(EXTRA_REQUESTED_PACKAGE); resultReceiver = getIntent().getParcelableExtra(EXTRA_RESULT_RECEIVER); - if (packageName == null || resultReceiver == null) { + boolean force = getIntent().getBooleanExtra(EXTRA_FORCE_ASK, false); + if (packageName == null || (resultReceiver == null && !force)) { answered = true; finish(); return; } - if (database.getApp(packageName) != null) { + if (!force && database.getApp(packageName) != null) { resultReceiver.send(Activity.RESULT_OK, Bundle.EMPTY); answered = true; finish(); @@ -80,7 +70,7 @@ public void onCreate(Bundle savedInstanceState) { answered = true; Bundle bundle = new Bundle(); bundle.putBoolean(EXTRA_EXPLICIT, true); - resultReceiver.send(Activity.RESULT_OK, bundle); + if (resultReceiver != null) resultReceiver.send(Activity.RESULT_OK, bundle); finish(); }) .setNegativeButton(R.string.deny, (dialog, which) -> { @@ -89,7 +79,7 @@ public void onCreate(Bundle savedInstanceState) { answered = true; Bundle bundle = new Bundle(); bundle.putBoolean(EXTRA_EXPLICIT, true); - resultReceiver.send(Activity.RESULT_CANCELED, bundle); + if (resultReceiver != null) resultReceiver.send(Activity.RESULT_CANCELED, bundle); finish(); }) .create() @@ -103,7 +93,7 @@ public void onCreate(Bundle savedInstanceState) { protected void onDestroy() { super.onDestroy(); if (!answered) { - resultReceiver.send(Activity.RESULT_CANCELED, Bundle.EMPTY); + if (resultReceiver != null) resultReceiver.send(Activity.RESULT_CANCELED, Bundle.EMPTY); } database.close(); } diff --git a/play-services-core/src/main/kotlin/org/microg/gms/accountaction/ErrorResolver.kt b/play-services-core/src/main/kotlin/org/microg/gms/accountaction/ErrorResolver.kt index 8f9e403647..8c10544625 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/accountaction/ErrorResolver.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/accountaction/ErrorResolver.kt @@ -53,10 +53,10 @@ fun Context.resolveAuthErrorMessage(s: String): Resolution? = if (s.startsWith(" SettingsContract.CheckIn.LAST_CHECK_IN ) SettingsContract.getSettings(this, SettingsContract.CheckIn.getContentUri(this), settingsProjection) { cursor -> - //val checkInEnabled = cursor.getInt(0) != 0 + val checkInEnabled = cursor.getInt(0) != 0 val lastCheckIn = cursor.getLong(1) - if (lastCheckIn <= 0) { + if (lastCheckIn <= 0 || !checkInEnabled) { // user is also asked to enable checkin if there had never been a successful checkin (network errors?) actions += UserAction.ENABLE_CHECKIN } diff --git a/play-services-core/src/main/kotlin/org/microg/gms/accountaction/UserInterventionComponents.kt b/play-services-core/src/main/kotlin/org/microg/gms/accountaction/UserInterventionComponents.kt index a5b4177869..71a8cab2ff 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/accountaction/UserInterventionComponents.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/accountaction/UserInterventionComponents.kt @@ -1,5 +1,10 @@ package org.microg.gms.accountaction +import android.app.Activity +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.os.ResultReceiver import androidx.compose.foundation.Canvas import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -21,49 +26,78 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.google.android.gms.R import org.microg.gms.accountaction.UserAction.* +import org.microg.gms.common.Constants +import org.microg.gms.ui.AskPushPermission +import kotlin.coroutines.resume + +const val ACTION_CHECKIN = "org.microg.gms.settings.DEVICE_REGISTRATION_SETTINGS" +const val ACTION_GCM = "org.microg.gms.settings.GCM_SETTINGS" +const val ACTION_GCM_APP = "org.microg.gms.settings.GCM_APP_SETTINGS" +const val URI_GCM_MICROG_APP = "x-gms-settings://gcm/${Constants.GMS_PACKAGE_NAME}" @Composable fun UserInterventionComponents(userActions: Map) { for ((index, action) in userActions.entries.withIndex()) { + val context = LocalContext.current as Activity when (action.component1()) { ENABLE_CHECKIN -> UserInterventionCommonComponent( title = stringResource(id = R.string.auth_action_step_enable_checkin), description = stringResource(id = R.string.auth_action_step_enable_checkin_description), sequenceNumber = index + 1, completed = action.component2() - ) + ) { + Intent(ACTION_CHECKIN).let { context.startActivityForResult(it, 0) } + } ENABLE_GCM -> UserInterventionCommonComponent( title = stringResource(id = R.string.auth_action_step_enable_gcm), description = stringResource(id = R.string.auth_action_step_enable_gcm_description), sequenceNumber = index + 1, completed = action.component2() - ) + ) { + Intent(ACTION_GCM).let { context.startActivityForResult(it, 1) } + } ALLOW_MICROG_GCM -> UserInterventionCommonComponent( title = stringResource(id = R.string.auth_action_step_allow_microg_gcm), description = stringResource(id = R.string.auth_action_step_allow_microg_gcm_description), sequenceNumber = index + 1, completed = action.component2() - ) + ) { + Intent(context, AskPushPermission::class.java).apply { + putExtra(AskPushPermission.EXTRA_REQUESTED_PACKAGE, Constants.GMS_PACKAGE_NAME) + putExtra(AskPushPermission.EXTRA_FORCE_ASK, true) + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS) + addFlags(Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT) + }.let { context.startActivity(it) } + } ENABLE_LOCKSCREEN -> UserInterventionCommonComponent( title = stringResource(id = R.string.auth_action_step_enable_lockscreen), description = stringResource(id = R.string.auth_action_step_enable_lockscreen_description), sequenceNumber = index + 1, completed = action.component2() - ) + ) { + runCatching { + Intent(android.provider.Settings.ACTION_SECURITY_SETTINGS).let { context.startActivity(it) } + }.onFailure { + Intent(android.provider.Settings.ACTION_SETTINGS).let { context.startActivity(it) } + } + + } REAUTHENTICATE -> TODO() } } } @Composable -fun UserInterventionCommonComponent(title: String, description: String, sequenceNumber: Int?, completed: Boolean) { - Surface(onClick = { /*TODO*/ }) { +fun UserInterventionCommonComponent(title: String, description: String, sequenceNumber: Int?, completed: Boolean, onClick: () -> Unit) { + Surface(onClick = onClick, enabled = !completed) { val color = if (completed) { colorResource(id = R.color.material_success) diff --git a/play-services-core/src/main/res/layout/ask_gcm.xml b/play-services-core/src/main/res/layout/ask_gcm.xml index 76014fd39e..7d88f53e03 100644 --- a/play-services-core/src/main/res/layout/ask_gcm.xml +++ b/play-services-core/src/main/res/layout/ask_gcm.xml @@ -15,8 +15,11 @@ + android:layout_height="match_parent" + android:minWidth="240dp" + tools:theme="@style/Theme.App.DayNight.Dialog.Alert.NoActionBar"> + tools:layout="@layout/device_registration_fragment"> + + @@ -75,6 +79,9 @@ + Date: Wed, 10 Apr 2024 13:27:45 +0200 Subject: [PATCH 011/132] Show user action UI notification and update automatically --- .../accountaction/AccountActionActivity.kt | 47 ++++++-- .../gms/accountaction/AccountNotification.kt | 6 +- .../microg/gms/accountaction/ErrorResolver.kt | 106 +++++++++++------- .../microg/gms/accountaction/Resolution.kt | 20 +++- .../UserInterventionComponents.kt | 46 +++++--- 5 files changed, 144 insertions(+), 81 deletions(-) diff --git a/play-services-core/src/main/kotlin/org/microg/gms/accountaction/AccountActionActivity.kt b/play-services-core/src/main/kotlin/org/microg/gms/accountaction/AccountActionActivity.kt index 571dd842c6..18abbe1167 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/accountaction/AccountActionActivity.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/accountaction/AccountActionActivity.kt @@ -5,7 +5,6 @@ import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.annotation.RequiresApi import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState @@ -17,6 +16,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateListOf import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.colorResource @@ -25,18 +25,43 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.google.android.gms.R +internal const val INTENT_KEY_USER_ACTION = "userAction" +internal const val INTENT_KEY_ACCOUNT_NAME = "accountName" + @RequiresApi(21) class AccountActionActivity : ComponentActivity() { + + // mutableStateMapOf() returns an unordered map + val taskMap: MutableList> = mutableStateListOf() + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + + if (savedInstanceState == null) { + val requirements = intent.getSerializableExtra(INTENT_KEY_USER_ACTION) as Array + taskMap.addAll(requirements.map { it to false }) + } + + val accountName = intent.getStringExtra(INTENT_KEY_ACCOUNT_NAME) ?: "" + setContent { - org.microg.gms.accountaction.Preview() + Content(accountName, taskMap.toMap()) { + finish() + } + } + } + + override fun onResume() { + super.onResume() + + for ((index, task) in taskMap.withIndex()) { + taskMap[index] = task.component1() to checkRequirementSatisfied(task.component1()) } } } @Composable -fun Content(userTasks: Map, finish: () -> Unit) { +fun Content(accountName: String, taskMap: Map, finish: () -> Unit) { Column { Column { Text( @@ -45,7 +70,7 @@ fun Content(userTasks: Map, finish: () -> Unit) { style = MaterialTheme.typography.headlineLarge ) Text( - text = stringResource(id = R.string.auth_action_activity_explanation, "admin@fynngodau.de"), + text = stringResource(id = R.string.auth_action_activity_explanation, accountName), modifier = Modifier.padding(16.dp), style = MaterialTheme.typography.bodyMedium ) @@ -53,10 +78,11 @@ fun Content(userTasks: Map, finish: () -> Unit) { } Surface(Modifier.fillMaxHeight()) { Column(Modifier.verticalScroll(rememberScrollState())) { - UserInterventionComponents(userActions = userTasks) + UserInterventionComponents(userActions = taskMap) + Button( onClick = finish, - enabled = false, + enabled = !taskMap.containsValue(false), colors = ButtonDefaults.buttonColors( containerColor = colorResource( id = R.color.login_blue_theme_primary @@ -77,11 +103,12 @@ fun Content(userTasks: Map, finish: () -> Unit) { @Composable fun Preview() { Content( + "admin@example.com", mapOf( - UserAction.ENABLE_CHECKIN to true, - UserAction.ENABLE_GCM to true, - UserAction.ALLOW_MICROG_GCM to false, - UserAction.ENABLE_LOCKSCREEN to false + Requirement.ENABLE_CHECKIN to true, + Requirement.ENABLE_GCM to true, + Requirement.ALLOW_MICROG_GCM to false, + Requirement.ENABLE_LOCKSCREEN to false ) ) {} } \ No newline at end of file diff --git a/play-services-core/src/main/kotlin/org/microg/gms/accountaction/AccountNotification.kt b/play-services-core/src/main/kotlin/org/microg/gms/accountaction/AccountNotification.kt index db96cc159b..bed4dbd578 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/accountaction/AccountNotification.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/accountaction/AccountNotification.kt @@ -18,16 +18,14 @@ import com.google.android.gms.R private const val CHANNEL_ID = "AccountNotification" -internal const val INTENT_KEY_USER_ACTION = "userAction" -internal const val INTENT_KEY_ACCOUNT_NAME = "accountName" @RequiresApi(21) -fun Context.sendAccountActionNotification(account: Account, action: UserAction) { +fun Context.sendAccountActionNotification(account: Account, action: UserSatisfyRequirements) { registerAccountNotificationChannel() val intent: PendingIntent =Intent(this, AccountActionActivity::class.java).apply { - putExtra(INTENT_KEY_USER_ACTION, action) + putExtra(INTENT_KEY_USER_ACTION, action.actions.toTypedArray()) putExtra(INTENT_KEY_ACCOUNT_NAME, account.name) }.let { PendingIntent.getActivity( diff --git a/play-services-core/src/main/kotlin/org/microg/gms/accountaction/ErrorResolver.kt b/play-services-core/src/main/kotlin/org/microg/gms/accountaction/ErrorResolver.kt index 8c10544625..96c7b3bc57 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/accountaction/ErrorResolver.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/accountaction/ErrorResolver.kt @@ -2,6 +2,7 @@ package org.microg.gms.accountaction import android.accounts.Account import android.content.Context +import android.os.Build import android.util.Log import kotlinx.coroutines.runBlocking import org.microg.gms.common.Constants @@ -33,6 +34,11 @@ const val DEVICE_MANAGEMENT_REQUIRED = "DeviceManagementRequired" */ const val DEVICE_MANAGEMENT_ADMIN_PENDING_APPROVAL = "DeviceManagementAdminPendingApproval" +/** + * Indicates that the token stored on the device is no longer valid. + */ +const val BAD_AUTHENTICATION = "BadAuthentication" + const val TAG = "GmsAccountErrorResolve" /** @@ -42,58 +48,65 @@ const val TAG = "GmsAccountErrorResolve" fun Context.resolveAuthErrorMessage(s: String): Resolution? = if (s.startsWith("Error=")) { resolveAuthErrorMessage(s.drop("Error=".length)) } else when (s) { - DEVICE_MANAGEMENT_SCREENLOCK_REQUIRED -> { + DEVICE_MANAGEMENT_SCREENLOCK_REQUIRED -> listOf( + Requirement.ENABLE_CHECKIN, + Requirement.ENABLE_GCM, + Requirement.ALLOW_MICROG_GCM, + Requirement.ENABLE_LOCKSCREEN + ) + .associateWith { checkRequirementSatisfied(it) } + .filterValues { satisfied -> !satisfied }.let { + if (it.isEmpty()) { + // all requirements are satisfied, crypt auth sync keys can be run + CryptAuthSyncKeys + } else { + // prompt user to satisfy missing requirements + UserSatisfyRequirements(it.keys) + } + } - val actions = mutableSetOf() + DEVICE_MANAGEMENT_ADMIN_PENDING_APPROVAL, DEVICE_MANAGEMENT_REQUIRED -> + NoResolution(NoResolutionReason.ADVANCED_DEVICE_MANAGEMENT_NOT_SUPPORTED) + BAD_AUTHENTICATION -> Reauthenticate + else -> null +}.also { Log.d(TAG, "Error was: $s. Diagnosis: $it.") } - val settingsProjection = arrayOf( - SettingsContract.CheckIn.ENABLED, - SettingsContract.CheckIn.LAST_CHECK_IN - ) - SettingsContract.getSettings(this, SettingsContract.CheckIn.getContentUri(this), settingsProjection) { cursor -> - val checkInEnabled = cursor.getInt(0) != 0 - val lastCheckIn = cursor.getLong(1) +fun Context.checkRequirementSatisfied(requirement: Requirement): Boolean = when (requirement) { + Requirement.ENABLE_CHECKIN -> isCheckinEnabled() + Requirement.ENABLE_GCM -> isGcmEnabled() + Requirement.ALLOW_MICROG_GCM -> isMicrogAppGcmAllowed() + Requirement.ENABLE_LOCKSCREEN -> isLockscreenConfigured() +} - if (lastCheckIn <= 0 || !checkInEnabled) { - // user is also asked to enable checkin if there had never been a successful checkin (network errors?) - actions += UserAction.ENABLE_CHECKIN - } - } +fun Context.isCheckinEnabled(): Boolean { + val settingsProjection = arrayOf( + SettingsContract.CheckIn.ENABLED, + SettingsContract.CheckIn.LAST_CHECK_IN + ) + return SettingsContract.getSettings(this, SettingsContract.CheckIn.getContentUri(this), settingsProjection) { cursor -> + val checkInEnabled = cursor.getInt(0) != 0 + val lastCheckIn = cursor.getLong(1) + + // user is also asked to enable checkin if there had never been a successful checkin (network errors?) + lastCheckIn > 0 && checkInEnabled + } +} - val gcmPrefs = GcmPrefs.get(this) - if (!gcmPrefs.isEnabled) { - actions += UserAction.ENABLE_GCM - } +fun Context.isGcmEnabled(): Boolean = GcmPrefs.get(this).isEnabled - val gcmDatabaseEntry = GcmDatabase(this).use { - it.getApp(Constants.GMS_PACKAGE_NAME) - } - if (gcmDatabaseEntry != null && +fun Context.isMicrogAppGcmAllowed(): Boolean { + val gcmPrefs = GcmPrefs.get(this) + val gcmDatabaseEntry = GcmDatabase(this).use { + it.getApp(Constants.GMS_PACKAGE_NAME) + } + return !(gcmDatabaseEntry != null && !gcmDatabaseEntry.allowRegister || gcmDatabaseEntry == null && - gcmPrefs.confirmNewApps - ) { - actions += UserAction.ALLOW_MICROG_GCM - } - - if (!isLockscreenConfigured()) { - actions += UserAction.ENABLE_LOCKSCREEN - } - - if (actions.isEmpty()) { - CryptAuthSyncKeys - } else { - UserIntervention(actions) - } - } - - DEVICE_MANAGEMENT_ADMIN_PENDING_APPROVAL, DEVICE_MANAGEMENT_REQUIRED -> - NoResolution(NoResolutionReason.ADVANCED_DEVICE_MANAGEMENT_NOT_SUPPORTED) - else -> null -}.also { Log.d(TAG, "Error was: $s. Diagnosis: $it.") } + gcmPrefs.confirmNewApps) +} fun Resolution.initiateFromBackgroundBlocking(context: Context, account: Account, retryFunction: RetryFunction): T? { when (this) { @@ -108,8 +121,15 @@ fun Resolution.initiateFromBackgroundBlocking(context: Context, account: Acc Log.w(TAG, "This account cannot be used with microG due to $reason") return null } - is UserIntervention -> { + is UserSatisfyRequirements -> { Log.w(TAG, "User intervention required! You need to ${actions.joinToString(", ")}.") + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + context.sendAccountActionNotification(account, this) + } + return null + } + Reauthenticate -> { + Log.w(TAG, "Your account credentials have expired! Please remove the account, then sign in again.") return null } } diff --git a/play-services-core/src/main/kotlin/org/microg/gms/accountaction/Resolution.kt b/play-services-core/src/main/kotlin/org/microg/gms/accountaction/Resolution.kt index befa6a4b7a..84c26361d8 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/accountaction/Resolution.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/accountaction/Resolution.kt @@ -1,5 +1,7 @@ package org.microg.gms.accountaction +import java.io.Serializable + sealed class Resolution /** @@ -11,19 +13,25 @@ sealed class Resolution data object CryptAuthSyncKeys : Resolution() /** - * Represents a situation in which user actions are required to fix - * the problem. + * Represents a situation in which user actions are required to satisfy + * the requirements that need to be fulfilled before the problem can be + * fixed. */ -data class UserIntervention(val actions: Set) : Resolution() +data class UserSatisfyRequirements(val actions: Set) : Resolution(), Serializable -enum class UserAction { +enum class Requirement { ENABLE_CHECKIN, ENABLE_GCM, ALLOW_MICROG_GCM, - ENABLE_LOCKSCREEN, - REAUTHENTICATE + ENABLE_LOCKSCREEN } +/** + * Represents a situation in which the user's authentication has become + * invalid, and they need to enter their credentials again. + */ +data object Reauthenticate : Resolution() + /** * Represents a situation that is known to be unsupported by microG. * Advise the user to remove the account. diff --git a/play-services-core/src/main/kotlin/org/microg/gms/accountaction/UserInterventionComponents.kt b/play-services-core/src/main/kotlin/org/microg/gms/accountaction/UserInterventionComponents.kt index 71a8cab2ff..3149b40b94 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/accountaction/UserInterventionComponents.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/accountaction/UserInterventionComponents.kt @@ -2,9 +2,6 @@ package org.microg.gms.accountaction import android.app.Activity import android.content.Intent -import android.net.Uri -import android.os.Bundle -import android.os.ResultReceiver import androidx.compose.foundation.Canvas import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -17,6 +14,7 @@ import androidx.compose.foundation.layout.width import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight import androidx.compose.material.icons.filled.Check +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme @@ -32,25 +30,23 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.google.android.gms.R -import org.microg.gms.accountaction.UserAction.* +import org.microg.gms.accountaction.Requirement.* import org.microg.gms.common.Constants import org.microg.gms.ui.AskPushPermission -import kotlin.coroutines.resume -const val ACTION_CHECKIN = "org.microg.gms.settings.DEVICE_REGISTRATION_SETTINGS" +const val ACTION_CHECKIN = "org.microg.gms.settings.CHECKIN_SETTINGS" const val ACTION_GCM = "org.microg.gms.settings.GCM_SETTINGS" -const val ACTION_GCM_APP = "org.microg.gms.settings.GCM_APP_SETTINGS" -const val URI_GCM_MICROG_APP = "x-gms-settings://gcm/${Constants.GMS_PACKAGE_NAME}" @Composable -fun UserInterventionComponents(userActions: Map) { +fun UserInterventionComponents(userActions: Map) { for ((index, action) in userActions.entries.withIndex()) { val context = LocalContext.current as Activity + val displayIndex = if (userActions.size > 1) index + 1 else null when (action.component1()) { ENABLE_CHECKIN -> UserInterventionCommonComponent( title = stringResource(id = R.string.auth_action_step_enable_checkin), description = stringResource(id = R.string.auth_action_step_enable_checkin_description), - sequenceNumber = index + 1, + sequenceNumber = displayIndex, completed = action.component2() ) { Intent(ACTION_CHECKIN).let { context.startActivityForResult(it, 0) } @@ -58,7 +54,7 @@ fun UserInterventionComponents(userActions: Map) { ENABLE_GCM -> UserInterventionCommonComponent( title = stringResource(id = R.string.auth_action_step_enable_gcm), description = stringResource(id = R.string.auth_action_step_enable_gcm_description), - sequenceNumber = index + 1, + sequenceNumber = displayIndex, completed = action.component2() ) { Intent(ACTION_GCM).let { context.startActivityForResult(it, 1) } @@ -66,7 +62,7 @@ fun UserInterventionComponents(userActions: Map) { ALLOW_MICROG_GCM -> UserInterventionCommonComponent( title = stringResource(id = R.string.auth_action_step_allow_microg_gcm), description = stringResource(id = R.string.auth_action_step_allow_microg_gcm_description), - sequenceNumber = index + 1, + sequenceNumber = displayIndex, completed = action.component2() ) { Intent(context, AskPushPermission::class.java).apply { @@ -80,7 +76,7 @@ fun UserInterventionComponents(userActions: Map) { ENABLE_LOCKSCREEN -> UserInterventionCommonComponent( title = stringResource(id = R.string.auth_action_step_enable_lockscreen), description = stringResource(id = R.string.auth_action_step_enable_lockscreen_description), - sequenceNumber = index + 1, + sequenceNumber = displayIndex, completed = action.component2() ) { runCatching { @@ -90,7 +86,6 @@ fun UserInterventionComponents(userActions: Map) { } } - REAUTHENTICATE -> TODO() } } } @@ -126,9 +121,15 @@ fun UserInterventionCommonComponent(title: String, description: String, sequence tint = Color.White ) } else { - sequenceNumber?.let { + if (sequenceNumber == null) { + Canvas(modifier = Modifier.size(12.dp).align(Alignment.Center)) { + drawCircle( + color = Color.White + ) + } + } else { Text( - text = it.toString(), + text = sequenceNumber.toString(), modifier = Modifier.align(Alignment.Center), style = LocalTextStyle.current.copy(color = Color.White) ) @@ -166,8 +167,17 @@ fun UserInterventionCommonComponent(title: String, description: String, sequence @Composable fun PreviewInterventionComponent() { Column { - UserInterventionComponents(userActions = - mapOf(ENABLE_CHECKIN to true, ENABLE_GCM to true, ALLOW_MICROG_GCM to false, ENABLE_LOCKSCREEN to false) + UserInterventionComponents( + userActions = mapOf( + ENABLE_CHECKIN to true, + ENABLE_GCM to true, + ALLOW_MICROG_GCM to false, + ENABLE_LOCKSCREEN to false + ) + ) + HorizontalDivider() + UserInterventionComponents( + userActions = mapOf(ENABLE_LOCKSCREEN to false) ) } } \ No newline at end of file From b30ad74340213a70eee9a41898d5d27770388f5b Mon Sep 17 00:00:00 2001 From: Fynn Godau Date: Wed, 10 Apr 2024 13:28:55 +0200 Subject: [PATCH 012/132] Revert "Call sync keys request before opening account settings" This reverts commit 6ca10d6919629af54b5692df65673a37f755061d. --- .../org/microg/gms/accountsettings/ui/WebViewHelper.kt | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/WebViewHelper.kt b/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/WebViewHelper.kt index 4534818ea1..a65b7a0de1 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/WebViewHelper.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/WebViewHelper.kt @@ -23,7 +23,6 @@ import kotlinx.coroutines.withContext import org.microg.gms.auth.AuthManager import org.microg.gms.common.Constants.GMS_PACKAGE_NAME import org.microg.gms.common.PackageUtils -import org.microg.gms.cryptauth.syncCryptAuthKeys import java.net.URLEncoder import java.util.* @@ -100,10 +99,8 @@ class WebViewHelper(private val activity: AppCompatActivity, private val webView } } - private suspend fun openWebWithAccount(accountName: String, url: String?) { + private fun openWebWithAccount(accountName: String, url: String?) { try { - syncCryptAuthKeys(activity, accountName) - val service = "weblogin:continue=" + URLEncoder.encode(url, "utf-8") val authManager = AuthManager(activity, accountName, GMS_PACKAGE_NAME, service) val authUrl = authManager.requestAuth(false)?.auth From 02034f16eab5a57574e889e85a40d9f988c78316 Mon Sep 17 00:00:00 2001 From: Fynn Godau Date: Wed, 10 Apr 2024 15:47:48 +0200 Subject: [PATCH 013/132] Try to imitate original instance id query more closely --- .../microg/gms/cryptauth/SyncKeysRequest.kt | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/play-services-core/src/main/kotlin/org/microg/gms/cryptauth/SyncKeysRequest.kt b/play-services-core/src/main/kotlin/org/microg/gms/cryptauth/SyncKeysRequest.kt index b233b9bab0..8db07cc130 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/cryptauth/SyncKeysRequest.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/cryptauth/SyncKeysRequest.kt @@ -11,6 +11,7 @@ import cryptauthv2.ClientAppMetadata import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.withContext +import okio.ByteString.Companion.toByteString import org.json.JSONArray import org.json.JSONObject import org.microg.gms.auth.AuthConstants @@ -36,7 +37,7 @@ private const val TAG = "SyncKeysRequest" private const val GCM_REGISTER_SENDER = "745476177629" private const val GCM_REGISTER_SUBTYPE = "745476177629" -private const val GCM_REGISTER_SUBSCIPTION = "745476177629" +private const val GCM_REGISTER_SUBSCRIPTION = "745476177629" private const val GCM_REGISTER_SCOPE = "GCM" private const val AFTER_REQUEST_DELAY = 500L @@ -49,12 +50,20 @@ suspend fun syncCryptAuthKeys(context: Context, accountName: String): JSONObject // Instance ID token for use in CryptAuth query val instanceToken = completeRegisterRequest(context, GcmDatabase(context), RegisterRequest().build(context) .checkin(lastCheckinInfo) - .app("com.google.android.gms", Constants.GMS_PACKAGE_SIGNATURE_SHA1, BuildConfig.VERSION_CODE) + .app("com.google.android.gms", CERTIFICATE.lowercase(), BuildConfig.VERSION_CODE) .sender(GCM_REGISTER_SENDER) - .extraParam("subscription", GCM_REGISTER_SUBSCIPTION) - .extraParam("X-subscription", GCM_REGISTER_SUBSCIPTION) + .extraParam("subscription", GCM_REGISTER_SUBSCRIPTION) + .extraParam("X-subscription", GCM_REGISTER_SUBSCRIPTION) .extraParam("subtype", GCM_REGISTER_SUBTYPE) .extraParam("X-subtype", GCM_REGISTER_SUBTYPE) + .extraParam("app_ver", BuildConfig.VERSION_CODE.toString()) + .extraParam("osv", "30") + .extraParam("cliv", "iid-202414000") + .extraParam("gmsv", BuildConfig.VERSION_CODE.toString()) + .extraParam("app_ver_name","%09d".format(BuildConfig.VERSION_CODE).let { + "${it.substring(0, 2)}.${it.substring(2, 4)}.${it.substring(4, 6)} (190800-{{cl}})" + }) + .info("s_mGjPgQdoQeQEb1aBJ6XhxiEx997Bg") .extraParam("scope", GCM_REGISTER_SCOPE) ).let { if (!it.containsKey(GcmConstants.EXTRA_REGISTRATION_ID)) { @@ -74,6 +83,7 @@ suspend fun syncCryptAuthKeys(context: Context, accountName: String): JSONObject val clientAppMetadata = ClientAppMetadata( application_specific_metadata = listOf( ApplicationSpecificMetadata( + gcm_registration_id = instanceToken.toByteArray().toByteString(), notification_enabled = true, device_software_version = "%09d".format(BuildConfig.VERSION_CODE).let { "${it.substring(0, 2)}.${it.substring(2, 4)}.${it.substring(4, 6)} (190800-{{cl}})" From 889c9d90086781b78399f9f02e0a1c41fe8c8dda Mon Sep 17 00:00:00 2001 From: Fynn Godau Date: Fri, 24 May 2024 12:32:14 +0200 Subject: [PATCH 014/132] Apply review --- .../src/main/java/org/microg/gms/ui/AskPushPermission.java | 6 +++--- .../org/microg/gms/accountaction/AccountActionActivity.kt | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/play-services-core/src/main/java/org/microg/gms/ui/AskPushPermission.java b/play-services-core/src/main/java/org/microg/gms/ui/AskPushPermission.java index 2c69fc1144..a99aeeb959 100644 --- a/play-services-core/src/main/java/org/microg/gms/ui/AskPushPermission.java +++ b/play-services-core/src/main/java/org/microg/gms/ui/AskPushPermission.java @@ -38,14 +38,14 @@ public void onCreate(Bundle savedInstanceState) { packageName = getIntent().getStringExtra(EXTRA_REQUESTED_PACKAGE); resultReceiver = getIntent().getParcelableExtra(EXTRA_RESULT_RECEIVER); - boolean force = getIntent().getBooleanExtra(EXTRA_FORCE_ASK, false); - if (packageName == null || (resultReceiver == null && !force)) { + boolean forceAsk = getIntent().getBooleanExtra(EXTRA_FORCE_ASK, false); + if (packageName == null || (resultReceiver == null && !forceAsk)) { answered = true; finish(); return; } - if (!force && database.getApp(packageName) != null) { + if (!forceAsk && database.getApp(packageName) != null) { resultReceiver.send(Activity.RESULT_OK, Bundle.EMPTY); answered = true; finish(); diff --git a/play-services-core/src/main/kotlin/org/microg/gms/accountaction/AccountActionActivity.kt b/play-services-core/src/main/kotlin/org/microg/gms/accountaction/AccountActionActivity.kt index 18abbe1167..b7663d578d 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/accountaction/AccountActionActivity.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/accountaction/AccountActionActivity.kt @@ -32,7 +32,7 @@ internal const val INTENT_KEY_ACCOUNT_NAME = "accountName" class AccountActionActivity : ComponentActivity() { // mutableStateMapOf() returns an unordered map - val taskMap: MutableList> = mutableStateListOf() + private val taskMap: MutableList> = mutableStateListOf() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) From 826b287e35b07bb0be1376e8aeedf9ee6262aa19 Mon Sep 17 00:00:00 2001 From: Fynn Godau Date: Fri, 24 May 2024 14:22:56 +0200 Subject: [PATCH 015/132] Open account action activity directly in some cases --- .../java/org/microg/gms/auth/AuthManager.java | 23 +++++++++++++++ .../accountaction/AccountActionActivity.kt | 11 +++++++ .../gms/accountaction/AccountNotification.kt | 5 +--- .../microg/gms/accountaction/ErrorResolver.kt | 29 +++++++++++++++++++ .../gms/accountsettings/ui/WebViewHelper.kt | 2 +- .../microg/gms/cryptauth/SyncKeysRequest.kt | 2 +- 6 files changed, 66 insertions(+), 6 deletions(-) diff --git a/play-services-core/src/main/java/org/microg/gms/auth/AuthManager.java b/play-services-core/src/main/java/org/microg/gms/auth/AuthManager.java index 492a8e551c..b3bb57e309 100644 --- a/play-services-core/src/main/java/org/microg/gms/auth/AuthManager.java +++ b/play-services-core/src/main/java/org/microg/gms/auth/AuthManager.java @@ -277,6 +277,29 @@ public AuthResponse requestAuthWithBackgroundResolution(boolean legacy) throws I } } + public AuthResponse requestAuthWithForegroundResolution(boolean legacy) throws IOException { + try { + return requestAuth(legacy); + } catch (NotOkayException e) { + if (e.getMessage() != null) { + Resolution errorResolution = ErrorResolverKt.resolveAuthErrorMessage(context, e.getMessage()); + if (errorResolution != null) { + return ErrorResolverKt.initiateFromForegroundBlocking( + errorResolution, + context, + getAccount(), + // infinite loop is prevented + () -> requestAuth(legacy) + ); + } else { + throw new IOException(e); + } + } else { + throw new IOException(e); + } + } + } + public AuthResponse requestAuth(boolean legacy) throws IOException { if (service.equals(AuthConstants.SCOPE_GET_ACCOUNT_ID)) { AuthResponse response = new AuthResponse(); diff --git a/play-services-core/src/main/kotlin/org/microg/gms/accountaction/AccountActionActivity.kt b/play-services-core/src/main/kotlin/org/microg/gms/accountaction/AccountActionActivity.kt index b7663d578d..d7fa0b24de 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/accountaction/AccountActionActivity.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/accountaction/AccountActionActivity.kt @@ -1,5 +1,8 @@ package org.microg.gms.accountaction +import android.accounts.Account +import android.content.Context +import android.content.Intent import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent @@ -58,6 +61,14 @@ class AccountActionActivity : ComponentActivity() { taskMap[index] = task.component1() to checkRequirementSatisfied(task.component1()) } } + + companion object { + fun createIntent(context: Context, account: Account, action: UserSatisfyRequirements) = + Intent(context, AccountActionActivity::class.java).apply { + putExtra(INTENT_KEY_USER_ACTION, action.actions.toTypedArray()) + putExtra(INTENT_KEY_ACCOUNT_NAME, account.name) + } + } } @Composable diff --git a/play-services-core/src/main/kotlin/org/microg/gms/accountaction/AccountNotification.kt b/play-services-core/src/main/kotlin/org/microg/gms/accountaction/AccountNotification.kt index bed4dbd578..1dec71ac1c 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/accountaction/AccountNotification.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/accountaction/AccountNotification.kt @@ -24,10 +24,7 @@ fun Context.sendAccountActionNotification(account: Account, action: UserSatisfyR registerAccountNotificationChannel() - val intent: PendingIntent =Intent(this, AccountActionActivity::class.java).apply { - putExtra(INTENT_KEY_USER_ACTION, action.actions.toTypedArray()) - putExtra(INTENT_KEY_ACCOUNT_NAME, account.name) - }.let { + val intent: PendingIntent = AccountActionActivity.createIntent(this, account, action).let { PendingIntent.getActivity( this, account.hashCode(), diff --git a/play-services-core/src/main/kotlin/org/microg/gms/accountaction/ErrorResolver.kt b/play-services-core/src/main/kotlin/org/microg/gms/accountaction/ErrorResolver.kt index 96c7b3bc57..1389ed9bdd 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/accountaction/ErrorResolver.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/accountaction/ErrorResolver.kt @@ -135,6 +135,35 @@ fun Resolution.initiateFromBackgroundBlocking(context: Context, account: Acc } } +fun Resolution.initiateFromForegroundBlocking(context: Context, account: Account, retryFunction: RetryFunction): T? { + when (this) { + CryptAuthSyncKeys -> { + Log.d(TAG, "Resolving account error by performing cryptauth sync keys call.") + runBlocking { + syncCryptAuthKeys(context, account) + } + return retryFunction.run() + } + is NoResolution -> { + Log.w(TAG, "This account cannot be used with microG due to $reason") + return null + } + is UserSatisfyRequirements -> { + Log.w(TAG, "User intervention required! You need to ${actions.joinToString(", ")}.") + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + AccountActionActivity.createIntent(context, account, this).let { + context.startActivity(it) + } + } + return null + } + Reauthenticate -> { + Log.w(TAG, "Your account credentials have expired! Please remove the account, then sign in again.") + return null + } + } +} + interface RetryFunction { @Throws(IOException::class) fun run(): T diff --git a/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/WebViewHelper.kt b/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/WebViewHelper.kt index a65b7a0de1..71bebf9f90 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/WebViewHelper.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/WebViewHelper.kt @@ -103,7 +103,7 @@ class WebViewHelper(private val activity: AppCompatActivity, private val webView try { val service = "weblogin:continue=" + URLEncoder.encode(url, "utf-8") val authManager = AuthManager(activity, accountName, GMS_PACKAGE_NAME, service) - val authUrl = authManager.requestAuth(false)?.auth + val authUrl = authManager.requestAuthWithForegroundResolution(false)?.auth if (authUrl?.contains("WILL_NOT_SIGN_IN") == true) { throw RuntimeException("Would not sign in") } diff --git a/play-services-core/src/main/kotlin/org/microg/gms/cryptauth/SyncKeysRequest.kt b/play-services-core/src/main/kotlin/org/microg/gms/cryptauth/SyncKeysRequest.kt index 8db07cc130..0c66d89645 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/cryptauth/SyncKeysRequest.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/cryptauth/SyncKeysRequest.kt @@ -119,7 +119,7 @@ suspend fun syncCryptAuthKeys(context: Context, accountName: String): JSONObject "syncSingleKeyRequests" to jsonArrayOf( jsonObjectOf( "keyName" to "PublicKey", - "keyHandles" to "ZGV2aWNlX2tleQo=" // base64 for `device_key`< + "keyHandles" to "ZGV2aWNlX2tleQo=" // base64 for `device_key` ) ), "clientMetadata" to jsonObjectOf( From 2a7b610826a2c0c99a2db23c2ce4b0ef1c1a0fe2 Mon Sep 17 00:00:00 2001 From: Fynn Godau Date: Fri, 24 May 2024 15:39:38 +0200 Subject: [PATCH 016/132] A step in the right direction? --- .../microg/gms/cryptauth/SyncKeysRequest.kt | 48 ++++++++++++++----- 1 file changed, 36 insertions(+), 12 deletions(-) diff --git a/play-services-core/src/main/kotlin/org/microg/gms/cryptauth/SyncKeysRequest.kt b/play-services-core/src/main/kotlin/org/microg/gms/cryptauth/SyncKeysRequest.kt index 0c66d89645..893b7a13a7 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/cryptauth/SyncKeysRequest.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/cryptauth/SyncKeysRequest.kt @@ -3,6 +3,7 @@ package org.microg.gms.cryptauth import android.accounts.Account import android.app.KeyguardManager import android.content.Context +import android.util.Base64 import android.util.Log import com.google.android.gms.BuildConfig import com.google.android.gms.common.Scopes @@ -28,6 +29,8 @@ import org.microg.gms.profile.Build import java.io.IOException import java.net.HttpURLConnection import java.net.URL +import java.security.KeyPairGenerator +import java.security.MessageDigest import kotlin.math.roundToInt private const val SYNC_KEY_URL = "https://cryptauthenrollment.googleapis.com/v1:syncKeys" @@ -35,12 +38,16 @@ private const val API_KEY = "AIzaSyAP-gfH3qvi6vgHZbSYwQ_XHqV_mXHhzIk" private const val CERTIFICATE = "58E1C4133F7441EC3D2C270270A14802DA47BA0E" private const val TAG = "SyncKeysRequest" -private const val GCM_REGISTER_SENDER = "745476177629" -private const val GCM_REGISTER_SUBTYPE = "745476177629" -private const val GCM_REGISTER_SUBSCRIPTION = "745476177629" +private const val GCM_REGISTER_SENDER = "16502139086" +private const val GCM_REGISTER_SUBTYPE = "16502139086" +private const val GCM_REGISTER_SUBSCRIPTION = "16502139086" private const val GCM_REGISTER_SCOPE = "GCM" +private const val GCM_REGISTER_CLIV = "iid-202414000" +private const val GCM_REGISTER_INFO = "0wqs6iYsl_URQEb1aBJ6XhzCbHSr-hg" -private const val AFTER_REQUEST_DELAY = 500L +private const val RSA_KEY_SIZE = 2048 + +private const val AFTER_REQUEST_DELAY = 750L suspend fun syncCryptAuthKeys(context: Context, account: Account): JSONObject? = syncCryptAuthKeys(context, account.name) suspend fun syncCryptAuthKeys(context: Context, accountName: String): JSONObject? { @@ -48,6 +55,7 @@ suspend fun syncCryptAuthKeys(context: Context, accountName: String): JSONObject val lastCheckinInfo = LastCheckinInfo.read(context) // Instance ID token for use in CryptAuth query + val instanceId = generateAppId() val instanceToken = completeRegisterRequest(context, GcmDatabase(context), RegisterRequest().build(context) .checkin(lastCheckinInfo) .app("com.google.android.gms", CERTIFICATE.lowercase(), BuildConfig.VERSION_CODE) @@ -57,14 +65,15 @@ suspend fun syncCryptAuthKeys(context: Context, accountName: String): JSONObject .extraParam("subtype", GCM_REGISTER_SUBTYPE) .extraParam("X-subtype", GCM_REGISTER_SUBTYPE) .extraParam("app_ver", BuildConfig.VERSION_CODE.toString()) - .extraParam("osv", "30") - .extraParam("cliv", "iid-202414000") + .extraParam("osv", "29") + .extraParam("cliv", GCM_REGISTER_CLIV) .extraParam("gmsv", BuildConfig.VERSION_CODE.toString()) + .extraParam("appid", instanceId) + .extraParam("scope", GCM_REGISTER_SCOPE) .extraParam("app_ver_name","%09d".format(BuildConfig.VERSION_CODE).let { "${it.substring(0, 2)}.${it.substring(2, 4)}.${it.substring(4, 6)} (190800-{{cl}})" }) - .info("s_mGjPgQdoQeQEb1aBJ6XhxiEx997Bg") - .extraParam("scope", GCM_REGISTER_SCOPE) + .info(GCM_REGISTER_INFO) ).let { if (!it.containsKey(GcmConstants.EXTRA_REGISTRATION_ID)) { Log.d(TAG, "No instance ID was gathered. Is GCM enabled, has there been a checkin?") @@ -72,7 +81,6 @@ suspend fun syncCryptAuthKeys(context: Context, accountName: String): JSONObject } it.getString(GcmConstants.EXTRA_REGISTRATION_ID)!! } - val instanceId = instanceToken.split(':')[0] // CryptAuth sync request tells server whether or not screenlock is enabled val cryptauthService = AuthConstants.SCOPE_OAUTH2 + Scopes.CRYPTAUTH @@ -172,12 +180,28 @@ suspend fun syncCryptAuthKeys(context: Context, accountName: String): JSONObject } } +/** + * Generates an app / instance ID based on the hash of the public key of an RSA keypair. + * The key itself is never used. + */ +fun generateAppId(): String { + val rsaGenerator = KeyPairGenerator.getInstance("RSA") + rsaGenerator.initialize(RSA_KEY_SIZE) + val keyPair = rsaGenerator.generateKeyPair() + + val digest = MessageDigest.getInstance("SHA1").digest(keyPair.public.encoded) + digest[0] = ((112 + (0xF and digest[0].toInt())) and 0xFF).toByte() + return Base64.encodeToString( + digest, 0, 8, Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING + ) +} + fun Context.isLockscreenConfigured(): Boolean { val service: KeyguardManager = getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { - return service.isDeviceSecure + return if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { + service.isDeviceSecure } else { - return service.isKeyguardSecure + service.isKeyguardSecure } } From 03a7ac17569a5cf754c15dbb9227e26e82ebf701 Mon Sep 17 00:00:00 2001 From: Fynn Godau Date: Sun, 4 Aug 2024 13:08:10 +0200 Subject: [PATCH 017/132] Complete cryptauth flow with EnrollKeys step --- .../microg/gms/accountaction/ErrorResolver.kt | 6 +- .../org/microg/gms/cryptauth/CryptAuthFlow.kt | 153 +++++++++++++ .../microg/gms/cryptauth/CryptAuthRequests.kt | 138 ++++++++++++ .../microg/gms/cryptauth/SyncKeysRequest.kt | 209 ------------------ 4 files changed, 294 insertions(+), 212 deletions(-) create mode 100644 play-services-core/src/main/kotlin/org/microg/gms/cryptauth/CryptAuthFlow.kt create mode 100644 play-services-core/src/main/kotlin/org/microg/gms/cryptauth/CryptAuthRequests.kt delete mode 100644 play-services-core/src/main/kotlin/org/microg/gms/cryptauth/SyncKeysRequest.kt diff --git a/play-services-core/src/main/kotlin/org/microg/gms/accountaction/ErrorResolver.kt b/play-services-core/src/main/kotlin/org/microg/gms/accountaction/ErrorResolver.kt index 1389ed9bdd..45e629b042 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/accountaction/ErrorResolver.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/accountaction/ErrorResolver.kt @@ -7,7 +7,7 @@ import android.util.Log import kotlinx.coroutines.runBlocking import org.microg.gms.common.Constants import org.microg.gms.cryptauth.isLockscreenConfigured -import org.microg.gms.cryptauth.syncCryptAuthKeys +import org.microg.gms.cryptauth.sendDeviceScreenlockState import org.microg.gms.gcm.GcmDatabase import org.microg.gms.gcm.GcmPrefs import org.microg.gms.settings.SettingsContract @@ -113,7 +113,7 @@ fun Resolution.initiateFromBackgroundBlocking(context: Context, account: Acc CryptAuthSyncKeys -> { Log.d(TAG, "Resolving account error by performing cryptauth sync keys call.") runBlocking { - syncCryptAuthKeys(context, account) + context.sendDeviceScreenlockState(account) } return retryFunction.run() } @@ -140,7 +140,7 @@ fun Resolution.initiateFromForegroundBlocking(context: Context, account: Acc CryptAuthSyncKeys -> { Log.d(TAG, "Resolving account error by performing cryptauth sync keys call.") runBlocking { - syncCryptAuthKeys(context, account) + context.sendDeviceScreenlockState(account) } return retryFunction.run() } diff --git a/play-services-core/src/main/kotlin/org/microg/gms/cryptauth/CryptAuthFlow.kt b/play-services-core/src/main/kotlin/org/microg/gms/cryptauth/CryptAuthFlow.kt new file mode 100644 index 0000000000..f19a27d932 --- /dev/null +++ b/play-services-core/src/main/kotlin/org/microg/gms/cryptauth/CryptAuthFlow.kt @@ -0,0 +1,153 @@ +package org.microg.gms.cryptauth + +import android.accounts.Account +import android.app.KeyguardManager +import android.content.Context +import android.os.Bundle +import android.util.Base64 +import android.util.Log +import com.google.android.gms.BuildConfig +import com.google.android.gms.common.Scopes +import cryptauthv2.ApplicationSpecificMetadata +import cryptauthv2.ClientAppMetadata +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.withContext +import okio.ByteString.Companion.toByteString +import org.json.JSONArray +import org.json.JSONObject +import org.microg.gms.auth.AuthConstants +import org.microg.gms.auth.AuthManager +import org.microg.gms.checkin.LastCheckinInfo +import org.microg.gms.common.Constants +import org.microg.gms.common.DeviceConfiguration +import org.microg.gms.common.Utils +import org.microg.gms.gcm.GcmConstants +import org.microg.gms.gcm.GcmDatabase +import org.microg.gms.gcm.RegisterRequest +import org.microg.gms.gcm.completeRegisterRequest +import org.microg.gms.profile.Build +import java.io.IOException +import java.net.HttpURLConnection +import java.net.URL +import java.security.KeyPairGenerator +import java.security.MessageDigest +import kotlin.math.roundToInt + +private const val TAG = "CryptAuthFlow" + +private const val GCM_REGISTER_SENDER = "16502139086" +private const val GCM_REGISTER_SUBTYPE = "16502139086" +private const val GCM_REGISTER_SUBSCRIPTION = "16502139086" +private const val GCM_REGISTER_SCOPE = "GCM" +private const val GCM_REGISTER_CLIV = "iid-202414000" +private const val GCM_REGISTER_INFO = "0wqs6iYsl_URQEb1aBJ6XhzCbHSr-hg" + +private const val RSA_KEY_SIZE = 2048 + +private const val AFTER_REQUEST_DELAY = 5000L + +suspend fun Context.sendDeviceScreenlockState(account: Account): Boolean = sendDeviceScreenlockState(account.name) + +suspend fun Context.sendDeviceScreenlockState(accountName: String): Boolean { + + // Ensure that device is checked in + val checkinInfo = LastCheckinInfo.read(this) + if (checkinInfo.androidId == 0L) { + Log.w(TAG, "Device is not checked in, as it doesn't have an Android ID. Cannot perform cryptauth flow.") + return false + } + + // Instance ID token for use in CryptAuth query + val instanceId = generateAppId() + val instanceToken = registerForCryptAuth(checkinInfo, instanceId).getString(GcmConstants.EXTRA_REGISTRATION_ID) + if (instanceToken == null) { + Log.w(TAG, "No instance ID was gathered. Is GCM enabled, has there been a checkin?") + return false + } + + // Auth token for use in CryptAuth query + val authToken = authenticateForCryptAuth(accountName) + if (authToken == null) { + Log.w(TAG, "Authentication failed. Cannot perform cryptauth flow.") + return false + } + + val cryptAuthSyncKeysResult = cryptAuthSyncKeys(authToken, instanceId, instanceToken, checkinInfo.androidId) + if (cryptAuthSyncKeysResult == null + || !cryptAuthSyncKeysResult.has(CRYPTAUTH_FIELD_SESSION_ID) + || cryptAuthSyncKeysResult.get(CRYPTAUTH_FIELD_SESSION_ID) !is String + ) { + Log.w(TAG, "CryptAuth syncKeys failed. Cannot complete flow.") + return false + } + + val session: String = cryptAuthSyncKeysResult.get(CRYPTAUTH_FIELD_SESSION_ID) as String + val cryptAuthEnrollKeysResult = cryptAuthEnrollKeys(authToken, session) + + return if (cryptAuthEnrollKeysResult != null) { + /* Give Google server some time to process the new information. + * This leads to higher success rate compared to sending + * the next query immediately after this one. Tests show that it + * makes sense to wait multiple seconds. + */ + delay(AFTER_REQUEST_DELAY) + true + } else { + false + } +} + +private suspend fun Context.registerForCryptAuth(checkinInfo: LastCheckinInfo, instanceId: String): Bundle = completeRegisterRequest( + context = this, + database = GcmDatabase(this), + request = RegisterRequest().build(this) + .checkin(checkinInfo) + .app("com.google.android.gms", CERTIFICATE.lowercase(), BuildConfig.VERSION_CODE) + .sender(GCM_REGISTER_SENDER) + .extraParam("subscription", GCM_REGISTER_SUBSCRIPTION) + .extraParam("X-subscription", GCM_REGISTER_SUBSCRIPTION) + .extraParam("subtype", GCM_REGISTER_SUBTYPE) + .extraParam("X-subtype", GCM_REGISTER_SUBTYPE) + .extraParam("app_ver", BuildConfig.VERSION_CODE.toString()) + .extraParam("osv", "29") + .extraParam("cliv", GCM_REGISTER_CLIV) + .extraParam("gmsv", BuildConfig.VERSION_CODE.toString()) + .extraParam("appid", instanceId) + .extraParam("scope", GCM_REGISTER_SCOPE) + .extraParam("app_ver_name","%09d".format(BuildConfig.VERSION_CODE).let { + "${it.substring(0, 2)}.${it.substring(2, 4)}.${it.substring(4, 6)} (190800-{{cl}})" + }) + .info(GCM_REGISTER_INFO) +) + +private suspend fun Context.authenticateForCryptAuth(accountName: String): String? { + val cryptAuthServiceOauth2 = AuthConstants.SCOPE_OAUTH2 + Scopes.CRYPTAUTH + val authManager = AuthManager(this, accountName, Constants.GMS_PACKAGE_NAME, cryptAuthServiceOauth2) + return withContext(Dispatchers.IO) { authManager.requestAuth(false).auth } +} + +/** + * Generates an app / instance ID based on the hash of the public key of an RSA keypair. + * The key itself is never used. + */ +fun generateAppId(): String { + val rsaGenerator = KeyPairGenerator.getInstance("RSA") + rsaGenerator.initialize(RSA_KEY_SIZE) + val keyPair = rsaGenerator.generateKeyPair() + + val digest = MessageDigest.getInstance("SHA1").digest(keyPair.public.encoded) + digest[0] = ((112 + (0xF and digest[0].toInt())) and 0xFF).toByte() + return Base64.encodeToString( + digest, 0, 8, Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING + ) +} + +fun Context.isLockscreenConfigured(): Boolean { + val service: KeyguardManager = getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager + return if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { + service.isDeviceSecure + } else { + service.isKeyguardSecure + } +} diff --git a/play-services-core/src/main/kotlin/org/microg/gms/cryptauth/CryptAuthRequests.kt b/play-services-core/src/main/kotlin/org/microg/gms/cryptauth/CryptAuthRequests.kt new file mode 100644 index 0000000000..982e365355 --- /dev/null +++ b/play-services-core/src/main/kotlin/org/microg/gms/cryptauth/CryptAuthRequests.kt @@ -0,0 +1,138 @@ +package org.microg.gms.cryptauth + +import android.content.Context +import android.util.Log +import com.google.android.gms.BuildConfig +import cryptauthv2.ApplicationSpecificMetadata +import cryptauthv2.ClientAppMetadata +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import okio.ByteString.Companion.toByteString +import org.json.JSONArray +import org.json.JSONObject +import org.microg.gms.common.Constants +import org.microg.gms.common.DeviceConfiguration +import org.microg.gms.common.Utils +import org.microg.gms.profile.Build +import java.io.IOException +import java.net.HttpURLConnection +import java.net.URL +import kotlin.math.roundToInt + +private const val TAG = "CryptAuthRequests" + +private const val CRYPTAUTH_BASE_URL = "https://cryptauthenrollment.googleapis.com/" +private const val CRYPTAUTH_METHOD_SYNC_KEYS = "v1:syncKeys" +private const val CRYPTAUTH_METHOD_ENROLL_KEYS = "v1:enrollKeys" + +private const val API_KEY = "AIzaSyAP-gfH3qvi6vgHZbSYwQ_XHqV_mXHhzIk" +internal const val CERTIFICATE = "58E1C4133F7441EC3D2C270270A14802DA47BA0E" + +internal const val CRYPTAUTH_FIELD_SESSION_ID = "randomSessionId" + + +internal suspend fun Context.cryptAuthSyncKeys(authToken: String, instanceId: String, instanceToken: String, androidId: Long): JSONObject? { + // CryptAuth sync request tells server whether or not screenlock is enabled + + val deviceConfig = DeviceConfiguration(this) + val clientAppMetadata = ClientAppMetadata( + application_specific_metadata = listOf( + ApplicationSpecificMetadata( + gcm_registration_id = instanceToken.toByteArray().toByteString(), + notification_enabled = true, + device_software_version = "%09d".format(BuildConfig.VERSION_CODE).let { + "${it.substring(0, 2)}.${it.substring(2, 4)}.${it.substring(4, 6)} (190800-{{cl}})" + }, + device_software_version_code = BuildConfig.VERSION_CODE.toLong(), + device_software_package = Constants.GMS_PACKAGE_NAME + ) + ), + instance_id = instanceId, + instance_id_token = instanceToken, + android_device_id = androidId, + locale = Utils.getLocale(this).toString().replace("_", "-"), + device_os_version = Build.DISPLAY ?: "", + device_os_version_code = Build.VERSION.SDK_INT.toLong(), + device_os_release = Build.VERSION.CODENAME?: "", + device_display_diagonal_mils = (deviceConfig.diagonalInch / 1000).roundToInt(), + device_model = Build.MODEL ?: "", + device_manufacturer = Build.MANUFACTURER ?: "", + device_type = ClientAppMetadata.DeviceType.ANDROID, + using_secure_screenlock = isLockscreenConfigured(), + bluetooth_radio_supported = true, // TODO actual value? doesn't seem relevant + // bluetooth_radio_enabled = false, + ble_radio_supported = true, // TODO: actual value? doesn't seem relevant + mobile_data_supported = true, // TODO: actual value? doesn't seem relevant + // droid_guard_response = "…" + ) + .encodeByteString() + .base64Url() + + val jsonBody = jsonObjectOf( + "applicationName" to Constants.GMS_PACKAGE_NAME, + "clientVersion" to "1.0.0", + "syncSingleKeyRequests" to jsonArrayOf( + jsonObjectOf( + "keyName" to "PublicKey", + "keyHandles" to "ZGV2aWNlX2tleQo=" // base64 for `device_key` + ) + ), + "clientMetadata" to jsonObjectOf( + "invocationReason" to "NEW_ACCOUNT" + ), + "clientAppMetadata" to clientAppMetadata, + ) + + return cryptAuthQuery(CRYPTAUTH_BASE_URL + CRYPTAUTH_METHOD_SYNC_KEYS, authToken, jsonBody) +} + +internal suspend fun Context.cryptAuthEnrollKeys(authToken: String, session: String): JSONObject? { + val jsonBody = jsonObjectOf( + CRYPTAUTH_FIELD_SESSION_ID to session, + "clientEphemeralDh" to "", + "enrollSingleKeyRequests" to JSONArray(), + ) + + return cryptAuthQuery(CRYPTAUTH_BASE_URL + CRYPTAUTH_METHOD_ENROLL_KEYS, authToken, jsonBody) +} + +private suspend fun Context.cryptAuthQuery(url: String, authToken: String, requestBody: JSONObject) = withContext( + Dispatchers.IO) { + val connection = (URL(url).openConnection() as HttpURLConnection).apply { + setRequestMethod("POST") + setDoInput(true) + setDoOutput(true) + setRequestProperty("x-goog-api-key", API_KEY) + setRequestProperty("x-android-package", Constants.GMS_PACKAGE_NAME) + setRequestProperty("x-android-cert", CERTIFICATE) + setRequestProperty("Authorization", "Bearer $authToken") + setRequestProperty("Content-Type", "application/json") + } + + Log.d(TAG, "-- Request --\n$requestBody") + val os = connection.outputStream + os.write(requestBody.toString().toByteArray()) + os.close() + + if (connection.getResponseCode() != 200) { + var error = connection.getResponseMessage() + try { + error = String(Utils.readStreamToEnd(connection.errorStream)) + } catch (e: IOException) { + // Ignore + } + throw IOException(error) + } + + val result = String(Utils.readStreamToEnd(connection.inputStream)) + Log.d(TAG, "-- Response --\n$result") + try { + JSONObject(result) + + } catch (e: Exception) { + null + } +} + +fun jsonObjectOf(vararg pairs: Pair): JSONObject = JSONObject(mapOf(*pairs)) +inline fun jsonArrayOf(vararg elements: T): JSONArray = JSONArray(arrayOf(*elements)) \ No newline at end of file diff --git a/play-services-core/src/main/kotlin/org/microg/gms/cryptauth/SyncKeysRequest.kt b/play-services-core/src/main/kotlin/org/microg/gms/cryptauth/SyncKeysRequest.kt deleted file mode 100644 index 893b7a13a7..0000000000 --- a/play-services-core/src/main/kotlin/org/microg/gms/cryptauth/SyncKeysRequest.kt +++ /dev/null @@ -1,209 +0,0 @@ -package org.microg.gms.cryptauth - -import android.accounts.Account -import android.app.KeyguardManager -import android.content.Context -import android.util.Base64 -import android.util.Log -import com.google.android.gms.BuildConfig -import com.google.android.gms.common.Scopes -import cryptauthv2.ApplicationSpecificMetadata -import cryptauthv2.ClientAppMetadata -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay -import kotlinx.coroutines.withContext -import okio.ByteString.Companion.toByteString -import org.json.JSONArray -import org.json.JSONObject -import org.microg.gms.auth.AuthConstants -import org.microg.gms.auth.AuthManager -import org.microg.gms.checkin.LastCheckinInfo -import org.microg.gms.common.Constants -import org.microg.gms.common.DeviceConfiguration -import org.microg.gms.common.Utils -import org.microg.gms.gcm.GcmConstants -import org.microg.gms.gcm.GcmDatabase -import org.microg.gms.gcm.RegisterRequest -import org.microg.gms.gcm.completeRegisterRequest -import org.microg.gms.profile.Build -import java.io.IOException -import java.net.HttpURLConnection -import java.net.URL -import java.security.KeyPairGenerator -import java.security.MessageDigest -import kotlin.math.roundToInt - -private const val SYNC_KEY_URL = "https://cryptauthenrollment.googleapis.com/v1:syncKeys" -private const val API_KEY = "AIzaSyAP-gfH3qvi6vgHZbSYwQ_XHqV_mXHhzIk" -private const val CERTIFICATE = "58E1C4133F7441EC3D2C270270A14802DA47BA0E" -private const val TAG = "SyncKeysRequest" - -private const val GCM_REGISTER_SENDER = "16502139086" -private const val GCM_REGISTER_SUBTYPE = "16502139086" -private const val GCM_REGISTER_SUBSCRIPTION = "16502139086" -private const val GCM_REGISTER_SCOPE = "GCM" -private const val GCM_REGISTER_CLIV = "iid-202414000" -private const val GCM_REGISTER_INFO = "0wqs6iYsl_URQEb1aBJ6XhzCbHSr-hg" - -private const val RSA_KEY_SIZE = 2048 - -private const val AFTER_REQUEST_DELAY = 750L - -suspend fun syncCryptAuthKeys(context: Context, account: Account): JSONObject? = syncCryptAuthKeys(context, account.name) -suspend fun syncCryptAuthKeys(context: Context, accountName: String): JSONObject? { - - val lastCheckinInfo = LastCheckinInfo.read(context) - - // Instance ID token for use in CryptAuth query - val instanceId = generateAppId() - val instanceToken = completeRegisterRequest(context, GcmDatabase(context), RegisterRequest().build(context) - .checkin(lastCheckinInfo) - .app("com.google.android.gms", CERTIFICATE.lowercase(), BuildConfig.VERSION_CODE) - .sender(GCM_REGISTER_SENDER) - .extraParam("subscription", GCM_REGISTER_SUBSCRIPTION) - .extraParam("X-subscription", GCM_REGISTER_SUBSCRIPTION) - .extraParam("subtype", GCM_REGISTER_SUBTYPE) - .extraParam("X-subtype", GCM_REGISTER_SUBTYPE) - .extraParam("app_ver", BuildConfig.VERSION_CODE.toString()) - .extraParam("osv", "29") - .extraParam("cliv", GCM_REGISTER_CLIV) - .extraParam("gmsv", BuildConfig.VERSION_CODE.toString()) - .extraParam("appid", instanceId) - .extraParam("scope", GCM_REGISTER_SCOPE) - .extraParam("app_ver_name","%09d".format(BuildConfig.VERSION_CODE).let { - "${it.substring(0, 2)}.${it.substring(2, 4)}.${it.substring(4, 6)} (190800-{{cl}})" - }) - .info(GCM_REGISTER_INFO) - ).let { - if (!it.containsKey(GcmConstants.EXTRA_REGISTRATION_ID)) { - Log.d(TAG, "No instance ID was gathered. Is GCM enabled, has there been a checkin?") - return null - } - it.getString(GcmConstants.EXTRA_REGISTRATION_ID)!! - } - - // CryptAuth sync request tells server whether or not screenlock is enabled - val cryptauthService = AuthConstants.SCOPE_OAUTH2 + Scopes.CRYPTAUTH - val authManager = AuthManager(context, accountName, Constants.GMS_PACKAGE_NAME, cryptauthService) - val authToken = withContext(Dispatchers.IO) { authManager.requestAuth(false).auth } - - val deviceConfig = DeviceConfiguration(context) - val clientAppMetadata = ClientAppMetadata( - application_specific_metadata = listOf( - ApplicationSpecificMetadata( - gcm_registration_id = instanceToken.toByteArray().toByteString(), - notification_enabled = true, - device_software_version = "%09d".format(BuildConfig.VERSION_CODE).let { - "${it.substring(0, 2)}.${it.substring(2, 4)}.${it.substring(4, 6)} (190800-{{cl}})" - }, - device_software_version_code = BuildConfig.VERSION_CODE.toLong(), - device_software_package = Constants.GMS_PACKAGE_NAME - ) - ), - instance_id = instanceId, - instance_id_token = instanceToken, - android_device_id = lastCheckinInfo.androidId, - locale = Utils.getLocale(context).toString().replace("_", "-"), - device_os_version = Build.DISPLAY ?: "", - device_os_version_code = Build.VERSION.SDK_INT.toLong(), - device_os_release = Build.VERSION.CODENAME?: "", - device_display_diagonal_mils = (deviceConfig.diagonalInch / 1000).roundToInt(), - device_model = Build.MODEL ?: "", - device_manufacturer = Build.MANUFACTURER ?: "", - device_type = ClientAppMetadata.DeviceType.ANDROID, - using_secure_screenlock = context.isLockscreenConfigured(), - bluetooth_radio_supported = true, // TODO actual value? doesn't seem relevant - // bluetooth_radio_enabled = false, - ble_radio_supported = true, // TODO: actual value? doesn't seem relevant - mobile_data_supported = true, // TODO: actual value? doesn't seem relevant - // droid_guard_response = "…" - ) - .encodeByteString() - .base64Url() - - val jsonBody = jsonObjectOf( - "applicationName" to Constants.GMS_PACKAGE_NAME, - "clientVersion" to "1.0.0", - "syncSingleKeyRequests" to jsonArrayOf( - jsonObjectOf( - "keyName" to "PublicKey", - "keyHandles" to "ZGV2aWNlX2tleQo=" // base64 for `device_key` - ) - ), - "clientMetadata" to jsonObjectOf( - "invocationReason" to "NEW_ACCOUNT" - ), - "clientAppMetadata" to clientAppMetadata, - ) - - return withContext(Dispatchers.IO) { - val connection = (URL(SYNC_KEY_URL).openConnection() as HttpURLConnection).apply { - setRequestMethod("POST") - setDoInput(true) - setDoOutput(true) - setRequestProperty("x-goog-api-key", API_KEY) - setRequestProperty("x-android-package", Constants.GMS_PACKAGE_NAME) - setRequestProperty("x-android-cert", CERTIFICATE) - setRequestProperty("Authorization", "Bearer $authToken") - setRequestProperty("Content-Type", "application/json") - } - Log.d(TAG, "-- Request --\n$jsonBody") - - val os = connection.outputStream - os.write(jsonBody.toString().toByteArray()) - os.close() - - if (connection.getResponseCode() != 200) { - var error = connection.getResponseMessage() - try { - error = String(Utils.readStreamToEnd(connection.errorStream)) - } catch (e: IOException) { - // Ignore - } - throw IOException(error) - } - - val result = String(Utils.readStreamToEnd(connection.inputStream)) - Log.d(TAG, "-- Response --\n$result") - try { - JSONObject(result) - .also { - /* Give Google server some time to process the new information. - * This leads to higher success rate compared to sending - * the next query immediately after this one. - */ - delay(AFTER_REQUEST_DELAY) - } - } catch (e: Exception) { - null - } - } -} - -/** - * Generates an app / instance ID based on the hash of the public key of an RSA keypair. - * The key itself is never used. - */ -fun generateAppId(): String { - val rsaGenerator = KeyPairGenerator.getInstance("RSA") - rsaGenerator.initialize(RSA_KEY_SIZE) - val keyPair = rsaGenerator.generateKeyPair() - - val digest = MessageDigest.getInstance("SHA1").digest(keyPair.public.encoded) - digest[0] = ((112 + (0xF and digest[0].toInt())) and 0xFF).toByte() - return Base64.encodeToString( - digest, 0, 8, Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING - ) -} - -fun Context.isLockscreenConfigured(): Boolean { - val service: KeyguardManager = getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager - return if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { - service.isDeviceSecure - } else { - service.isKeyguardSecure - } -} - -fun jsonObjectOf(vararg pairs: Pair): JSONObject = JSONObject(mapOf(*pairs)) -inline fun jsonArrayOf(vararg elements: T): JSONArray = JSONArray(arrayOf(*elements)) \ No newline at end of file From 36fedfaf97b6ec7cb4347ce3eb4a3e4dbc95c3f1 Mon Sep 17 00:00:00 2001 From: Marvin W Date: Wed, 25 Sep 2024 11:39:02 +0200 Subject: [PATCH 018/132] Auth: Make sure we throw the IOException as before, so we don't run into null referenes later --- .../main/java/org/microg/gms/auth/AuthManager.java | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/play-services-core/src/main/java/org/microg/gms/auth/AuthManager.java b/play-services-core/src/main/java/org/microg/gms/auth/AuthManager.java index b3bb57e309..c231ab7930 100644 --- a/play-services-core/src/main/java/org/microg/gms/auth/AuthManager.java +++ b/play-services-core/src/main/java/org/microg/gms/auth/AuthManager.java @@ -13,6 +13,7 @@ import android.net.Uri; import android.util.Log; +import androidx.annotation.NonNull; import org.microg.gms.accountaction.ErrorResolverKt; import org.microg.gms.accountaction.Resolution; import org.microg.gms.common.NotOkayException; @@ -254,6 +255,7 @@ private boolean isSystemApp() { } } + @NonNull public AuthResponse requestAuthWithBackgroundResolution(boolean legacy) throws IOException { try { return requestAuth(legacy); @@ -261,13 +263,15 @@ public AuthResponse requestAuthWithBackgroundResolution(boolean legacy) throws I if (e.getMessage() != null) { Resolution errorResolution = ErrorResolverKt.resolveAuthErrorMessage(context, e.getMessage()); if (errorResolution != null) { - return ErrorResolverKt.initiateFromBackgroundBlocking( + AuthResponse response = ErrorResolverKt.initiateFromBackgroundBlocking( errorResolution, context, getAccount(), // infinite loop is prevented () -> requestAuth(legacy) ); + if (response == null) throw new IOException(e); + return response; } else { throw new IOException(e); } @@ -277,6 +281,7 @@ public AuthResponse requestAuthWithBackgroundResolution(boolean legacy) throws I } } + @NonNull public AuthResponse requestAuthWithForegroundResolution(boolean legacy) throws IOException { try { return requestAuth(legacy); @@ -284,13 +289,15 @@ public AuthResponse requestAuthWithForegroundResolution(boolean legacy) throws I if (e.getMessage() != null) { Resolution errorResolution = ErrorResolverKt.resolveAuthErrorMessage(context, e.getMessage()); if (errorResolution != null) { - return ErrorResolverKt.initiateFromForegroundBlocking( + AuthResponse response = ErrorResolverKt.initiateFromForegroundBlocking( errorResolution, context, getAccount(), // infinite loop is prevented () -> requestAuth(legacy) ); + if (response == null) throw new IOException(e); + return response; } else { throw new IOException(e); } @@ -300,6 +307,7 @@ public AuthResponse requestAuthWithForegroundResolution(boolean legacy) throws I } } + @NonNull public AuthResponse requestAuth(boolean legacy) throws IOException { if (service.equals(AuthConstants.SCOPE_GET_ACCOUNT_ID)) { AuthResponse response = new AuthResponse(); From 95361b44afe8dd895bf61f0af50479df9702ee89 Mon Sep 17 00:00:00 2001 From: DaVinci9196 <150454414+DaVinci9196@users.noreply.github.com> Date: Mon, 11 Nov 2024 04:49:01 +0800 Subject: [PATCH 019/132] IAP: Implement old getBuyIntent through forwarding to new one (#2534) --- .../java/org/microg/vending/billing/InAppBillingServiceImpl.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/vending-app/src/main/java/org/microg/vending/billing/InAppBillingServiceImpl.kt b/vending-app/src/main/java/org/microg/vending/billing/InAppBillingServiceImpl.kt index 2d3d0311f4..a331746994 100644 --- a/vending-app/src/main/java/org/microg/vending/billing/InAppBillingServiceImpl.kt +++ b/vending-app/src/main/java/org/microg/vending/billing/InAppBillingServiceImpl.kt @@ -232,7 +232,8 @@ class InAppBillingServiceImpl(private val context: Context) : IInAppBillingServi developerPayload: String? ): Bundle { if (Log.isLoggable(TAG, Log.DEBUG)) Log.d(TAG, "getBuyIntent(apiVersion=$apiVersion, packageName=$packageName, sku=$sku, type=$type, developerPayload=$developerPayload)") - return resultBundle(BillingResponseCode.BILLING_UNAVAILABLE, "Not yet implemented") + return runCatching { getBuyIntentExtraParams(apiVersion, packageName!!, sku!!, type!!, developerPayload, null) } + .getOrDefault(resultBundle(BillingResponseCode.BILLING_UNAVAILABLE, "Not yet implemented")) } override fun getPurchases( From 510cc7ad3bdd87dbf63d6d7fe736b35b7db0f660 Mon Sep 17 00:00:00 2001 From: DaVinci9196 <150454414+DaVinci9196@users.noreply.github.com> Date: Mon, 11 Nov 2024 04:50:03 +0800 Subject: [PATCH 020/132] Games: Store player details per account, not per game (#2485) Co-authored-by: Marvin W --- .../gms/games/GamesConfigurationService.kt | 24 ++++----- .../org/microg/gms/games/GamesService.kt | 2 +- .../kotlin/org/microg/gms/games/extensions.kt | 52 +++++++------------ 3 files changed, 30 insertions(+), 48 deletions(-) diff --git a/play-services-core/src/main/kotlin/org/microg/gms/games/GamesConfigurationService.kt b/play-services-core/src/main/kotlin/org/microg/gms/games/GamesConfigurationService.kt index 10c0da6556..cdc66df39d 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/games/GamesConfigurationService.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/games/GamesConfigurationService.kt @@ -62,23 +62,19 @@ class GamesConfigurationService : Service() { } MSG_GET_PLAYER -> { - val packageName = msg.data?.getString(MSG_DATA_PACKAGE_NAME) val account = msg.data?.getParcelable(MSG_DATA_ACCOUNT) - val player = if (packageName != null && account != null) getPlayer(packageName, account) else null + val player = if (account != null) getPlayer(account) else null bundleOf( - MSG_DATA_PACKAGE_NAME to packageName, MSG_DATA_ACCOUNT to account, MSG_DATA_PLAYER to player ) } MSG_SET_PLAYER -> { - val packageName = msg.data?.getString(MSG_DATA_PACKAGE_NAME) val account = msg.data?.getParcelable(MSG_DATA_ACCOUNT) val player = msg.data?.getString(MSG_DATA_PLAYER) - if (packageName != null && account != null) setPlayer(packageName, account, player) + if (account != null) setPlayer(account, player) bundleOf( - MSG_DATA_PACKAGE_NAME to packageName, MSG_DATA_ACCOUNT to account, MSG_DATA_PLAYER to player ) @@ -141,18 +137,18 @@ class GamesConfigurationService : Service() { return getPackageNameSuffix(packageName) + ":" + account.name } - private fun getPlayer(packageName: String, account: Account): String? { - val player = preferences.getString(PREF_PLAYER_PREFIX + getPackageAndAccountSuffix(packageName, account), null) + private fun getPlayer(account: Account): String? { + val player = preferences.getString(PREF_PLAYER_PREFIX + getPackageAndAccountSuffix(GAMES_PACKAGE_NAME, account), null) if (player.isNullOrBlank()) return null return player } - private fun setPlayer(packageName: String, account: Account, player: String?) { + private fun setPlayer(account: Account, player: String?) { val editor: SharedPreferences.Editor = preferences.edit() if (player.isNullOrBlank()) { - editor.remove(PREF_PLAYER_PREFIX + getPackageAndAccountSuffix(packageName, account)) + editor.remove(PREF_PLAYER_PREFIX + getPackageAndAccountSuffix(GAMES_PACKAGE_NAME, account)) } else { - editor.putString(PREF_PLAYER_PREFIX + getPackageAndAccountSuffix(packageName, account), player) + editor.putString(PREF_PLAYER_PREFIX + getPackageAndAccountSuffix(GAMES_PACKAGE_NAME, account), player) } editor.apply() } @@ -208,21 +204,19 @@ class GamesConfigurationService : Service() { }) } - suspend fun getPlayer(context: Context, packageName: String, account: Account): String? { + suspend fun getPlayer(context: Context, account: Account): String? { return singleRequest(context, Message.obtain().apply { what = MSG_GET_PLAYER data = bundleOf( - MSG_DATA_PACKAGE_NAME to packageName, MSG_DATA_ACCOUNT to account ) }).data?.getString(MSG_DATA_PLAYER) } - suspend fun setPlayer(context: Context, packageName: String, account: Account, player: String?) { + suspend fun setPlayer(context: Context, account: Account, player: String?) { singleRequest(context, Message.obtain().apply { what = MSG_SET_PLAYER data = bundleOf( - MSG_DATA_PACKAGE_NAME to packageName, MSG_DATA_ACCOUNT to account, MSG_DATA_PLAYER to player ) diff --git a/play-services-core/src/main/kotlin/org/microg/gms/games/GamesService.kt b/play-services-core/src/main/kotlin/org/microg/gms/games/GamesService.kt index 59195112dd..411e703741 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/games/GamesService.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/games/GamesService.kt @@ -91,7 +91,7 @@ class GamesService : BaseService(TAG, GmsService.GAMES) { return@launchWhenStarted sendSignInRequired() } - val player = JSONObject(GamesConfigurationService.getPlayer(this@GamesService, packageName, account)).toPlayer() + val player = JSONObject(GamesConfigurationService.getPlayer(this@GamesService, account)).toPlayer() callback.onPostInitCompleteWithConnectionInfo( CommonStatusCodes.SUCCESS, diff --git a/play-services-core/src/main/kotlin/org/microg/gms/games/extensions.kt b/play-services-core/src/main/kotlin/org/microg/gms/games/extensions.kt index 1d2d171bbf..1349cc0367 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/games/extensions.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/games/extensions.kt @@ -216,9 +216,9 @@ suspend fun performGamesSignIn( val authManager = AuthManager(context, account.name, packageName, "oauth2:${realScopes.joinToString(" ")}") if (realScopes.size == 1) authManager.setItCaveatTypes("2") if (permitted) authManager.isPermitted = true - val authResponse = withContext(Dispatchers.IO) { authManager.requestAuthWithBackgroundResolution(true) } + var authResponse = withContext(Dispatchers.IO) { authManager.requestAuthWithBackgroundResolution(true) } if (authResponse.auth == null) return false - if (authResponse.issueAdvice != "stored" || GamesConfigurationService.getPlayer(context, packageName, account) == null) { + if (authResponse.issueAdvice != "stored" || GamesConfigurationService.getPlayer(context, account) == null) { suspend fun fetchSelfPlayer() = suspendCoroutine { continuation -> queue.add( object : JsonObjectRequest( @@ -237,39 +237,27 @@ suspend fun performGamesSignIn( val result = try { fetchSelfPlayer() } catch (e: Exception) { - if (e is VolleyError && e.networkResponse?.statusCode == 404) { - registerForGames(context, account, queue) - fetchSelfPlayer() + if (e is VolleyError) { + val statusCode = e.networkResponse?.statusCode + when (statusCode) { + 404 -> { + registerForGames(context, account, queue) + fetchSelfPlayer() + } + 403 -> { + val gameAuthManager = AuthManager(context, account.name, GAMES_PACKAGE_NAME, authManager.service) + gameAuthManager.isPermitted = authManager.isPermitted + authResponse = withContext(Dispatchers.IO) { gameAuthManager.requestAuth(true) } + if (authResponse.auth == null) return false + fetchSelfPlayer() + } + else -> throw e + } } else { throw e } } - GamesConfigurationService.setPlayer(context, packageName, account, result.toString()) - if (packageName != GAMES_PACKAGE_NAME) { - try { - suspendCoroutine { continuation -> - queue.add(object : Request(Method.POST, "https://www.googleapis.com/games/v1/applications/played", { - continuation.resumeWithException(it) - }) { - override fun parseNetworkResponse(response: NetworkResponse): Response { - if (response.statusCode == 204) return success(Unit, null) - return Response.error(VolleyError(response)) - } - - override fun deliverResponse(response: Unit) { - continuation.resume(response) - } - - override fun getHeaders(): MutableMap { - return mutableMapOf( - "Authorization" to "OAuth ${authResponse.auth}" - ) - } - }) - } - } catch (ignored: Exception) { - } - } + GamesConfigurationService.setPlayer(context, account, result.toString()) } return true -} \ No newline at end of file +} From 4e064a1a8477bfa78b48349f1086877bacfa7fe1 Mon Sep 17 00:00:00 2001 From: Marvin W Date: Sat, 9 Nov 2024 10:37:23 +0100 Subject: [PATCH 021/132] Location: Support different sources within the same moving SSID Also add support for more Austrian trains and new Air Canada --- .../location/network/wifi/MovingWifiHelper.kt | 472 ++++++++++-------- 1 file changed, 272 insertions(+), 200 deletions(-) diff --git a/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/wifi/MovingWifiHelper.kt b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/wifi/MovingWifiHelper.kt index c4d50000fa..38cad06f05 100644 --- a/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/wifi/MovingWifiHelper.kt +++ b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/wifi/MovingWifiHelper.kt @@ -10,14 +10,12 @@ import android.location.Location import android.net.ConnectivityManager import android.net.ConnectivityManager.TYPE_WIFI import android.os.Build.VERSION.SDK_INT -import android.util.Log import androidx.core.content.getSystemService import androidx.core.location.LocationCompat import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.json.JSONArray import org.json.JSONObject -import org.microg.gms.location.network.TAG import java.net.HttpURLConnection import java.net.URL import java.text.SimpleDateFormat @@ -33,6 +31,7 @@ private val MOVING_WIFI_HOTSPOTS = setOf( // Canada "Air Canada", "ACWiFi", + "ACWiFi.com", // Czech Republic "CDWiFi", // France @@ -99,7 +98,7 @@ private val PHONE_HOTSPOT_KEYWORDS = setOf( */ val WifiDetails.isMoving: Boolean get() { - if (open && MOVING_WIFI_HOTSPOTS.contains(ssid)) { + if (MOVING_WIFI_HOTSPOTS.contains(ssid)) { return true } if (PHONE_HOTSPOT_KEYWORDS.any { ssid?.contains(it) == true }) { @@ -116,231 +115,304 @@ class MovingWifiHelper(private val context: Context) { suspend fun retrieveMovingLocation(current: WifiDetails): Location { if (!isLocallyRetrievable(current)) throw IllegalArgumentException() val connectivityManager = context.getSystemService() ?: throw IllegalStateException() - val url = URL(MOVING_WIFI_HOTSPOTS_LOCALLY_RETRIEVABLE[current.ssid]) - return withContext(Dispatchers.IO) { - val network = if (isLocallyRetrievable(current) && SDK_INT >= 23) { - @Suppress("DEPRECATION") - (connectivityManager.allNetworks.singleOrNull { - val networkInfo = connectivityManager.getNetworkInfo(it) - networkInfo?.type == TYPE_WIFI && networkInfo.isConnected - }) - } else { - null - } - val connection = (if (SDK_INT >= 21) { - network?.openConnection(url) - } else { - null - } ?: url.openConnection()) as HttpURLConnection + val sources = MOVING_WIFI_HOTSPOTS_LOCALLY_RETRIEVABLE[current.ssid]!! + val exceptions = mutableListOf() + for (source in sources) { try { - connection.doInput = true - if (connection.responseCode != 200) throw RuntimeException("Got error") - parseInput(current.ssid!!, connection.inputStream.readBytes()) - } finally { - connection.inputStream.close() - connection.disconnect() + val url = URL(source.url) + return withContext(Dispatchers.IO) { + val network = if (isLocallyRetrievable(current) && SDK_INT >= 23) { + @Suppress("DEPRECATION") + (connectivityManager.allNetworks.singleOrNull { + val networkInfo = connectivityManager.getNetworkInfo(it) + networkInfo?.type == TYPE_WIFI && networkInfo.isConnected + }) + } else { + null + } + val connection = (if (SDK_INT >= 21) { + network?.openConnection(url) + } else { + null + } ?: url.openConnection()) as HttpURLConnection + try { + connection.doInput = true + if (connection.responseCode != 200) throw RuntimeException("Got error") + val location = Location(current.ssid ?: "wifi") + source.parse(location, connection.inputStream.readBytes()) + } finally { + connection.inputStream.close() + connection.disconnect() + } + } + } catch (e: Exception) { + exceptions.add(e) } } + if (exceptions.size == 1) throw exceptions.single() + throw RuntimeException(exceptions.joinToString("\n")) } - private fun parseWifiOnIce(location: Location, data: ByteArray): Location { - val json = JSONObject(data.decodeToString()) - if (json.getString("gpsStatus") != "VALID") throw RuntimeException("GPS not valid") - location.accuracy = 100f - location.time = json.getLong("serverTime") - 15000L - location.latitude = json.getDouble("latitude") - location.longitude = json.getDouble("longitude") - json.optDouble("speed").takeIf { !it.isNaN() }?.let { - location.speed = (it / 3.6).toFloat() - LocationCompat.setSpeedAccuracyMetersPerSecond(location, location.speed * 0.1f) - } - return location - } + fun isLocallyRetrievable(wifi: WifiDetails): Boolean = + MOVING_WIFI_HOTSPOTS_LOCALLY_RETRIEVABLE.containsKey(wifi.ssid) - private fun parseOebb(location: Location, data: ByteArray): Location { - val json = JSONObject(data.decodeToString()).getJSONObject("latestStatus") - location.accuracy = 100f - runCatching { SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.US).parse(json.getString("dateTime"))?.time }.getOrNull()?.let { location.time = it } - location.latitude = json.getJSONObject("gpsPosition").getDouble("latitude") - location.longitude = json.getJSONObject("gpsPosition").getDouble("longitude") - json.getJSONObject("gpsPosition").optDouble("orientation").takeIf { !it.isNaN() }?.let { - location.bearing = it.toFloat() - LocationCompat.setBearingAccuracyDegrees(location, 90f) - } - json.optDouble("speed").takeIf { !it.isNaN() }?.let { - location.speed = (it / 3.6).toFloat() - LocationCompat.setSpeedAccuracyMetersPerSecond(location, location.speed * 0.1f) + companion object { + abstract class MovingWifiLocationSource(val url: String) { + abstract fun parse(location: Location, data: ByteArray): Location } - return location - } - private fun parseFlixbus(location: Location, data: ByteArray): Location { - val json = JSONObject(data.decodeToString()) - location.accuracy = 100f - location.latitude = json.getDouble("latitude") - location.longitude = json.getDouble("longitude") - json.optDouble("speed").takeIf { !it.isNaN() }?.let { - location.speed = it.toFloat() - LocationCompat.setSpeedAccuracyMetersPerSecond(location, location.speed * 0.1f) + private val SOURCE_WIFI_ON_ICE = object : MovingWifiLocationSource("https://iceportal.de/api1/rs/status") { + override fun parse(location: Location, data: ByteArray): Location { + val json = JSONObject(data.decodeToString()) + if (json.getString("gpsStatus") != "VALID") throw RuntimeException("GPS not valid") + location.accuracy = 100f + location.time = json.getLong("serverTime") - 15000L + location.latitude = json.getDouble("latitude") + location.longitude = json.getDouble("longitude") + json.optDouble("speed").takeIf { !it.isNaN() }?.let { + location.speed = (it / 3.6).toFloat() + LocationCompat.setSpeedAccuracyMetersPerSecond(location, location.speed * 0.1f) + } + return location + } } - return location - } - private fun parsePassengera(location: Location, data: ByteArray): Location { - val json = JSONObject(data.decodeToString()) - location.accuracy = 100f - location.latitude = json.getDouble("gpsLat") - location.longitude = json.getDouble("gpsLng") - json.optDouble("speed").takeIf { !it.isNaN() }?.let { - location.speed = (it / 3.6).toFloat() - LocationCompat.setSpeedAccuracyMetersPerSecond(location, location.speed * 0.1f) + private val SOURCE_OEBB_1 = object : MovingWifiLocationSource("https://railnet.oebb.at/assets/modules/fis/combined.json") { + override fun parse(location: Location, data: ByteArray): Location { + val json = JSONObject(data.decodeToString()).getJSONObject("latestStatus") + location.accuracy = 100f + runCatching { SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.US).parse(json.getString("dateTime"))?.time }.getOrNull()?.let { location.time = it } + location.latitude = json.getJSONObject("gpsPosition").getDouble("latitude") + location.longitude = json.getJSONObject("gpsPosition").getDouble("longitude") + json.getJSONObject("gpsPosition").optDouble("orientation").takeIf { !it.isNaN() }?.let { + location.bearing = it.toFloat() + LocationCompat.setBearingAccuracyDegrees(location, 90f) + } + json.optDouble("speed").takeIf { !it.isNaN() }?.let { + location.speed = (it / 3.6).toFloat() + LocationCompat.setSpeedAccuracyMetersPerSecond(location, location.speed * 0.1f) + } + return location + } } - json.optDouble("altitude").takeIf { !it.isNaN() }?.let { location.altitude = it } - return location - } - private fun parseDisplayUgo(location: Location, data: ByteArray): Location { - val json = JSONArray(data.decodeToString()).getJSONObject(0) - location.accuracy = 100f - location.latitude = json.getDouble("latitude") - location.longitude = json.getDouble("longitude") - runCatching { SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.US).parse(json.getString("created_at"))?.time }.getOrNull()?.let { location.time = it } - json.optDouble("speed_kilometers_per_hour").takeIf { !it.isNaN() }?.let { - location.speed = (it / 3.6).toFloat() - LocationCompat.setSpeedAccuracyMetersPerSecond(location, location.speed * 0.1f) - } - json.optDouble("altitude_meters").takeIf { !it.isNaN() }?.let { location.altitude = it } - json.optDouble("bearing_in_degree").takeIf { !it.isNaN() }?.let { - location.bearing = it.toFloat() - LocationCompat.setBearingAccuracyDegrees(location, 90f) + private val SOURCE_OEBB_2 = object : MovingWifiLocationSource("https://railnet.oebb.at/api/gps") { + override fun parse(location: Location, data: ByteArray): Location { + val root = JSONObject(data.decodeToString()) + if (root.has("JSON")) { + val json = root.getJSONObject("JSON") + if (!json.isNull("error")) throw RuntimeException("Error: ${json.get("error")}"); + location.accuracy = 100f + location.latitude = json.getDouble("lat") + location.longitude = json.getDouble("lon") + json.optDouble("speed").takeIf { !it.isNaN() }?.let { + location.speed = (it / 3.6).toFloat() + LocationCompat.setSpeedAccuracyMetersPerSecond(location, location.speed * 0.1f) + } + } else if (root.optDouble("Latitude").let { !it.isNaN() && it.isFinite() && it > 0.1 }) { + location.accuracy = 100f + location.latitude = root.getDouble("Latitude") + location.longitude = root.getDouble("Longitude") + } else { + throw RuntimeException("Unsupported: $root") + } + return location + } } - return location - } - private fun parsePanasonic(location: Location, data: ByteArray): Location { - val json = JSONObject(data.decodeToString()) - location.accuracy = 100f - location.latitude = json.getJSONObject("current_coordinates").getDouble("latitude") - location.longitude = json.getJSONObject("current_coordinates").getDouble("longitude") - json.optDouble("ground_speed_knots").takeIf { !it.isNaN() }?.let { - location.speed = (it * KNOTS_TO_METERS_PER_SECOND).toFloat() - LocationCompat.setSpeedAccuracyMetersPerSecond(location, location.speed * 0.1f) - } - json.optDouble("altitude_feet").takeIf { !it.isNaN() }?.let { location.altitude = it * FEET_TO_METERS } - json.optDouble("true_heading_degree").takeIf { !it.isNaN() }?.let { - location.bearing = it.toFloat() - LocationCompat.setBearingAccuracyDegrees(location, 90f) + private val SOURCE_FLIXBUS = object : MovingWifiLocationSource("https://media.flixbus.com/services/pis/v1/position") { + override fun parse(location: Location, data: ByteArray): Location { + val json = JSONObject(data.decodeToString()) + location.accuracy = 100f + location.latitude = json.getDouble("latitude") + location.longitude = json.getDouble("longitude") + json.optDouble("speed").takeIf { !it.isNaN() }?.let { + location.speed = it.toFloat() + LocationCompat.setSpeedAccuracyMetersPerSecond(location, location.speed * 0.1f) + } + return location + } } - return location - } - private fun parseBoardConnect(location: Location, data: ByteArray): Location { - val json = JSONObject(data.decodeToString()) - location.accuracy = 100f - location.latitude = json.getDouble("lat") - location.longitude = json.getDouble("lon") - runCatching { SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.US).parse(json.getString("utc"))?.time }.getOrNull()?.let { location.time = it } - json.optDouble("groundSpeed").takeIf { !it.isNaN() }?.let { - location.speed = (it * KNOTS_TO_METERS_PER_SECOND).toFloat() - LocationCompat.setSpeedAccuracyMetersPerSecond(location, location.speed * 0.1f) + class PassengeraLocationSource(base: String) : MovingWifiLocationSource("$base/portal/api/vehicle/realtime") { + override fun parse(location: Location, data: ByteArray): Location { + val json = JSONObject(data.decodeToString()) + location.accuracy = 100f + location.latitude = json.getDouble("gpsLat") + location.longitude = json.getDouble("gpsLng") + json.optDouble("speed").takeIf { !it.isNaN() }?.let { + location.speed = (it / 3.6).toFloat() + LocationCompat.setSpeedAccuracyMetersPerSecond(location, location.speed * 0.1f) + } + json.optDouble("altitude").takeIf { !it.isNaN() }?.let { location.altitude = it } + return location + } } - json.optDouble("altitude").takeIf { !it.isNaN() }?.let { location.altitude = it * FEET_TO_METERS } - json.optDouble("heading").takeIf { !it.isNaN() }?.let { - location.bearing = it.toFloat() - LocationCompat.setBearingAccuracyDegrees(location, 90f) + private val SOURCE_PASSENGERA_MAV = PassengeraLocationSource("http://portal.mav.hu") + private val SOURCE_PASSENGERA_CD = PassengeraLocationSource("http://cdwifi.cz") + + private val SOURCE_DISPLAY_UGO = object : MovingWifiLocationSource("https://api.ife.ugo.aero/navigation/positions") { + override fun parse(location: Location, data: ByteArray): Location { + val json = JSONArray(data.decodeToString()).getJSONObject(0) + location.accuracy = 100f + location.latitude = json.getDouble("latitude") + location.longitude = json.getDouble("longitude") + runCatching { SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.US).parse(json.getString("created_at"))?.time }.getOrNull()?.let { location.time = it } + json.optDouble("speed_kilometers_per_hour").takeIf { !it.isNaN() }?.let { + location.speed = (it / 3.6).toFloat() + LocationCompat.setSpeedAccuracyMetersPerSecond(location, location.speed * 0.1f) + } + json.optDouble("altitude_meters").takeIf { !it.isNaN() }?.let { location.altitude = it } + json.optDouble("bearing_in_degree").takeIf { !it.isNaN() }?.let { + location.bearing = it.toFloat() + LocationCompat.setBearingAccuracyDegrees(location, 90f) + } + return location + } } - return location - } - - private fun parseSncf(location: Location, data: ByteArray): Location { - val json = JSONObject(data.decodeToString()) - if(json.has("fix") && json.getInt("fix") == -1) throw RuntimeException("GPS not valid") - location.accuracy = 100f - location.latitude = json.getDouble("latitude") - location.longitude = json.getDouble("longitude") - json.optDouble("speed").takeIf { !it.isNaN() }?.let { - location.speed = it.toFloat() - LocationCompat.setSpeedAccuracyMetersPerSecond(location, location.speed * 0.1f) + + private val SOURCE_INFLIGHT_PANASONIC = object : MovingWifiLocationSource("https://services.inflightpanasonic.aero/inflight/services/flightdata/v2/flightdata") { + override fun parse(location: Location, data: ByteArray): Location { + val json = JSONObject(data.decodeToString()) + location.accuracy = 100f + location.latitude = json.getJSONObject("current_coordinates").getDouble("latitude") + location.longitude = json.getJSONObject("current_coordinates").getDouble("longitude") + json.optDouble("ground_speed_knots").takeIf { !it.isNaN() }?.let { + location.speed = (it * KNOTS_TO_METERS_PER_SECOND).toFloat() + LocationCompat.setSpeedAccuracyMetersPerSecond(location, location.speed * 0.1f) + } + json.optDouble("altitude_feet").takeIf { !it.isNaN() }?.let { location.altitude = it * FEET_TO_METERS } + json.optDouble("true_heading_degree").takeIf { !it.isNaN() }?.let { + location.bearing = it.toFloat() + LocationCompat.setBearingAccuracyDegrees(location, 90f) + } + return location + } } - location.time = json.getLong("timestamp") - json.optDouble("heading").takeIf { !it.isNaN() }?.let { - location.bearing = it.toFloat() - LocationCompat.setBearingAccuracyDegrees(location, 90f) + + class BoardConnectLocationSource(base: String) : MovingWifiLocationSource("$base/map/api/flightData") { + override fun parse(location: Location, data: ByteArray): Location { + val json = JSONObject(data.decodeToString()) + location.accuracy = 100f + location.latitude = json.getDouble("lat") + location.longitude = json.getDouble("lon") + runCatching { SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.US).parse(json.getString("utc"))?.time }.getOrNull()?.let { location.time = it } + json.optDouble("groundSpeed").takeIf { !it.isNaN() }?.let { + location.speed = (it * KNOTS_TO_METERS_PER_SECOND).toFloat() + LocationCompat.setSpeedAccuracyMetersPerSecond(location, location.speed * 0.1f) + } + json.optDouble("altitude").takeIf { !it.isNaN() }?.let { location.altitude = it * FEET_TO_METERS } + json.optDouble("heading").takeIf { !it.isNaN() }?.let { + location.bearing = it.toFloat() + LocationCompat.setBearingAccuracyDegrees(location, 90f) + } + return location + } } - return location - } + private val SOURCE_LUFTHANSA_FLYNET_EUROPE = BoardConnectLocationSource("https://www.lufthansa-flynet.com") + private val SOURCE_LUFTHANSA_FLYNET_EUROPE_2 = BoardConnectLocationSource("https://ww2.lufthansa-flynet.com") + private val SOURCE_AUSTRIAN_FLYNET_EUROPE = BoardConnectLocationSource("https://www.austrian-flynet.com") - private fun parseAirCanada(location: Location, data: ByteArray): Location { - val json = JSONObject(data.decodeToString()).getJSONObject("gpsData") - location.accuracy = 100f - location.latitude = json.getDouble("latitude") - location.longitude = json.getDouble("longitude") - json.optLong("utcTime").takeIf { it != 0L }?.let { location.time = it } - json.optDouble("altitude").takeIf { !it.isNaN() }?.let { location.altitude = it * FEET_TO_METERS } - json.optDouble("horizontalVelocity").takeIf { !it.isNaN() }?.let { - location.speed = (it * MILES_PER_HOUR_TO_METERS_PER_SECOND).toFloat() - LocationCompat.setSpeedAccuracyMetersPerSecond(location, location.speed * 0.1f) + class SncfLocationSource(base: String) : MovingWifiLocationSource("$base/router/api/train/gps") { + override fun parse(location: Location, data: ByteArray): Location { + val json = JSONObject(data.decodeToString()) + if(json.has("fix") && json.getInt("fix") == -1) throw RuntimeException("GPS not valid") + location.accuracy = 100f + location.latitude = json.getDouble("latitude") + location.longitude = json.getDouble("longitude") + json.optDouble("speed").takeIf { !it.isNaN() }?.let { + location.speed = it.toFloat() + LocationCompat.setSpeedAccuracyMetersPerSecond(location, location.speed * 0.1f) + } + location.time = json.getLong("timestamp") + json.optDouble("heading").takeIf { !it.isNaN() }?.let { + location.bearing = it.toFloat() + LocationCompat.setBearingAccuracyDegrees(location, 90f) + } + return location + } } - return location - } + private val SOURCE_SNCF = SncfLocationSource("https://wifi.sncf") + private val SOURCE_SNCF_INTERCITES = SncfLocationSource("https://wifi.intercites.sncf") + private val SOURCE_LYRIA = SncfLocationSource("https://wifi.tgv-lyria.com") + private val SOURCE_NORMANDIE = SncfLocationSource("https://wifi.normandie.fr") - private fun parseHotsplots(location: Location, data: ByteArray): Location { - val json = JSONObject(data.decodeToString()) - location.accuracy = 100f - location.latitude = json.getDouble("lat") - location.longitude = json.getDouble("lng") - json.optLong("ts").takeIf { it != 0L }?.let { location.time = it * 1000 } - json.optDouble("speed").takeIf { !it.isNaN() }?.let { - location.speed = it.toFloat() - LocationCompat.setSpeedAccuracyMetersPerSecond(location, location.speed * 0.1f) + private val SOURCE_OUIFI = object : MovingWifiLocationSource("https://ouifi.ouigo.com:8084/api/gps") { + override fun parse(location: Location, data: ByteArray): Location { + val json = JSONObject(data.decodeToString()) + if(json.has("fix") && json.getInt("fix") == -1) throw RuntimeException("GPS not valid") + location.accuracy = 100f + location.latitude = json.getDouble("latitude") + location.longitude = json.getDouble("longitude") + json.optDouble("speed").takeIf { !it.isNaN() }?.let { + location.speed = it.toFloat() + LocationCompat.setSpeedAccuracyMetersPerSecond(location, location.speed * 0.1f) + } + location.time = json.getLong("timestamp") + json.optDouble("heading").takeIf { !it.isNaN() }?.let { + location.bearing = it.toFloat() + LocationCompat.setBearingAccuracyDegrees(location, 90f) + } + return location + } } - return location - } - private fun parseInput(ssid: String, data: ByteArray): Location { - val location = Location(ssid) - return when (ssid) { - "WIFIonICE" -> parseWifiOnIce(location, data) - "OEBB" -> parseOebb(location, data) - "FlixBus", "FlixBus Wi-Fi", "FlixTrain Wi-Fi" -> parseFlixbus(location, data) - "CDWiFi", "MAVSTART-WIFI" -> parsePassengera(location, data) - "AegeanWiFi" -> parseDisplayUgo(location, data) - "Cathay Pacific", "Telekom_FlyNet", "KrisWorld", "SWISS Connect", "Edelweiss Entertainment" -> parsePanasonic(location, data) - "FlyNet", "Austrian FlyNet" -> parseBoardConnect(location, data) - "ACWiFi" -> parseAirCanada(location, data) - "OUIFI", "_SNCF_WIFI_INOUI", "_SNCF_WIFI_INTERCITES", "_WIFI_LYRIA", "NormandieTrainConnecte" -> parseSncf(location, data) - "agilis-Wifi" -> parseHotsplots(location, data) - else -> throw UnsupportedOperationException() + private val SOURCE_AIR_CANADA = object : MovingWifiLocationSource("https://airbornemedia.inflightinternet.com/asp/api/flight/info") { + override fun parse(location: Location, data: ByteArray): Location { + val json = JSONObject(data.decodeToString()).getJSONObject("gpsData") + location.accuracy = 100f + location.latitude = json.getDouble("latitude") + location.longitude = json.getDouble("longitude") + json.optLong("utcTime").takeIf { it != 0L }?.let { location.time = it } + json.optDouble("altitude").takeIf { !it.isNaN() }?.let { location.altitude = it * FEET_TO_METERS } + json.optDouble("horizontalVelocity").takeIf { !it.isNaN() }?.let { + location.speed = (it * MILES_PER_HOUR_TO_METERS_PER_SECOND).toFloat() + LocationCompat.setSpeedAccuracyMetersPerSecond(location, location.speed * 0.1f) + } + return location + } } - } - fun isLocallyRetrievable(wifi: WifiDetails): Boolean = - MOVING_WIFI_HOTSPOTS_LOCALLY_RETRIEVABLE.containsKey(wifi.ssid) + private val SOURCE_HOTSPLOTS = object : MovingWifiLocationSource("http://hsp.hotsplots.net/status.json") { + override fun parse(location: Location, data: ByteArray): Location { + val json = JSONObject(data.decodeToString()) + location.accuracy = 100f + location.latitude = json.getDouble("lat") + location.longitude = json.getDouble("lng") + json.optLong("ts").takeIf { it != 0L }?.let { location.time = it * 1000 } + json.optDouble("speed").takeIf { !it.isNaN() }?.let { + location.speed = it.toFloat() + LocationCompat.setSpeedAccuracyMetersPerSecond(location, location.speed * 0.1f) + } + return location + } + } - companion object { - private val MOVING_WIFI_HOTSPOTS_LOCALLY_RETRIEVABLE = mapOf( - "WIFIonICE" to "https://iceportal.de/api1/rs/status", - "OEBB" to "https://railnet.oebb.at/assets/modules/fis/combined.json", - "FlixBus" to "https://media.flixbus.com/services/pis/v1/position", - "FlixBus Wi-Fi" to "https://media.flixbus.com/services/pis/v1/position", - "FlixTrain Wi-Fi" to "https://media.flixbus.com/services/pis/v1/position", - "MAVSTART-WIFI" to "http://portal.mav.hu/portal/api/vehicle/realtime", - "AegeanWiFi" to "https://api.ife.ugo.aero/navigation/positions", - "Telekom_FlyNet" to "https://services.inflightpanasonic.aero/inflight/services/flightdata/v2/flightdata", - "Cathay Pacific" to "https://services.inflightpanasonic.aero/inflight/services/flightdata/v2/flightdata", - "KrisWorld" to "https://services.inflightpanasonic.aero/inflight/services/flightdata/v2/flightdata", - "SWISS Connect" to "https://services.inflightpanasonic.aero/inflight/services/flightdata/v2/flightdata", - "Edelweiss Entertainment" to "https://services.inflightpanasonic.aero/inflight/services/flightdata/v2/flightdata", - "FlyNet" to "https://ww2.lufthansa-flynet.com/map/api/flightData", - "CDWiFi" to "http://cdwifi.cz/portal/api/vehicle/realtime", - "ACWiFi" to "https://airbornemedia.inflightinternet.com/asp/api/flight/info", - "OUIFI" to "https://ouifi.ouigo.com:8084/api/gps", - "_SNCF_WIFI_INOUI" to "https://wifi.sncf/router/api/train/gps", - "_SNCF_WIFI_INTERCITES" to "https://wifi.intercites.sncf/router/api/train/gps", - "_WIFI_LYRIA" to "https://wifi.tgv-lyria.com/router/api/train/gps", - "NormandieTrainConnecte" to "https://wifi.normandie.fr/router/api/train/gps", - "agilis-Wifi" to "http://hsp.hotsplots.net/status.json", - "Austrian FlyNet" to "https://www.austrian-flynet.com/map/api/flightData", + private val MOVING_WIFI_HOTSPOTS_LOCALLY_RETRIEVABLE: Map> = mapOf( + "WIFIonICE" to listOf(SOURCE_WIFI_ON_ICE), + "OEBB" to listOf(SOURCE_OEBB_2, SOURCE_OEBB_1), + "FlixBus" to listOf(SOURCE_FLIXBUS), + "FlixBus Wi-Fi" to listOf(SOURCE_FLIXBUS), + "FlixTrain Wi-Fi" to listOf(SOURCE_FLIXBUS), + "MAVSTART-WIFI" to listOf(SOURCE_PASSENGERA_MAV), + "AegeanWiFi" to listOf(SOURCE_DISPLAY_UGO), + "Telekom_FlyNet" to listOf(SOURCE_INFLIGHT_PANASONIC), + "Cathay Pacific" to listOf(SOURCE_INFLIGHT_PANASONIC), + "KrisWorld" to listOf(SOURCE_INFLIGHT_PANASONIC), + "SWISS Connect" to listOf(SOURCE_INFLIGHT_PANASONIC), + "Edelweiss Entertainment" to listOf(SOURCE_INFLIGHT_PANASONIC), + "FlyNet" to listOf(SOURCE_LUFTHANSA_FLYNET_EUROPE, SOURCE_LUFTHANSA_FLYNET_EUROPE_2), + "CDWiFi" to listOf(SOURCE_PASSENGERA_CD), + "Air Canada" to listOf(SOURCE_AIR_CANADA), + "ACWiFi" to listOf(SOURCE_AIR_CANADA), + "ACWiFi.com" to listOf(SOURCE_AIR_CANADA), + "OUIFI" to listOf(SOURCE_OUIFI), + "_SNCF_WIFI_INOUI" to listOf(SOURCE_SNCF), + "_SNCF_WIFI_INTERCITES" to listOf(SOURCE_SNCF_INTERCITES), + "_WIFI_LYRIA" to listOf(SOURCE_LYRIA), + "NormandieTrainConnecte" to listOf(SOURCE_NORMANDIE), + "agilis-Wifi" to listOf(SOURCE_HOTSPLOTS), + "Austrian FlyNet" to listOf(SOURCE_AUSTRIAN_FLYNET_EUROPE), ) } } From de7447cf434875fc25edd32257b8e34778b8e8b9 Mon Sep 17 00:00:00 2001 From: Dan Date: Fri, 27 Sep 2024 22:31:12 +0000 Subject: [PATCH 022/132] Translated using Weblate (Ukrainian) Currently translated at 100.0% (50 of 50 strings) Translation: microG/play-services-location: core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-location-core-strings/uk/ Translated using Weblate (Ukrainian) Currently translated at 100.0% (232 of 232 strings) Translation: microG/play-services-core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-core-strings/uk/ Translated using Weblate (Ukrainian) Currently translated at 100.0% (249 of 249 strings) Translation: microG/play-services-core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-core-strings/uk/ --- .../src/main/res/values-uk/strings.xml | 22 ++++++++++++++++++- .../core/src/main/res/values-uk/strings.xml | 15 +++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/play-services-core/src/main/res/values-uk/strings.xml b/play-services-core/src/main/res/values-uk/strings.xml index 02c9f961ee..4f44e19296 100644 --- a/play-services-core/src/main/res/values-uk/strings.xml +++ b/play-services-core/src/main/res/values-uk/strings.xml @@ -23,7 +23,7 @@ %1$s бажав би використовувати: Google Account Manager Вибачте… - Застосунок на вашому пристрої намагається увійти до облікового запису Google.. \u0020Якщо ви очікували на це, натисніть кнопку Увійти для під\'єднання до сторінки авторизації Google, інакше натисніть Скасувати, аби перейти назад до застосунку, що викликав це вікно. + Застосунок на вашому пристрої намагається увійти до облікового запису Google.\n\nЯкщо ви очікували на це, натисніть кнопку Увійти для під\'єднання до сторінки авторизації Google, інакше натисніть Скасувати, аби перейти назад до застосунку, що викликав це вікно. Увійти Ваш пристрій встановлює зв\'язок із серверами Google для авторизації. \n @@ -255,4 +255,24 @@ %1$s хоче отримати доступ до вашого облікового запису під видом %2$s by %3$s. Це може надати йому привілейований доступ до вашого облікового запису. Автоматично додавати безплатні застосунки до бібліотеки Безплатні застосунки можуть перевіряти, чи були вони завантажені з Google Play. Автоматично додавайте безплатні програми до бібліотеки свого облікового запису, щоб завжди проходити перевірку всіх доступних вам безоплатних застосунків. + Обмежені сервіси microG + Я розумію + Ви користуєтеся «Обмежені сервіси microG». На відміну від звичайних сервісів microG, цей варіант працює лише із застосунками, що використовують бібліотеки microG, а не з тими, що є у Google Play. Це означає, що більшість застосунків ігноруватимуть ці служби. + Увімкнути реєстрацію пристрою + Увімкнути хмарні повідомлення + Відповідно до ваших налаштувань, microG потребує вашого дозволу, перш ніж зареєструватися у хмарних повідомленнях. + Натисніть, щоби виконати крок + Крок виконано + Завершити + Налаштувати надійне блокування екрана + Ваш обліковий запис Google управляється на вашому робочому місці або у навчальному закладі. Ваш адміністратор вирішив, що пристрої потребують надійного блокування екрана, перш ніж отримати доступ до даних облікового запису.\n\nНалаштуйте пароль, PIN-код або графічний ключ для блокування екрана. + Сповіщення про обліковий запис Google + Сповіщає, коли один з ваших облікових записів Google потребує додаткового налаштування перед використанням або коли обліковий запис несумісний з microG. + Потрібні дії з обліковим записом + Ваш обліковий запис Google потребує додаткового налаштування. + Виконайте наведені нижче дії, щоби мати змогу використовувати свій обліковий запис Google %s на цьому пристрої. + Завершення налаштування облікового запису Google + Ваш пристрій повинен зареєструватися в Google принаймні один раз.\n\nВи можете вимкнути реєстрацію пристрою в Google після завершення налаштування облікового запису. + Дозволити хмарні повідомлення для microG + Ви можете вимкнути хмарні повідомлення після завершення налаштування облікового запису. \ No newline at end of file diff --git a/play-services-location/core/src/main/res/values-uk/strings.xml b/play-services-location/core/src/main/res/values-uk/strings.xml index 189f4d23e7..0a0ed1f252 100644 --- a/play-services-location/core/src/main/res/values-uk/strings.xml +++ b/play-services-location/core/src/main/res/values-uk/strings.xml @@ -35,4 +35,19 @@ Шлях /v1/geolocate буде додано автоматично. Якщо постачальник послуг з визначення місцяположення вимагає ключ, його можна додати як параметр запиту до кореневої URL-адреси. Власна URL-адреса служби Надайте сервісу microG доступ до місцеположення + Отримувати місцеположення на основі Wi-Fi мережевого сервісу визначення розташування. + Запит від мережевого сервісу + Отримувати розташування веж мобільного зв\'язку з мережевого сервісу визначення розташування. + Власний + Виберіть мережевий сервіс визначення розташування + Умови / Приватність + Імпортувати або експортувати дані про розташування + Експортувати локальну базу даних розташування Wi-Fi + Імпортувати дані про розташування з файлу + Імпортовано %1$d записів. + Щоб продовжити користуватися мережевими сервісами визначення розташування, вам потрібно вибрати сервіс даних про розташування. + Запит від мережевого сервісу + Експортувати локальну базу даних розташування стільникових веж + Запропоновано + Потрібне налаштування \ No newline at end of file From ba763ed07559b8f85045c17d8b6a23f8d47db768 Mon Sep 17 00:00:00 2001 From: gallegonovato Date: Sat, 28 Sep 2024 09:12:24 +0000 Subject: [PATCH 023/132] Translated using Weblate (Spanish) Currently translated at 100.0% (232 of 232 strings) Translation: microG/play-services-core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-core-strings/es/ --- play-services-core/src/main/res/values-es/strings.xml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/play-services-core/src/main/res/values-es/strings.xml b/play-services-core/src/main/res/values-es/strings.xml index c802703aa5..9395f11f97 100644 --- a/play-services-core/src/main/res/values-es/strings.xml +++ b/play-services-core/src/main/res/values-es/strings.xml @@ -256,4 +256,7 @@ Esto podría tardar algunos minutos." %1$s quiere acceder a tu cuenta como si fuera %2$s by %3$s. Esto podría otorgarle acceso privilegiado a su cuenta. Agregar automáticamente aplicaciones gratuitas a la biblioteca Las aplicaciones gratuitas pueden comprobar si se han descargado de Google Play. Añade aplicaciones gratuitas automáticamente a la biblioteca de tu cuenta para que siempre pasen la comprobación todas las aplicaciones gratuitas que estén disponibles actualmente. + Servicios limitados de microG + Entiendo + Está utilizando los Servicios Limitados de microG. A diferencia de los Servicios microG habituales, este sabor sólo funciona con aplicaciones que utilizan bibliotecas microG, no con las de Google Play. Esto significa que la mayoría de las aplicaciones ignorarán estos servicios. \ No newline at end of file From 66df6a16db9ce5da6743c33f48a36885675d5d16 Mon Sep 17 00:00:00 2001 From: LucasMZ Date: Sat, 28 Sep 2024 19:06:15 +0000 Subject: [PATCH 024/132] Translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (232 of 232 strings) Translation: microG/play-services-core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-core-strings/pt_BR/ Translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (50 of 50 strings) Translation: microG/play-services-location: core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-location-core-strings/pt_BR/ --- .../src/main/res/values-pt-rBR/strings.xml | 3 +++ .../core/src/main/res/values-pt-rBR/strings.xml | 10 +++++----- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/play-services-core/src/main/res/values-pt-rBR/strings.xml b/play-services-core/src/main/res/values-pt-rBR/strings.xml index 24b51e348d..d9422e4d77 100644 --- a/play-services-core/src/main/res/values-pt-rBR/strings.xml +++ b/play-services-core/src/main/res/values-pt-rBR/strings.xml @@ -260,4 +260,7 @@ Isso pode demorar alguns minutos." %1$s deseja acessar sua conta como se fosse %2$s por %3$s. Isto pode concede-lô acesso privilegiado à sua conta. Automaticamente adicionar apps grátis à biblioteca Apps grátis podem verificar que foram baixados da Google Play. Automaticamente adicione apps grátis à sua biblioteca para que essas verificações passem pra você. + Serviços Limitados microG + Você está usando os Serviços Limitados microG. Diferente dos Serviços microG comum, esta variante só funciona com apps que usam as bibliotecas do microG, não aquelas do Google Play. Isto significa que a maioria dos aplicativos irão ignorar estes serviços. + Eu entendo \ No newline at end of file diff --git a/play-services-location/core/src/main/res/values-pt-rBR/strings.xml b/play-services-location/core/src/main/res/values-pt-rBR/strings.xml index 5be3d10e10..cdbca64e28 100644 --- a/play-services-location/core/src/main/res/values-pt-rBR/strings.xml +++ b/play-services-location/core/src/main/res/values-pt-rBR/strings.xml @@ -6,10 +6,10 @@ Localização por redes móveis Tradutor de endereço Solicitar da Mozilla - Solicitar localização baseada em redes Wi-Fi próximas pelos serviços de localização da Mozilla. + Obter localização baseada em Wi-Fi dos serviços de localização da Mozilla. Solicitar do ponto de acesso - Solicitar a localização por Wi-Fi diretamente pelos pontos de acesso que suportam isso quando conectado. - Lembrar pelo GPS + Obter localização baseada em Wi-Fi diretamente de pontos de acessos suportados quando conectado. + Lembrar do GPS Guardar localmente informações de localização de redes Wi-Fi quando o GPS é usado. Solicitar da Mozilla Solicitar a localização de redes móveis pelo serviço de localização da Mozilla. @@ -39,7 +39,7 @@ Solicitar de serviço online Obter localizações de torres de redes móveis de um serviço online. Customizado - Selecionar serviço de localização online + Escolher serv. de localização online Termos / Privacidade Configuração obrigatória Exportar banco de dados local de localização Wi-Fi @@ -47,7 +47,7 @@ Importar dados de localização por arquivo Obter localização baseada em Wi-Fi através de um serviço online. Sugerido - Importar ou exportar dados de localização + Importar/exportar dados de localização Para continuar usando os serviços de localização online, você precisa selecionar um serviço de dados de localização. %1$d relatórios importados. \ No newline at end of file From 51cdc6faa6779c6019996a207b920c870d2a690f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A4=A7=E7=8E=8B=E5=8F=AB=E6=88=91=E6=9D=A5=E5=B7=A1?= =?UTF-8?q?=E5=B1=B1?= Date: Sun, 29 Sep 2024 00:42:50 +0000 Subject: [PATCH 025/132] Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 100.0% (232 of 232 strings) Translation: microG/play-services-core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-core-strings/zh_Hans/ --- play-services-core/src/main/res/values-zh-rCN/strings.xml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/play-services-core/src/main/res/values-zh-rCN/strings.xml b/play-services-core/src/main/res/values-zh-rCN/strings.xml index d56fec06fb..41a46d39cb 100644 --- a/play-services-core/src/main/res/values-zh-rCN/strings.xml +++ b/play-services-core/src/main/res/values-zh-rCN/strings.xml @@ -241,4 +241,7 @@ microG GmsCore 内置一套自由的 SafetyNet 实现,但是官方服务器要 <b><xliff:g example=\"F-Droid\">%1$s</xliff:g></b> 想以 <xliff:g example=\"F-Droid Inc.\">%3$s</xliff:g></b> 的 <b><xliff:g example=\"F-Droid\">%2$s</xliff:g> 访问你的账户。这可能授予访问你账户的权限。 自动添加免费应用到库中 免费应用可能会检查它们是否曾经从 Google Play 被下载过。自动添加免费应用到你的账户库中可以让你一直通过当前对你可用的所有免费应用的检查。 + microG 有限服务 + 我理解 + 你在使用 microG 有限服务版本。和通常的 microG 服务不同,此版本仅适用使用 microG 库的应用,而非在 Google Play 上的应用。 这意味着多数应用会忽略这些服务。 \ No newline at end of file From 2ad69d135a8c4cc7b6556ff59eddfdb9dc250fe5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Engelbrektson?= Date: Sun, 29 Sep 2024 06:21:11 +0000 Subject: [PATCH 026/132] Translated using Weblate (Swedish) Currently translated at 100.0% (232 of 232 strings) Translation: microG/play-services-core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-core-strings/sv/ Translated using Weblate (Swedish) Currently translated at 100.0% (50 of 50 strings) Translation: microG/play-services-location: core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-location-core-strings/sv/ Translated using Weblate (Swedish) Currently translated at 100.0% (24 of 24 strings) Translation: microG/play-services-fido: core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-fido-core-strings/sv/ Translated using Weblate (Swedish) Currently translated at 100.0% (249 of 249 strings) Translation: microG/play-services-core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-core-strings/sv/ --- .../src/main/res/values-sv/strings.xml | 22 ++++++++++++++++++- .../core/src/main/res/values-sv/strings.xml | 5 +++++ .../core/src/main/res/values-sv/strings.xml | 15 +++++++++++++ 3 files changed, 41 insertions(+), 1 deletion(-) diff --git a/play-services-core/src/main/res/values-sv/strings.xml b/play-services-core/src/main/res/values-sv/strings.xml index a841de3349..e9459d835d 100644 --- a/play-services-core/src/main/res/values-sv/strings.xml +++ b/play-services-core/src/main/res/values-sv/strings.xml @@ -114,7 +114,7 @@ %1$s kräver din autentisering för att komma åt ditt Google-konto. microG-tjänster Systemet ger signaturspoofingtillstånd: - En app på din enhet försöker logga in på ett Google-konto. \u0020Om detta var avsiktligt, använd knappen Logga in för att ansluta till Googles inloggningssida, om inte, tryck på Avbryt för att gå tillbaka till programmet som gjorde att den här dialogrutan dök upp. + En app på din enhet försöker logga in på ett Google-konto.\n\nOm detta var avsiktligt, använd knappen Logga in för att ansluta till Googles inloggningssida, om inte, tryck på Avbryt för att gå tillbaka till programmet som gjorde att den här dialogrutan dök upp. Batterioptimering ignorerad: För att fortsätta kommer microG att dela namn, e-postadress och profilbild för ditt Google-konto med %1$s. Autentisering krävs @@ -242,4 +242,24 @@ %1$s vill komma åt ditt konto som om det var %2$s by %3$s. Detta kan ge F-Droid privilegierad tillgång till ditt konto. Lägg automatiskt till gratisappar i biblioteket Gratisappar kan kontrollera om de har laddats ner från Google Play. Lägg automatiskt till gratisappar i ditt kontobibliotek för att alltid klara kontrollen för alla gratisappar som för närvarande är tillgängliga för dig. + Jag förstår + microG Limited Services + Du använder microG Limited Services. Till skillnad från de vanliga mikroG-tjänsterna fungerar denna smak endast med appar som använder mikroG-bibliotek, inte de på Google Play. Detta innebär att de flesta appar ignorerar dessa tjänster. + Ditt Google-konto behöver ytterligare inställning. + Tillåt Cloud Messaging för microG + Konfigurera säkert skärmlås + Tryck för att utföra steg + Kontoåtgärder krävs + Google kontomeddelanden + Slutför följande steg för att kunna använda ditt Google-konto %s på den här enheten. + Meddelar när en av dina Google-konton kräver ytterligare inställning innan det kan användas eller när ett konto är oförenligt med microG. + Slutför installation av ditt Google-konto + Aktivera enhetsregistrering + Din enhet måste registreras hos Google minst en gång.\n\nDu kan inaktivera Google-enhetsregistrering efter att kontoinställningen är klar. + Aktivera Cloud Messaging + Du kan inaktivera Cloud Messaging efter att kontoinställningen är klar. + Enligt dina inställningar, behöver microG tillstånd från dig innan det kan registreras för Cloud Messaging. + Slutför + Ditt Google-konto hanteras av din arbetsplats eller utbildningsinstitution. Din administratör bestämde att enheter behöver ett säkert skärmlås innan de kan komma åt kontodata.\n\nAnge ett lösenord, PIN eller mönsterlåsskärm. + Steg färdigställt \ No newline at end of file diff --git a/play-services-fido/core/src/main/res/values-sv/strings.xml b/play-services-fido/core/src/main/res/values-sv/strings.xml index 146a42dad7..244e1f409f 100644 --- a/play-services-fido/core/src/main/res/values-sv/strings.xml +++ b/play-services-fido/core/src/main/res/values-sv/strings.xml @@ -19,4 +19,9 @@ Använd säkerhetsnyckel med Bluetooth Säkerhetsnycklar fungerar med Bluetooth, NFC och USB. Välj hur du vill använda nyckeln. Använd säkerhetsnyckel med NFC + 4 till 63 tecken + Vänligen ange PIN-kod för din autentisering + OK + Avbryt + Fel PIN-kod! \ No newline at end of file diff --git a/play-services-location/core/src/main/res/values-sv/strings.xml b/play-services-location/core/src/main/res/values-sv/strings.xml index f75a4493de..f2f74f0651 100644 --- a/play-services-location/core/src/main/res/values-sv/strings.xml +++ b/play-services-location/core/src/main/res/values-sv/strings.xml @@ -35,4 +35,19 @@ Återställ På detta sätt kan du ange en anpassad tjänst-URL. Ogiltiga värden kan leda till att platstjänster inte svarar eller är helt otillgängliga. Anpassad service-URL + Förfrågan från online-tjänst + Förfrågan från online-tjänst + Hämta Wi-Fi-baserad plats från platstjänst online. + Hämta mobila nätverks mastplatser från platstjänster online. + Anpassat + Välj platstjänst online + Villkor / Integritet + Konfiguration krävs + För att fortsätta använda platstjänster online måste du välja en platsdatatjänst. + Exportera lokal Wi-Fi-platsdatabas + Exportera lokal platsdatabas för mobilmaster + Importera platsdata från fil + Importerade %1$d poster. + Föreslagen + Importera eller exportera platsdata \ No newline at end of file From ce4a553ec7ee9583855e562a7c63520cce0e587f Mon Sep 17 00:00:00 2001 From: Fjuro Date: Sun, 29 Sep 2024 08:32:32 +0000 Subject: [PATCH 027/132] Translated using Weblate (Czech) Currently translated at 100.0% (232 of 232 strings) Translation: microG/play-services-core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-core-strings/cs/ Translated using Weblate (Czech) Currently translated at 100.0% (249 of 249 strings) Translation: microG/play-services-core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-core-strings/cs/ --- .../src/main/res/values-cs/strings.xml | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/play-services-core/src/main/res/values-cs/strings.xml b/play-services-core/src/main/res/values-cs/strings.xml index 8cc5c64d86..0c6fb07699 100644 --- a/play-services-core/src/main/res/values-cs/strings.xml +++ b/play-services-core/src/main/res/values-cs/strings.xml @@ -169,7 +169,7 @@ Služby microG Vteřinku… Pokračováním umožníte této aplikaci a společnosti Google používat vaše informace v souladu s jejich příslušnými smluvními podmínkami a zásadami ochrany osobních údajů. - Aplikace ve vašem zařízení se pokouší přihlásit do účtu Google. \u0020Pokud to byl záměr, použijte tlačítko Přihlásit se pro připojení se na přihlašovací stránku společnosti Google. Pokud ne, klepněte na Zrušit pro návrat zpět do aplikace, která vyvolala tento dialog. + Aplikace ve vašem zařízení se pokouší přihlásit do účtu Google.\n\nPokud to byl záměr, použijte tlačítko Přihlásit se pro připojení se na přihlašovací stránku společnosti Google. Pokud ne, klepněte na tlačítko Zrušit pro návrat zpět do aplikace, která vyvolala tento dialog. Nemáte přístup k síti. \n \nMůže se jednat o dočasný problém nebo vaše zařízení Android nemá přístup k mobilním datům. Zkuste to znovu, až budete připojeni k mobilní síti, nebo se připojte k síti Wi-Fi. @@ -242,4 +242,24 @@ Aplikace %1$s žádá o přístup k vašemu účtu, jako by byla aplikací %2$s od vývojáře %3$s. Tímto jí můžete udělit privilegovaný přístup k vašemu účtu. Automaticky přidávat bezplatné aplikace do knihovny Bezplatné aplikace mohou kontrolovat, zda byly staženy z Google Play. Automaticky přidávat bezplatné aplikace do knihovny vašeho účtu, abyste vždy prošli kontrolou u všech bezplatných aplikací, které máte aktuálně k dispozici. + Omezené služby microG + Používáte Omezené služby microG. Na rozdíl od klasických Služeb microG funguje tato varianta pouze s aplikacemi, které používají knihovny microG, nikoli ale s těmi, které používají Google Play. To znamená, že většina aplikací bude tyto služby ignorovat. + Rozumím + Upozornění účtu Google + Povolte cloudové zprávy + Po dokončení nastavení účtu můžete cloudové zprávy zakázat. + Povolte cloudové zprávy pro microG + Vaše zařízení se musí alespoň jednou zaregistrovat u společnosti Google.\n\nPo dokončení nastavení účtu můžete registraci zařízení Google zakázat. + Vyžadována akce účtu + Dokončete následující kroky, abyste mohli používat účet Google %s na tomto zařízení. + Povolte registrace zařízení + Váš účet Google potřebuje další nastavení. + Upozorní vás, pokud některý z účtů Google vyžaduje před použitím další nastavení nebo když účet není kompatibilní s microG. + Dokončete nastavení svého účtu Google + Dokončit + Krok dokončen + V závislosti na vašich předvolbách od vás potřebuje microG oprávnění, aby se mohl zaregistrovat do služby cloudových zpráv. + Váš účet Google spravuje vaše pracoviště nebo vzdělávací instituce. Váš správce rozhodl, že zařízení musí mít před přístupem k datům účtu bezpečný zámek obrazovky.\n\nNastavte zámek obrazovky s heslem, kódem PIN nebo vzorem. + Klepněte pro provedení kroku + Nastavte bezpečný zámek obrazovky \ No newline at end of file From 5c385e921d6a4d7de936474cf738902c69bfa3f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aindri=C3=BA=20Mac=20Giolla=20Eoin?= Date: Sat, 28 Sep 2024 10:04:02 +0000 Subject: [PATCH 028/132] Translated using Weblate (Irish) Currently translated at 100.0% (232 of 232 strings) Translation: microG/play-services-core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-core-strings/ga/ Translated using Weblate (Irish) Currently translated at 100.0% (249 of 249 strings) Translation: microG/play-services-core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-core-strings/ga/ --- .../src/main/res/values-ga/strings.xml | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/play-services-core/src/main/res/values-ga/strings.xml b/play-services-core/src/main/res/values-ga/strings.xml index 6b0a06259e..304e485a74 100644 --- a/play-services-core/src/main/res/values-ga/strings.xml +++ b/play-services-core/src/main/res/values-ga/strings.xml @@ -10,7 +10,7 @@ Tá do ghléas ag bunú nasc le freastalaithe Google chun tú a shíniú isteach. \n \nD\'fhéadfadh sé seo cúpla soicind a thógáil. - Tá aip ar do ghléas ag iarraidh síniú isteach ar Chuntas Google. Más rud é go raibh sé seo d\'aon ghnó, úsáid an cnaipe Sínigh isteach chun nascadh le leathanach síniú isteach Google, mura bhfuil, brúigh Cealaigh chun dul siar go dtí an feidhmchlár a léirigh an dialóg seo suas. + Tá aip ar do ghléas ag iarraidh síniú isteach ar Chuntas Google.\n\nMás rud é go raibh sé seo d\'aon ghnó, úsáid an cnaipe Sínigh isteach chun nascadh le leathanach síniú isteach Google, mura bhfuil, brúigh Cealaigh chun dul siar go dtí an feidhmchlár a léirigh an dialóg seo suas. Roghnaigh cuntas Cuir cuntas eile leis %1$s ag iarraidh rochtain a fháil ar do chuntas amhail is dá mba %2$s le %3$s. Seans go dtabharfaidh sé seo rochtain phribhléideach ar do chuntas dó. @@ -242,4 +242,24 @@ Lean ar aghaidh mar %1$s Cuir aipeanna saor in aisce leis an leabharlann go huathoibríoch Féadfaidh aipeanna saor in aisce a sheiceáil cibé an ndearnadh iad a íoslódáil ó Google Play. Cuir aipeanna saor in aisce le do leabharlann chuntais go huathoibríoch chun an tseic ar gach aip saor in aisce atá ar fáil duit faoi láthair a chur ar aghaidh i gcónaí. + Tá tú ag úsáid Seirbhísí Teoranta MicroG. Murab ionann agus na gnáthsheirbhísí microG, ní oibríonn an blas seo ach le haipeanna a úsáideann leabharlanna microG, ní iad siúd ar Google Play. Ciallaíonn sé seo go ndéanfaidh formhór na n-iarratas neamhaird ar na seirbhísí seo. + Seirbhísí Teoranta microG + Tuigim + Foláirimh chuntas Google + Gníomh cuntais ag teastáil + Críochnaigh do chuntas Google a shocrú + De réir do shainroghanna, teastaíonn cead uait ó microG sular féidir leis é féin a chlárú le haghaidh Cloud Messaging. + Cumraigh glas scáileáin slán + Cliceáil chun céim a dhéanamh + Céim críochnaithe + Tá socruithe breise de dhíth ar do Chuntas Google. + Comhlánaigh na céimeanna seo a leanas le bheith in ann do Chuntas Google %s a úsáid ar an ngléas seo. + Ní mór le do ghléas clárú le Google uair amháin ar a laghad.\n\nIs féidir leat clárú gléas Google a dhíchumasú tar éis socrú an chuntais a bheith críochnaithe. + Tugtar fógra nuair a bhíonn socruithe breise de dhíth ar cheann de do chuntais Google sular féidir é a úsáid nó nuair nach bhfuil cuntas ag luí le microG. + Cumasaigh clárú gléas + Tá do chuntas Google á bhainistiú ag d\'ionad oibre nó institiúid oideachais. Chinn do riarthóir go bhfuil glas scáileáin slán de dhíth ar ghléasanna sular féidir leo rochtain a fháil ar shonraí cuntais.\n\nSocraigh pasfhocal, UAP, nó scáileán glas patrún. + Cumasaigh Cloud Messaging + Is féidir leat Cloud Messaging a dhíchumasú nuair a bhíonn socrú an chuntais críochnaithe. + Ceadaigh Cloud Messaging le haghaidh microG + Críochnaigh \ No newline at end of file From ba49e92e7bf2711547ae0ae5a09e36691cdc11bc Mon Sep 17 00:00:00 2001 From: QSkill Date: Sat, 28 Sep 2024 11:06:44 +0000 Subject: [PATCH 029/132] Translated using Weblate (Arabic) Currently translated at 66.3% (154 of 232 strings) Translation: microG/play-services-core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-core-strings/ar/ Translated using Weblate (Arabic) Currently translated at 21.0% (4 of 19 strings) Translation: microG/vending-app Translate-URL: https://hosted.weblate.org/projects/microg/vending-app/ar/ Translated using Weblate (Arabic) Currently translated at 100.0% (50 of 50 strings) Translation: microG/play-services-location: core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-location-core-strings/ar/ Translated using Weblate (Arabic) Currently translated at 66.3% (154 of 232 strings) Translation: microG/play-services-core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-core-strings/ar/ Translated using Weblate (Arabic) Currently translated at 100.0% (4 of 4 strings) Translation: microG/play-services-ads-identifier: core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-ads-identifier-core-strings/ar/ Translated using Weblate (Arabic) Currently translated at 100.0% (19 of 19 strings) Translation: microG/vending-app Translate-URL: https://hosted.weblate.org/projects/microg/vending-app/ar/ Translated using Weblate (Arabic) Currently translated at 87.8% (137 of 156 strings) Translation: microG/play-services-core: permissions Translate-URL: https://hosted.weblate.org/projects/microg/play-services-core-permissions/ar/ Translated using Weblate (Arabic) Currently translated at 100.0% (50 of 50 strings) Translation: microG/play-services-location: core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-location-core-strings/ar/ Translated using Weblate (Arabic) Currently translated at 100.0% (156 of 156 strings) Translation: microG/play-services-core: permissions Translate-URL: https://hosted.weblate.org/projects/microg/play-services-core-permissions/ar/ Translated using Weblate (Arabic) Currently translated at 61.8% (154 of 249 strings) Translation: microG/play-services-core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-core-strings/ar/ Translated using Weblate (Arabic) Currently translated at 62.2% (155 of 249 strings) Translation: microG/play-services-core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-core-strings/ar/ --- .../core/src/main/res/values-ar/strings.xml | 8 +-- .../src/main/res/values-ar/permissions.xml | 65 ++++++++++++++++--- .../src/main/res/values-ar/strings.xml | 8 ++- .../core/src/main/res/values-ar/strings.xml | 53 ++++++++++++++- .../src/main/res/values-ar/strings.xml | 18 +++++ 5 files changed, 135 insertions(+), 17 deletions(-) diff --git a/play-services-ads-identifier/core/src/main/res/values-ar/strings.xml b/play-services-ads-identifier/core/src/main/res/values-ar/strings.xml index f97014b069..7d308cd97a 100644 --- a/play-services-ads-identifier/core/src/main/res/values-ar/strings.xml +++ b/play-services-ads-identifier/core/src/main/res/values-ar/strings.xml @@ -1,7 +1,7 @@ - إشعار معرّف الإعلان - إذن معرّف الإعلان - يتيح للتطبيق تلقي إشعار عند تحديث معرّف الإعلان أو تحديد تفضيلات تتبع الإعلانات للمستخدم. - يتيح لتطبيق الناشر بالوصول إلى معرف إعلان صالح بشكل مباشر أو غير مباشر. + إشعار مُعَرِّف الإعلان + إذن مُعَرِّف الإعلان + يتيح للتطبيق تلقي إشعار عند تحديث مُعَرِّف الإعلان أو تحديد تفضيلات تتبع الإعلانات للمستخدم. + يتيح لتطبيق الناشر بالوصول إلى مُعَرِّف إعلان صالح بشكل مباشر أو غير مباشر. \ No newline at end of file diff --git a/play-services-core/src/main/res/values-ar/permissions.xml b/play-services-core/src/main/res/values-ar/permissions.xml index 3ebf8d2656..410a7947ce 100644 --- a/play-services-core/src/main/res/values-ar/permissions.xml +++ b/play-services-core/src/main/res/values-ar/permissions.xml @@ -41,15 +41,15 @@ جوجل Wi-Fi يتيح للتطبيق الوصول إلى جوجل Wi-Fi من خلال أي حساب جوجل مرتبط. اسم المستخدم على يوتيوب - عرض سجل أنشطة تطبيقات جوجل - عرض بيانات أدسنس - عرض وإدارة بيانات أدسنس - عرض بيانات تحليلات جوجل - عرض وإدارة بيانات تحليلات جوجل + عرض سِجِل أنشطة تطبيقات جوجل + عرض بياناتك في أدسنس + عرض وإدارة بياناتك في أدسنس + عرض بياناتك في تحليلات جوجل + عرض وإدارة بياناتك في تحليلات جوجل الوصول إلى مطور جوجل بلاي على أندرويد نطاق إدارة محرك التطبيقات. إذن قراءة وكتابة API ترحيل المجموعات. - عرض وإدارة إعدادات مجموعة تطبيقات جوجل + عرض وإدارة إعداداتك في مجموعة تطبيقات جوجل إذن القراءة والكتابة لـ API إدارة التراخيص. يتيح للتطبيق الوصول إلى جميع خدمات جوجل من خلال أي حساب جوجل مرتبط. خدمات أندرويد @@ -83,9 +83,9 @@ يتيح للتطبيق الوصول إلى يوتيوب من خلال أي حساب جوجل مرتبط. يتيح للتطبيق الوصول إلى اسم المستخدم على يوتيوب من خلال أي حساب جوجل مرتبط. إدارة إعدادات حساب المشتري في منصة التبادلات الإعلانية - عرض وإدارة بيانات أدسنس الخاصة بالاستضافة والحسابات المرتبطة - عرض بيانات منصة التبادلات الإعلانية - عرض وإدارة بيانات منصة التبادلات الإعلانية + عرض وإدارة بياناتك في أدسنس الخاصة بالاستضافة والحسابات المرتبطة + عرض بياناتك في منصة التبادلات الإعلانية + عرض وإدارة بياناتك في منصة التبادلات الإعلانية بالنسبة لمسؤولي ومستخدمي البائعين المعتمدين، إذن القراءة والكتابة عند الاختبار في وضع الملعب للـ API، أو إذن القراءة والكتابة عند استدعاء عملية API مباشرة. بالإضافة إلى نطاق بروتوكول أوث للقراءة والكتابة بشكل عام، استخدم نطاق بروتوكول أوث للقراءة فقط عند استرجاع بيانات العميل. الوصول إلى API لتدقيق الإدارة بصلاحية القراءة فقط @@ -109,4 +109,51 @@ يتيح الوصول إلى مجلد بيانات التطبيقات عرض تطبيقات جوجل درايف الخاصة بك عرض وإدارة ملفات جوجل درايف التي قمت بفتحها أو إنشائها باستخدام هذا التطبيق + نطاق خاص يستخدم للسماح للمستخدمين بالموافقة على تثبيت التطبيقات + عرض البيانات الوصفية للملفات والمستندات في جوجل درايف + عرض الملفات والمستندات في جوجل درايف + تعديل سلوك النصوص البرمجية النصية لتطبيقات جوجل + عرض وإدارة الملفات والمستندات في جوجل درايف + عرض حساب فريبيس + سَجِّل الدخول إلى فريبيس بحسابك + إدارة جداول الدمج من جوجل + عرض جداول الدمج من جوجل + نطاق للوصول إلى بيانات ألعاب جوجل بلاي. + إدارة بيانات GAN الخاص بك + عرض بيانات GAN الخاص بك + نطاق الجدول الزمني الزجاجي + إنشاء، قراءة، تحديث، وحذف المسودات. إرسال الرسائل والمسودات. + قراءة جميع الموارد وبياناتها الوصفية - لا توجد عمليات كتابة. + إدارة أفضل موقع متاح لك وسِجِل مواقعك + إدارة موقعك على مستوى المدينة وسِجِل مواقعك + إدارة أفضل موقع متاح لك + إدارة موقعك على مستوى المدينة + عرض وإدارة بيانات محرك خرائط جوجل + عرض بيانات محرك خرائط جوجل + عرض خرائط جوحل وإدارتها لتجربة الجوال + عرض بياناتك في أوركوت + إدارة نشاطك في أوركوت + اعرف من أنت على جوجل + المراسلة السحابية للكروم + جميع عمليات القراءة/الكتابة باستثناء عمليات الحذف الفوري والدائم للمواضيع والرسائل، متجاوزًا سلة المهملات. + تعرف على اسمك ومعلوماتك الأساسية وقائمة الأشخاص الذين تتواصل معهم على جوجل+ + إدارة بياناتك في واجهة برمجة تطبيقات التنبؤ من جوجل + عرض بيانات منتجاتك + إدارة قائمة المواقع والنطاقات التي تتحكم فيها + الوصول للقراءة/الكتابة إلى واجهة برمجة تطبيقات محتوى التسوق. + استهلاك المهام من قوائم مهامك + إدارة مهامك + إدارة مهامك + عرض مهامك + واجهة برمجة تطبيقات التتبع للخرائط جوجل، يسمح هذا النطاق بالوصول للقراءة والكتابة إلى بيانات مشروعك. + إدارة عناوين goo.gl الخاصة بك + عرض المعلومات الأساسية حول حسابك + إدارة حسابك في يوتيوب + عرض حسابك في يوتيوب + إدارة فيديوهاتك في يوتيوب + عرض التقارير النقدية لمحتواك على يوتيوب + عرض تحليلات يوتيوب لقناتك + عرض عنوان بريدك الإلكتروني + عرض وإدارة أصولك والمحتوى المرتبط على يوتيوب + إدارة عمليات التحقق من موقعك الجديد مع جوجل \ No newline at end of file diff --git a/play-services-core/src/main/res/values-ar/strings.xml b/play-services-core/src/main/res/values-ar/strings.xml index 18b92b63a6..cf243143f8 100644 --- a/play-services-core/src/main/res/values-ar/strings.xml +++ b/play-services-core/src/main/res/values-ar/strings.xml @@ -74,7 +74,7 @@ إعداد خدمات مايكرو-جي. اصبر قليلًا… جوجل - يحاول تطبيق على جهازك تسجيل الدخول إلى حساب جوجل. \u0020إن كان هذا مقصودًا، استخدم زر تسجيل الدخول للاتصال بصفحة تسجيل الدخول الخاصة بجوجل. إن لم يكن الأمر كذلك، اضغط على إلغاء للعودة إلى التطبيق الذي تسبب في ظهور هذه الرسالة. + يحاول تطبيق على جهازك تسجيل الدخول إلى حساب جوجل.\n\nإن كان هذا مقصودًا، استخدم زر تسجيل الدخول للاتصال بصفحة تسجيل الدخول الخاصة بجوجل. إن لم يكن الأمر كذلك، اضغط على إلغاء للعودة إلى التطبيق الذي تسبب في ظهور هذه الرسالة. يحتاج %1$s إذنك للوصول إلى حسابك على جوجل. يريد %1$s الوصول إلى حسابك وكأنه %2$s من %3$s. هذه قد يمنحه وصولًا مميزًا إلى حسابك. استمع إلى رسائل C2DM @@ -115,7 +115,7 @@ عند تفعيل هذا الخيار، لن تتضمن طلبات المصادقة اسم الجهاز، مما قد يسمح للأجهزة غير المصرح بها بتسجيل الدخول، لكن قد يؤدي ذلك إلى عواقب غير متوقعة. عند تفعيل هذا الخيار، ستتمكن جميع التطبيقات من رؤية عنوان البريد الإلكتروني لحسابات جوجل الخاصة بك دون الحصول على إذن مسبق. مُعَرِّف أندرويد - يسجل جهازك عند خدمات جوجل ويُنشئ معرّفًا فريدًا للجهاز. سيقوم مايكرو-جي بإزالة الأجزاء المُمَيِزة لك باستثناء اسم حساب جوجل الخاص بك في بيانات التسجيل. + يُسَجِلُ جهازك عند خدمات جوجل ويُنشئ معرّفًا فريدًا للجهاز. سيقوم مايكرو-جي بإزالة الأجزاء المُمَيِزة لك باستثناء اسم حساب جوجل الخاص بك في بيانات التسجيل. ليس مسجَّلًا آخر تسجيل: %1$s تسجيل الجهاز @@ -134,7 +134,7 @@ فترة التحقق: %1$s نبذه عن خدمات مايكرو-جي معلومات اﻹصدار والمكتبات الرمجية المستخدمة - خطأ في إلغاء التسجيل + حدث خطأ أثناء إلغاء التسجيل لم يعد مثبَّتًا إلغاء التسجيل لم تصل رسائل بعد @@ -164,4 +164,6 @@ التطبيقات المسجّلة التطبيقات غير المسجّلة الشبكات المستخدمة لإشعارات الدفع + خدمات مايكرو-جي المحدودة + تنبيهات حساب جوجل \ No newline at end of file diff --git a/play-services-location/core/src/main/res/values-ar/strings.xml b/play-services-location/core/src/main/res/values-ar/strings.xml index a6b3daec93..e7c28c9f71 100644 --- a/play-services-location/core/src/main/res/values-ar/strings.xml +++ b/play-services-location/core/src/main/res/values-ar/strings.xml @@ -1,2 +1,53 @@ - \ No newline at end of file + + الموقع + الوصول الأخير + موقع شبكة الجوال + محلل العناوين + طلب من موزيلا + موقع شبكة الـ Wi-Fi + طلب من خدمة عبر الإنترنت + حصول على موقع شبكة الـ Wi-Fi من خدمة تحديد الموقع من موزيلا. + حصول على موقع شبكة الـ Wi-Fi من خدمة تحديد الموقع عبر الإنترنت. + طلب من نقطة الاتصال + تخزين مواقع الـ Wi-Fi محلياً عند استخدام GPS. + طلب من موزيلا + حصول على مواقع الأبراج الخلوية لشبكة الجوال من خدمة تحديد الموقع من موزيلا. + تذكُّر من GPS + طلب من خدمة عبر الإنترنت + حصول على مواقع الأبراج الخلوية لشبكة الجوال من خدمة تحديد الموقع من خدمة عبر اﻹنترنت. + تذكُّر من GPS + تخزين مواقع شبكة الجوال محلياً عند استخدام GPS. + استخدام نوميناتيم + حلّ العناوين باستخدام خدمة نوميناتيم من خريطة الشارع المفتوحة. + تطبيقات ذات إمكانية الوصول إلى الموقع + آخر وصول: %1$s + استخدام الموقع التقريبي دائمًا + قم دائمًا بتقديم المواقع التقريبي لهذا التطبيق، مع تجاهل مستوى الإذن الخاص به. + للحصول على تجربة أفضل، قم بتشغيل موقع الجهاز، والذي يستخدم خدمة تحديد الموقع لمايكرو-جي + للمتابعة، قم بتشغيل موقع الجهاز، والذي يستخدم خدمة تحديد الموقع لمايكرو-جي + سيحتاج جهازك إلى: + استخدام GPS، وWi-Fi، وشبكة الجوال، والمستشعرات + منح أذونات الموقع لخدمة مايكرو-جي + لا شكرًا + موافق + إعادة الضبط + استخدام خدمة تحديد الموقع لمايكرو-جي؛ كجزء من هذه الخدمة، قد يقوم مايكرو-جي بجمع بيانات الموقع بشكل دوري ومُخفي للمصدر واستخدام هذه البيانات لتحسين دقة الموقع والخدمات القائمة على الموقع. + تعيين رابط الخدمة + اختر خدمة تحديد الموقع عبر الإنترنت + رابط مخصص للخدمة + مخصص + يسمح هذا بتعيين رابط مخصص للخدمة. القيم غير الصالحة قد تؤدي إلى عدم استجابة خدمات الموقع. + الشروط / الخصوصية + مقترح + استيراد أو تصدير بيانات الموقع + التهيئة مطلوبة + لمتابعة استخدام خدمات الموقع عبر الإنترنت، تحتاج إلى تحديد خدمة بيانات الموقع. + تصدير قاعدة بيانات مواقع الـ Wi-Fi المحلية + تصدير قاعدة بيانات مواقع الأبراج الخلوية المحلية + تم استيراد %1$d سِجِل. + حصول على موقع شبكة الـ Wi-Fi مباشرةً من نقاط الاتصال المدعومة عند الاتصال بها. + لمزيد من التفاصيل، انتقل إلى إعدادات الموقع. + يتم إلحاق المسار \"v1/geolocate/\" تلقائيًا. إذا كان موفِّر الموقع يتطلب مفتاحًا، فيمكن إلحاقه كمعلمة استعلام إلى جذر الرابط. + استيراد بيانات الموقع من ملف + \ No newline at end of file diff --git a/vending-app/src/main/res/values-ar/strings.xml b/vending-app/src/main/res/values-ar/strings.xml index 0a56150062..849953629f 100644 --- a/vending-app/src/main/res/values-ar/strings.xml +++ b/vending-app/src/main/res/values-ar/strings.xml @@ -1,4 +1,22 @@ مرافق مايكرو-جي + لا يمكن استخدام مرافق مايكرو-جي بشكل مستقل. يرجى تثبيت خدمات مايكرو-جي لاستخدام مايكرو-جي. + لا يمكن استخدام مرافق مايكرو-جي بشكل مستقل. تم فتح إعدادات خدمات مايكرو-جي بدلًا من ذلك. + إشعارات التراخيص + تعذر على %1$s التحقق من الترخيص + إن قام التطبيق بتصرفات غير متوقعة، فسجل الدخول إلى حساب جوجل الذي اشتريت التطبيق بواسطته. + سَجِّل الدخول + تجاهل + الدفع غير متاح حاليًا + تأكيد عملية الشراء + أنت غير متصلٍ بالإنترنت. يرجى التأكد من تشغيل شبكة الـ Wi-Fi أو شبكة الجوال أَعِد المحاولة. + كلمة المرور غير صحيحة. + حدث خطأ مجهول، يرجى الخروج والمحاولة مرة أخرى. + أدخل كلمة المرور + تذكر تسجيل دخولي على هذا الجهاز + هل نسيت كلمة المرور؟ + معرفة المزيد + تحقق + يتم إشعارك عندما يحاول تطبيق من التحقق من صحة ترخيصه، ولكنك لم تقم بتسجيل الدخول إلى أي حساب جوجل. \ No newline at end of file From 10f3147fb415bb685b6c3df19fcf47a71ae560df Mon Sep 17 00:00:00 2001 From: Leo Alvesson Date: Mon, 30 Sep 2024 14:10:16 +0000 Subject: [PATCH 030/132] Translated using Weblate (French) Currently translated at 87.9% (204 of 232 strings) Translation: microG/play-services-core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-core-strings/fr/ Translated using Weblate (French) Currently translated at 100.0% (4 of 4 strings) Translation: microG/play-services-core: plurals Translate-URL: https://hosted.weblate.org/projects/microg/play-services-core-plurals/fr/ Translated using Weblate (French) Currently translated at 100.0% (156 of 156 strings) Translation: microG/play-services-core: permissions Translate-URL: https://hosted.weblate.org/projects/microg/play-services-core-permissions/fr/ Translated using Weblate (French) Currently translated at 100.0% (13 of 13 strings) Translation: microG/play-services-base: core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-base-core-strings/fr/ Translated using Weblate (French) Currently translated at 1.6% (1 of 59 strings) Translation: microG/play-services-nearby: core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-nearby-core-strings/fr/ Translated using Weblate (French) Currently translated at 100.0% (4 of 4 strings) Translation: microG/play-services-ads-identifier: core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-ads-identifier-core-strings/fr/ Translated using Weblate (French) Currently translated at 16.6% (1 of 6 strings) Translation: microG/play-services-oss-licenses: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-oss-licenses-strings/fr/ Translated using Weblate (French) Currently translated at 36.8% (7 of 19 strings) Translation: microG/vending-app Translate-URL: https://hosted.weblate.org/projects/microg/vending-app/fr/ Translated using Weblate (French) Currently translated at 100.0% (3 of 3 strings) Translation: microG/play-services-auth-api-phone: core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-auth-api-phone-core-strings/fr/ Translated using Weblate (French) Currently translated at 4.1% (1 of 24 strings) Translation: microG/play-services-fido: core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-fido-core-strings/fr/ Translated using Weblate (French) Currently translated at 20.0% (1 of 5 strings) Translation: microG/play-services-droidguard: core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-droidguard-core-strings/fr/ Translated using Weblate (French) Currently translated at 100.0% (232 of 232 strings) Translation: microG/play-services-core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-core-strings/fr/ Translated using Weblate (French) Currently translated at 23.7% (14 of 59 strings) Translation: microG/play-services-nearby: core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-nearby-core-strings/fr/ Translated using Weblate (French) Currently translated at 100.0% (50 of 50 strings) Translation: microG/play-services-location: core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-location-core-strings/fr/ Translated using Weblate (French) Currently translated at 100.0% (6 of 6 strings) Translation: microG/play-services-oss-licenses: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-oss-licenses-strings/fr/ Translated using Weblate (French) Currently translated at 100.0% (19 of 19 strings) Translation: microG/vending-app Translate-URL: https://hosted.weblate.org/projects/microg/vending-app/fr/ Translated using Weblate (French) Currently translated at 100.0% (24 of 24 strings) Translation: microG/play-services-fido: core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-fido-core-strings/fr/ Translated using Weblate (French) Currently translated at 100.0% (5 of 5 strings) Translation: microG/play-services-droidguard: core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-droidguard-core-strings/fr/ Translated using Weblate (French) Currently translated at 100.0% (232 of 232 strings) Translation: microG/play-services-core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-core-strings/fr/ Translated using Weblate (French) Currently translated at 100.0% (59 of 59 strings) Translation: microG/play-services-nearby: core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-nearby-core-strings/fr/ Translated using Weblate (French) Currently translated at 100.0% (50 of 50 strings) Translation: microG/play-services-location: core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-location-core-strings/fr/ Translated using Weblate (French) Currently translated at 100.0% (249 of 249 strings) Translation: microG/play-services-core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-core-strings/fr/ --- .../core/src/main/res/values-fr/strings.xml | 7 +- .../core/src/main/res/values-fr/strings.xml | 6 +- .../core/src/main/res/values-fr/strings.xml | 8 +- .../src/main/res/values-fr/permissions.xml | 173 ++++++++++++++-- .../src/main/res/values-fr/plurals.xml | 12 +- .../src/main/res/values-fr/strings.xml | 190 ++++++++++++++++-- .../core/src/main/res/values-fr/strings.xml | 8 +- .../core/src/main/res/values-fr/strings.xml | 27 ++- .../core/src/main/res/values-fr/strings.xml | 54 ++++- .../core/src/main/res/values-fr/strings.xml | 68 ++++++- .../src/main/res/values-fr/strings.xml | 9 +- .../src/main/res/values-fr/strings.xml | 13 ++ 12 files changed, 520 insertions(+), 55 deletions(-) diff --git a/play-services-ads-identifier/core/src/main/res/values-fr/strings.xml b/play-services-ads-identifier/core/src/main/res/values-fr/strings.xml index a6b3daec93..c82f144a93 100644 --- a/play-services-ads-identifier/core/src/main/res/values-fr/strings.xml +++ b/play-services-ads-identifier/core/src/main/res/values-fr/strings.xml @@ -1,2 +1,7 @@ - \ No newline at end of file + + Permission de l\'identifiant publicitaire + Autorise une application affichant de la publicité à accéder directement ou indirectement à un identifiant publicitaire valide. + Notification de l\'identifiant publicitaire + Autorise une application à être notifiée de la modification de l\'identifiant publicitaire ou de la limitation du suivi publicitaire de l\'utilisateur. + \ No newline at end of file diff --git a/play-services-auth-api-phone/core/src/main/res/values-fr/strings.xml b/play-services-auth-api-phone/core/src/main/res/values-fr/strings.xml index a6b3daec93..6bd087dd3c 100644 --- a/play-services-auth-api-phone/core/src/main/res/values-fr/strings.xml +++ b/play-services-auth-api-phone/core/src/main/res/values-fr/strings.xml @@ -1,2 +1,6 @@ - \ No newline at end of file + + Autoriser %s à lire le message ci-dessous et saisir le code ? + Autoriser + Refuser + \ No newline at end of file diff --git a/play-services-base/core/src/main/res/values-fr/strings.xml b/play-services-base/core/src/main/res/values-fr/strings.xml index baa3b65fe8..b615a4bc86 100644 --- a/play-services-base/core/src/main/res/values-fr/strings.xml +++ b/play-services-base/core/src/main/res/values-fr/strings.xml @@ -2,7 +2,7 @@ + --> Avancé Aucun Désactivé @@ -10,4 +10,10 @@ Automatique Manuel Actif en arrière-plan + Ouvrir + Exclure %1$s de l\'optimisation de la batterie ou modifier les paramètres des notifications pour désactiver cette notification. + Act. + Désact. + Tout voir + %1$s fonctionne en arrière-plan. \ No newline at end of file diff --git a/play-services-core/src/main/res/values-fr/permissions.xml b/play-services-core/src/main/res/values-fr/permissions.xml index fb6b4efb08..a443789139 100644 --- a/play-services-core/src/main/res/values-fr/permissions.xml +++ b/play-services-core/src/main/res/values-fr/permissions.xml @@ -13,30 +13,161 @@ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. ~ See the License for the specific language governing permissions and ~ limitations under the License. - --> - - + --> Tous les services Google - Permets aux applications d\'accéder à tous les services Google par le biais de tous comptes associés à Google - + accéder à tous les services Google par le biais de tout compte Google associé. Services Android - Permets aux applications d\'accéder aux services Android par le biais de tous comptes associés à Google + accéder aux services Android par le biais de tout compte Google associé. Google AdSense - Pub - Permets aux applications d\'accéder à AdSense par le biais de tous comptes associés à Google - Google AdWords -Pub - Permets aux applications d\'accéder à AdWords par le biais de tous comptes associés à Google - Google App Engine - conception et hébergement d\'applications web basée sur les serveurs de Google. - Permets aux applications d\'accéder à Google App Engine le biais de tous comptes associés à Google + accéder à AdSense par le biais de tout compte Google associé. + Google AdWords - Pub + accéder à AdWords par le biais de tout compte Google associé. + Google App Engine - conception et hébergement d\'applications web basé sur les serveurs de Google + accéder à Google App Engine par le biais de tout compte Google associé. Google Blogger - Blogueur - Permets aux applications d\'accéder à Blogger par le biais de tous comptes associés à Google + accéder à Blogger par le biais de tout compte Google associé. Calendrier Google - Permets aux applications d\'accéder au Calendrier par le biais de tous comptes associés à Google - Google Contacts - Gérer vos contacts - Permets aux applications d\'accéder aux contacts par le biais de tous comptes associés à Google + accéder au Calendrier Google par le biais de tout compte Google associé. + Contacts Google + accéder à Contacts Google par le biais de tout compte Google associé. Dodgeball - Fournisseur de logiciels de réseaux sociaux basé sur l\'emplacement des fournisseurs de téléphonie mobile - Permets aux applications d\'accéder à Dodgeball par le biais de tous comptes associés à Google - Google Finance - Google Finance est un service en ligne publié par Google en 2006. Il permet de suivre le cours des actions et des devises. - Permets aux applications d\'accéder à Google Finance par le biais de tous comptes associés à Google. - Google Base - Google a mis officiellement en ligne ce service le 16 novembre 2005 et permet de mettre en ligne et de référencer tout type de contenu en termes d\'information. - Permets aux applications d\'accéder à Google Base par le biais de tous comptes associés à Google. - \ No newline at end of file + accéder à Dodgeball par le biais de tout compte Google associé. + Google Finance - Service en ligne publié par Google permettant de suivre le cours des actions et des devises + accéder à Google Finance par le biais de tout compte Google associé. + Google Base - service permettant de mettre en ligne et de référencer tout type de contenu en termes d\'information + accéder à Google Base par le biais de tout compte Google associé. + Accéder à Google Play Développeur Android + Pour les utilisateurs et administrateurs revendeurs, accès lecture/écriture lors de tests dans la sandbox de l\'API, ou accès lecture/écriture lors d\'un appel direct pour une opération API. + Accès lecture/écriture à l\'API Groups Migration. + Gérer vos calendriers + Gérer vos données GAN + Voir vos calendriers + Voir et gérer vos données d\'impression Google Cloud + Voir et gérer vos ressources Google Compute Engine + Voir vos ressources Google Compute Engine + Voir vos données dans Google Cloud Storage + Voir et gérer vos jobs Google Maps Coordinate + Gérer vos données et permissions dans Google Cloud Storage + Gérer vos données dans Google Cloud Storage + Voir et gérer les rapports DoubleClick for Advertisers + Autoriser l\'accès au dossier Application Data + Voir vos applications Google Drive + Voir et gérer les fichiers que vous avez ouvert ou créé avec l\'application Google Drive + Cadre spécial utilisé pour permettre aux utilisateurs d\'approuver l\'installation d\'une application + Voir les métadonnées pour les fichiers et documents dans votre Google Drive + Voir les fichiers et documents dans votre Google Drive + Modifier le comportement de vos scripts Google Apps Script + Voir et gérer les fichiers et documents dans votre Google Drive + voir votre compte Freebase + Se connecter à votre compte Freebase + Gérer vos Tables Fusion + Voir vos Tables Fusion + Cadre pour accéder aux données de Google Play Games. + Voir vos données GAN + Étendue de la chronologie Glass + CloudMessaging pour Chrome + Créer, lire, modifier et supprimer des brouillons. Envoyer des messages et brouillons. + Toutes les opérations de lecture/écriture excepté les suppressions immédiates et permanentes de conversations et messages, sans passer par la corbeille. + Lire toutes les ressources et leurs métadonnées - pas d\'opération d\'écriture. + Gérer votre meilleure position disponible et historique de localisation + Gérer votre localisation approximative (nom de la ville) et historique de localisation + Gérer votre meilleur position disponible + Gérer votre localisation (ville) + Voir et gérer vos données Google Maps Engine + Voir vos données Google Maps Engine + Voir vos données Orkut + Voir et gérer votre expérience mobile Google Maps + Gérer votre activité Orkut + Identifier vos nom, informations basiques et liste de personnes auxquelles vous êtes connectées sur Google+ + Identifier qui vous êtes sur Google + Gérer vos données dans l\'API Google Prediction + Voir vos données produit + Gérer la liste des sites et domaines que vous contrôlez + Gérer les vérifications de votre nouveau site avec Google + Consommer les tâches de vos listes de tâches + Accès lecture/écriture à l\'API Shopping Content. + Gérer vos tâches + Voir votre adresse mail + API Google Maps Tracks, ce cadre permet un accès lecture/écriture aux données de votre projet. + Gérer vos URLs raccourcies goo.gl + Voir les informations de base sur votre compte + Gérer votre compte YouTube + Gérer vos vidéos YouTube + Voir et gérer vos actifs et contenus associés sur YouTube + Voir votre compte YouTube + Voir les rapports monétaires YouTube Analytics pour votre contenu YouTube + Voir les rapports YouTube Analytics pour votre contenu YouTube + Google Voice - Voix Google + accéder à JotSpot par le biais de tout compte Google associé. + accéder à iGoogle par le biais de tout compte Google associé. + JotSpot + Knol + accéder à Knol par le biais de tout compte Google associé. + Picasa Web Albums - Albums Web Picasa + accéder à Picasa Web Albums par le biais de tout compte Google associé. + Google Actualités + Google Maps + accéder à Google Maps par le biais de tout compte Google associé. + Mail Google + accéder à Google Mail par le biais de tout compte Google associé. + accéder à Google Actualités par le biais de tout compte Google associé. + Google Notebook + accéder à Google Notebook par le biais de tout compte Google associé. + Orkut + accéder à Orkut par le biais de tout compte Google associé. + Google Livres + accéder à Google Livres par le biais de tout compte Google associé. + Google Checkout accounts - Déconnexion comptes Google + Google Checkout QA accounts _ Déconnexion comptes Google QA + Google Checkout Sandbox accounts - Déconnexion comptes Google Sandbox + accéder à Google Checkout QA accounts par le biais de tout compte Google associé. + accéder à Google Checkout accounts par le biais de tout compte Google associé. + accéder à Google Checkout Sandbox accounts par le biais de tout compte Google associé. + accéder à Google Webmaster Tools par le biais de tout compte Google associé. + Google Webmaster Tools - Outils Google Webmaster + accéder à la recherche vocale par le biais de tout compte Google associé. + Recherche Vocale + accéder aux noms d\'utilisateurs YouTube par le biais de tout compte Google associé. + Voir et gérer vos données Ad Exchange + Gérer vos tâches + Voir vos tâches + Google Groups - Groupes Google + accéder à Google Voice par le biais de tout compte Google associé. + accéder à Google Groups par le biais de tout compte Google associé. + Google Health - Santé Google + accéder à Google Health par le biais de tout compte Google associé. + iGoogle + Reconnaissance Vocale personnalisée + accéder à la reconnaissance vocale personnalisée par le biais de tout compte Google associé. + Google Talk + accéder à Google Talk par le biais de tout compte Google associé. + Google Wi-Fi + accéder à Google Wi-Fi par le biais de tout compte Google associé. + Google Spreadsheets + accéder à Google Spreadsheets par le biais de tout compte Google associé. + Google Docs + accéder à Google Docs par le biais de tout compte Google associé. + YouTube + accéder à YouTube par le biais de tout compte Google associé. + noms d\'utilisateurs YouTube + Voir l\'historique d\'activité de vos applications Google + Gérer votre configuration de compte acheteur Ad Exchange + Voir vos données Ad Exchange + Voir vos données AdSense + Voir et gérer vos données hôte AdSense et comptes associés + Voir et gérer vos données AdSense + Voir vos données Google Analytics + Voir et gérer vos données Google Analytics + Périmètre App Engine Admin. + Voir et gérer les paramètres d\'un Groupe Google Apps + En plus du cadre global lecture/écriture OAuth, utiliser le contexte lecture seule OAuth lors de la récupération des données du client. + Accès lecture/écriture à l\'API License Manager. + Accès à l\'API Admin Audit en lecture seule + Cadre pour l\'utilisation du service App State. + Voir vos données dans Google BigQuery + Voir et gérer vos données dans Google BigQuery + Gérer votre compte Blogger + Voir votre compte Blogger + Gérer vos livres + Voir vos jobs Google Coordinate + \ No newline at end of file diff --git a/play-services-core/src/main/res/values-fr/plurals.xml b/play-services-core/src/main/res/values-fr/plurals.xml index 8731b34b6f..2781c57693 100644 --- a/play-services-core/src/main/res/values-fr/plurals.xml +++ b/play-services-core/src/main/res/values-fr/plurals.xml @@ -13,23 +13,25 @@ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. ~ See the License for the specific language governing permissions and ~ limitations under the License. - --> - - + --> %1$d fournisseur configuré + %1$d fournisseurs configurés %1$d fournisseurs configurés %1$d application enregistrée + %1$d applications enregistrées %1$d applications enregistrées - Une autorisation requises pour le fonctionnement correct de microG Service est manquante. - Plusieurs autorisations requises pour le fonctionnement correct de microG Service sont manquantes. + Une autorisation requise pour le bon fonctionnement des services microG est manquante. + Plusieurs autorisations requises pour le bon fonctionnement des services microG sont manquantes. + Plusieurs autorisations requises pour le bon fonctionnement des services microG sont manquantes. Demander la autorisation manquante + Demander les autorisations manquantes Demander les autorisations manquantes \ No newline at end of file diff --git a/play-services-core/src/main/res/values-fr/strings.xml b/play-services-core/src/main/res/values-fr/strings.xml index ead087b404..0f22ea63a4 100644 --- a/play-services-core/src/main/res/values-fr/strings.xml +++ b/play-services-core/src/main/res/values-fr/strings.xml @@ -14,7 +14,7 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License. --> - microG Services + Services microG Paramètres de microG Juste une seconde… Google @@ -44,18 +44,18 @@ Ceci peut prendre plusieurs minutes." Enregistrement du terminal auprès de Google Cloud Messaging Google SafetyNet - Google Play Jeux - %1$s voudrait utiliser Play Jeux + Jeux Google Play + %1$s voudrait utiliser Jeux Google Play Pour utiliser Play Jeux il est requis d’installer l’application Google Play Jeux. L’application peut poursuivre sans Play Jeux, mais il est possible qu’elle se comporte de manière imprévue. Sélectionner un emplacement La sélection d’emplacement n’est pas encore disponible. Sélectionner cette position - Lieux environnants. + Lieux environnants (%1$.7f, %2$.7f) - microG Services: La permission %1$s est manquante. + Services microG : L\'autorisation %1$s est manquante Réseau mobile Wi-Fi - Roaming + Itinérance Autres réseaux @@ -68,15 +68,15 @@ Ceci peut prendre plusieurs minutes." C’est une indication forte que la ROM supporte l’usurpation de signature mais que celle-ci requiert une action supplémentaire pour être activée. Merci de consulter la documentation sur les éventuelles étapes nécessaires. Le système usurpe la signature : Merci de consulter la documentation sur les éventuelles étapes nécessaires. - Play Services (GmsCore) - Play Store (Phonesky) - Services Framework (GSF) + Services microG (GmsCore) + Compagnon microG (Phonesky) + microG Services Framework (GSF) %1$s installé : Installez l’application %1$s ou tout autre compatible. Merci de consulter la documentation pour obtenir la liste des applications compatibles. %1$s dispose de la bonne signature : Soit l’application %1$s installée n’est pas compatible, soit l’usurpation de signature n’est pas activée pour celle-ci. Merci de consulter la documentation sur les applications et ROMs compatibles. Optimisations de la batterie ignorées : - Appuyez ici pour désactiver les optimisations de la batterie. Des applications peuvent mal se comporter si vous ne le faites pas. + Appuyez ici pour désactiver les optimisations de la batterie. Des applications peuvent mal fonctionner si vous ne le faites pas. À propos @@ -87,7 +87,7 @@ Ceci peut prendre plusieurs minutes." Services Test Optimisations de la batterie activées - Vous avez activé Cloud Messaging mais l’optimisation de la batterie est activée pour microG Services. Afin de recevoir les notifications push vous devriez désactiver les optimisations de la batterie. + Vous avez activé Cloud Messaging mais l’optimisation de la batterie est activée pour les services microG. Afin de recevoir les notifications poussées vous devriez désactiver les optimisations de la batterie. Désactiver les optimisations de la batterie Autorisations manquantes Préférences du compte @@ -97,15 +97,16 @@ Ceci peut prendre plusieurs minutes." Si désactivé, l’utilisateur est interrogé avant que la demande d’autorisation d’une application soit envoyée à Google. Certaines applications échoueront à utiliser le compte Google si ceci est désactivé. Enregistre votre terminal auprès des services Google et crée un identifiant unique. microG retire les identifiants autres que le nom de votre compte Google des informations d’enregistrement. Plus - Cloud Messaging est un fournisseur de notifications push utilisé par beaucoup d’applications tierces. Pour l’utiliser vous devez activer l’enregistrement du terminal. + Cloud Messaging est un fournisseur de notifications poussées utilisé par beaucoup d’applications tierces. Pour l’utiliser vous devez activer l’enregistrement du terminal. Intervalle des signaux de présence Cloud Messaging - L’intervalle en secondes auquel le système signale sa présence aux serveurs de Google. Augmenter ce nombre réduira la consommation de batterie mais peu induire un délai dans la réception des messages push.\nDéprécié, sera remplacé dans une prochaine version. + L’intervalle en secondes auquel le système signale sa présence aux serveurs de Google. Augmenter ce nombre réduira la consommation de batterie mais peu induire un délai dans la réception des messages poussées. +\nDéprécié, sera remplacé dans une prochaine version. Applications utilisant Cloud Messaging Liste des applications actuellement enregistrées auprès de Cloud Messaging. Confirmation pour les nouvelles applications Demander avant d’enregistrer une nouvelle application auprès de Cloud Messaging Intervalle de ping : %1$s - À propos de microG Services + À propos des services microG Informations de version et librairies utilisées Erreur lors du désenregistrement Cette application n’est plus installée @@ -115,15 +116,170 @@ Ceci peut prendre plusieurs minutes." Dernier message : %1$s Enregistrée Enregistrée depuis : %1$s - Désenregistrer %1$s? + Désenregistrer %1$s ? Certaines applications ne se réenregistrent pas et/ou ne fournisse pas de moyens de le faire manuellement. Ces applications peuvent ne plus fonctionner correctement après le désenregistrement.\nContinuer ? - Vous avez empêché une application déjà enregistrée de s’enregistrer pour recevoir des notifications push.\nVoulez-vous la désenregistrer maintenant pour qu’elle ne reçoive plus de notifications push à l’avenir ? + Vous avez empêché une application déjà enregistrée de s’enregistrer pour recevoir des notifications poussées. +\nVoulez-vous la désenregistrer maintenant pour qu’elle ne reçoive plus de notifications poussées à l’avenir ? Messages : %1$d (%2$d octets) Déconnecté Connecté depuis %1$s - Google SafetyNet est un système de certification du terminal, assurant que celui-ci est correctement sécurisé et compatible avec Android CTS. Certaines applications utilisent SafetyNet pour des raisons de sécurité ou comme prérequis anti-altérations.\n\nmicroG GmsCore contient une implantation libre de SafetyNet, mais les serveurs officiels requièrent que les requêtes SafetyNet soient signées par le système propriétaire DroidGuard. + Google SafetyNet est un système de certification du terminal, assurant que celui-ci est correctement sécurisé et compatible avec Android CTS. Certaines applications utilisent SafetyNet pour des raisons de sécurité ou comme prérequis anti-altérations. +\n +\nLes services microG contiennent une implantation libre de SafetyNet, mais les serveurs officiels requièrent que les requêtes SafetyNet soient signées par le système propriétaire DroidGuard. Tester la certification SafetyNet Mode d’opération Paramétrer les services microG. Se connecter + Vitesse du véhicule + Accéder à la vitesse de votre véhicule + accéder aux informations relatives au niveau de carburant de votre véhicule + Kilométrage du véhicule + Compte + Ajouter un compte Google + Tous les tests passés avec succès + Échec : %s + Profil appareil + Ajouter et gérer les comptes Google + Alerte : %s + En cours… + Réel + INACTIF + Automatique : %s + ReCaptcha : %s + Copier les données JSON JWS + Conseil + %1$s veut accéder à votre compte en tant que %2$s par %3$s. Ceci pourrait lui donner un accès privilégié à votre compte. + Choisir un compte + Ajouter un autre compte + Avant d\'utiliser cette appli, revoir ses %1$s et %2$s. + accéder aux informations de votre véhicule + accéder à la chaine de la marque du véhicule pour échanger des informations spécifiques à celui-ci + Retirer le nom du terminal lors de l\'authentification + Si activé, les requêtes d\'authentification n’incluront pas le nom du terminal, ce qui peut permettre à des appareils non autorisés à se connecter, mais peut aussi avoir des conséquences imprévisibles. + Comptes Google + Paramètres + Comptes + Attestation : %s + ReCaptcha Enterprise : %s + Type d\'évaluation + Applis utilisant SafetyNet + Effacer les requêtes récentes + Système : %s + Données de la réponse + Donnés de la requête + Recevoir des notifications poussées + Services limités microG + Autoriser à %1$s un accès privilégié à %2$s ? + pour continuer vers %1$s + Autoriser à vous connecter vers %1$s + Autoriser et partager + conditions générales + Pour continuer, microG va fournir à %1$s les éléments suivants de votre compte Google : nom, adresse mail et image de profil. + politique de confidentialité + lire la configuration des services Google + provision des services microG + Autorise l\'appli à configurer les services microG sans interaction de l’utilisateur + Informations du véhicule + Niveau de carburant du véhicule + accéder au informations relatives au kilométrage de votre véhicule + Chaine de la marque du véhicule + Démarrer l\'application en arrière-plan pour recevoir les messages poussés arrivants. + Applis utilisant les messages poussés + Applis enregistrées + Test ReCAPTCHA Enterprise + L’exécution de DroidGuard n\'est pas supportée sur cet appareil. Les services SafetyNet pourraient mal fonctionner. + Natif + Importer un profil spécifique + Spécifique : %s + Si activé, toutes les applications de cet appareil auront la possibilité d\'accéder aux adresses mail de vos comptes Google sans autorisation préalable. + Authentification via l\'enregistrement du terminal + Autoriser les applis à accéder aux comptes + Non enregistré + Dernier enregistrement : %1$s + Enregistrer l\'appareil + Android ID + Statut de la réponse + Échec CTS + %s minutes + Une application de votre appareil tente de se connecter à un compte Google. +\n +\nSi c\'est intentionnel, utilisez le bouton Se connecter pour vous rendre sur la page de connexion Google. +\nAutrement, utilisez le bouton Annuler pour retourner dans l\'application qui a affiché cette fenêtre. + Dernière utilisation : %1$s + services Play Store + Si désactivé, les requêtes d\'authentification ne seront pas liées à l\'enregistrement du terminal, ce qui peut permettre à des appareils non autorisés à se connecter, mais peut aussi avoir des conséquences imprévisibles. + Autoriser %1$s à s\'enregistrer pour recevoir des notifications poussées ? + Autoriser l\'enregistrement + Autoriser l\'appli à s’enregistrer pour recevoir des notifications poussées. + Démarrer l\'appli sur un message poussé + Applis désenregistrées + Réseaux à utiliser pour les messages poussés + Autoriser l\'attestation du terminal + Test ReCAPTCHA + Importer un profil spécifique depuis un fichier + Numéro d\'enregistrement + Choisir le profil + Utilisations récentes + Nonce (Hex) + Type de requête + Date de la requête + Données de base + Jeton + Copié dans le presse-papiers ! + Intégrité et CTS passés avec succès + Échec Intégrité + Pas encore complété + Pas de résultat + JSON invalide + ACTIF / Automatique : %s + ACTIF / Manuel : %s + %s secondes + Votre appareil est en train d\'établir une connexion avec les serveurs de Google pour vous authentifier. +\n +\nCela peut prendre quelques secondes. + Statut + Annuler + Continuer + Choisir un compte + pour continuer vers %1$s + Se connecter avec Google + Cette fonctionnalité est expérimentale et peut causer des pertes d\'argent. Vous êtes prévenus. + En cours de connexion + Récupération de licences actif + Ajouter automatiquement les applis gratuites à la bibliothèque + Les applis gratuites peuvent vérifier si elles ont été téléchargées depuis Google Play. Ajouter automatiquement les applis gratuites à la bibliothèque de votre compte afin que toutes les applis gratuites à votre disposition passent systématiquement ces vérifications. + Continuer en tant que %1$s + Se reconnecter à %1$s avec Google + Je comprends + Récupération de licences Google Play + Sauvegarde actuellement impossible + Facturation Google Play + Traiter les requêtes de facturation + Si activé, certaines applis pourront effectuer des achats ou démarrer des abonnements via le service de facturation Google Play. + Certaines applis pourraient nécessiter aussi d\'activer la vérification de licences pour vérifier vos achats. + Certaines applis exigent de vérifier que vous les avez bien achetées sur Google Play. Quand exigé par une appli, microG peut télécharger une preuve d\'achat en provenance de Google. Si désactivé ou aucun compte Google sur l\'appareil, ces requêtes de vérification de licences sont ignorées. + Vous utilisez les services limités microG. Contrairement aux services microG standard, cette version fonctionne uniquement avec les librairies microG, et non celles de Google Play. Cela signifie que la plupart des applications ignoreront ces services. + Feedback actuellement impossible + Récupération de licences inactif + En cours de connexion en tant que %1$s + En continuant, Google partagera avec %1$s votre nom, adresse mail et image de profil. Consultez la politique de confidentialité et les conditions générales de %1$s. + Répondre aux requêtes de vérification de licence + Vous pouvez gérer Se Connecter avec Google dans votre compte Google. + Alertes de compte Google + Action requise sur le compte + Votre compte Google nécessite un paramètrage additionnel. + Terminez la configuration de votre compte Google + Complétez les étapes suivantes pour pouvoir utiliser votre compte Google %s sur cet appareil. + Activer l\'enregistrement du terminal + Activer Cloud Messaging + Vous pouvez désactiver Cloud Messaging une fois la configuration du compte terminée. + Autoriser Cloud Messaging pour microG + En accord avec votre choix, microG nécessite votre permission avant de pouvoir s\'enregistrer pour Cloud Messaging. + Configurer le verrouillage de sécurité de l\'écran + Appuyez pour effectuer l\'action + Indique lorsqu\'un de vos comptes Google requiert un paramètrage additionnel avant utilisation ou quand un compte est incompatible avec microG. + Votre appareil nécessite de s\'enregistrer à Google au moins une fois.\n\nVous pouvez désactiver l\'enregistrement de l\'appareil à Google une fois la configuration du compte terminée. + Votre compte Google est géré par votre entreprise ou votre établissement. Votre administrateur a établi que cet appareil nécessite un verrouillage d\'écran sécurisé avant de pouvoir accéder aux données du compte.\n\nVeuillez configurer un mot de passe, un code PIN ou un modèle de verrouillage écran. + Étape complétée + Terminer \ No newline at end of file diff --git a/play-services-droidguard/core/src/main/res/values-fr/strings.xml b/play-services-droidguard/core/src/main/res/values-fr/strings.xml index a6b3daec93..d972aba4f6 100644 --- a/play-services-droidguard/core/src/main/res/values-fr/strings.xml +++ b/play-services-droidguard/core/src/main/res/values-fr/strings.xml @@ -1,2 +1,8 @@ - \ No newline at end of file + + Mode d\'opération DroidGuard + Embarqué + A distance + Exécuter localement DroidGuard + Exécuter DroidGuard via une connexion réseau + \ No newline at end of file diff --git a/play-services-fido/core/src/main/res/values-fr/strings.xml b/play-services-fido/core/src/main/res/values-fr/strings.xml index a6b3daec93..656dcabeb8 100644 --- a/play-services-fido/core/src/main/res/values-fr/strings.xml +++ b/play-services-fido/core/src/main/res/values-fr/strings.xml @@ -1,2 +1,27 @@ - \ No newline at end of file + + Utilisez votre clé de sécurité avec %1$s + Merci de saisir le code PIN de votre authentifiant + 4 à 63 caractères + OK + Annuler + Mauvais code PIN saisi ! + Utiliser votre clé de sécurité avec %1$s contribue à protéger vos données privées. + Commencer + %1$s agit en tant que navigateur de confiance pour utiliser votre clé de sécurité avec %2$s. + Oui, %1$s est mon navigateur de confiance et peut être autorisé à utiliser des clés de sécurité avec des sites web tiers. + Choisir comment utiliser votre clé de sécurité + Les clés de sécurité fonctionnent via Bluetooth, NFC ou USB. Choisissez comment utiliser votre clé. + Vérifiez votre identité + %1$s nécessite de vérifier que c\'est bien vous. + Connecter votre clé de sécurité USB + Connectez votre clé de sécurité à un port USB ou connectez-là avec un câble USB. Si votre clé a un bouton ou un disque doré, appuyez dessus. + Connecter votre clé de sécurité NFC + Tenez votre clé contre l\'arrière de votre appareil jusqu\'à la fin des vibrations + Utiliser votre clé de sécurité via Bluetooth + Utiliser votre clé de sécurité via NFC + Utiliser votre clé de sécurité via USB + Utiliser le verrouillage écran de cet appareil + Merci de connecter votre clé de sécurité USB. + Merci d\'appuyer sur l\'anneau ou disque doré sur %1$s. + \ No newline at end of file diff --git a/play-services-location/core/src/main/res/values-fr/strings.xml b/play-services-location/core/src/main/res/values-fr/strings.xml index c25a660b73..02b2c0b253 100644 --- a/play-services-location/core/src/main/res/values-fr/strings.xml +++ b/play-services-location/core/src/main/res/values-fr/strings.xml @@ -1,9 +1,53 @@ - - Location + + Localisation Accès récents - Localisation Wi-Fi - Localisation du réseau mobile + Localisation basée sur le Wi-Fi + Localisation basée sur le réseau mobile Résolution d\'adresses - Requête depuis Mozilla + Obtention depuis Mozilla + Pour continuer, activez la localisation de l\'appareil, le service de localisation de microG sera utilisé + Obtention depuis un service en ligne + Récupérer la localisation basée sur le Wi-Fi depuis un service de localisation en ligne. + Récupérer la localisation basée sur le Wi-Fi depuis une connexion hot-spot supportée. + Obtention depuis Mozilla + Mémorisation depuis le GPS + Enregistrer localement la localisation des réseaux Wi-Fi lorsque le GPS est utilisé. + Récupérer la localisation basée sur le réseau mobile depuis le service de localisation de Mozilla. + Obtention depuis un service en ligne + Récupérer la localisation basée sur le réseau mobile depuis un service de localisation en ligne. + Mémorisation depuis le GPS + Enregistrer localement la localisation des tours du réseau mobile lorsque le GPS est utilisé. + Utiliser Nominatim + Déterminer les adresses en utilisant Nominatim d\'OpenStreetMap. + Applis ayant accès à la localisation + Toujours fournir une localisation approximative à cette appli, et ignorer son niveau de permission. + Dernier accès : %1$s + Forcer la localisation approximative + GPS, Wi-Fi, réseaux mobiles et capteurs + Accorder aux services microG l\'accès à la localisation + Votre appareil va utiliser : + Non merci + OK + Personnalisée + Choisir le service de localisation en ligne + Termes et confidentialité + Suggéré + Importer/exporter les données de localisation + Configuration requise + Pour continuer à utiliser les services de localisation en ligne, vous devez choisir un service de données de localisation. + Exporter la base de données locale de la localisation basée sur le Wi-Fi + Exporter la base de données locale de la localisation des tours du réseau mobile + Importer les données de localisation depuis un fichier + %1$d enregistrements importés. + Pour une meilleure expérience, activez la localisation de l\'appareil, le service de localisation de microG sera utilisé + Pour plus de détails, consultez les paramètres de localisation. + Obtention depuis un hot-spot + Utiliser le service de localisation de microG. Via l\'utilisation de ce service, microG peut collecter périodiquement les données de localisation et les utiliser de façon anonyme pour améliorer la précision de la localisation et des services basés sur la localisation. + Le chemin de géolocalisation /v1/ est automatiquement attaché. Si le fournisseur de localisation exige une clé, elle peut être rattachée en tant que paramètre de la requête à l\'URL racine. + Configurer l\'URL du service + Réinitialisation + Ceci permet de modifier l\'URL du service. Une valeur incorrecte peut causer une perte de réactivité des services de localisation, voire leur indisponibilité totale. + URL personnalisée du service + Récupérer la localisation basée sur le Wi-Fi depuis le service de localisation de Mozilla. \ No newline at end of file diff --git a/play-services-nearby/core/src/main/res/values-fr/strings.xml b/play-services-nearby/core/src/main/res/values-fr/strings.xml index a6b3daec93..d4444eb626 100644 --- a/play-services-nearby/core/src/main/res/values-fr/strings.xml +++ b/play-services-nearby/core/src/main/res/values-fr/strings.xml @@ -1,2 +1,68 @@ - \ No newline at end of file + + Notifications d\'exposition inactives + Malheureusement, votre appareil est seulement partiellement compatible avec les notifications d\'exposition. Vous pouvez être notifié des contacts à risque mais vous ne pourrez pas notifier les autres. + %1$d IDs dans la dernière heure + Le Bluetooth doit être activé pour recevoir les notifications d\'exposition. + L\'accès à la localisation est nécessaire pour recevoir les notifications d\'exposition. + Le Bluetooth et l\'accès à la localisation doivent être activés pour recevoir les notifications d\'exposition. + Les notifications d\'exposition nécessitent des permissions additionnelles pour fonctionner + Notifications d\'exposition + Ouvrir les paramètres de localisation + Activer le Bluetooth + Pour activer les notifications d\'exposition, ouvrez une appli les prenant en charge. + Malheureusement, votre appareil n\'est pas compatible avec les notifications d\'exposition. + Applis utilisant les notifications d\'exposition + IDs collectés + Il y a %1$d minutes environ + Traité %1$d clés de diagnostic. + %1$d expositions à risque rapportées : + Partager vos IDs avec %1$s ? + %1$s nécessite des autorisations supplémentaires. + Mis à jour : %1$s + %1$s, %2$s + %1$s IDs par heure + %1$s, score de risque%2$d + %1$d appelle à %2$s + %1$d IDs collectés + Votre téléphone a besoin d\'utiliser le Bluetooth pour collecter et partager de manière sécurisée les IDs avec les autres appareils à proximité. %1$s peut vous notifier si vous avez été exposé à une personne ayant indiqué un diagnostic positif. La date, la durée et la force du signal liées à une exposition seront partagées avec cette application. + En désactivant les Notifications d’exposition, vous ne serez plus informé si vous avez été exposé à une personne ayant indiqué un diagnostic positif. + Activer + Nouvelles autorisations nécessaires + ID actuellement diffusé + Moins de 5 minutes + Exposition distante + Exposition proche + Appuyez pour accorder les autorisations nécessaires aux Notifications d\'exposition + Expositions rapportées + Pas de rencontres à risque rapportées. + Usage de l\'API sur les 14 derniers jours + Note : Le score de risque est défini par l\'appli. Une valeur élevée peut faire référence à un risque faible et vice-versa. + Pas d\'enregistrements + Supprimer + Supprimer tous les IDs collectés + La suppression des IDs collectés rendra impossible de vous informer en cas de diagnostic positif d\'un contact sur les 14 derniers jours. + Supprimer quand même + Exporter + Exporter les IDs collectés pour analyse étendue via une autre appli. + L\'API Notifications d\'exposition permet aux applis de vous informer si vous avez été exposé à une personne ayant indiqué un diagnostique positif. +\n +\nLa date, la durée et la force du signal liées à une exposition seront partagées avec l\'appli correspondante. + Quand l\'API Notifications d\'exposition est activée, votre appareil collecte de manière passive les IDs (appelés identifiants roulants de proximité) des appareils à proximité. +\n +\nLorsque des utilisateurs indiquent un diagnostic positif, leur ID peut être partagé. Votre appareil compare les IDs collectés aux IDs diagnostiqués positifs connus et calcule votre risque d\'infection. + Utiliser les Notifications d\'exposition + Activer les Notifications d\'exposition ? + Activer + Désactiver les Notifications d\'exposition ? + Désactiver + Vos IDs des 14 derniers jours seront utilisés dans le but d\'informer les autres que vous avez été proche d\'une exposition potentielle. +\n +\nVotre identité ou vos résultats de test ne seront pas partagés avec d\'autres personnes. + Partager + Accorder + Le Bluetooth doit être activé. + L\'accès à la localisation est nécessaire. + On y est presque ! Vous aurez besoin d\'activer la localisation en arrière-plan en sélectionnant \"Toujours autorisé\" sur l\'écran suivant. Puis revenez en arrière. + Modifier les paramètres + \ No newline at end of file diff --git a/play-services-oss-licenses/src/main/res/values-fr/strings.xml b/play-services-oss-licenses/src/main/res/values-fr/strings.xml index a6b3daec93..99bb5a2215 100644 --- a/play-services-oss-licenses/src/main/res/values-fr/strings.xml +++ b/play-services-oss-licenses/src/main/res/values-fr/strings.xml @@ -1,2 +1,9 @@ - \ No newline at end of file + + Une erreur s\'est produite en récupérant la licence. + Cette appli n\'a pas de licences libres. + Licences libres + Info sur les licences en cours de chargement. + Liste des licences en cours de chargement. + Détails des licences pour les logiciels libres + \ No newline at end of file diff --git a/vending-app/src/main/res/values-fr/strings.xml b/vending-app/src/main/res/values-fr/strings.xml index 2b73eac077..c6b1506282 100644 --- a/vending-app/src/main/res/values-fr/strings.xml +++ b/vending-app/src/main/res/values-fr/strings.xml @@ -6,4 +6,17 @@ S\'identifier Notifications de licence Ignorer + Compagnon microG + Pay actuellement impossible + Confirmer l\'achat + Non connecté à internet. Merci de vérifier que le Wi-Fi ou les données mobiles sont actifs et réessayer. + Le mot de passe saisi est incorrect. + Erreur inconnue, merci de quitter et réessayer. + Saisir le mot de passe + Se souvenir de moi sur cet appareil + Mot de passe oublié ? + En savoir plus + Vérifier + Le compagnon microG ne peut pas être utilisé seul. Merci d\'installer les services microG pour utiliser microG. + Le compagnon microG ne peut pas être directement utilisé. Consultez plutôt les paramètres des services microG. \ No newline at end of file From 37c17d67d43f3e46a11c0266d03f5ac7c7b42826 Mon Sep 17 00:00:00 2001 From: siwusu Date: Mon, 30 Sep 2024 00:56:30 +0000 Subject: [PATCH 031/132] Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 100.0% (50 of 50 strings) Translation: microG/play-services-location: core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-location-core-strings/zh_Hans/ --- .../core/src/main/res/values-zh-rCN/strings.xml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/play-services-location/core/src/main/res/values-zh-rCN/strings.xml b/play-services-location/core/src/main/res/values-zh-rCN/strings.xml index a4066e2b04..ac80672208 100644 --- a/play-services-location/core/src/main/res/values-zh-rCN/strings.xml +++ b/play-services-location/core/src/main/res/values-zh-rCN/strings.xml @@ -35,4 +35,19 @@ /v1/geolocate路径会自动添加。如果位置提供商需要密钥,则可以将其作为查询参数附加到根URL。 自定义服务URL 向 microG 服务授予位置权限 + 从在线位置服务获取基于Wi-Fi的位置。 + 来自在线服务的请求 + 从在线位置服务获取基于移动网络的位置。 + 自定义 + 选择在线定位服务 + 条款/隐私 + 建议 + 导入或导出位置数据 + 需要的配置 + 导出本地基站位置数据库 + 从文件导入位置数据 + 已导入 %1$d 条记录。 + 来自在线服务的请求 + 要继续使用在线位置服务,您需要选择一个位置数据服务。 + 导出本地Wi-Fi位置数据库 \ No newline at end of file From 2f57cc38e02114cf232bd3d1f2dacb5099b8058a Mon Sep 17 00:00:00 2001 From: rehork Date: Tue, 1 Oct 2024 15:47:52 +0000 Subject: [PATCH 032/132] Translated using Weblate (Polish) Currently translated at 100.0% (232 of 232 strings) Translation: microG/play-services-core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-core-strings/pl/ Translated using Weblate (Polish) Currently translated at 100.0% (249 of 249 strings) Translation: microG/play-services-core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-core-strings/pl/ --- .../src/main/res/values-pl/strings.xml | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/play-services-core/src/main/res/values-pl/strings.xml b/play-services-core/src/main/res/values-pl/strings.xml index 354b859b7d..ef0cd0499b 100644 --- a/play-services-core/src/main/res/values-pl/strings.xml +++ b/play-services-core/src/main/res/values-pl/strings.xml @@ -189,7 +189,7 @@ Spróbuj ponownie później." Skopiowano do schowka! Aplikacje korzystające z powiadomień ‘push‘ Własny: %s - Aplikacja na twoim urządzeniu próbuje zalogować się na konto Google. \u0020Jeśli było to zamierzone, użyj przycisku Zaloguj, aby połączyć się ze stroną logowania Google, w przeciwnym wypadku, użyj przycisku Anuluj, aby wrócić z powrotem do aplikacji, która spowodowała wyświetlenie tego okna dialogowego. + Aplikacja na twoim urządzeniu próbuje zalogować się na konto Google.\n\nJeśli było to zamierzone, użyj przycisku Zaloguj, aby połączyć się ze stroną logowania Google, w przeciwnym wypadku, użyj przycisku Anuluj, aby wrócić z powrotem do aplikacji, która spowodowała wyświetlenie tego okna dialogowego. Sprawdź usługę ReCAPTCHA Aby kontynuować, microG udostępni aplikacji %1$s nazwę, adres e-mail i zdjęcie profilowe twojego konta Google. Uwierzytelniaj za pomocą rejestracji urządzenia @@ -254,4 +254,24 @@ Spróbuj ponownie później." %1$s chce uzyskać dostęp do konta tak, jakby było %2$s autorstwa %3$s. Może to zapewnić mu uprzywilejowany dostęp do konta użytkownika. Automatycznie dodawaj bezpłatne aplikacje do biblioteki Darmowe aplikacje mogą sprawdzać, czy zostały pobrane ze Sklepu Google Play. Automatycznie dodawaj darmowe aplikacje do biblioteki konta, aby zawsze sprawdzać wszystkie aktualnie dostępne darmowe aplikacje. + Ograniczone usługi microG + Rozumiem + Korzystasz z ograniczonych usług microG. W przeciwieństwie do zwykłych usług microG, ten wariant działa tylko z aplikacjami korzystającymi z bibliotek microG, a nie z tych w Google Play. Oznacza to, że większość aplikacji zignoruje te usługi. + Włącz rejestrację urządzenia + Zezwalaj na Cloud Messaging dla microG + Skonfiguruj bezpieczną blokadę ekranu + Alerty dotyczące konta Google + Zgodnie z twoimi preferencjami, microG potrzebuje twojej zgody, zanim będzie mogło zarejestrować się w usłudze Cloud Messaging. + Zakończ konfigurację konta Google + Powiadamia, gdy jedno z kont Google wymaga dodatkowej konfiguracji przed użyciem lub gdy konto jest niekompatybilne z microG. + Wymagane działanie na koncie + Konto Google wymaga dodatkowej konfiguracji. + Wykonaj następujące kroki, aby móc korzystać z konta Google %s na tym urządzeniu. + Cloud Messaging możesz wyłączyć po zakończeniu konfiguracji konta. + Urządzenie musi zarejestrować się w Google przynajmniej raz.\n\nRejestrację urządzenia Google można wyłączyć po zakończeniu konfiguracji konta. + Włącz Cloud Messaging + Twoje konto Google jest zarządzane przez twoje miejsce pracy lub instytucję edukacyjną. Administrator zdecydował, że urządzenia wymagają bezpiecznej blokady ekranu przed uzyskaniem dostępu do danych konta.\n\nUstaw hasło, kod PIN lub wzór blokady ekranu. + Dotknij, aby wykonać etap + Etap zakończony + Zakończ \ No newline at end of file From 2002c0f421f044d0a5f58edc80d2ec73616fe098 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ph=E1=BA=A1m=20Ng=E1=BB=8Dc=20V=C5=A9?= Date: Wed, 2 Oct 2024 09:16:31 +0000 Subject: [PATCH 033/132] Translated using Weblate (Vietnamese) Currently translated at 100.0% (4 of 4 strings) Translation: microG/play-services-ads-identifier: core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-ads-identifier-core-strings/vi/ Translated using Weblate (Vietnamese) Currently translated at 100.0% (13 of 13 strings) Translation: microG/play-services-base: core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-base-core-strings/vi/ Translated using Weblate (Vietnamese) Currently translated at 28.8% (45 of 156 strings) Translation: microG/play-services-core: permissions Translate-URL: https://hosted.weblate.org/projects/microg/play-services-core-permissions/vi/ Translated using Weblate (Vietnamese) Currently translated at 13.7% (32 of 232 strings) Translation: microG/play-services-core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-core-strings/vi/ Translated using Weblate (Vietnamese) Currently translated at 100.0% (156 of 156 strings) Translation: microG/play-services-core: permissions Translate-URL: https://hosted.weblate.org/projects/microg/play-services-core-permissions/vi/ Translated using Weblate (Vietnamese) Currently translated at 100.0% (4 of 4 strings) Translation: microG/play-services-core: plurals Translate-URL: https://hosted.weblate.org/projects/microg/play-services-core-plurals/vi/ Translated using Weblate (Vietnamese) Currently translated at 100.0% (5 of 5 strings) Translation: microG/play-services-droidguard: core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-droidguard-core-strings/vi/ Translated using Weblate (Vietnamese) Currently translated at 100.0% (24 of 24 strings) Translation: microG/play-services-fido: core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-fido-core-strings/vi/ Translated using Weblate (Vietnamese) Currently translated at 100.0% (50 of 50 strings) Translation: microG/play-services-location: core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-location-core-strings/vi/ Translated using Weblate (Vietnamese) Currently translated at 100.0% (19 of 19 strings) Translation: microG/vending-app Translate-URL: https://hosted.weblate.org/projects/microg/vending-app/vi/ Translated using Weblate (Vietnamese) Currently translated at 100.0% (232 of 232 strings) Translation: microG/play-services-core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-core-strings/vi/ Translated using Weblate (Vietnamese) Currently translated at 100.0% (59 of 59 strings) Translation: microG/play-services-nearby: core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-nearby-core-strings/vi/ Translated using Weblate (Vietnamese) Currently translated at 100.0% (232 of 232 strings) Translation: microG/play-services-core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-core-strings/vi/ Translated using Weblate (Vietnamese) Currently translated at 100.0% (232 of 232 strings) Translation: microG/play-services-core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-core-strings/vi/ Translated using Weblate (Vietnamese) Currently translated at 100.0% (232 of 232 strings) Translation: microG/play-services-core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-core-strings/vi/ Translated using Weblate (Vietnamese) Currently translated at 100.0% (4 of 4 strings) Translation: microG/play-services-core: plurals Translate-URL: https://hosted.weblate.org/projects/microg/play-services-core-plurals/vi/ Translated using Weblate (Vietnamese) Currently translated at 100.0% (249 of 249 strings) Translation: microG/play-services-core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-core-strings/vi/ --- .../core/src/main/res/values-vi/strings.xml | 8 +- .../core/src/main/res/values-vi/strings.xml | 6 +- .../src/main/res/values-vi/permissions.xml | 151 ++++++++++- .../src/main/res/values-vi/plurals.xml | 15 +- .../src/main/res/values-vi/strings.xml | 248 +++++++++++++++++- .../core/src/main/res/values-vi/strings.xml | 8 +- .../core/src/main/res/values-vi/strings.xml | 27 +- .../core/src/main/res/values-vi/strings.xml | 52 +++- .../core/src/main/res/values-vi/strings.xml | 62 ++++- .../src/main/res/values-vi/strings.xml | 22 +- 10 files changed, 564 insertions(+), 35 deletions(-) diff --git a/play-services-ads-identifier/core/src/main/res/values-vi/strings.xml b/play-services-ads-identifier/core/src/main/res/values-vi/strings.xml index 4075236ae5..50c665e072 100644 --- a/play-services-ads-identifier/core/src/main/res/values-vi/strings.xml +++ b/play-services-ads-identifier/core/src/main/res/values-vi/strings.xml @@ -1,7 +1,7 @@ - Cho phép một ứng dụng nhận được thông báo khi ID quảng cáo hoặc giới hạn tùy chọn theo dõi quảng cáo của người dùng được cập nhật. - Quyền của Mã Quảng cáo - Thông báo của Mã Quảng cáo - Cho phép ứng dụng của nhà xuất bản truy cập trực tiếp hoặc gián tiếp vào một mã quảng cáo hợp lệ. + Cho phép ứng dụng nhận thông báo khi ID quảng cáo hoặc tùy chọn giới hạn theo dõi quảng cáo của người dùng được cập nhật. + Quyền ID quảng cáo + Thông báo ID quảng cáo + Cho phép ứng dụng của nhà xuất bản truy cập trực tiếp hoặc gián tiếp vào ID quảng cáo hợp lệ. \ No newline at end of file diff --git a/play-services-base/core/src/main/res/values-vi/strings.xml b/play-services-base/core/src/main/res/values-vi/strings.xml index 65b49ad8ee..99385cbb1d 100644 --- a/play-services-base/core/src/main/res/values-vi/strings.xml +++ b/play-services-base/core/src/main/res/values-vi/strings.xml @@ -1,13 +1,13 @@ - Loại trừ 1%1$s từ Tối ưu hoá pin hoặc thay đổi cài đặt của thông báo để ẩn thông báo này. + Bạn có thể loại trừ %1$s khỏi tính năng tối ưu hóa pin hoặc thay đổi cài đặt ứng dụng để ẩn thông báo này. Nâng cao Xem tất cả Tự động Không Mở - Tắt - Bật + Đã tắt + Đã bật Tắt Hoạt động trong nền Thủ công diff --git a/play-services-core/src/main/res/values-vi/permissions.xml b/play-services-core/src/main/res/values-vi/permissions.xml index 3d6d0ef8b3..15e438d9bb 100644 --- a/play-services-core/src/main/res/values-vi/permissions.xml +++ b/play-services-core/src/main/res/values-vi/permissions.xml @@ -1,24 +1,159 @@ Danh bạ - Cho phép ứng dụng truy cập Danh bạ thông qua bất kỳ tài khoản Google nào được liên kết. + Cho phép ứng dụng truy cập vào Danh bạ thông qua bất kỳ tài khoản Google nào được liên kết. Dịch vụ Android Cho phép ứng dụng truy cập các dịch vụ Android thông qua bất cứ tài khoản Google nào được liên kết. AdWords - Cho phép ứng dụng truy cập AdWords thông qua bất cứ tài khoản Google nào được liên kết. + Cho phép ứng dụng truy cập vào AdWords thông qua bất cứ tài khoản Google nào được liên kết. Google App Engine Blogger - Cho phép ứng dụng truy cập Blogger thông qua bất cứ tài khoản Google nào được liên kết. + Cho phép ứng dụng truy cập vào Blogger thông qua bất cứ tài khoản Google nào được liên kết. Lịch Google - Cho phép ứng dụng truy cập Lịch Google thông qua bất cứ tài khoản Google nào được liên kết. + Cho phép ứng dụng truy cập vào Lịch Google thông qua bất cứ tài khoản Google nào được liên kết. Xem tài khoản YouTube của bạn Xem báo cáo phân tích YouTube cho nội dung YouTube của bạn Tất cả dịch vụ Gooogle Cho phép ứng dụng truy cập tất cả các dịch vụ của Google thông qua bất cứ tài khoản Google nào được liên kết. AdSense - Cho phép ứng dụng truy cập AdSense thông qua bất cứ tài khoản Google nào được liên kết. - Cho phép ứng dụng truy cập Google App Engine thông qua bất cứ tài khoản Google nào được liên kết. - Xem báo cáo tiền tệ YouTube Analytics cho nội dung YouTube của bạn + Cho phép ứng dụng truy cập vào AdSense thông qua bất cứ tài khoản Google nào được liên kết. + Cho phép ứng dụng truy cập vào Google App Engine thông qua bất cứ tài khoản Google nào được liên kết. + Xem báo cáo tiền tệ của YouTube Analytics cho nội dung YouTube của bạn Quản lý video YouTube của bạn - Xem và quản lý nội dung của bạn và nội dung liên quan trên YouTube + Quản lý tài sản cùng nội dung liên quan của bạn trên YouTube + Dodgeball + Google Finance + Cho phép ứng dụng truy cập vào Dodgeball thông qua bất kỳ tài khoản Google nào được liên kết. + Cho phép ứng dụng truy cập vào Google Finance thông qua bất kỳ tài khoản Google nào được liên kết. + Google Base + Cho phép ứng dụng truy cập vào Google Base thông qua bất cứ tài khoản Google nào được liên kết. + Cho phép ứng dụng truy cập vào Google Voice thông qua bất cứ tài khoản Google nào được liên kết. + Google Groups + Google Voice + Cho phép ứng dụng truy cập vào Google Groups thông qua bất cứ tài khoản Google nào được liên kết. + Google Health + Cho phép ứng dụng truy cập vào Google Health thông qua bất cứ tài khoản Google nào được liên kết. + iGoogle + Cho phép ứng dụng truy cập vào iGoogle thông qua bất kỳ tài khoản Google nào được liên kết. + JotSpot + Cho phép ứng dụng truy cập vào JotSpot thông qua bất kỳ tài khoản Google nào được liên kết. + Cho phép ứng dụng truy cập vào Knol thông qua bất kỳ tài khoản Google nào được liên kết. + Album Web Picasa + Knol + Cho phép ứng dụng truy cập vào Picasa Web Albums thông qua bất cứ tài khoản Google nào được liên kết. + Google Maps + Cho phép ứng dụng truy cập vào Google Maps thông qua bất cứ tài khoản Google nào được liên kết. + Google Mail + Cho phép ứng dụng truy cập vào Google Mail thông qua bất cứ tài khoản Google nào được liên kết. + Cho phép ứng dụng truy cập vào Google News thông qua bất cứ tài khoản Google nào được liên kết. + Cho phép ứng dụng truy cập vào Google Nootbook thông qua bất kỳ tài khoản Google nào được liên kết. + Cho phép ứng dụng truy cập vào Orkut thông qua bất kỳ tài khoản Google nào được liên kết. + Google Notebook + Orkut + Google Tin tức + Google Tìm kiếm sách + Cho phép ứng dụng truy cập vào Tài khoản QA của Google Checkout thông qua bất cứ tài khoản Google nào được liên kết. + Google Webmaster Tools + Cho phép ứng dụng truy cập vào Google Sách thông qua bất kỳ tài khoản Google nào được liên kết. + Tài khoản Google Checkout + Tài khoản QA của Google Checkout + Tài khoản Sandbox của Google Checkout + Cho phép ứng dụng truy cập vào Tài khoản Sandbox của Google Checkout thông qua bất cứ tài khoản Google nào được liên kết. + Cho phép ứng dụng truy cập vào Google Webmaster Tools thông qua bất cứ tài khoản Google nào được liên kết. + Voice Search + Cho phép ứng dụng truy cập vào Voice Search thông qua bất kỳ tài khoản Google nào được liên kết. + Personalized Speech Recognition + Cho phép ứng dụng truy cập vào Personalized Speech Recognition thông qua bất kỳ tài khoản Google nào được liên kết. + Google Talk + Cho phép ứng dụng truy cập vào Google Talk thông qua bất kỳ tài khoản Google nào được liên kết. + Cho phép ứng dụng truy cập vào Google Wi-Fi thông qua bất cứ tài khoản Google nào được liên kết. + Cho phép ứng dụng truy cập vào Google Spreadsheets thông qua bất cứ tài khoản Google nào được liên kết. + Cho phép ứng dụng truy cập vào Google Tài liệu thông qua bất cứ tài khoản Google nào được liên kết. + YouTube + Cho phép ứng dụng truy cập vào Youtube thông qua bất cứ tài khoản Google nào được liên kết. + Google Wi-Fi + Tên người dùng YouTube + Cho phép ứng dụng truy cập vào Tên người dùng Youtube thông qua bất kỳ tài khoản Google nào được liên kết. + Xem lịch sử hoạt động Google Apps của bạn + Quản lý cấu hình tài khoản của người mua Ad Exchange + Xem dữ liệu Ad Exchange của bạn + Quản lý dữ liệu máy chủ AdSense của bạn và các tài khoản liên quan + Quản lý dữ liệu Ad Exchange của bạn + Xem dữ liệu AdSense của bạn + Quản lý dữ liệu AdSense của bạn + Xem dữ liệu Google Analytics của bạn + Quản lý dữ liệu Google Analytics của bạn + Truy cập vào Google Play Android Developer + Phạm vi quyền quản trị của App Engine. + Quản lý cài đặt của Google Apps Group + Cấp quyền đọc/viết cho quản trị viên và người dùng đại lý khi thử nghiệm trong sandbox của API, hoặc khi gọi trực tiếp hoạt động API. + Cấp quyền đọc/viết cho License Manager API. + Cấp quyền đọc và viết cho Groups Migration API. + Ngoài phạm vi OAuth đọc/ghi tổng thể, hãy sử dụng phạm vi OAuth chỉ đọc khi truy xuất dữ liệu của khách hàng. + Cấp quyền truy cập chỉ đọc cho Admin Audit API + Phạm vi sử dụng dịch vụ App State. + Xem dữ liệu của bạn trong Google BigQuery + Quản lý dữ liệu của bạn trong Google BigQuery + Quản lý tài khoản Blogger của bạn + Xem tài khoản Blogger của bạn + Quản lý sách của bạn + Quản lý lịch của bạn + Xem lịch của bạn + Quản lý dữ liệu in đám mây của Google + Xem tài nguyên Google Compute Engine của bạn + Quản lý tài nguyên Google Compute Engine của bạn + Xem công việc Google Coordinate của bạn + Quản lý công việc Google Coordinate của bạn + Quản lý dữ liệu và quyền của bạn trong Google Cloud Storage + Xem dữ liệu của bạn trong Google Cloud Storage + Quản lý dữ liệu của bạn trong Google Cloud Storage + Quản lý báo cáo DoubleClick for Advertisers + Cho phép truy cập vào thư mục Dữ liệu ứng dụng + Xem ứng dụng Google Drive của bạn + Quản lý các tệp Google Drive mà bạn đã mở hoặc tạo bằng ứng dụng này + Phạm vi đặc biệt được sử dụng để cho phép người dùng chấp thuận cài đặt ứng dụng + Xem metadata cho các tệp và tài liệu trong Google Drive của bạn + Xem tệp và tài liệu trong Google Drive của bạn + Chỉnh sửa cách thức hoạt động Google Apps Script của bạn + Quản lý tệp và tài liệu trong Google Drive của bạn + xem tài khoản Freebase của bạn + Quản lý Fusion Tables của bạn + Đăng nhập vào Freebase + Xem Fusion Tables của bạn + Phạm vi truy cập dữ liệu từ Google Play Games. + Quản lý dữ liệu GAN của bạn + Xem dữ liệu GAN của bạn + Phạm vi dòng thời gian Glass + CloudMessaging cho Chrome + Tạo, đọc, cập nhật và xóa bản nháp. Gửi tin nhắn và bản nháp. + Tất cả các thao tác đọc/ghi ngoại trừ việc xóa vĩnh viễn các chuỗi và tin nhắn, bỏ qua Thùng rác. + Đọc tất cả tài nguyên và metadata của chúng—không thực hiện thao tác ghi. + Quản lý vị trí tốt nhất có sẵn và lịch sử vị trí của bạn + Quản lý vị trí thành phố và lịch sử vị trí của bạn + Quản lý vị trí tốt nhất của bạn + Quản lý vị trí thành phố của bạn + Quản lý dữ liệu Google Maps Engine của bạn + Xem dữ liệu Google Maps Engine của bạn + Quản lý trải nghiệm Google Maps dành cho thiết bị di động của bạn + Quản lý hoạt động Orkut của bạn + Xem dữ liệu Orkut của bạn + Biết tên, thông tin cơ bản và danh sách những người bạn kết nối trên Google+ + Biết bạn là ai trên Google + Quản lý dữ liệu của bạn trong Google Prediction API + Xem dữ liệu sản phẩm của bạn + Quản lý danh sách trang web và tên miền bạn kiểm soát + Quản lý xác minh trang web mới của bạn với Google + Cấp quyền đọc/ghi vào Shopping Content API. + Xử lý các tác vụ từ Taskqueues của bạn + Quản lý Tasks của bạn + Quản lý các tác vụ của bạn + Xem các tác vụ của bạn + Google Maps Tracks API, phạm vi này cho phép truy cập đọc và ghi vào dữ liệu của dự án bạn. + Quản lý các URL rút gọn goo.gl của bạn + Xem địa chỉ email của bạn + Xem thông tin cơ bản về tài khoản của bạn + Quản lý tài khoản Youtube của bạn + Google Spreadsheets + Google Tài liệu + Cho phép ứng dụng truy cập vào tài khoản của Google Checkout thông qua bất cứ tài khoản Google nào được liên kết. \ No newline at end of file diff --git a/play-services-core/src/main/res/values-vi/plurals.xml b/play-services-core/src/main/res/values-vi/plurals.xml index a6b3daec93..b4708892c4 100644 --- a/play-services-core/src/main/res/values-vi/plurals.xml +++ b/play-services-core/src/main/res/values-vi/plurals.xml @@ -1,2 +1,15 @@ - \ No newline at end of file + + + %1$d backend được cấu hình + + + %1$d Ứng dụng đã đăng ký + + + Hiện chưa cấp các quyền truy cập cần thiết để Dịch vụ microG hoạt động một cách bình thường. + + + Yêu cầu cấp quyền còn thiếu + + \ No newline at end of file diff --git a/play-services-core/src/main/res/values-vi/strings.xml b/play-services-core/src/main/res/values-vi/strings.xml index d0eff52f76..92f0ca8fb8 100644 --- a/play-services-core/src/main/res/values-vi/strings.xml +++ b/play-services-core/src/main/res/values-vi/strings.xml @@ -1,18 +1,18 @@ - microG Services + Dịch vụ microG Cài đặt microG Thiết lập các dịch vụ của microG. - Đã xảy ra sự cố giao tiếp với máy chủ Google. + Đã xảy ra sự cố khi kết nối với máy chủ Google. \n \nHãy thử lại sau. Thiết bị của bạn đang liên lạc với Google để lưu thông tin vào tài khoản của bạn. \n -\nViệc này có thể mất vài phút. +\nĐiều này có thể mất vài phút. Cho phép Từ chối Yêu cầu xác thực - 1%1$s yêu cầu xác thực từ bạn để truy cập tài khoản Google của bạn. + %1$s cần bạn ủy quyền để truy cập vào tài khoản Google của bạn. Chọn tài khoản Thêm tài khoản khác Cho phép và chia sẻ @@ -22,17 +22,239 @@ Đợi một chút… Bằng cách tiếp tục, bạn cho phép ứng dụng này và Google sử dụng thông tin của mình theo các điều khoản tương ứng của dịch vụ và chính sách bảo mật. Đăng nhập - Thiết bị của bạn đang thiết lập kết nối với máy chủ của Google để đăng nhập cho bạn. + Thiết bị của bạn đang thiết lập kết nối với máy chủ của Google để đăng nhập. \n -\nĐiều này cần khoảng vài giây để thực hiện. - Bạn hiện tại không có kết nối mạng. +\nĐiều này có thể mất vài giây. + Hiện bạn không có kết nối mạng. \n -\nĐây có thể là sự cố tạm thời hoặc thiết bị Android của bạn có thể không được cung cấp cho các dịch vụ dữ liệu. Thử lại khi kết nối với mạng di động hoặc kết nối với mạng Wi-Fi. - để tiếp tục 1%1$s - Cho phép bạn đăng nhập vào 1%1$s - Để tiếp tục, microG sẽ chia sẻ tên, địa chỉ email và ảnh hồ sơ từ tải khoản Google của bạn với 1%1$s. +\nĐây có thể là sự cố tạm thời hoặc thiết bị của bạn có thể chưa được cung cấp các dịch vụ dữ liệu. Hãy bật dữ liệu di động hoặc kết nối với Wi-Fi và thử lại. + để tiếp tục %1$s + Cho phép bạn đăng nhập vào %1$s + Để tiếp tục, microG sẽ chia sẻ tên, địa chỉ email và ảnh hồ sơ từ tải khoản Google của bạn với %1$s. Google - Quản lí tài khoản Google + Quản Lí Tài Khoản Google Xin lỗi… - Một ứng dụng trên thiết bị của bạn đang cố gắng đăng nhập vào tài khoản Google. \u0020Nếu điều này là do bạn thực hiện, hãy sử dụng nút Đăng nhập để kết nối với trang đăng nhập của Google, nếu không, nhấn Hủy để quay trở lại ứng dụng khiến hộp thoại này xuất hiện. + Một ứng dụng trên thiết bị của bạn đang cố gắng đăng nhập vào một tài khoản Google.\n\nNếu đây là hành động có mục đích, hãy nhấn vào nút Đăng nhập để kết nối đến trang đăng nhập của Google, nếu không, hãy nhấn Hủy để quay lại ứng dụng ban đầu. + Dịch vụ Hạn chế microG + Cho phép %1$s quyền truy cập đặc quyền vào %2$s\? + %1$s muốn truy cập vào tài khoản của bạn như thể nó là %2$s của %3$s. Điều này có thể cấp quyền truy cập đặc quyền vào tài khoản của bạn. + %1$s muốn: + %1$s muốn sử dụng: + Huỷ + Đăng nhập bằng Google + để tiếp tục đến %1$s + trao đổi và nhận thông báo đồng bộ từ máy chủ của Google + Mở rộng quyền truy cập vào các dịch vụ của Google + cung cấp dịch vụ microG + Cho phép ứng dụng cấu hình các dịch vụ microG mà không cần tương tác của người dùng + Tốc độ xe + Truy cập kênh nhà cung cấp xe của bạn để trao đổi thông tin cụ thể về xe + Đăng ký thiết bị Google + Google SafetyNet + Để sử dụng Play Games, bạn cần cài đặt ứng dụng Google Play Games. Ứng dụng có thể tiếp tục mà không cần Play Games, nhưng có khả năng ứng dụng sẽ hoạt động không như mong đợi. + Dữ liệu di dộng + Cloud Messaging + Hỗ trợ giả mạo chữ ký + Hệ thống + Hệ thống giả mạo chữ ký: + Thiếu quyền + Tùy chọn tài khoản + Thông tin cá nhân & quyền riêng tư + Khi bật, tất cả ứng dụng trên thiết bị này sẽ có thể xem địa chỉ email của Tài khoản Google của bạn mà không cần sự uỷ quyền trước. + Loại bỏ tên thiết bị để xác thực + Khi bật, các yêu cầu xác thực sẽ không bao gồm tên thiết bị, điều này có thể cho phép các thiết bị không được ủy quyền mà vẫn đăng nhập, nhưng có thể gây ra hậu quả không lường trước được. + Đăng ký thiết bị của bạn với các dịch vụ của Google và tạo một mã định danh thiết bị duy nhất. microG loại bỏ các bit nhận dạng khác ngoài tên tài khoản Google của bạn từ dữ liệu đăng ký. + ID Android + Chưa được đăng ký + Đăng ký lần cuối: %1$s + Đăng ký thiết bị + Trạng thái + Thêm + Tài khoản Google + Thêm và quản lý tài khoản Google + Cài đặt + Tài khoản + Tài khoản + Thêm tài khoản Google + Danh sách các ứng dụng hiện đang được đăng ký sử dụng Cloud Messaging. + Hỏi trước khi đăng ký một ứng dụng mới để nhận thông báo đẩy + Giới thiệu về Dịch vụ microG + Thông tin phiên bản và thư viện đã sử dụng + Lỗi khi hủy đăng ký + Đã đăng ký từ: %1$s + Hủy đăng ký %1$s\? + Một số ứng dụng không tự động đăng ký lại và/hoặc không cung cấp tùy chọn để thực hiện thủ công. Những ứng dụng này có thể không hoạt động chính xác sau khi hủy đăng ký.\nBạn có muốn tiếp tục không? + Thông báo: %1$d (%2$d byte) + Nhận thông báo đẩy + Cho phép %1$s đăng ký nhận thông báo đẩy? + Cho phép đăng ký + Ứng dụng sử dụng thông báo đẩy + Đang chạy… + Chứng thực: %s + ReCaptcha: %s + ReCaptcha Enterprise: %s + Sao chép dữ liệu JSON JWS + Lời khuyên + Nonce (Hex) + Thời gian yêu cầu + Loại yêu cầu + Token + Đã sao chép vào bảng nhớ tạm! + Đã vượt qua Integrity và kiểm tra CTS + Tự động thêm ứng dụng miễn phí vào thư viện + Ứng dụng miễn phí có thể kiểm tra xem chúng đã được tải xuống từ Google Play hay chưa. Tự động thêm ứng dụng miễn phí vào thư viện tài khoản của bạn để luôn vượt qua kiểm tra đối với tất cả các ứng dụng miễn phí hiện có sẵn cho bạn. + Tiếp tục + Đang đăng nhập cho bạn + Đăng nhập lại vào %1$s bằng Google + Đang đăng nhập với tư cách %1$s + Bằng cách tiếp tục, Google sẽ chia sẻ tên, địa chỉ email và ảnh hồ sơ của bạn với %1$s. Xem Chính sách quyền riêng tư và Điều khoản dịch vụ của %1$s. + Bạn đang sử dụng Dịch vụ microG Limited. Không giống như các Dịch vụ microG thông thường, phiên bản này chỉ hoạt động với các ứng dụng sử dụng thư viện microG, không phải các ứng dụng trên Google Play. Điều này có nghĩa là hầu hết các ứng dụng sẽ bỏ qua các dịch vụ này. + Nghe các thông báo trạng thái nội bộ + đọc cấu hình dịch vụ Google + nghe các thông báo C2DM + gửi các thông báo C2DM đến các ứng dụng khác + Mức nhiên liệu của xe + Số km xe đã đi + Truy cập tốc độ xe của bạn + Thông tin xe + Truy cập thông tin về số km đã đi của xe bạn + Truy cập thông tin xe của bạn + Truy cập thông tin mức nhiên liệu của xe bạn + Kênh nhà cung cấp xe + %1$s muốn sử dụng Play Games + Dịch vụ Play Store + Google Play Games + Chọn địa điểm + Trình chọn địa điểm hiện chưa có sẵn. + Chọn địa điểm này + Địa điểm gần đây + Dịch vụ microG: Không có quyền %1$s + (%1$.7f, %2$.7f) + Chuyển vùng quốc tế + Các mạng khác + Wi-Fi + Hệ thống cấp quyền giả mạo chữ ký: + Hệ thống có hỗ trợ giả mạo chữ ký: + Đây là một chỉ báo mạnh cho thấy ROM hỗ trợ giả mạo chữ ký, nhưng cần thêm thao tác để kích hoạt nó. Vui lòng kiểm tra tài liệu để biết về các bước cần thiết. + %1$s đã cài đặt: + Cửa hàng Play (Phonesky) + Khung dịch vụ (GSF) + Tắt tối ưu hóa pin: + Giới thiệu + Chạm vào đây để tắt tối ưu hóa pin. Không làm như vậy có thể khiến các ứng dụng hoạt động không bình thường. + Thành phần + Đã bật tính năng tối ưu hóa pin + Bạn đã bật Cloud Messaging nhưng cũng bật tối ưu hóa pin cho Dịch vụ microG. Để thông báo đẩy, bạn nên tắt tối ưu hóa pin. + Đăng nhập & bảo mật + Cho phép ứng dụng tìm tài khoản + Tin tưởng Google về quyền truy cập ứng dụng + Khi tắt, người dùng sẽ được hỏi trước khi yêu cầu uỷ quyền của ứng dụng được gửi đến Google. Một số ứng dụng sẽ không sử dụng được tài khoản Google nếu tắt tính năng này. + Xác thực bằng cách đăng ký thiết bị + Khi tắt, các yêu cầu xác thực sẽ không được liên kết với việc đăng ký thiết bị, điều này có thể cho phép các thiết bị không được uỷ quyền mà vẫn đăng nhập, nhưng có thể gây ra hậu quả không lường trước được. + Khoảng thời gian kiểm tra kết nối của Cloud Messaging + Cloud Messaging là nhà cung cấp dịch vụ thông báo đẩy được nhiều ứng dụng của bên thứ ba sử dụng. Để sử dụng, bạn phải bật đăng ký thiết bị. + Các ứng dụng sử dụng Cloud Messaging + Xác nhận ứng dụng mới + Không còn được cài đặt + Đã đăng ký + Chưa nhận được thông báo nào cho đến nay + Hủy đăng ký + Chưa đăng ký + Thông báo cuối cùng: %1$s + Đã ngắt kết nối + Đã kết nối từ %1$s + Khởi chạy ứng dụng khi ở chế độ nền để nhận các tin nhắn đẩy. + Cho phép xác thực thiết bị + Kiểm tra chứng nhận SafetyNet + Kiểm tra ReCAPTCHA + Đã vượt qua tất cả các bài kiểm tra + Thất bại: %s + Cảnh báo: %s + Thực thi DroidGuard không được hỗ trợ trên thiết bị này. Các dịch vụ SafetyNet có thể hoạt động không bình thường. + Ứng dụng sử dụng SafetyNet + Xóa các yêu cầu gần đây + Số sê-ri + Nhập hồ sơ thiết bị từ tệp + Chọn hồ sơ + Hồ sơ thiết bị + Sử dụng gần đây + Trạng thái phản hồi + Dữ liệu phản hồi + Yêu cầu dữ liệu + Một số ứng dụng yêu cầu xác thực rằng bạn đã mua chúng trên Google Play. Khi được ứng dụng yêu cầu, microG có thể tải xuống bằng chứng mua hàng từ Google. Nếu bị vô hiệu hóa hoặc không có tài khoản Google được thêm vào, các yêu cầu xác minh giấy phép sẽ bị bỏ qua. + Hiện tại không thể phản hồi + Hiện tại không thể sao lưu + Thanh toán Google Play + Cảnh báo: Tính năng này đang trong giai đoạn thử nghiệm và có thể dẫn đến mất tiền. + %1$s có chữ ký chính xác: + ROM của bạn không có hỗ trợ gốc cho việc giả mạo chữ ký. Bạn vẫn có thể sử dụng Xposed hoặc các hệ thống khác để giả mạo chữ ký. Vui lòng kiểm tra tài liệu về ROM nào hỗ trợ giả mạo chữ ký và cách sử dụng microG trên ROM không được hỗ trợ. + Hoặc là %1$s đã cài đặt không tương thích hoặc tính năng giả mạo chữ ký không hoạt động đối với nó. Vui lòng kiểm tra tài liệu về các ứng dụng và ROM nào tương thích. + Cấu hình + Dịch vụ của Google + Dịch vụ định vị + Dịch vụ + Mạng sử dụng cho thông báo đẩy + Chế độ hoạt động + Lần sử dụng cuối cùng: %1$s + Ứng dụng đã đăng ký + Gốc + Tùy chỉnh: %s + Tự động: %s + Thực + Loại đánh giá + Hệ thống: %s + Nhập hồ sơ tùy chỉnh + Chưa hoàn thành + Không có kết quả + BẬT/Thủ công: %s + %s giây + %s phút + Tắt cấp phép + Bật cấp phép + Cấp phép Google Play + Phản hồi các yêu cầu xác thực giấy phép + Xử lý yêu cầu thanh toán + Sau khi được bật, một số ứng dụng có thể hoàn tất giao dịch mua hoặc bắt đầu đăng ký thông qua dịch vụ thanh toàn Google Play. + Một số ứng dụng có thể yêu cầu bạn cũng phải bật xác thực giấy phép để xác minh các giao dịch mua của bạn. + Bạn có thể quản lý Đăng nhập bằng Google trong tài khoản Google của bạn. + Kiểm tra CTS không thành công + Các gói đã cài đặt + Vui lòng kiểm tra tài liệu để biết những bước nào có thể được yêu cầu. + Cài đặt ứng dụng %1$s hoặc ứng dụng tương thích. Vui lòng kiểm tra tài liệu về ứng dụng nào tương thích. + Dịch vụ Play (GmsCore) + Kiểm tra + Tắt tối ưu hóa pin + Khoảng thời gian tính bằng giây để hệ thống gửi tín hiệu đến máy chủ Google. Tăng giá trị này sẽ giảm tiêu thụ pin, nhưng có thể gây ra sự chậm trễ trong việc nhận thông báo đẩy.\nTính năng này đã bị loại bỏ và sẽ được thay thế trong các phiên bản sau. + Khoảng thời gian ping: %1$s + Bạn đã từ chối một ứng dụng đăng ký nhận thông báo đẩy đã được đăng ký.\nBạn có muốn hủy đăng ký ngay bây giờ để ứng dụng không nhận được tin nhắn đẩy trong tương lai không? + Cho phép ứng dụng đăng ký nhận thông báo đẩy. + Khởi chạy ứng dụng khi có thông báo đẩy + Ứng dụng chưa đăng ký + Google SafetyNet là hệ thống chứng nhận thiết bị, đảm bảo rằng thiết bị được bảo mật đúng cách và tương thích với Android CTS. Một số ứng dụng sử dụng SafetyNet vì lý do bảo mật hoặc như một điều kiện tiên quyết để bảo vệ chống giả mạo.\n\nmicroG GmsCore chứa một triển khai miễn phí của SafetyNet, nhưng máy chủ chính thức yêu cầu các yêu cầu SafetyNet phải được ký bằng hệ thống DroidGuard độc quyền. + Kiểm tra ReCAPTCHA Enterprise + Dữ liệu cơ bản + Integrity không thành công + JSON không hợp lệ + TẮT + BẬT/Tự động: %s + Tiếp tục như %1$s + Chọn tài khoản + Tôi hiểu + Hoàn thành các bước sau để có thể sử dụng tài khoản Google %s trên thiết bị này. + Tài khoản Google của bạn được quản lý bởi nơi làm việc hoặc cơ sở giáo dục của bạn. Quản trị viên của bạn đã quyết định rằng các thiết bị cần có màn hình khóa bảo mật trước khi chúng có thể truy cập dữ liệu tài khoản.\n\nVui lòng thiết lập màn hình khóa bằng mật khẩu, mã PIN hoặc hình vẽ. + Cảnh báo tài khoản Google + Hoàn tất thiết lập tài khoản Google của bạn + Thông báo khi một trong các tài khoản Google của bạn cần thiết lập thêm trước khi có thể sử dụng hoặc khi một tài khoản không tương thích với microG. + Tài khoản Google của bạn cần được thiết lập thêm. + Hành động tài khoản bắt buộc + Cho phép Cloud Messaging cho microG + Theo tuỳ chọn của bạn, microG cần có sự cho phép của bạn trước khi có thể tự đăng ký dịch vụ Cloud Messaging. + Kích hoạt Cloud Messaging + Bạn có thể tắt Cloud Messaging sau khi hoàn tất quá trình thiết lập tài khoản. + Kích hoạt đăng kí thiết bị + Thiết bị của bạn cần phải đăng ký với Google ít nhất một lần.\n\nBạn có thể tắt đăng ký thiết bị Google sau khi thiết lập tài khoản hoàn tất. + Cấu hình khóa màn hình bảo mật + Hoàn tất + Nhấn để thực hiện + Bước đã hoàn thành \ No newline at end of file diff --git a/play-services-droidguard/core/src/main/res/values-vi/strings.xml b/play-services-droidguard/core/src/main/res/values-vi/strings.xml index a6b3daec93..f899a28342 100644 --- a/play-services-droidguard/core/src/main/res/values-vi/strings.xml +++ b/play-services-droidguard/core/src/main/res/values-vi/strings.xml @@ -1,2 +1,8 @@ - \ No newline at end of file + + Chế độ hoạt động của DroidGuard + Nhúng + Sử dụng runtime của DroidGuard cục bộ + Từ xa + Kết nối đến runtime của DroidGuard qua mạng + \ No newline at end of file diff --git a/play-services-fido/core/src/main/res/values-vi/strings.xml b/play-services-fido/core/src/main/res/values-vi/strings.xml index a6b3daec93..c1b3ca4a85 100644 --- a/play-services-fido/core/src/main/res/values-vi/strings.xml +++ b/play-services-fido/core/src/main/res/values-vi/strings.xml @@ -1,2 +1,27 @@ - \ No newline at end of file + + Sử dụng khóa bảo mật của bạn với %1$s + Sử dụng khóa bảo mật với %1$s giúp bảo vệ dữ liệu riêng tư của bạn. + Bắt đầu + %1$s hoạt động như một trình duyệt đáng tin cậy để sử dụng khóa bảo mật của bạn với %2$s. + Vâng, %1$s là trình duyệt đáng tin cậy của tôi và được phép sử dụng khóa bảo mật với các trang web của bên thứ ba. + Chọn cách sử dụng khóa bảo mật của bạn + Khóa bảo mật hoạt động với Bluetooth, NFC và USB. Chọn cách bạn muốn sử dụng khóa của mình. + Xác minh danh tính của bạn + %1$s cần xác minh đó là bạn. + Kết nối khóa bảo mật USB của bạn + Kết nối khóa bảo mật NFC của bạn + Giữ khóa của bạn nằm phẳng trên mặt sau của thiết bị cho đến khi nó ngừng rung + Sử dụng khóa bảo mật với Bluetooth + Sử dụng khóa bảo mật với NFC + Sử dụng khóa bảo mật với USB + Sử dụng thiết bị này với khóa màn hình + Vui lòng kết nối khóa bảo mật USB của bạn. + Vui lòng nhấn vào vòng tròn hoặc đĩa vàng trên %1$s. + Vui lòng nhập mã PIN cho thiết bị xác thực của bạn + 4 đến 63 ký tự + ĐƯỢC RỒI + Nhập sai mã PIN! + Huỷ + Kết nối khóa bảo mật của bạn với cổng USB hoặc kết nối bằng cáp USB. Nếu khóa của bạn có nút hoặc đĩa vàng, hãy nhấn vào đó ngay. + \ No newline at end of file diff --git a/play-services-location/core/src/main/res/values-vi/strings.xml b/play-services-location/core/src/main/res/values-vi/strings.xml index c6ff4c44e3..15092ff4ea 100644 --- a/play-services-location/core/src/main/res/values-vi/strings.xml +++ b/play-services-location/core/src/main/res/values-vi/strings.xml @@ -1,5 +1,53 @@ - + Vị trí - Các yêu cầu vị trí gần đây + Truy cập gần đây + Lấy vị trí dựa trên Wi-Fi từ dịch vụ định vị trực tuyến. + Yêu cầu từ dịch vụ trực tuyến + Lưu trữ vị trí Wi-Fi cục bộ khi sử dụng GPS. + Yêu cầu từ Mozilla + Lấy vị trí trạm phát di động từ Dịch vụ vị trí Mozilla. + Yêu cầu từ dịch vụ trực tuyến + Lấy vị trí trạm phát sóng di động từ dịch vụ định vị trực tuyến. + Ghi nhớ từ GPS + Lưu trữ vị trí mạng di động cục bộ khi sử dụng GPS. + Luôn trả về vị trí không chính xác cho ứng dụng này, bỏ qua mức độ quyền của ứng dụng. + Để có trải nghiệm tốt hơn, hãy bật chức năng định vị thiết bị và sử dụng dịch vụ định vị của microG + Cấp quyền vị trí cho Dịch vụ microG + Tuỳ chỉnh + Chọn dịch vụ định vị trực tuyến + Điều khoản/Quyền riêng tư + Đề xuất + Nhập hoặc xuất dữ liệu vị trí + Cấu hình yêu cầu + Để tiếp tục sử dụng dịch vụ định vị trực tuyến, bạn cần chọn một dịch vụ dữ liệu vị trí. + Xuất cơ sở dữ liệu vị trí Wi-Fi cục bộ + Xuất cơ sở dữ liệu vị trí trạm phát sóng di động địa phương + Nhập dữ liệu vị trí từ tệp + Đã nhập %1$d bản ghi. + Vị trí Wi-Fi + Vị trí dữ liệu di động + Bộ phân giải địa chỉ + Ghi nhớ từ GPS + Yêu cầu từ Mozilla + Lấy vị trí dựa trên Wi-Fi từ Dịch vụ vị trí Mozilla. + Yêu cầu từ Hotspot + Lấy vị trí Wi-Fi trực tiếp từ các điểm phát sóng được hỗ trợ khi đã kết nối. + Phân giải địa chỉ bằng OpenStreetMap Nominatim. + Sử dụng Nominatim + Lần truy cập cuối: %1$s + Buộc vị trí không chính xác + Ứng dụng có quyền truy cập vị trí + Thiết bị của bạn cần phải: + Để có thể tiếp tục, hãy bật chức năng định vị thiết bị và sử dụng dịch vụ định vị của microG + Sử dụng GPS, Wi‑Fi, dữ liệu di động và cảm biến + Để biết chi tiết, hãy vào phần cài đặt vị trí. + Không, cảm ơn + Sử dụng dịch vụ định vị microG; như một phần của dịch vụ này, microG có thể thu thập dữ liệu vị trí theo định kỳ và sử dụng dữ liệu này theo cách ẩn danh để cải thiện độ chính xác của vị trí và các dịch vụ dựa trên vị trí. + ĐƯỢC RỒI + Cấu hình URL dịch vụ + URL dịch vụ tùy chỉnh + Đặt lại + Điều này cho phép thiết lập URL dịch vụ tùy chỉnh. Các giá trị không hợp lệ có thể khiến dịch vụ vị trí không phản hồi hoặc hoàn toàn không khả dụng. + Đường dẫn /v1/geocate được tự động thêm vào. Nếu nhà cung cấp vị trí yêu cầu khóa, khóa này có thể được thêm vào như một tham số truy vấn vào URL gốc. \ No newline at end of file diff --git a/play-services-nearby/core/src/main/res/values-vi/strings.xml b/play-services-nearby/core/src/main/res/values-vi/strings.xml index a6b3daec93..0384023aea 100644 --- a/play-services-nearby/core/src/main/res/values-vi/strings.xml +++ b/play-services-nearby/core/src/main/res/values-vi/strings.xml @@ -1,2 +1,62 @@ - \ No newline at end of file + + Thông báo Tiếp xúc đang không hoạt động + Cần bật Bluetooth để nhận Thông báo Tiếp xúc. + Cần quyền truy cập Vị trí để nhận Thông báo Tiếp xúc. + Thông báo Tiếp xúc cần thêm các quyền để hoạt động + Thông báo Tiếp xúc + Mở cài đặt Vị trí + Rất tiếc, thiết bị của bạn không tương thích với Thông báo Tiếp xúc. + Cần bật Bluetooth và quyền truy cập Vị trí để nhận Thông báo Tiếp xúc. + Ứng dụng sử dụng Thông báo Tiếp xúc + Các ID đã thu thập + ID đang được phát sóng + Các tiếp xúc đã được báo cáo + Ít hơn 5 phút + Đã cập nhật: %1$s + Khoảng %1$d phút + tiếp xúc gần + tiếp xúc xa + %1$s, %2$s + Đã xử trí khoá chẩn đoán %1$d. + Không có trường hợp tiếp xúc nào được báo cáo. + Lưu ý: Điểm rủi ro được xác định bởi ứng dụng. Số điểm cao có thể liên quan đến rủi ro thấp hoặc ngược lại. + Sử dụng API trong 14 ngày qua + %1$d gọi đến %2$s + Đã báo cáo các trường hợp tiếp xúc với %1$d: + %1$s, điểm rủi ro %2$d + %1$d ID đã thu thập + %1$s ID mỗi giờ + Xoá + Xoá tất cả ID đã thu thập + Không có bản ghi + Việc xóa các ID đã thu thập sẽ khiến bạn không thể nhận thông báo nếu bất kỳ ai trong số các liên lạc của bạn trong 14 ngày qua được chẩn đoán. + Vẫn xoá + Xuất + Xuất ID đã thu thập để phân tích mở rộng bằng ứng dụng khác. + Khi đã kích hoạt API Thông báo Tiếp xúc, thiết bị của bạn sẽ thụ động thu thập các ID (gọi là Rolling Proximity Identifiers, hoặc RPIs) từ các thiết bị gần đó.\n\nKhi chủ sở hữu thiết được chẩn đoán dương tính, ID của họ có thể được chia sẻ. Thiết bị của bạn kiểm tra xem có ID nào đã biết được chẩn đoán dương tính trùng khớp với các ID đã thu thập hay không và tính toán mức độ rủi ro nhiễm bệnh của bạn. + Sử dụng Thông báo Tiếp xúc + Bật Thông báo Tiếp xúc? + Bật + Tắt Thông báo Tiếp xúc? + Sau khi tắt Thông báo Tiếp xúc, bạn sẽ không còn nhận được thông báo khi bạn đã tiếp xúc với ai đó đã được chẩn đoán dương tính. + Tắt + Chia sẻ ID của bạn với %1$s\? + ID của bạn trong 14 ngày qua sẽ được sử dụng để giúp thông báo cho người khác rằng bạn đã ở gần những trường hợp phơi nhiễm tiềm tàng.\n\nDanh tính hoặc kết quả xét nghiệm của bạn sẽ không được chia sẻ với người khác. + Chia sẻ + %1$s cần thêm quyền. + Cấp quyền + Cần phải bật Bluetooth. + Cần phải có quyền truy cập vị trí. + Bật + Nhấn để cấp các quyền cần thiết cho Thông báo Tiếp xúc + Gần xong rồi! Bạn sẽ cần bật quyền truy cập vị trí nền bằng cách chọn tùy chọn \'Cho phép mọi lúc\' trên màn hình tiếp theo. Sau đó nhấn quay lại. + Cập nhật cài đặt + Để bật Thông báo Tiếp xúc, hãy mở bất kỳ ứng dụng nào hỗ trợ tính năng này. + Bật Bluetooth + Rất tiếc, thiết bị của bạn chỉ tương thích một phần với Thông báo Tiếp xúc. Bạn có thể nhận thông báo về các tiếp xúc có nguy cơ, nhưng sẽ không thể thông báo cho người khác. + %1$d ID trong giờ qua + API Thông báo Tiếp xúc cho phép các ứng dụng thông báo cho bạn nếu bạn đã tiếp xúc với ai đó được chẩn đoán là dương tính.\n\nNgày, thời gian, và cường độ tín hiệu liên quan đến lần tiếp xúc sẽ được chia sẻ với ứng dụng tương ứng. + Điện thoại của bạn cần sử dụng Bluetooth để thu thập và chia sẻ an toàn các ID với các điện thoại khác ở gần. \u0020%1$s có thể thông báo cho bạn nếu bạn đã tiếp xúc với ai đó được chẩn đoán dương tính. \u0020Ngày, thời gian và cường độ tín hiệu liên quan đến một lần tiếp xúc sẽ được chia sẻ với ứng dụng. + Cần có Quyền mới + \ No newline at end of file diff --git a/vending-app/src/main/res/values-vi/strings.xml b/vending-app/src/main/res/values-vi/strings.xml index a6b3daec93..d198e898ca 100644 --- a/vending-app/src/main/res/values-vi/strings.xml +++ b/vending-app/src/main/res/values-vi/strings.xml @@ -1,2 +1,22 @@ - \ No newline at end of file + + Đối tác microG + Không thể sử dụng Đối tác microG một cách độc lập. Thay vào đó, hãy mở cài đặt Dịch vụ microG. + Không thể sử dụng Đối tác microG một cách độc lập. Vui lòng cài đặt Dịch vụ microG để tiếp tục sử dụng. + Thông báo giấy phép + Thông báo khi ứng dụng cố gắng xác thực giấy phép nhưng bạn chưa đăng nhập vào bất kỳ tài khoản Google nào. + %1$s không thể xác minh giấy phép + Nếu ứng dụng hoạt động bất thường, hãy đăng nhập vào tài khoản Google mà bạn đã dùng để mua ứng dụng. + Đăng nhập + Bỏ qua + Hiện tại không thể thanh toán + Xác nhận mua hàng + Không kết nối được với Internet. Vui lòng kết nối Wi-Fi hoặc dữ liệu di động và thử lại. + Mật khẩu bạn nhập không đúng. + Lỗi không xác định, vui lòng thoát và thử lại. + Nhập mật khẩu của bạn + Nhớ thông tin đăng nhập của tôi trên thiết bị này + Quên mật khẩu? + Tìm hiểu thêm + Xác minh + \ No newline at end of file From 151bb9af4044f2f773bf894d4d09722216f7f5f4 Mon Sep 17 00:00:00 2001 From: NEXI Date: Thu, 3 Oct 2024 19:16:30 +0000 Subject: [PATCH 034/132] Translated using Weblate (Serbian) Currently translated at 100.0% (232 of 232 strings) Translation: microG/play-services-core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-core-strings/sr/ --- play-services-core/src/main/res/values-sr/strings.xml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/play-services-core/src/main/res/values-sr/strings.xml b/play-services-core/src/main/res/values-sr/strings.xml index 32b7f9ce73..8fb81053ab 100644 --- a/play-services-core/src/main/res/values-sr/strings.xml +++ b/play-services-core/src/main/res/values-sr/strings.xml @@ -260,4 +260,7 @@ %1$s жели да приступе вашем налогу као да је %2$s од %3$s. Ово би му могло дати привилеговани приступ вашем налогу. Бесплатне апликације могу да провере да ли су преузете са Google Play-а. Аутоматски додајте бесплатне апликације у библиотеку налога да бисте увек прошли проверу за све бесплатне апликације које су вам тренутно доступне. Аутоматски додај бесплатне апликације у библиотеку + microG ограничене услуге + Разумем + Користите microG ограничене услуге. За разлику од уобичајених microG услуга, ова верзија функционише само са апликацијама које користе microG библиотеке, а не са онима на Google Play-у. То значи да ће већина апликација занемарити ове услуге. \ No newline at end of file From 629e7fbe2bcf8669a6736764bd0f2ac6bab5e3af Mon Sep 17 00:00:00 2001 From: Misaka <79515833+misakazip@users.noreply.github.com> Date: Thu, 3 Oct 2024 11:31:26 +0000 Subject: [PATCH 035/132] Translated using Weblate (Japanese) Currently translated at 100.0% (13 of 13 strings) Translation: microG/play-services-base: core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-base-core-strings/ja/ Translated using Weblate (Japanese) Currently translated at 6.7% (4 of 59 strings) Translation: microG/play-services-nearby: core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-nearby-core-strings/ja/ Translated using Weblate (Japanese) Currently translated at 51.9% (81 of 156 strings) Translation: microG/play-services-core: permissions Translate-URL: https://hosted.weblate.org/projects/microg/play-services-core-permissions/ja/ Translated using Weblate (Japanese) Currently translated at 75.0% (18 of 24 strings) Translation: microG/play-services-fido: core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-fido-core-strings/ja/ Translated using Weblate (Japanese) Currently translated at 100.0% (5 of 5 strings) Translation: microG/play-services-droidguard: core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-droidguard-core-strings/ja/ --- .../core/src/main/res/values-ja/strings.xml | 2 +- .../src/main/res/values-ja/permissions.xml | 1 + .../core/src/main/res/values-ja/strings.xml | 8 ++++++- .../core/src/main/res/values-ja/strings.xml | 21 ++++++++++++++++++- .../core/src/main/res/values-ja/strings.xml | 7 ++++++- 5 files changed, 35 insertions(+), 4 deletions(-) diff --git a/play-services-base/core/src/main/res/values-ja/strings.xml b/play-services-base/core/src/main/res/values-ja/strings.xml index d69b0d336f..aef68da780 100644 --- a/play-services-base/core/src/main/res/values-ja/strings.xml +++ b/play-services-base/core/src/main/res/values-ja/strings.xml @@ -12,7 +12,7 @@ 自動 手動 On - Off + オフ バックグラウンドで有効 %1$s をバックグラウンドで実行しています。 %1$s をバッテリー最適化から除外するか、通知設定でこの通知を非表示にしてください。 diff --git a/play-services-core/src/main/res/values-ja/permissions.xml b/play-services-core/src/main/res/values-ja/permissions.xml index a759cdb5bc..c2127069b2 100644 --- a/play-services-core/src/main/res/values-ja/permissions.xml +++ b/play-services-core/src/main/res/values-ja/permissions.xml @@ -100,4 +100,5 @@ Google Play Androd Developer へのアクセス Groups Migration API への読み取り/書き込みアクセス。 AdSense のデータを表示 + 管理監査APIへの読み取り専用のアクセス \ No newline at end of file diff --git a/play-services-droidguard/core/src/main/res/values-ja/strings.xml b/play-services-droidguard/core/src/main/res/values-ja/strings.xml index a6b3daec93..9845c8e278 100644 --- a/play-services-droidguard/core/src/main/res/values-ja/strings.xml +++ b/play-services-droidguard/core/src/main/res/values-ja/strings.xml @@ -1,2 +1,8 @@ - \ No newline at end of file + + DroidGuard操作モード + 組み込み型 + ローカルの DroidGuard ランタイムを使用する + リモート + ネットワーク経由でDroidGuardランタイムに接続する + \ No newline at end of file diff --git a/play-services-fido/core/src/main/res/values-ja/strings.xml b/play-services-fido/core/src/main/res/values-ja/strings.xml index a6b3daec93..d7ba6ecff5 100644 --- a/play-services-fido/core/src/main/res/values-ja/strings.xml +++ b/play-services-fido/core/src/main/res/values-ja/strings.xml @@ -1,2 +1,21 @@ - \ No newline at end of file + + セキュリティキーは、Bluetooth、NFC、USBで動作します。キーの使用方法を選択してください。 + 振動が止まるまで、キーをデバイスの背面に平らにかざします + NFCでセキュリティキーを使用する + USBでセキュリティキーを使用する + 4から63文字 + %1$sでセキュリティキーを使用する + %1$sでセキュリティキーを使用すると、プライベートデータを保護できます。 + 始める + %1$sは、%2$sでセキュリティキーを使用する信頼できるブラウザとして機能します。 + はい、%1$sは私の信頼できるブラウザであり、サードパーティのWebサイトでセキュリティキーを使用することが許可されるべきです。 + セキュリティキーの使用方法を選択してください + %1$sはあなたであることを確認する必要があります。 + USBセキュリティキーを接続する + セキュリティキーをUSBポートに接続するか、USBケーブルで接続します。キーにボタンやゴールドディスクがある場合は、今すぐタップしてください。 + NFCセキュリティキーを接続する + Bluetoothでセキュリティキーを使用する + このデバイスでスクリーンロックを使用してください + USBセキュリティキーを接続してください。 + \ No newline at end of file diff --git a/play-services-nearby/core/src/main/res/values-ja/strings.xml b/play-services-nearby/core/src/main/res/values-ja/strings.xml index a6b3daec93..0a4118f569 100644 --- a/play-services-nearby/core/src/main/res/values-ja/strings.xml +++ b/play-services-nearby/core/src/main/res/values-ja/strings.xml @@ -1,2 +1,7 @@ - \ No newline at end of file + + Bluetooth を有効にする + 位置情報の設定を開く + 収集されたID + 現在のブロードキャストID + \ No newline at end of file From b16eb908407a8c1ac87892378c96b6d43e10b877 Mon Sep 17 00:00:00 2001 From: Igor Sorocean Date: Tue, 8 Oct 2024 06:00:57 +0000 Subject: [PATCH 036/132] Translated using Weblate (Romanian) Currently translated at 100.0% (232 of 232 strings) Translation: microG/play-services-core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-core-strings/ro/ Translated using Weblate (Romanian) Currently translated at 100.0% (249 of 249 strings) Translation: microG/play-services-core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-core-strings/ro/ --- .../src/main/res/values-ro/strings.xml | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/play-services-core/src/main/res/values-ro/strings.xml b/play-services-core/src/main/res/values-ro/strings.xml index a32dbd43d9..e14fcaa99e 100644 --- a/play-services-core/src/main/res/values-ro/strings.xml +++ b/play-services-core/src/main/res/values-ro/strings.xml @@ -55,7 +55,7 @@ \n \nAcest lucru poate dura câteva secunde. Permite și distribuie - O aplicație de pe dispozitivul tău încearcă să se conecteze la un cont Google. \u0020Dacă acest lucru a fost intenționat, utilizează butonul Conectare pentru a te conecta la pagina de conectare Google, dacă nu, apasă Anulare pentru a reveni la aplicația care a generat acest dialog. + O aplicație de pe dispozitivul tău încearcă să se conecteze la un cont Google. \n\nDacă acest lucru a fost intenționat, utilizează butonul Conectare pentru a te conecta la pagina de conectare Google, dacă nu, apasă Anulare pentru a reveni la aplicația care a generat acest dialog. Configurează serviciile microG. Importă profil personalizat Tipul solicitării @@ -256,4 +256,24 @@ Permiți pentru %1$s acces privilegiat la %2$s\? Adaugă automat aplicațiile gratuite în bibliotecă Aplicațiile gratuite pot verifica dacă au fost descărcate de pe Google Play. Adaugă automat aplicațiile gratuite în biblioteca contului pentru a trece întotdeauna verificarea pentru toate aplicațiile gratuite disponibile în prezent. + Servicii limitate microG + Utilizezi serviciile limitate microG. Spre deosebire de serviciile microG obișnuite, această versiune funcționează numai cu aplicațiile care folosesc biblioteci microG, nu cu cele de pe Google Play. Aceasta înseamnă că majoritatea aplicațiilor vor ignora aceste servicii. + Am înțeles + Alerte pentru contul Google + Contul Google necesită o configurare suplimentară. + Finalizează configurarea contului Google + Activează înregistrarea dispozitivului + Dispozitivul trebuie să se înregistreze la Google cel puțin o dată.\n\nPoți dezactiva înregistrarea dispozitivului Google după finalizarea configurării contului. + Permite mesageria în cloud pentru microG + În funcție de preferințele tale, microG are nevoie de permisiunea ta înainte de a se putea înregistra pentru mesageria în cloud. + Clic pentru a efectua pasul + Pas finalizat + Finalizare + Configurează blocarea securizată a ecranului + Notifică atunci când unul dintre conturile Google necesită o configurare suplimentară înainte de a putea fi utilizat sau când un cont este incompatibil cu microG. + Este necesară acțiunea în cont + Parcurge următorii pași pentru a putea folosi contul Google %s pe acest dispozitiv. + Poți dezactiva mesageria în cloud după finalizarea configurării contului. + Activează mesageria în cloud + Contul Google este gestionat de locul de muncă sau de instituția de învățământ. Administratorul a decis că dispozitivele au nevoie de o blocare securizată a ecranului înainte de a putea accesa datele contului.\n\nConfigurează o parolă, un cod PIN sau un model de deblocare al ecranului. \ No newline at end of file From 80b2d34d5f6b7ee5413e9794527a1f18280582f1 Mon Sep 17 00:00:00 2001 From: Angelo Schirinzi Date: Tue, 8 Oct 2024 12:02:00 +0000 Subject: [PATCH 037/132] Translated using Weblate (Italian) Currently translated at 100.0% (232 of 232 strings) Translation: microG/play-services-core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-core-strings/it/ Translated using Weblate (Italian) Currently translated at 100.0% (50 of 50 strings) Translation: microG/play-services-location: core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-location-core-strings/it/ Translated using Weblate (Italian) Currently translated at 100.0% (24 of 24 strings) Translation: microG/play-services-fido: core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-fido-core-strings/it/ --- .../src/main/res/values-it/strings.xml | 6 ++++++ .../core/src/main/res/values-it/strings.xml | 5 +++++ .../core/src/main/res/values-it/strings.xml | 19 +++++++++++++++++-- 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/play-services-core/src/main/res/values-it/strings.xml b/play-services-core/src/main/res/values-it/strings.xml index 50756054eb..6c6dfefb9d 100644 --- a/play-services-core/src/main/res/values-it/strings.xml +++ b/play-services-core/src/main/res/values-it/strings.xml @@ -244,4 +244,10 @@ Questa operazione può richiedere alcuni secondi." Alcune applicazioni potrebbero richiedere anche di abilitare la verifica della licenza per verificare i tuoi acquisti. Questa funzionalità è ancora sperimentale e potrebbe comportare la perdita di denaro. Sei stato avvisato. Permetti a %1$s l\'accesso privilegiato a %2$s\? + microG Limited Services + Aggiungi automaticamente app gratuite alla libreria + Le app gratuite possono verificare se sono state scaricate da Google Play. Aggiungi automaticamente le app gratuite alla libreria del tuo account per superare sempre il controllo per tutte le app gratuite attualmente disponibili per te. + Ho capito + %1$s wants to access your account as if it was %2$s by %3$s. Ciò potrebbe garantirgli un accesso privilegiato al tuo account. + Stai utilizzando microG Limited Services. A differenza dei soliti servizi microG, questa versione funziona solo con le app che utilizzano le librerie microG, non quelle su Google Play. Ciò significa che la maggior parte delle applicazioni ignorerà questi servizi. \ No newline at end of file diff --git a/play-services-fido/core/src/main/res/values-it/strings.xml b/play-services-fido/core/src/main/res/values-it/strings.xml index 2830bf9e92..440f54845b 100644 --- a/play-services-fido/core/src/main/res/values-it/strings.xml +++ b/play-services-fido/core/src/main/res/values-it/strings.xml @@ -19,4 +19,9 @@ Usa chiave di sicurezza via Bluetooth Le chiavi di sicurezza funzionano via Bluetooth, NFC e USB. Scegli come vuoi usare la tua chiave. Usa chiave di sicurezza con l\'NFC + Inserisci il PIN per l\'autenticazione + Da 4 a 63 caratteri + OK + Annulla + PIN inserito errato! \ No newline at end of file diff --git a/play-services-location/core/src/main/res/values-it/strings.xml b/play-services-location/core/src/main/res/values-it/strings.xml index ce7c50bd5e..03f83aaf03 100644 --- a/play-services-location/core/src/main/res/values-it/strings.xml +++ b/play-services-location/core/src/main/res/values-it/strings.xml @@ -9,7 +9,7 @@ Ottieni una geolocalizzazione basata sul Wi-Fi da Mozilla Location Service. Salva sul dispositivo informazioni sul posizionamento delle reti Wi-Fi quando il GPS è in uso. Richiedi a Mozilla - Salva sul dispositivo informazioni sul posizionamento delle antenne delle reti cellulari quando il GPS è in uso. + Memorizza localmente le posizioni basate sulla rete mobile quando si utilizza il GPS. Deduci dal GPS Deduci dal GPS Ottieni una geolocalizzazione basata sul Wi-Fi direttamente da un hotspot che la supporta, quando connesso. @@ -20,7 +20,7 @@ Cerca gli indirizzi usando OpenStreetMap Nominatim. Geolocalizzazione basata sul Wi-Fi Ricerca degli indirizzi - Ottieni informazioni sul posizionamento delle antenne delle reti cellulari da Mozilla Location Service. + Ottieni la posizione basata sulle torri cellulari della rete mobile da Mozilla Location Service. OK No, grazie Per ulteriori dettagli, vai alle impostazioni di localizzazione. @@ -35,4 +35,19 @@ Configura l\'URL del servizio Questo consente di impostare un URL del servizio personalizzato. Valori non validi possono rendere i servizi di localizzazione instabili o completamente non disponibili. URL del servizio personalizzato + Richiesta dal servizio online + Ottieni la posizione basata su Wi-Fi dal servizio di localizzazione online. + Richiesta dal servizio online + Ottieni la posizione basata sulle torri cellulari della rete mobile dal servizio di localizzazione online. + Seleziona il servizio di localizzazione online + Termini / Privacy + Suggeriti + Personalizzato + Importare o esportare dati sulla posizione + Configurazione richiesta + Per continuare a utilizzare i servizi di localizzazione online, è necessario selezionare un servizio dati sulla localizzazione. + Esportare il database della posizione Wi-Fi locale + Importa i dati sulla posizione da file + Esportare il database locale delle posizioni basate sulle torri cellulari + Importati il %1$d dei registri. \ No newline at end of file From 5f31c321e3f8b7e1abd0f95a7d09ed4435fc0672 Mon Sep 17 00:00:00 2001 From: Sean Date: Tue, 8 Oct 2024 08:32:37 +0000 Subject: [PATCH 038/132] Translated using Weblate (Spanish) Currently translated at 100.0% (156 of 156 strings) Translation: microG/play-services-core: permissions Translate-URL: https://hosted.weblate.org/projects/microg/play-services-core-permissions/es/ --- play-services-core/src/main/res/values-es/permissions.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/play-services-core/src/main/res/values-es/permissions.xml b/play-services-core/src/main/res/values-es/permissions.xml index 3b04e83218..d908d640ab 100644 --- a/play-services-core/src/main/res/values-es/permissions.xml +++ b/play-services-core/src/main/res/values-es/permissions.xml @@ -72,7 +72,7 @@ Permite a la aplicación acceder a todos los servicios de Herramientas de Google Webmaster a través de cualquier cuenta de Google asociada. Búsqueda por voz Permite a la aplicación acceder a todos los servicios de Búsqueda por voz a través de cualquier cuenta de Google asociada. - Personalized Speech Recognition + Reconocimiento de Voz Personalizado Permite a la aplicación acceder a todos los servicios de Personalized Speech Recognition a través de cualquier cuenta de Google asociada. Google Talk Permite a la aplicación acceder a todos los servicios de Google Talk a través de cualquier cuenta de Google asociada. From 628a00b323d5d3705129426bec77dfdf7441b4b3 Mon Sep 17 00:00:00 2001 From: HARIRAK SRITHAM Date: Tue, 8 Oct 2024 21:13:06 +0200 Subject: [PATCH 039/132] Added translation using Weblate (Thai) Added translation using Weblate (Thai) Added translation using Weblate (Thai) Added translation using Weblate (Thai) Added translation using Weblate (Thai) Added translation using Weblate (Thai) Added translation using Weblate (Thai) Added translation using Weblate (Thai) Added translation using Weblate (Thai) Added translation using Weblate (Thai) Added translation using Weblate (Thai) Added translation using Weblate (Thai) --- .../core/src/main/res/values-th/strings.xml | 2 ++ .../core/src/main/res/values-th/strings.xml | 2 ++ play-services-base/core/src/main/res/values-th/strings.xml | 2 ++ play-services-core/src/main/res/values-th/permissions.xml | 2 ++ play-services-core/src/main/res/values-th/plurals.xml | 2 ++ play-services-core/src/main/res/values-th/strings.xml | 2 ++ .../core/src/main/res/values-th/strings.xml | 2 ++ play-services-fido/core/src/main/res/values-th/strings.xml | 2 ++ play-services-location/core/src/main/res/values-th/strings.xml | 2 ++ play-services-nearby/core/src/main/res/values-th/strings.xml | 2 ++ play-services-oss-licenses/src/main/res/values-th/strings.xml | 2 ++ vending-app/src/main/res/values-th/strings.xml | 2 ++ 12 files changed, 24 insertions(+) create mode 100644 play-services-ads-identifier/core/src/main/res/values-th/strings.xml create mode 100644 play-services-auth-api-phone/core/src/main/res/values-th/strings.xml create mode 100644 play-services-base/core/src/main/res/values-th/strings.xml create mode 100644 play-services-core/src/main/res/values-th/permissions.xml create mode 100644 play-services-core/src/main/res/values-th/plurals.xml create mode 100644 play-services-core/src/main/res/values-th/strings.xml create mode 100644 play-services-droidguard/core/src/main/res/values-th/strings.xml create mode 100644 play-services-fido/core/src/main/res/values-th/strings.xml create mode 100644 play-services-location/core/src/main/res/values-th/strings.xml create mode 100644 play-services-nearby/core/src/main/res/values-th/strings.xml create mode 100644 play-services-oss-licenses/src/main/res/values-th/strings.xml create mode 100644 vending-app/src/main/res/values-th/strings.xml diff --git a/play-services-ads-identifier/core/src/main/res/values-th/strings.xml b/play-services-ads-identifier/core/src/main/res/values-th/strings.xml new file mode 100644 index 0000000000..a6b3daec93 --- /dev/null +++ b/play-services-ads-identifier/core/src/main/res/values-th/strings.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/play-services-auth-api-phone/core/src/main/res/values-th/strings.xml b/play-services-auth-api-phone/core/src/main/res/values-th/strings.xml new file mode 100644 index 0000000000..a6b3daec93 --- /dev/null +++ b/play-services-auth-api-phone/core/src/main/res/values-th/strings.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/play-services-base/core/src/main/res/values-th/strings.xml b/play-services-base/core/src/main/res/values-th/strings.xml new file mode 100644 index 0000000000..a6b3daec93 --- /dev/null +++ b/play-services-base/core/src/main/res/values-th/strings.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/play-services-core/src/main/res/values-th/permissions.xml b/play-services-core/src/main/res/values-th/permissions.xml new file mode 100644 index 0000000000..a6b3daec93 --- /dev/null +++ b/play-services-core/src/main/res/values-th/permissions.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/play-services-core/src/main/res/values-th/plurals.xml b/play-services-core/src/main/res/values-th/plurals.xml new file mode 100644 index 0000000000..a6b3daec93 --- /dev/null +++ b/play-services-core/src/main/res/values-th/plurals.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/play-services-core/src/main/res/values-th/strings.xml b/play-services-core/src/main/res/values-th/strings.xml new file mode 100644 index 0000000000..a6b3daec93 --- /dev/null +++ b/play-services-core/src/main/res/values-th/strings.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/play-services-droidguard/core/src/main/res/values-th/strings.xml b/play-services-droidguard/core/src/main/res/values-th/strings.xml new file mode 100644 index 0000000000..a6b3daec93 --- /dev/null +++ b/play-services-droidguard/core/src/main/res/values-th/strings.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/play-services-fido/core/src/main/res/values-th/strings.xml b/play-services-fido/core/src/main/res/values-th/strings.xml new file mode 100644 index 0000000000..a6b3daec93 --- /dev/null +++ b/play-services-fido/core/src/main/res/values-th/strings.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/play-services-location/core/src/main/res/values-th/strings.xml b/play-services-location/core/src/main/res/values-th/strings.xml new file mode 100644 index 0000000000..a6b3daec93 --- /dev/null +++ b/play-services-location/core/src/main/res/values-th/strings.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/play-services-nearby/core/src/main/res/values-th/strings.xml b/play-services-nearby/core/src/main/res/values-th/strings.xml new file mode 100644 index 0000000000..a6b3daec93 --- /dev/null +++ b/play-services-nearby/core/src/main/res/values-th/strings.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/play-services-oss-licenses/src/main/res/values-th/strings.xml b/play-services-oss-licenses/src/main/res/values-th/strings.xml new file mode 100644 index 0000000000..a6b3daec93 --- /dev/null +++ b/play-services-oss-licenses/src/main/res/values-th/strings.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/vending-app/src/main/res/values-th/strings.xml b/vending-app/src/main/res/values-th/strings.xml new file mode 100644 index 0000000000..a6b3daec93 --- /dev/null +++ b/vending-app/src/main/res/values-th/strings.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file From 50b6a1282c9f2879ffcff9196e93b2abf76b429f Mon Sep 17 00:00:00 2001 From: DaVinci9196 Date: Wed, 9 Oct 2024 08:09:46 +0000 Subject: [PATCH 040/132] Translated using Weblate (Spanish) Currently translated at 99.5% (231 of 232 strings) Translation: microG/play-services-core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-core-strings/es/ --- play-services-core/src/main/res/values-es/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/play-services-core/src/main/res/values-es/strings.xml b/play-services-core/src/main/res/values-es/strings.xml index 9395f11f97..25d54eb6e6 100644 --- a/play-services-core/src/main/res/values-es/strings.xml +++ b/play-services-core/src/main/res/values-es/strings.xml @@ -24,7 +24,7 @@ %1$s quiere usar: Administrador de cuentas de Google Lo sentimos… - Una aplicación de tu dispositivo está intentando iniciar sesión en una cuenta de Google. \u0020Si esto fue intencional, use el botón Iniciar sesión para conectarse a la página de inicio de sesión de Google, si no, presione Cancelar para volver a la aplicación que hizo que apareciera este cuadro de diálogo. + Una aplicación de tu dispositivo está intentando iniciar sesión en una cuenta de Google. \u0020Si esto fue intencional, usa el botón Iniciar sesión para conectarte a la página de inicio de sesión de Google, si no, presione Cancelar para volver a la aplicación que hizo que apareciera este cuadro de diálogo. Iniciar sesión Tu dispositivo está estableciendo una conexión con los servidores de Google para iniciar sesión. \n From b7e3b41ffdd4b40e858c5a74ace6c229c182fdc8 Mon Sep 17 00:00:00 2001 From: HARIRAK SRITHAM Date: Wed, 9 Oct 2024 04:14:46 +0000 Subject: [PATCH 041/132] Translated using Weblate (Thai) Currently translated at 100.0% (4 of 4 strings) Translation: microG/play-services-ads-identifier: core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-ads-identifier-core-strings/th/ Translated using Weblate (Thai) Currently translated at 100.0% (3 of 3 strings) Translation: microG/play-services-auth-api-phone: core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-auth-api-phone-core-strings/th/ Translated using Weblate (Thai) Currently translated at 100.0% (13 of 13 strings) Translation: microG/play-services-base: core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-base-core-strings/th/ Translated using Weblate (Thai) Currently translated at 11.5% (18 of 156 strings) Translation: microG/play-services-core: permissions Translate-URL: https://hosted.weblate.org/projects/microg/play-services-core-permissions/th/ Translated using Weblate (Thai) Currently translated at 100.0% (4 of 4 strings) Translation: microG/play-services-core: plurals Translate-URL: https://hosted.weblate.org/projects/microg/play-services-core-plurals/th/ Translated using Weblate (Thai) Currently translated at 7.3% (17 of 232 strings) Translation: microG/play-services-core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-core-strings/th/ Translated using Weblate (Thai) Currently translated at 100.0% (5 of 5 strings) Translation: microG/play-services-droidguard: core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-droidguard-core-strings/th/ Translated using Weblate (Thai) Currently translated at 100.0% (24 of 24 strings) Translation: microG/play-services-fido: core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-fido-core-strings/th/ Translated using Weblate (Thai) Currently translated at 74.0% (37 of 50 strings) Translation: microG/play-services-location: core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-location-core-strings/th/ Translated using Weblate (Thai) Currently translated at 100.0% (59 of 59 strings) Translation: microG/play-services-nearby: core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-nearby-core-strings/th/ Translated using Weblate (Thai) Currently translated at 100.0% (6 of 6 strings) Translation: microG/play-services-oss-licenses: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-oss-licenses-strings/th/ Translated using Weblate (Thai) Currently translated at 100.0% (19 of 19 strings) Translation: microG/vending-app Translate-URL: https://hosted.weblate.org/projects/microg/vending-app/th/ Translated using Weblate (Thai) Currently translated at 39.7% (62 of 156 strings) Translation: microG/play-services-core: permissions Translate-URL: https://hosted.weblate.org/projects/microg/play-services-core-permissions/th/ Translated using Weblate (Thai) Currently translated at 26.7% (62 of 232 strings) Translation: microG/play-services-core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-core-strings/th/ Translated using Weblate (Thai) Currently translated at 100.0% (50 of 50 strings) Translation: microG/play-services-location: core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-location-core-strings/th/ Translated using Weblate (Thai) Currently translated at 48.7% (76 of 156 strings) Translation: microG/play-services-core: permissions Translate-URL: https://hosted.weblate.org/projects/microg/play-services-core-permissions/th/ Translated using Weblate (Thai) Currently translated at 79.4% (124 of 156 strings) Translation: microG/play-services-core: permissions Translate-URL: https://hosted.weblate.org/projects/microg/play-services-core-permissions/th/ Translated using Weblate (Thai) Currently translated at 40.0% (93 of 232 strings) Translation: microG/play-services-core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-core-strings/th/ --- .../core/src/main/res/values-th/strings.xml | 7 +- .../core/src/main/res/values-th/strings.xml | 6 +- .../core/src/main/res/values-th/strings.xml | 16 ++- .../src/main/res/values-th/permissions.xml | 127 +++++++++++++++++- .../src/main/res/values-th/plurals.xml | 15 ++- .../src/main/res/values-th/strings.xml | 96 ++++++++++++- .../core/src/main/res/values-th/strings.xml | 8 +- .../core/src/main/res/values-th/strings.xml | 27 +++- .../core/src/main/res/values-th/strings.xml | 53 +++++++- .../core/src/main/res/values-th/strings.xml | 62 ++++++++- .../src/main/res/values-th/strings.xml | 9 +- .../src/main/res/values-th/strings.xml | 22 ++- 12 files changed, 436 insertions(+), 12 deletions(-) diff --git a/play-services-ads-identifier/core/src/main/res/values-th/strings.xml b/play-services-ads-identifier/core/src/main/res/values-th/strings.xml index a6b3daec93..0204aa564c 100644 --- a/play-services-ads-identifier/core/src/main/res/values-th/strings.xml +++ b/play-services-ads-identifier/core/src/main/res/values-th/strings.xml @@ -1,2 +1,7 @@ - \ No newline at end of file + + อนุญาติรหัสประจำตัวของโฆษณา + การแจ้งเตือน รหัสประจำตัวของโฆษณา + อนุญาตให้แอปผู้เผยแพร่เข้าถึง รหัสประจำตัวของโฆษณาที่ถูกต้องได้โดยตรงหรือโดยอ้อม + อนุญาตให้แอปรับการแจ้งเตือนเมื่อมีการอัปเดต รหัสประจำตัวของโฆษณา หรือ การตั้งค่าการติดตามโฆษณาของผู้ใช้ + \ No newline at end of file diff --git a/play-services-auth-api-phone/core/src/main/res/values-th/strings.xml b/play-services-auth-api-phone/core/src/main/res/values-th/strings.xml index a6b3daec93..9c741abb3f 100644 --- a/play-services-auth-api-phone/core/src/main/res/values-th/strings.xml +++ b/play-services-auth-api-phone/core/src/main/res/values-th/strings.xml @@ -1,2 +1,6 @@ - \ No newline at end of file + + อนุญาตให้ %s อ่านข้อความด้านล่างและกรอกรหัส? + อนุญาต + ปฏิเสธ + \ No newline at end of file diff --git a/play-services-base/core/src/main/res/values-th/strings.xml b/play-services-base/core/src/main/res/values-th/strings.xml index a6b3daec93..30244e2e33 100644 --- a/play-services-base/core/src/main/res/values-th/strings.xml +++ b/play-services-base/core/src/main/res/values-th/strings.xml @@ -1,2 +1,16 @@ - \ No newline at end of file + + ทำงานในพื้นหลัง + %1$s กำลังทำงานอยู่ในพื้นหลัง + ไม่รวม %1$s จากการเพิ่มประสิทธิภาพแบตเตอรี่หรือเปลี่ยนการตั้งค่าการแจ้งเตือนเพื่อซ่อนการแจ้งเตือนนี้ + ขั้นสูง + ไม่มี + ดูทั้งหมด + เปิด + ปิดการทำงาน + เปิดใช้งานแล้ว + อัตโนมัติ + คู่มือ + เปิด + ปิด + \ No newline at end of file diff --git a/play-services-core/src/main/res/values-th/permissions.xml b/play-services-core/src/main/res/values-th/permissions.xml index a6b3daec93..b66a26774e 100644 --- a/play-services-core/src/main/res/values-th/permissions.xml +++ b/play-services-core/src/main/res/values-th/permissions.xml @@ -1,2 +1,127 @@ - \ No newline at end of file + + บริการทั้งหมดของ Google + อนุญาตให้แอปเข้าถึงบริการ Google ทั้งหมดที่เชื่อมโยงผ่านทางบัญชีของ Google + บริการแอนดรอยด์ + อนุญาตให้แอปเข้าถึง AdSense ผ่านบัญชี Google ที่เกี่ยวข้อง + Google Ads + Google App Engine + อนุญาตให้แอปเข้าถึง Google App Engine ผ่านทางบัญชี Google ที่เชื่อมโยง + AdSense + Blogger + Google Calendar + อนุญาตให้แอปเข้าถึง Google Calendar ผ่านทางบัญชี Google ที่เชื่อมโยง + รายชื่อ + Dodgeball + อนุญาตให้แอปเข้าถึง Dodgeball ผ่านทางบัญชี Google ที่เชื่อมโยง + อนุญาตให้แอปเข้าถึงบริการ Android ผ่านทางบัญชี Google ที่เชื่อมโยง + อนุญาตให้แอปเข้าถึง Google Ads ผ่านทางบัญชี Google ที่เชื่อมโยง + อนุญาตให้แอปเข้าถึง Blogger ผ่านทางบัญชี Google ที่เชื่อมโยง + อนุญาตให้แอปเข้าถึงรายชื่อผ่านบัญชี Google ที่เชื่อมโยง + อนุญาตให้แอปเข้าถึง Google Finance ผ่านบัญชี Google ที่เชื่อมโยง + อนุญาตให้แอปเข้าถึง Google Base ผ่านทางบัญชี Google ที่เชื่อมโยง + Google Voice + อนุญาตให้แอปเข้าถึง Google Voice ผ่านทางบัญชี Google ที่เชื่อมโยง + Google Finance + Google Base + Google Health + อนุญาตให้แอปเข้าถึง Google Health ผ่านทางบัญชี Google ที่เชื่อมโยง + iGoogle + Google News + อนุญาตให้แอปเข้าถึง Google News ผ่านทางบัญชี Google ที่เชื่อมโยง + Orkut + อนุญาตให้แอปเข้าถึง Orkut ผ่านทางบัญชี Google ที่เชื่อมโยง + Google Book Search + บัญชี Google Checkout + อนุญาตให้แอปเข้าถึงบัญชี Google Checkout ผ่านทางบัญชี Google ที่เชื่อมโยง + อนุญาตให้แอปเข้าถึงบัญชี Google Checkout QA ผ่านทางบัญชี Google ที่เชื่อมโยง + บัญชี Google Checkout Sandbox + เครื่องมือสำหรับผู้ดูแลเว็บของ Google + อนุญาตให้แอปเข้าถึง Google Webmaster Tools ผ่านทางบัญชี Google ที่เชื่อมโยง + การค้นหาด้วยเสียง + การจดจำเสียงพูดส่วนบุคคล + Google Talk + อนุญาตให้แอปเข้าถึง Google Talk ผ่านทางบัญชี Google ที่เชื่อมโยง + Google Wi-Fi + อนุญาตให้แอปเข้าถึง Google Wi-Fi ผ่านบัญชี Google ที่เชื่อมโยง + อนุญาตให้แอปเข้าถึง Google Spreadsheets ผ่านทางบัญชี Google ที่เชื่อมโยง + Google Docs + YouTube + อนุญาตให้แอปเข้าถึง YouTube ผ่านทางบัญชี Google ที่เชื่อมโยง + ชื่อผู้ใช้ YouTube + Google Groups + อนุญาตให้แอปเข้าถึง Google Groups ผ่านทางบัญชี Google ที่เกี่ยวข้อง + อนุญาตให้แอปเข้าถึง Google Mail ผ่านทางบัญชี Google ที่เชื่อมโยง + Google Notebook + อนุญาตให้แอปเข้าถึง Google Notebook ผ่านทางบัญชี Google ที่เชื่อมโยง + อนุญาตให้แอปเข้าถึง Google Book Search ผ่านทางบัญชี Google ที่เชื่อมโยง + บัญชี QA ของ Google Checkout + อนุญาตให้แอปเข้าถึงบัญชี Google Checkout Sandbox ผ่านทางบัญชี Google ที่เชื่อมโยง + อนุญาตให้แอปเข้าถึงการค้นหาด้วยเสียงผ่านบัญชี Google ที่เชื่อมโยง + อนุญาตให้แอปเข้าถึงการจดจำคำพูดส่วนบุคคลผ่านทางบัญชี Google ที่เชื่อมโยง + Google Spreadsheets + อนุญาตให้แอปเข้าถึง Google Docs ผ่านทางบัญชี Google ที่เชื่อมโยง + อนุญาตให้แอปเข้าถึงชื่อผู้ใช้ YouTube ที่ใช้กับบัญชี Google ที่เชื่อมโยง + อนุญาตให้แอปเข้าถึง iGoogle ผ่านทางบัญชี Google ที่เชื่อมโยง + ฮอตสปอต + อนุญาตให้แอปเข้าถึง HotSpot ผ่านทางบัญชี Google ที่เชื่อมโยง + Knol + อนุญาตให้แอปเข้าถึง สารานุกรมออนไลน์ ผ่านบัญชี Google ที่เชื่อมโยง + อัลบั้มเว็บ Picasa + อนุญาตให้แอปเข้าถึง Picasa Web Albums ผ่านทางบัญชี Google ที่เชื่อมโยง + แผนที่ Google + อนุญาตให้แอปเข้าถึง Google Maps ผ่านบัญชี Google ที่เชื่อมโยง + Google Mail + ดูประวัติกิจกรรมของ Google Apps ของคุณ + จัดการการกำหนดค่าบัญชีผู้ซื้อ Ad Exchange ของคุณ + ดูข้อมูล Ad Exchange ของคุณ + ดูข้อมูล Ad Exchange ของคุณ + นอกเหนือจากขอบเขตการอ่าน/เขียน OAuth โดยรวมแล้ว ให้ใช้ขอบเขต OAuth แบบอ่านอย่างเดียวเมื่อดึงข้อมูลของลูกค้า + จัดการบัญชีบล็อกเกอร์ของคุณ + ดูข้อมูลเมตาสำหรับไฟล์และเอกสารใน Google Drive ของคุณ + ดูและจัดการงาน Google Maps Coordinate ของคุณ + ดูและจัดการข้อมูลการพิมพ์บนคลาวด์ของ Google ของคุณ + ดูและจัดการไฟล์ Google Drive ที่คุณเปิดหรือสร้างด้วยแอปนี้ + ดูข้อมูลของคุณใน Google Cloud Storage + ดูและจัดการไฟล์และเอกสารใน Google Drive ของคุณ + ดูที่อยู่อีเมลของคุณ + Google Maps Tracks API, ขอบเขตนี้อนุญาตให้เข้าถึงแบบอ่านและเขียนข้อมูลของโครงการของคุณได้ + ดูข้อมูลพื้นฐานเกี่ยวกับบัญชีของคุณ + การจัดการของคุณ วิดีโอ YouTube + ดูและจัดการข้อมูลโฮสต์ AdSense และบัญชีที่เกี่ยวข้องของคุณ + ดูข้อมูล AdSense ของคุณ + ดูและจัดการข้อมูล AdSense ของคุณ + ขอบเขตการดูแลระบบแอปเอนจิ้น + การเข้าถึงแบบอ่านและเขียนไปยัง Groups Migration API + ดูและจัดการการตั้งค่าของกลุ่ม Google Apps + การเข้าถึงแบบอ่าน/เขียนสำหรับ License Manager API + สำหรับผู้ดูแลระบบตัวแทนจำหน่ายและผู้ใช้มีสิทธิ์การอ่าน/เขียนเมื่อทำการทดสอบในแซนด์บ็อกซ์ของ API หรือมีสิทธิ์การอ่าน/เขียนเมื่อเรียกการดำเนินการ API โดยตรง + การเข้าถึง API การตรวจสอบผู้ดูแลระบบแบบอ่านอย่างเดียว + ขอบเขตการใช้งานบริการ App State + ดูข้อมูลของคุณใน Google BigQuery + ดูและจัดการข้อมูลของคุณใน Google BigQuery + จัดการบัญชีบล็อกเกอร์ของคุณ + จัดการหนังสือของคุณ + จัดการปฏิทินของคุณ + ดูปฏิทินของคุณ + ดูทรัพยากร Google Compute Engine ของคุณ + ดูและจัดการทรัพยากร Google Compute Engine ของคุณ + ดูงาน Google Coordinate ของคุณ + จัดการข้อมูลและสิทธิ์ของคุณใน Google Cloud Storage + จัดการข้อมูลของคุณใน Google Cloud Storage + ดูและจัดการรายงาน DoubleClick สำหรับผู้โฆษณา + อนุญาตให้เข้าถึงโฟลเดอร์ข้อมูลแอปพลิเคชัน + ดูแอป Google Drive ของคุณ + ขอบเขตพิเศษที่ใช้เพื่อให้ผู้ใช้สามารถอนุมัติการติดตั้งแอปได้ + ดูไฟล์และเอกสารใน Google Drive ของคุณ + ปรับเปลี่ยนพฤติกรรมสคริปต์ Google Apps ของคุณ + ดูบัญชี Freebase ของคุณ + จัดการ URL สั้นของ goo.gl ของคุณ + ดูงานของคุณ + จัดการงานของคุณ + ใช้ภารกิจจากคิวภารกิจของคุณ + จัดการงานของคุณ + การจัดการของคุณ บัญชี YouTube + ดูและจัดการสินทรัพย์ของคุณและเนื้อหาที่เกี่ยวข้องกับ YouTube + ดูบัญชี YouTube ของคุณ + \ No newline at end of file diff --git a/play-services-core/src/main/res/values-th/plurals.xml b/play-services-core/src/main/res/values-th/plurals.xml index a6b3daec93..503e96b330 100644 --- a/play-services-core/src/main/res/values-th/plurals.xml +++ b/play-services-core/src/main/res/values-th/plurals.xml @@ -1,2 +1,15 @@ - \ No newline at end of file + + + %1$d กำหนดค่า แบ็คเอนด์แล้ว + + + %1$d ลงทะเบียนแอปแล้ว + + + การอนุญาตที่จำเป็นในการทำงานอย่างถูกต้องของบริการ microG ไม่ผ่าน + + + การร้องขอการอนุญาตผิดพลาด + + \ No newline at end of file diff --git a/play-services-core/src/main/res/values-th/strings.xml b/play-services-core/src/main/res/values-th/strings.xml index a6b3daec93..7ad59fe224 100644 --- a/play-services-core/src/main/res/values-th/strings.xml +++ b/play-services-core/src/main/res/values-th/strings.xml @@ -1,2 +1,96 @@ - \ No newline at end of file + + การตั้งค่า microG + รอสักครู่… + Google + %1$s ต้องการใช้: + ผู้จัดการบัญชี Google + เสียใจ… + ลงชื่อเข้าใช้ + มีปัญหาในการสื่อสารกับเซิร์ฟเวอร์ของ Google\n\nลองอีกครั้งในภายหลัง + อุปกรณ์ของคุณกำลังติดต่อ Google เพื่อบันทึกข้อมูลลงในบัญชีของคุณ\n\nอาจใช้เวลาสองสามนาที + อนุญาต + บริการของ microG + ตั้งค่าบริการ microG + การดำเนินการต่อถือว่าคุณยินยอมให้แอปนี้และ Google ใช้ข้อมูลของคุณตามข้อกำหนดในการให้บริการและนโยบายความเป็นส่วนตัวของแต่ละแอป + %1$s ต้องการ: + แอปบนอุปกรณ์ของคุณกำลังพยายามลงชื่อเข้าใช้บัญชี Google หากตั้งใจ ให้ใช้ปุ่ม ลงชื่อเข้าใช้ เพื่อเชื่อมต่อกับหน้าลงชื่อเข้าใช้ของ Google หากไม่ได้ตั้งใจ ให้กด ยกเลิก เพื่อกลับไปยังแอปพลิเคชันที่ทำให้กล่องโต้ตอบนี้ปรากฏขึ้น + อุปกรณ์ของคุณกำลังสร้างการเชื่อมต่อกับเซิร์ฟเวอร์ของ Google เพื่อลงชื่อเข้าใช้ให้คุณ\n\nอาจใช้เวลาสักครู่ + คุณไม่ได้เชื่อมต่อกับเครือข่าย\n\nนี่อาจเป็นปัญหาชั่วคราวหรืออุปกรณ์ Android ของคุณอาจไม่มีการให้บริการข้อมูล ลองอีกครั้งเมื่อเชื่อมต่อกับเครือข่ายมือถือหรือเชื่อมต่อกับเครือข่าย Wi-Fi + microG Limited Services + ปฏิเสธ + จำเป็นต้องมีการพิสูจน์ตัวตน + %1$s ต้องได้รับอนุญาตจากคุณจึงจะเข้าถึงบัญชี Google ของคุณได้ + %1$s ต้องการเข้าถึงบัญชีของคุณราวกับว่าเป็น %2$s โดย %3$s การกระทำดังกล่าวอาจทำให้บัญชีดังกล่าวได้รับสิทธิ์พิเศษในการเข้าถึงบัญชีของคุณ + เลือกบัญชี + เพื่อดำเนินการต่อ %1$s + เพิ่มบัญชีอื่น + อนุญาตให้คุณลงชื่อเข้าใช้ %1$s + อนุญาตและแบ่งปัน + ให้บริการไมโครจี + อนุญาตให้แอปกำหนดค่าบริการ microG โดยไม่ต้องมีการโต้ตอบจากผู้ใช้ + ความเร็วรถ + เข้าถึงความเร็วรถของคุณ + ข้อมูลรถยนต์ + เข้าถึงข้อมูลรถของคุณ + ระดับน้ำมันเชื้อเพลิงรถยนต์ + ระยะทางการใช้รถ + เข้าถึงข้อมูลไมล์รถของคุณ + ช่องทางจำหน่ายรถยนต์ + การลงทะเบียนอุปกรณ์ Google + การส่งข้อความบนคลาวด์ + บริการ Play Store + Google Play Games + %1$s ต้องการใช้ Play Games + หากต้องการใช้ Play Games จำเป็นต้องติดตั้งแอป Google Play Games แอปพลิเคชันอาจทำงานต่อไปโดยไม่มี Play Games แต่ก็เป็นไปได้ที่แอปพลิเคชันจะทำงานผิดปกติ + เลือกสถานที่ + ตัวเลือกสถานที่ยังไม่พร้อมใช้งาน + สถานที่ใกล้เคียง + (%1$.7f, %2$.7f) + เครือข่ายมือถือ + โรมมิ่ง + เครือข่ายอื่นๆ + รองรับการปลอมแปลงลายเซ็นดิจิตอล + แพ็คเกจที่ติดตั้ง + ระบบ + ระบบมีรองรับการปลอมลายเซ็นดิจิตอล: + อนุญาตให้ %1$s เข้าถึง %2$s อย่างมีสิทธิพิเศษหรือไม่? + ในการดำเนินการต่อ microG จะแบ่งปันชื่อ ที่อยู่อีเมล และรูปโปรไฟล์ของบัญชี Google ของคุณกับ %1$s + เข้าถึงข้อมูลระดับน้ำมันเชื้อเพลิงของรถของคุณ + เข้าถึงช่องทางผู้จำหน่ายรถของคุณเพื่อแลกเปลี่ยนข้อมูลเฉพาะรถ + Google SafetyNet + เลือกตำแหน่งนี้ + บริการ microG: ขาดการอนุญาตให้ %1$s + Wi-Fi + ที่ติดตั้งไว้ %1$s เข้ากันไม่ได้ หรือการปลอมลายเซ็นไม่ได้เปิดใช้งาน โปรดตรวจสอบเอกสารเกี่ยวกับแอปพลิเคชันและ ROM ที่เข้ากันได้ + แตะที่นี่เพื่อปิดใช้งานการเพิ่มประสิทธิภาพแบตเตอรี่ หากไม่ทำเช่นนี้ อาจทำให้แอปพลิเคชันทำงานผิดปกติ + ติดตั้งแอปพลิเคชัน %1$s หรือแอปพลิเคชันที่เข้ากันได้ โปรดตรวจสอบเอกสารประกอบว่าแอปพลิเคชันใดที่เข้ากันได้ + ก่อนใช้แอปนี้ โปรดอ่าน %1$s และ %2$s + นโยบายความเป็นส่วนตัว + ข้อกําหนดในการให้บริการ + ฟังสถานะภายในของการออกอากาศ + อ่านการกําหนดค่า Google + ดักข้อความ C2DM + ส่งข้อความ C2DM ไปยังแอปอื่น ๆ + การแลกเปลี่ยนข้อความและรับการแจ้งเตือนจากเซิร์ฟเวอร์ Google + เข้าถึงบริการ Google + ROM ของคุณไม่มีการสนับสนุนดั้งเดิมสำหรับการปลอมลายเซ็น คุณยังสามารถใช้ Xposed หรือระบบอื่นเพื่อปลอมลายเซ็นได้ โปรดตรวจสอบเอกสารประกอบเกี่ยวกับ ROM ที่รองรับการปลอมลายเซ็นและวิธีใช้ microG บน ROM ที่ไม่รองรับ + ระบบให้สิทธิ์ในการปลอมลายเซ็น: + นี่เป็นตัวบ่งชี้ที่ชัดเจนว่า ROM รองรับการปลอมลายเซ็น แต่ต้องมีการดำเนินการเพิ่มเติมเพื่อเปิดใช้งาน โปรดตรวจสอบเอกสารเกี่ยวกับขั้นตอนที่อาจจำเป็น + ระบบปลอมลายเซ็น: + โปรดตรวจสอบเอกสารเกี่ยวกับขั้นตอนที่อาจจำเป็น + Play Services (GmsCore) + Play Store (Phonesky) + Services Framework (GSF) + %1$s ติดตั้งแล้ว: + %1$s มีลายเซ็นที่ถูกต้อง: + ละเว้นการเพิ่มประสิทธิภาพแบตเตอรี่: + เกี่ยวกับ + ส่วนประกอบ + การกําหนดค่า + Google Services + การบริการระบุตำแหน่ง + บริการ + ทดสอบ + เปิดใช้งานการเพิ่มประสิทธิภาพแบตเตอรี่แล้ว + \ No newline at end of file diff --git a/play-services-droidguard/core/src/main/res/values-th/strings.xml b/play-services-droidguard/core/src/main/res/values-th/strings.xml index a6b3daec93..b5edb200bd 100644 --- a/play-services-droidguard/core/src/main/res/values-th/strings.xml +++ b/play-services-droidguard/core/src/main/res/values-th/strings.xml @@ -1,2 +1,8 @@ - \ No newline at end of file + + ฝังตัว + ระยะไกล + เชื่อมต่อกับรันไทม์ DroidGuard ผ่านเครือข่าย + โหมดการทำงานของ DroidGuard + ใช้รันไทม์ DroidGuard ในเครื่อง + \ No newline at end of file diff --git a/play-services-fido/core/src/main/res/values-th/strings.xml b/play-services-fido/core/src/main/res/values-th/strings.xml index a6b3daec93..c1463b08e9 100644 --- a/play-services-fido/core/src/main/res/values-th/strings.xml +++ b/play-services-fido/core/src/main/res/values-th/strings.xml @@ -1,2 +1,27 @@ - \ No newline at end of file + + การใช้คีย์ความปลอดภัยของคุณกับ %1$s จะช่วยปกป้องข้อมูลส่วนตัวของคุณ + เริ่มต้นใช้งาน + เลือกวิธีใช้คีย์ความปลอดภัยของคุณ + คีย์ความปลอดภัยทำงานร่วมกับ Bluetooth, NFC และ USB เลือกวิธีที่คุณต้องการใช้คีย์ของคุณ + ยืนยันตัวตนของคุณ + %1$s จำเป็นต้องยืนยันว่าเป็นคุณ + เชื่อมต่อคีย์ความปลอดภัย NFC ของคุณ + ใช้คีย์ความปลอดภัยร่วมกับ Bluetooth + ใช้คีย์ความปลอดภัยกับ NFC + ใช้คีย์ความปลอดภัยกับ USB + โปรดเชื่อมต่อคีย์ความปลอดภัย USB ของคุณ + กรุณาแตะที่วงกลมสีทองหรือที่วงกลม %1$s. + กรุณากรอกรหัส PIN สำหรับผู้ตรวจสอบสิทธิ์ของคุณ + ตกลง + ป้อน PIN ผิด! + ใช้คีย์ความปลอดภัยของคุณกับ %1$s + %1$s ทำหน้าที่เป็นเบราว์เซอร์ที่เชื่อถือได้เพื่อใช้รหัสความปลอดภัยของคุณกับ %2$s + ใช่ %1$s เป็นเบราว์เซอร์ที่เชื่อถือได้ของฉัน และควรได้รับอนุญาตให้ใช้คีย์ความปลอดภัยกับเว็บไซต์บุคคลที่สาม + เชื่อมต่อคีย์ความปลอดภัย USB ของคุณ + เชื่อมต่อกุญแจความปลอดภัยของคุณกับพอร์ต USB หรือเชื่อมต่อด้วยสาย USB หากกุญแจของคุณมีปุ่มหรือวงกลมสีทอง ให้แตะทันที + ถือกุญแจของคุณให้แนบกับด้านหลังของอุปกรณ์จนกว่าจะหยุดสั่น + ใช้เครื่องนี้ด้วยการล็อคหน้าจอ + 4 ถึง 63 ตัวอักษร + ยกเลิก + \ No newline at end of file diff --git a/play-services-location/core/src/main/res/values-th/strings.xml b/play-services-location/core/src/main/res/values-th/strings.xml index a6b3daec93..f89dea0ced 100644 --- a/play-services-location/core/src/main/res/values-th/strings.xml +++ b/play-services-location/core/src/main/res/values-th/strings.xml @@ -1,2 +1,53 @@ - \ No newline at end of file + + ตำแหน่งเครือข่ายมือถือ + ดึงข้อมูลตำแหน่งที่ใช้ Wi-Fi จาก บริการค้นหาตำแหน่ง ของ Mozilla + การร้องขอจากบริการออนไลน์ + คำขอจาก Hotspot + จดจำจาก GPS + จัดเก็บตำแหน่ง Wi-Fi ไว้ในเครื่องเมื่อใช้งาน GPS + คำขอจาก Mozilla + ดึงข้อมูลตำแหน่งเสาสัญญาณเครือข่ายมือถือจากบริการค้นหาตำแหน่งของ Mozilla Location Service + ดึงตำแหน่งจากเสาของเครือข่ายมือถือจากบริการระบุตำแหน่งออนไลน์ + แก้ไขที่อยู่โดยใช้ พิกัดภูมิศาสตร์ ของ OpenStreetMap + ใช้งานพิกัดทางภูมิศาสตร์ + เข้าถึงล่าสุด: %1$s + ตำแหน่งคร่าวๆ + ส่งตำแหน่งคร่าวๆ ให้กับแอปนี้เสมอ โดยไม่สนใจระดับของการอนุญาต + ใช้ GPS, Wi-Fi, เครือข่ายมือถือ และเซ็นเซอร์ + ให้สิทธิ์เข้าถึงตำแหน่งแก่บริการ microG + สำหรับรายละเอียด ให้ไปที่การตั้งค่าตำแหน่ง + ไม่ล่ะ ขอบคุณ + ตกลง + กำหนดค่า URL + ล้างค่า + ที่ตั้ง + การเข้าถึงล่าสุด + ตำแหน่งของ Wi-Fi + ตัวแก้ไขที่อยู่ + การร้องขอจาก Mozilla + ดึงข้อมูลตำแหน่งที่ใช้ Wi-Fi จากบริการระบุตำแหน่งออนไลน์ + ดึงข้อมูลตำแหน่ง Wi-Fi โดยตรงจาก Hotspot ที่รองรับเมื่อเชื่อมต่อ + คำขอจากบริการออนไลน์ + หากต้องการดำเนินการต่อ ให้เปิดตำแหน่งอุปกรณ์ซึ่งใช้บริการตำแหน่งของ microG + อุปกรณ์ของคุณจะต้อง: + จัดเก็บตำแหน่งเครือข่ายมือถือในเครื่องเมื่อใช้ GPS + จดจำจาก GPS + แอพที่เข้าถึงตำแหน่งได้ + เพื่อประสบการณ์ที่ดีขึ้น ให้เปิดตำแหน่งอุปกรณ์ซึ่งใช้บริการตำแหน่งของ microG + ใช้บริการระบุตำแหน่ง microG ในฐานะส่วนหนึ่งของบริการนี้ microG อาจรวบรวมข้อมูลตำแหน่งเป็นระยะ ๆ และใช้ข้อมูลนี้ในลักษณะที่ไม่ระบุชื่อเพื่อปรับปรุงความแม่นยำของตำแหน่งและบริการตามตำแหน่ง + ช่วยให้สามารถตั้งค่า URL ที่กำหนดเองได้ URL ค่าที่ไม่ถูกต้องอาจส่งผลให้บริการระบุตำแหน่งไม่ตอบสนองหรือไม่พร้อมใช้งาน + เส้นทาง /v1/geolocate จะถูกผนวกโดยอัตโนมัติ หากผู้ให้บริการตำแหน่งต้องการคีย์ ก็สามารถผนวกเป็นพารามิเตอร์การค้นหาในจุดแรกสุดของ URL ได้ + URL บริการที่กำหนดเอง + กำหนดเอง + เลือกบริการระบุตำแหน่งออนไลน์ + เงื่อนไข/ความเป็นส่วนตัว + ข้อเสนอแนะ + นำเข้าหรือส่งออกข้อมูลตำแหน่งที่ตั้ง + จำเป็นต้องมีการกำหนดค่า + หากต้องการใช้งานบริการระบุตำแหน่งออนไลน์ต่อไป คุณจะต้องเลือกบริการข้อมูลระบุตำแหน่ง + ส่งออกฐานข้อมูลตำแหน่ง Wi-Fi ในพื้นที่ + ส่งออกฐานข้อมูลตำแหน่งเสาโทรศัพท์มือถือในพื้นที่ + นำเข้าข้อมูลตำแหน่งจากไฟล์ + นำเข้า %1$d บันทึก + \ No newline at end of file diff --git a/play-services-nearby/core/src/main/res/values-th/strings.xml b/play-services-nearby/core/src/main/res/values-th/strings.xml index a6b3daec93..c8b17abff0 100644 --- a/play-services-nearby/core/src/main/res/values-th/strings.xml +++ b/play-services-nearby/core/src/main/res/values-th/strings.xml @@ -1,2 +1,62 @@ - \ No newline at end of file + + การแจ้งเตือนการสัมผัสไม่ได้ใช้งาน + จำเป็นต้องเปิดใช้งาน Bluetooth เพื่อรับการแจ้งเตือนการสัมผัส + จำเป็นต้องเข้าถึงตำแหน่งเพื่อรับการแจ้งเตือนการสัมผัส + จำเป็นต้องเปิดใช้งานบลูทูธและการเข้าถึงตำแหน่งเพื่อรับการแจ้งเตือนการสัมผัส + การแจ้งเตือนการสัมผัส + เพื่อเปิดการใช้งานการแจ้งเตือนการสัมผัส ให้เปิดแอปใด ๆ ที่รองรับ + เปิดใช้งาน Bluetooth + ขออภัย อุปกรณ์ของคุณเข้ากันได้กับการแจ้งเตือนความเสี่ยงเพียงบางส่วนเท่านั้น คุณสามารถรับการแจ้งเตือนเกี่ยวกับผู้ติดต่อที่มีความเสี่ยงได้ แต่จะไม่สามารถแจ้งเตือนผู้อื่นได้ + ขออภัย อุปกรณ์ของคุณไม่รองรับการแจ้งเตือนการสัมผัส + แอปกำลังใช้การแจ้งเตือนการสัมผัส + เก็บรวบรวมรหัสประจำตัว + รหัสที่กำลังกระจายสัญญาณในปัจจุบัน + การรายงานการสัมผัส + อัปเดต: %1$s + น้อยกว่า 5 นาที + เกี่ยวกับ %1$d นาที + การสัมผัสในบริเวณใกล้เคียง + การสัมผัสระยะไกล + %1$s, %2$s + ประมวลผล %1$dกุญแจสำหรับการวินิจฉัยแล้ว + รายงาน %1$dการเผชิญเหตุ: + การใช้งาน API ในช่วง 14 วันที่ผ่านมา + %1$d เรียกไปยัง %2$s + %1$s รหัสประจำตัวต่อชั่วโมง + ลบรหัสประจำตัวทั้งหมด + ลบต่อไป + ส่งออก + ส่งออก รหัสประจำตัว ที่เก็บรวบรวมไว้เพื่อการวิเคราะห์เพิ่มเติมด้วยแอปอื่น + ใช้งานการแจ้งเตือนการสัมผัส + เปิดใช้งานการแจ้งเตือนการสัมผัส ? + เปิด + ปิดการแจ้งเตือนการสัมผัส? + หลังจากปิดใช้งานการแจ้งเตือนการสัมผัสแล้ว คุณจะไม่ได้รับการแจ้งเตือนอีกต่อไปเมื่อคุณสัมผัสกับบุคคลที่รายงานว่าได้รับการวินิจฉัยว่าติดเชื้อ + ปิด + แบ่งปัน รหัสประจำตัว ของคุณกับ %1$s ? + ข้อมูลประจำตัวของคุณในช่วง 14 วันที่ผ่านมาจะถูกใช้เพื่อแจ้งให้ผู้อื่นทราบว่าคุณอยู่ใกล้ๆ และมีความเสี่ยงต่อการติดเชื้อ\n\nข้อมูลประจำตัวหรือผลการทดสอบของคุณจะไม่ถูกเปิดเผยให้ผู้อื่นทราบ + แบ่งปัน + อนุญาต + จำเป็นต้องเปิดใช้งาน Bluetooth + เปิดใช้งาน + ต้องมีการอนุญาตใหม่ + แตะเพื่อให้สิทธิ์ที่จำเป็นในการแจ้งเตือนการสัมผัส + เปิดการตั้งค่าตำแหน่ง + ไม่มีการรายงานการเผชิญเหตุการสัมผัส + การแจ้งเตือนการสัมผัสต้องได้รับอนุญาตเพิ่มเติมจึงจะทำงานได้ + %1$d รหัสประจำตัว ในชั่วโมงที่ผ่านมา + %1$s คะแนนความเสี่ยง %2$d + หมายเหตุ: คะแนนความเสี่ยงถูกกำหนดโดยแอป ตัวเลขสูงอาจหมายถึงความเสี่ยงต่ำหรือในทางกลับกัน + %1$d รวบรวม รหัสประจำตัวแล้ว + ลบ + ไม่มีข้อมูลบันทึกไว้ + การลบ รหัสประจำตัว ที่เก็บรวบรวมไว้จะทำให้ไม่สามารถแจ้งให้คุณทราบได้ในกรณีที่ผู้ติดต่อของคุณในช่วง 14 วันล่าสุดได้รับการวินิจฉัย + API การแจ้งเตือนการสัมผัสช่วยให้แอปสามารถแจ้งให้คุณทราบหากคุณสัมผัสกับบุคคลที่รายงานว่าได้รับการวินิจฉัยว่าติดเชื้อ\n\nวันที่ ระยะเวลา และความแรงของสัญญาณที่เกี่ยวข้องกับการสัมผัสจะถูกแชร์กับแอปที่เกี่ยวข้อง + ขณะที่เปิดใช้งาน API “การแจ้งเตือนการสัมผัส” อุปกรณ์ของคุณจะทำการรวบรวม รหัสประจำตัว (เรียกว่า Rolling Proximity Identifiers หรือ RPI) จากอุปกรณ์ใกล้เคียงโดยอัตโนมัติ\n\nเมื่อเจ้าของอุปกรณ์รายงานว่าได้รับการวินิจฉัยว่าเป็นบวก รหัสประจำตัว ของพวกเขาจะถูกแชร์ได้ อุปกรณ์ของคุณจะทำการตรวจสอบว่ามี รหัสประจำตัว ที่ได้รับการวินิจฉัยที่ทราบแล้วตรงกับ รหัสประจำตัว ที่รวบรวมไว้หรือไม่ และคำนวณความเสี่ยงต่อการติดเชื้อของคุณ + โทรศัพท์ของคุณจำเป็นต้องใช้ Bluetooth เพื่อรวบรวมและแบ่งปัน รหัสประจำตัว กับโทรศัพท์เครื่องอื่นที่อยู่ใกล้เคียงอย่างปลอดภัย %1$s สามารถแจ้งเตือนคุณได้หากคุณสัมผัสกับบุคคลที่รายงานว่าได้รับการวินิจฉัยว่าติดเชื้อ วันที่ ระยะเวลา และความแรงของสัญญาณที่เกี่ยวข้องกับการสัมผัสจะถูกแชร์กับแอป + อัปเดตการตั้งค่า + %1$s ต้องได้รับการอนุญาตเพิ่มเติม + จำเป็นต้องมีการเข้าถึงตำแหน่ง + เกือบเสร็จแล้ว! คุณจะต้องเปิดใช้งานการเข้าถึงตำแหน่งพื้นหลังโดยเลือกตัวเลือก \"อนุญาตตลอดเวลา\" ในหน้าจอถัดไป จากนั้นกดย้อนกลับ + \ No newline at end of file diff --git a/play-services-oss-licenses/src/main/res/values-th/strings.xml b/play-services-oss-licenses/src/main/res/values-th/strings.xml index a6b3daec93..1fd242f6ea 100644 --- a/play-services-oss-licenses/src/main/res/values-th/strings.xml +++ b/play-services-oss-licenses/src/main/res/values-th/strings.xml @@ -1,2 +1,9 @@ - \ No newline at end of file + + แอปนี้ไม่มีใบอนุญาตโอเพนซอร์สใดๆ + กำลังโหลดรายการใบอนุญาต + รายละเอียดใบอนุญาตสำหรับซอฟต์แวร์โอเพ่นซอร์ส + เกิดข้อผิดพลาดขณะดึงใบอนุญาต + ใบอนุญาตโอเพ่นซอร์ส + กำลังโหลดข้อมูลใบอนุญาต + \ No newline at end of file diff --git a/vending-app/src/main/res/values-th/strings.xml b/vending-app/src/main/res/values-th/strings.xml index a6b3daec93..18a586f6b0 100644 --- a/vending-app/src/main/res/values-th/strings.xml +++ b/vending-app/src/main/res/values-th/strings.xml @@ -1,2 +1,22 @@ - \ No newline at end of file + + %1$s ไม่สามารถตรวจสอบใบอนุญาตได้ + หากแอปทำงานผิดปกติ ให้ลงชื่อเข้าใช้บัญชี Google ที่คุณใช้ซื้อแอป + เข้าสู่ระบบ + microG Companion + microG Companion ไม่สามารถใช้งานแบบสแตนด์อโลนได้ ให้เปิดการตั้งค่าบริการจาก microG แทน + microG Companion ไม่สามารถใช้งานแบบสแตนด์อโลนได้ โปรดติดตั้งบริการของ microG เพื่อเข้าใช้งาน + การแจ้งเตือนใบอนุญาต + แจ้งเตือนเมื่อแอปพยายามตรวจสอบใบอนุญาต แต่คุณไม่ได้ลงชื่อเข้าใช้บัญชี Google ใดๆ + เพิกเฉย + ชำระเงินไม่ได้ ณ ขณะนี้ + ยืนยันการชำระเงิน + ไม่ได้เชื่อมต่ออินเทอร์เน็ต โปรดตรวจสอบให้แน่ใจว่าเปิด Wi-Fi หรือเครือข่ายมือถือแล้วลองอีกครั้ง + รหัสผ่านที่คุณป้อนไม่ถูกต้อง + ข้อผิดพลาดที่ไม่รู้จัก กรุณาออกแล้วลองอีกครั้ง + กรอกรหัสผ่านของคุณ + จดจำการเข้าสู่ระบบของฉันบนอุปกรณ์นี้ + ลืมรหัสผ่าน? + เรียนรู้เพิ่มเติม + ตรวจสอบ + \ No newline at end of file From 0f56749df3b01be64c086aba5e6a5b4e1d4f291f Mon Sep 17 00:00:00 2001 From: gallegonovato Date: Wed, 9 Oct 2024 08:25:07 +0000 Subject: [PATCH 042/132] Translated using Weblate (Spanish) Currently translated at 100.0% (232 of 232 strings) Translation: microG/play-services-core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-core-strings/es/ Translated using Weblate (Spanish) Currently translated at 100.0% (249 of 249 strings) Translation: microG/play-services-core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-core-strings/es/ Translated using Weblate (Spanish) Currently translated at 100.0% (4 of 4 strings) Translation: microG/play-services-ads-identifier: core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-ads-identifier-core-strings/es/ --- .../core/src/main/res/values-es/strings.xml | 2 +- .../src/main/res/values-es/strings.xml | 32 +++++++++++++------ 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/play-services-ads-identifier/core/src/main/res/values-es/strings.xml b/play-services-ads-identifier/core/src/main/res/values-es/strings.xml index 90c47f7266..8f45e6af02 100644 --- a/play-services-ads-identifier/core/src/main/res/values-es/strings.xml +++ b/play-services-ads-identifier/core/src/main/res/values-es/strings.xml @@ -3,5 +3,5 @@ Permiso de identificación publicitaria Permite que una aplicación de editor acceda directa o indirectamente a un ID de publicidad válido. Notificación del ID de publicidad - Permite que una aplicación reciba una notificación cuando se actualiza el ID de publicidad o la preferencia de limitar el seguimiento de anuncios del usuario. + Permite que una aplicación reciba una notificación cuando se actualiza la preferencia de ID de publicidad o de seguimiento de anuncios del usuario. \ No newline at end of file diff --git a/play-services-core/src/main/res/values-es/strings.xml b/play-services-core/src/main/res/values-es/strings.xml index 25d54eb6e6..a6bfa5ec37 100644 --- a/play-services-core/src/main/res/values-es/strings.xml +++ b/play-services-core/src/main/res/values-es/strings.xml @@ -24,11 +24,9 @@ %1$s quiere usar: Administrador de cuentas de Google Lo sentimos… - Una aplicación de tu dispositivo está intentando iniciar sesión en una cuenta de Google. \u0020Si esto fue intencional, usa el botón Iniciar sesión para conectarte a la página de inicio de sesión de Google, si no, presione Cancelar para volver a la aplicación que hizo que apareciera este cuadro de diálogo. + Una aplicación en tu dispositivo está intentando iniciar sesión en una cuenta de Google.\n\nSi esto fue intencionado, use el botón Iniciar sesión para conectarse a la página de inicio de sesión de Google; si no, presione Cancelar para volver a la aplicación que causó que este diálogo apareciera. Iniciar sesión - Tu dispositivo está estableciendo una conexión con los servidores de Google para iniciar sesión. -\n -\nEsto puede tardar unos segundos. + Tu dispositivo está estableciendo una conexión con los servidores de Google para iniciar sesión.\n\nEsto puede tardar unos segundos. "No tienes una conexión a Internet. Esto podría ser un problema temporal o tu dispositivo Android no esta configurado para los servicios de datos. Prueba otra vez cuando estés conectado a una red móvil o Wi-Fi." @@ -114,8 +112,7 @@ Esto podría tardar algunos minutos." Añadir cuenta de Google Cloud Messaging es un proveedor de notificaciones push usado por muchas aplicaciones. Para usarlo debes habilitar el registro del dispositivo. Intervalo del Cloud Messaging heartbeat - El intervalo en segundos para que el sistema realice un heartbeat a los servidores de Google. Aumentar este número reducirá el consumo de batería, pero puede causar retrasos en los mensajes push. -\nObsoleto, será reemplazado en futuras versiones. + El intervalo en segundos para que el sistema realice un heartbeat a los servidores de Google. Aumentar este número reducirá el consumo de batería, pero puede causar retrasos en los mensajes push.\nObsoleto, será reemplazado en futuras versiones. Aplicaciones usando Cloud Messaging Lista de aplicaciones registradas actualmente para Cloud Messagging. Confirmar nuevas aplicaciones @@ -132,10 +129,8 @@ Esto podría tardar algunos minutos." Registrado Registrado desde: %1$s ¿Cancelar el registro %1$s? - Algunas aplicaciones no se vuelven a registrar automáticamente y/o no ofrecen la opción de hacerlo manualmente. Es posible que estas aplicaciones no funcionen correctamente después de anular el registro. -\n¿Desea continuar? - Has denegado el registro de una aplicación para recibir notificaciones push que ya está registrada. -\n¿Quieres anular el registro ahora para que no reciba mensajes push en el futuro? + Algunas aplicaciones no se vuelven a registrar automáticamente y/o no ofrecen la opción de hacerlo manualmente. Es posible que estas aplicaciones no funcionen correctamente después de anular el registro.\n¿Desea continuar? + Has denegado el registro de una aplicación para recibir notificaciones push que ya está registrada.\n¿Quieres anular el registro ahora para que no reciba mensajes push en el futuro? Mensajes: %1$d (%2$d bytes) Desconectado Conectado desde %1$s @@ -259,4 +254,21 @@ Esto podría tardar algunos minutos." Servicios limitados de microG Entiendo Está utilizando los Servicios Limitados de microG. A diferencia de los Servicios microG habituales, este sabor sólo funciona con aplicaciones que utilizan bibliotecas microG, no con las de Google Play. Esto significa que la mayoría de las aplicaciones ignorarán estos servicios. + Acción en la cuenta requerida + Su cuenta de Google necesita configuración adicional. + Habilitar Cloud Messaging + De acuerdo con sus preferencias, microG necesita permiso de usted antes de que pueda registrarse para Cloud Messaging. + Permitir Cloud Messaging para microG + Configurar bloqueo de pantalla seguro + Haga clic para realizar el paso + Paso completado + Alertas de la cuenta de Google + Notifica cuando una de sus cuentas de Google requiere una configuración adicional antes de poder usarse o cuando una cuenta es incompatible con microG. + Complete los siguientes pasos para poder usar su cuenta de Google %s en este dispositivo. + Finalizar la configuración de su cuenta de Google + Su dispositivo debe registrarse en Google al menos una vez.\n\nPuede desactivar el registro de dispositivos de Google después de que se complete la configuración de la cuenta. + Habilitar el registro del dispositivo + Puede desactivar Cloud Messaging después de que se complete la configuración de la cuenta. + Finalizar + Su cuenta de Google la administra su lugar de trabajo o institución educativa. Su administrador decidió que los dispositivos necesitan un bloqueo de pantalla seguro antes de poder acceder a los datos de la cuenta.\n\nConfigure una contraseña, PIN o patrón de bloqueo de pantalla. \ No newline at end of file From c9c0b4a65a6e28264774a2a9fdced006a3d1ba89 Mon Sep 17 00:00:00 2001 From: Anonymous Date: Mon, 28 Oct 2024 15:54:01 +0000 Subject: [PATCH 043/132] Translated using Weblate (German) Currently translated at 90.7% (226 of 249 strings) Translation: microG/play-services-core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-core-strings/de/ --- play-services-core/src/main/res/values-de/strings.xml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/play-services-core/src/main/res/values-de/strings.xml b/play-services-core/src/main/res/values-de/strings.xml index f31c4209fd..03e39e0d2a 100644 --- a/play-services-core/src/main/res/values-de/strings.xml +++ b/play-services-core/src/main/res/values-de/strings.xml @@ -24,9 +24,7 @@ %1$s versucht zu nutzen: Google Konto Manager Entschuldigung… - Eine App hat versucht, sich bei einem Google-Konto anzumelden. -\n -\nFalls dies beabsichtigt war, nutze die Schaltfläche Anmelden-, um Googles Anmeldeseite aufzurufen, andernfalls drücke Abbrechen, um zur App, die diesen Dialog verursacht hat, zurückzukehren. + Eine App hat versucht, sich bei einem Google-Konto anzumelden. \n \nFalls dies beabsichtigt war, nutze die Schaltfläche Anmelden-, um Googles Anmeldeseite aufzurufen, andernfalls drücke Abbrechen, um zur App, die diesen Dialog verursacht hat, zurückzukehren. Einloggen "Dein Gerät verbindet sich mit den Google-Servern, um dich einzuloggen From 428bc5e2f3394bd6355cb8aa9dd1070a61f439cd Mon Sep 17 00:00:00 2001 From: Anonymous Date: Mon, 28 Oct 2024 15:54:14 +0000 Subject: [PATCH 044/132] Translated using Weblate (Serbian) Currently translated at 92.7% (231 of 249 strings) Translation: microG/play-services-core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-core-strings/sr/ --- play-services-core/src/main/res/values-sr/strings.xml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/play-services-core/src/main/res/values-sr/strings.xml b/play-services-core/src/main/res/values-sr/strings.xml index 8fb81053ab..fa300199db 100644 --- a/play-services-core/src/main/res/values-sr/strings.xml +++ b/play-services-core/src/main/res/values-sr/strings.xml @@ -192,9 +192,7 @@ Копирано у привремену меморију! Апликације које користе push-обавештења Прилагођено: %s - Апликација на вашем уређају покушава да се пријави на Google налог. -\n -\nАко је ово било намерно, користите дугме Пријави се да бисте се повезали са Google страницом за пријављивање, ако није, притисните Откажи да бисте се вратили на апликацију која је изазвала да се овај дијалог прикаже. + Апликација на вашем уређају покушава да се пријави на Google налог. \n \nАко је ово било намерно, користите дугме Пријави се да бисте се повезали са Google страницом за пријављивање, ако није, притисните Откажи да бисте се вратили на апликацију која је изазвала да се овај дијалог прикаже. Тестирај ReCAPTCHA Да бисте наставили, microG ће делити име, имејл адресу и слику профила вашег Google налога са %1$s . Аутентификуј регистрацијом уређаја From 1c560926af850f06758a82e61fb4b4cedc8e34f4 Mon Sep 17 00:00:00 2001 From: Anonymous Date: Mon, 28 Oct 2024 15:53:59 +0000 Subject: [PATCH 045/132] Translated using Weblate (Asturian) Currently translated at 4.0% (10 of 249 strings) Translation: microG/play-services-core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-core-strings/ast/ --- play-services-core/src/main/res/values-ast/strings.xml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/play-services-core/src/main/res/values-ast/strings.xml b/play-services-core/src/main/res/values-ast/strings.xml index 0fd3e19543..b8f53480ef 100644 --- a/play-services-core/src/main/res/values-ast/strings.xml +++ b/play-services-core/src/main/res/values-ast/strings.xml @@ -9,9 +9,7 @@ Configuración de microG Xestor de cuentes de Google Servicios de microG - Hai una aplicación del preséu que tenta d\'aniciar la sesión nuna cuenta de Google -\n -\nSi esta solicitú foi intencionada, usa\'l botón Aniciar la sesión pa conectate a la páxina d\'aniciu de sesión de Google, si non primi Encaboxar pa volver a l\'aplicación que fexo qu\'esti diálogu apaeciere. + Hai una aplicación del preséu que tenta d\'aniciar la sesión nuna cuenta de Google \n \nSi esta solicitú foi intencionada, usa\'l botón Aniciar la sesión pa conectate a la páxina d\'aniciu de sesión de Google, si non primi Encaboxar pa volver a l\'aplicación que fexo qu\'esti diálogu apaeciere. Configura los servicios de microG. %1$s quier usar: \ No newline at end of file From b92da24be12cc66919816fda8ed66e54a761abdf Mon Sep 17 00:00:00 2001 From: Anonymous Date: Mon, 28 Oct 2024 15:54:11 +0000 Subject: [PATCH 046/132] Translated using Weblate (Dutch) Currently translated at 84.7% (211 of 249 strings) Translation: microG/play-services-core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-core-strings/nl/ --- play-services-core/src/main/res/values-nl/strings.xml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/play-services-core/src/main/res/values-nl/strings.xml b/play-services-core/src/main/res/values-nl/strings.xml index e1d8a1d61a..5ed68eb232 100644 --- a/play-services-core/src/main/res/values-nl/strings.xml +++ b/play-services-core/src/main/res/values-nl/strings.xml @@ -15,9 +15,7 @@ Als u doorgaat, geeft u deze app en Google toestemming uw gegevens te gebruiken in overeenstemming met hun respectieve servicevoorwaarden en privacybeleid. Google Sorry… - An app on your device is trying to sign in to a Google account. -\n -\nIf this was intentional, use the Sign in button to connect to Google’s sign-in page, if not, press Cancel to go back to the application that caused this dialog to show up. + An app on your device is trying to sign in to a Google account. \n \nIf this was intentional, use the Sign in button to connect to Google’s sign-in page, if not, press Cancel to go back to the application that caused this dialog to show up. Your device is establishing a connection to Google’s servers to sign you in. \n \nThis can take a few seconds. From 65fb8f196e6083b8b6bc0954d7109640d988f370 Mon Sep 17 00:00:00 2001 From: Anonymous Date: Mon, 28 Oct 2024 15:54:15 +0000 Subject: [PATCH 047/132] Translated using Weblate (Turkish) Currently translated at 91.5% (228 of 249 strings) Translation: microG/play-services-core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-core-strings/tr/ --- play-services-core/src/main/res/values-tr/strings.xml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/play-services-core/src/main/res/values-tr/strings.xml b/play-services-core/src/main/res/values-tr/strings.xml index 2bfc89122a..fb94c7eb04 100644 --- a/play-services-core/src/main/res/values-tr/strings.xml +++ b/play-services-core/src/main/res/values-tr/strings.xml @@ -25,9 +25,7 @@ %1$s şunu istiyor: %1$s şunu kullanmak istiyor: Google Hesap Yöneticisi - Cihazınızdaki bir uygulama, Google hesabına giriş yapmaya çalışıyor. -\n -\nEğer bu istediğiniz bir şey ise, Oturum aç tuşunu kullanarak Google\'ın oturum açma sayfasına bağlanın, eğer değilse, İptal tuşuna basarak bu menünün çıkmasına neden olan uygulamaya geri dönün. + Cihazınızdaki bir uygulama, Google hesabına giriş yapmaya çalışıyor. \n \nEğer bu istediğiniz bir şey ise, Oturum aç tuşunu kullanarak Google\'ın oturum açma sayfasına bağlanın, eğer değilse, İptal tuşuna basarak bu menünün çıkmasına neden olan uygulamaya geri dönün. Oturum aç Hay aksi… Cihazınız, oturum açmanız için Google sunucuları ile iletişim kuruyor. From c004cd086027f5208ef68b55012be652bcfac670 Mon Sep 17 00:00:00 2001 From: Leo Alvesson Date: Tue, 29 Oct 2024 10:05:32 +0000 Subject: [PATCH 048/132] Translated using Weblate (Italian) Currently translated at 93.1% (232 of 249 strings) Translation: microG/play-services-core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-core-strings/it/ Translated using Weblate (Italian) Currently translated at 100.0% (156 of 156 strings) Translation: microG/play-services-core: permissions Translate-URL: https://hosted.weblate.org/projects/microg/play-services-core-permissions/it/ --- .../src/main/res/values-it/permissions.xml | 10 +++++----- play-services-core/src/main/res/values-it/strings.xml | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/play-services-core/src/main/res/values-it/permissions.xml b/play-services-core/src/main/res/values-it/permissions.xml index ab1d9ab950..34f884d249 100644 --- a/play-services-core/src/main/res/values-it/permissions.xml +++ b/play-services-core/src/main/res/values-it/permissions.xml @@ -100,8 +100,8 @@ Accesso in lettura e scrittura per l\'API Groups Migration. Visualizza e gestisci le impostazioni di un gruppo di applicazioni Google Accesso in lettura e scrittura per l\'API License Manager. - Leggi e scrivi gli accessi per amministratori ed utenti che eseguono test nel contenitore delle API, oppure leggi e scrivi gli accessi quando viene evocata una operazione API direttamente - In aggiunta all\'ambito complessivo di lettura e scrittura OAuth, utilizza l\'ambito di sola lettura OAuth quando richiedi i dati dell\'utente + Leggi e scrivi gli accessi per amministratori ed utenti che eseguono test nel contenitore delle API, oppure leggi e scrivi gli accessi quando viene evocata una operazione API direttamente. + In aggiunta all\'ambito complessivo di lettura e scrittura OAuth, utilizza l\'ambito di sola lettura OAuth quando richiedi i dati dell\'utente. Accedi in sola lettura per la API Admin Audit Ambito di utilizzo del servizio App State. Visualizza i tuoi dati di Google BigQuery @@ -137,8 +137,8 @@ Visualizza i tuoi dati GAN Messaggistica cloud per Chrome Ambito della sequenza temporale di Glass - Crea, leggi, aggiorna, cancella bozze ed invia messaggi - Tutte le operazioni di lettura e scrittura ad eccezione dell\'immediata e permanente cancellazione dei threads e dei messaggi, bypassando il cestino + Crea, leggi, aggiorna, cancella bozze ed invia messaggi. + Tutte le operazioni di lettura e scrittura ad eccezione dell\'immediata e permanente cancellazione dei threads e dei messaggi, bypassando il cestino. Visualizza tutte le risorse e i rispettivi metadati—nessuna operazione in scrittura. Gestisci la tua migliore posizione disponibile e la cronologia delle localizzazioni Gestisci la tua localizzazione a livello urbano e la cronologia delle localizzazioni @@ -160,7 +160,7 @@ Gestisci le tue attività Gestisci le tue attività Visualizza le tue attività - API per le tracce delle mappe Google: questo ambito permette la lettura e scrittura nella cartella dati del tuo progetto + API per le tracce delle mappe Google: questo ambito permette la lettura e scrittura nella cartella dati del tuo progetto. Gestisci i tuoi collegamenti rapidi goo.gl Visualizza il tuo indirizzo di posta elettronica Visualizza le informazioni di base sul tuo account diff --git a/play-services-core/src/main/res/values-it/strings.xml b/play-services-core/src/main/res/values-it/strings.xml index 6c6dfefb9d..86bb1b21f8 100644 --- a/play-services-core/src/main/res/values-it/strings.xml +++ b/play-services-core/src/main/res/values-it/strings.xml @@ -24,7 +24,7 @@ %1$s vorrebbe utilizzare: Gestione dell\'account Google Ci dispiace… - Un\'applicazione sul tuo dispositivo sta tentando di effettuare l\'accesso ad un account Google. \u0020Se ciò era intenzionale, utilizza il pulsante Accedi per collegarti alla pagina di autenticazione di Google, altrimenti premi Annulla per tornare all\'applicazione che ha aperto questa schermata. + Un\'applicazione sul tuo dispositivo sta tentando di effettuare l\'accesso ad un account Google.\n\nSe ciò era intenzionale, utilizza il pulsante Accedi per collegarti alla pagina di autenticazione di Google, altrimenti premi Annulla per tornare all\'applicazione che ha aperto questa schermata. Accedi "Il tuo dispositivo sta stabilendo la connessione con i server di Google per autenticarti. @@ -214,7 +214,7 @@ Questa operazione può richiedere alcuni secondi." Impostazioni Verifica delle licenze di Google Play Non è possibile effettuare backup al momento - Account + Accounts Pagamenti di Google Play Gestisci le richieste di pagamento Se l\'opzione è abilitata, alcune app potranno completare acquisti o sottoscrivere abbonamenti tramite il servizio di pagamenti di Google Play. From f672b4483a1e6eb753cd5d40095e94e5329278d7 Mon Sep 17 00:00:00 2001 From: Leo Alvesson Date: Tue, 29 Oct 2024 10:01:54 +0000 Subject: [PATCH 049/132] Translated using Weblate (Portuguese (Brazil)) Currently translated at 93.1% (232 of 249 strings) Translation: microG/play-services-core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-core-strings/pt_BR/ --- play-services-core/src/main/res/values-pt-rBR/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/play-services-core/src/main/res/values-pt-rBR/strings.xml b/play-services-core/src/main/res/values-pt-rBR/strings.xml index d9422e4d77..a5b47a48a3 100644 --- a/play-services-core/src/main/res/values-pt-rBR/strings.xml +++ b/play-services-core/src/main/res/values-pt-rBR/strings.xml @@ -167,7 +167,7 @@ Isso pode demorar alguns minutos." Permitir iniciar sessão ao %1$s Para continuar, o microG irá compartilhar o nome, endereço de email, e foto de perfil de sua conta do Google com %1$s. Feedback não disponível no momento - Um app no seu dispositivo está tentando fazer login numa conta do Google. \u0020Se isso for intencional, use o botão Iniciar sessão para se conectar a página de login do Google, se não, toque em Cancelar para voltar para o aplicativo que fez com que este diálogo aparecesse. + Um app no seu dispositivo está tentando fazer login numa conta do Google. Se isso for intencional, use o botão Iniciar sessão para se conectar a página de login do Google, se não, toque em Cancelar para voltar para o aplicativo que fez com que este diálogo aparecesse. provisionar serviços microG SafetyNet do Google Serviços do Google From 8b79b2f12fbc4c0ac6f7567e72777aca9efddf4e Mon Sep 17 00:00:00 2001 From: NEXI Date: Mon, 28 Oct 2024 22:20:35 +0000 Subject: [PATCH 050/132] Translated using Weblate (Serbian) Currently translated at 100.0% (249 of 249 strings) Translation: microG/play-services-core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-core-strings/sr/ Translated using Weblate (Serbian) Currently translated at 100.0% (4 of 4 strings) Translation: microG/play-services-core: plurals Translate-URL: https://hosted.weblate.org/projects/microg/play-services-core-plurals/sr/ Translated using Weblate (Serbian) Currently translated at 100.0% (156 of 156 strings) Translation: microG/play-services-core: permissions Translate-URL: https://hosted.weblate.org/projects/microg/play-services-core-permissions/sr/ Translated using Weblate (Serbian) Currently translated at 100.0% (50 of 50 strings) Translation: microG/play-services-location: core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-location-core-strings/sr/ Translated using Weblate (Serbian) Currently translated at 100.0% (249 of 249 strings) Translation: microG/play-services-core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-core-strings/sr/ Translated using Weblate (Serbian) Currently translated at 100.0% (24 of 24 strings) Translation: microG/play-services-fido: core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-fido-core-strings/sr/ --- .../src/main/res/values-sr/permissions.xml | 2 +- .../src/main/res/values-sr/plurals.xml | 6 ++--- .../src/main/res/values-sr/strings.xml | 25 ++++++++++++++++--- .../core/src/main/res/values-sr/strings.xml | 4 +-- .../core/src/main/res/values-sr/strings.xml | 4 +-- 5 files changed, 29 insertions(+), 12 deletions(-) diff --git a/play-services-core/src/main/res/values-sr/permissions.xml b/play-services-core/src/main/res/values-sr/permissions.xml index abc67e6375..e1e97505fd 100644 --- a/play-services-core/src/main/res/values-sr/permissions.xml +++ b/play-services-core/src/main/res/values-sr/permissions.xml @@ -77,7 +77,7 @@ Гласовна претрага Дозвољава апликацији да приступа Google групама преко било ког повезаног Google налога. Google мапе - Управљање конфигурацијом Ad Exchange налога купца + Управљање подешавањем Ad Exchange налога купца Размена порука у облаку за Chrome Дозвољава апликацији да приступи AdSense-у преко било ког повезаног Google налога. Преглед и управљање вашим Ad Exchange подацима diff --git a/play-services-core/src/main/res/values-sr/plurals.xml b/play-services-core/src/main/res/values-sr/plurals.xml index e50c62186c..9afb42aefc 100644 --- a/play-services-core/src/main/res/values-sr/plurals.xml +++ b/play-services-core/src/main/res/values-sr/plurals.xml @@ -30,8 +30,8 @@ Захтевај недостајуће дозволе - %1$d бекенд конфигурисан - %1$d бекенда конфигурисана - %1$d бекендова конфигурисано + %1$d бекенд подешен + %1$d бекенда подешена + %1$d бекендова подешено \ No newline at end of file diff --git a/play-services-core/src/main/res/values-sr/strings.xml b/play-services-core/src/main/res/values-sr/strings.xml index fa300199db..42b6efa488 100644 --- a/play-services-core/src/main/res/values-sr/strings.xml +++ b/play-services-core/src/main/res/values-sr/strings.xml @@ -123,7 +123,7 @@ Google SafetyNet је систем сертификације уређаја, који осигурава да је уређај правилно обезбеђен и компатибилан са Android CTS. Неке апликације користе SafetyNet из безбедносних разлога или као предуслов за заштиту од неовлашћеног приступа. \n \nmicroG GmsCore садржи бесплатну имплементацију SafetyNet-а, али званични сервер захтева да SafetyNet захтеви буду потписани коришћењем власничког система DroidGuard. - читање конфигурације Google услуге + читање подешавања Google услуге Пријави се Увезите профил уређаја из фајла Копирај JSON JWS податке @@ -131,7 +131,7 @@ Када је омогућено, све апликације на овом уређају ће моћи да виде имејл адресу ваших Google налога без претходног овлашћења. Савет %s секунди - Конфигурација + Подешавање Google услуге Неважећи JSON Очисти недавне захтеве @@ -165,7 +165,7 @@ Последња употреба: %1$s Будите упитани пре регистровања нове апликације да бисте примали push-обавештења Покрени апликацију при push-обавештењу - Дозвољава апликацији да конфигурише microG услуге без интеракције корисника + Дозвољава апликацији да подешава microG услуге без интеракције корисника ReCaptcha: %s Примај push-обавештења Прођени сви тестови @@ -192,7 +192,7 @@ Копирано у привремену меморију! Апликације које користе push-обавештења Прилагођено: %s - Апликација на вашем уређају покушава да се пријави на Google налог. \n \nАко је ово било намерно, користите дугме Пријави се да бисте се повезали са Google страницом за пријављивање, ако није, притисните Откажи да бисте се вратили на апликацију која је изазвала да се овај дијалог прикаже. + Апликација на вашем уређају покушава да се пријави на Google налог. \n\nАко је ово било намерно, користите дугме Пријави се да бисте се повезали са Google страницом за пријављивање, ако није, притисните Откажи да бисте се вратили на апликацију која је изазвала да се овај дијалог прикаже. Тестирај ReCAPTCHA Да бисте наставили, microG ће делити име, имејл адресу и слику профила вашег Google налога са %1$s . Аутентификуј регистрацијом уређаја @@ -261,4 +261,21 @@ microG ограничене услуге Разумем Користите microG ограничене услуге. За разлику од уобичајених microG услуга, ова верзија функционише само са апликацијама које користе microG библиотеке, а не са онима на Google Play-у. То значи да ће већина апликација занемарити ове услуге. + Неопходна радња на налогу + Вашем Google налогу је потребно додатно подешавање. + Омогући регистрацију уређаја + Омогући размену поруку у облаку + Омогући размену порука у облаку за microG + Додирните да бисте извршили корак + Корак завршен + Заврши + Завршите следеће кораке да бисте могли да користите свој Google налог %s на овом уређају. + Обавештења Google налога + Обавештава када неки од ваших Google налога захтева додатно подешавање пре него што се може користити или када налог није компатибилан са microG-ом. + Ваш уређај мора да се региструје на Google бар једном.\n\nМожете да онемогућите Google регистрацију уређаја након што је подешавање налога завршено. + Завршите подешавање свог Google налога + Према вашим преференцама, microG-у је потребна ваша дозвола да би могао да се региструје за размену порука у облаку. + Можете онемогућити размену порука у облаку након што је подешавање налога завршено. + Подесите безбедно закључавање екрана + Вашим Google налогом управља ваше радно место или образовна институција. Ваш администратор је одлучио да је уређајима потребно безбедно закључавање екрана да би могли да приступе подацима налога.\n\nПодесите лозинку, PIN или шаблон за закључавање екрана. \ No newline at end of file diff --git a/play-services-fido/core/src/main/res/values-sr/strings.xml b/play-services-fido/core/src/main/res/values-sr/strings.xml index 78a903790f..1025c22359 100644 --- a/play-services-fido/core/src/main/res/values-sr/strings.xml +++ b/play-services-fido/core/src/main/res/values-sr/strings.xml @@ -19,9 +19,9 @@ Користите безбедносни кључ са Bluetooth-ом Безбедносни кључеви раде са Bluetooth-ом, NFC-ом и USB-ом. Изаберите како желите да користите свој кључ. Користите безбедносни кључ са NFC-ом - Унесите ПИН за ваш аутентификатор + Унесите PIN за ваш аутентификатор од 4 до 63 знакова ОК Откажи - Унет је погрешан ПИН! + Унет је погрешан PIN! \ No newline at end of file diff --git a/play-services-location/core/src/main/res/values-sr/strings.xml b/play-services-location/core/src/main/res/values-sr/strings.xml index 7521783fb3..4f0cb6cda6 100644 --- a/play-services-location/core/src/main/res/values-sr/strings.xml +++ b/play-services-location/core/src/main/res/values-sr/strings.xml @@ -29,7 +29,7 @@ Користите GPS, Wi-Fi, мобилне податке и сензоре За детаље, идите на подешавања локације. За боље искуство, укључите локацију уређаја који користи услугу локације microG-а - Конфигуриши URL услуге + Подеси URL услуге Ресетуј Ово омогућава постављање прилагођеног URL-а услуге. Неважеће вредности могу довести до тога да услуге локације не реагују или буду потпуно недоступне. Путања /v1/geolocate се аутоматски додаје. Ако пружалац локације захтева кључ, он се може додати као параметар упита основном URL-у. @@ -49,5 +49,5 @@ Избор онлајн услуге локације Услови / Приватност Увоз или извоз података о локацији - Неопходна конфигурација + Неопходно подешавање \ No newline at end of file From 7bda0f371244dc3ce7c7bead5253e9711158139a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A4=A7=E7=8E=8B=E5=8F=AB=E6=88=91=E6=9D=A5=E5=B7=A1?= =?UTF-8?q?=E5=B1=B1?= Date: Mon, 28 Oct 2024 23:44:46 +0000 Subject: [PATCH 051/132] Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 100.0% (249 of 249 strings) Translation: microG/play-services-core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-core-strings/zh_Hans/ Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 100.0% (19 of 19 strings) Translation: microG/vending-app Translate-URL: https://hosted.weblate.org/projects/microg/vending-app/zh_Hans/ --- .../src/main/res/values-zh-rCN/strings.xml | 17 +++++++++++++++++ .../src/main/res/values-zh-rCN/strings.xml | 2 +- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/play-services-core/src/main/res/values-zh-rCN/strings.xml b/play-services-core/src/main/res/values-zh-rCN/strings.xml index 41a46d39cb..64e3a71af2 100644 --- a/play-services-core/src/main/res/values-zh-rCN/strings.xml +++ b/play-services-core/src/main/res/values-zh-rCN/strings.xml @@ -244,4 +244,21 @@ microG GmsCore 内置一套自由的 SafetyNet 实现,但是官方服务器要 microG 有限服务 我理解 你在使用 microG 有限服务版本。和通常的 microG 服务不同,此版本仅适用使用 microG 库的应用,而非在 Google Play 上的应用。 这意味着多数应用会忽略这些服务。 + Google 账户警告 + 需要账户操作 + 启用设备注册 + 你的设备需要至少向 Google 注册一次。\n\n在账户设置完成后,你可以停用 Google 设备注册。 + 启用云消息传递 + 账户设置完成后,你可以停用云消息传递。 + 允许 microG 的云消息传递 + 根据你的首选项,microG 需要你的许可才能注册云消息传递。 + 配置安全屏幕锁 + 单击执行设置 + 步骤已完成 + 完成 + 在你多个 Google 账户中的一个需要额外设置才能使用或者一个账户不兼容 microG 时进行通知。 + 完成 Google 账户设置 + 你的 Google 账户需要额外设置。 + 要能在这台设备上使用你的 Google 账户 %s 请完成下列步骤。 + 你的 Google 账户受工作场所或教育机构管理。你的管理员决定设备在可以访问账户数据前需要设置安全屏幕锁。\n\n请设置一个密码、PIN或手势屏幕锁。 \ No newline at end of file diff --git a/vending-app/src/main/res/values-zh-rCN/strings.xml b/vending-app/src/main/res/values-zh-rCN/strings.xml index 1dd87d3fed..89db7dd662 100644 --- a/vending-app/src/main/res/values-zh-rCN/strings.xml +++ b/vending-app/src/main/res/values-zh-rCN/strings.xml @@ -13,7 +13,7 @@ 了解详情 验证 许可证通知 - 当应用尝试验证其许可证但您未登录任何 Google 帐户时发出通知。 + 当应用尝试验证其许可证但您未登录任何 Google 账户时发出通知。 %1$s无法验证许可证 如果应用出现异常,请登录您购买该应用所使用的 Google 帐号。 登录 From 681596129166860d8abe2e06eb1d0ea02f50db52 Mon Sep 17 00:00:00 2001 From: LucasMZ Date: Tue, 29 Oct 2024 19:17:53 +0000 Subject: [PATCH 052/132] Translated using Weblate (Portuguese (Brazil)) Currently translated at 99.1% (247 of 249 strings) Translation: microG/play-services-core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-core-strings/pt_BR/ Translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (249 of 249 strings) Translation: microG/play-services-core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-core-strings/pt_BR/ --- .../src/main/res/values-pt-rBR/strings.xml | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/play-services-core/src/main/res/values-pt-rBR/strings.xml b/play-services-core/src/main/res/values-pt-rBR/strings.xml index a5b47a48a3..d176b1169e 100644 --- a/play-services-core/src/main/res/values-pt-rBR/strings.xml +++ b/play-services-core/src/main/res/values-pt-rBR/strings.xml @@ -167,7 +167,7 @@ Isso pode demorar alguns minutos." Permitir iniciar sessão ao %1$s Para continuar, o microG irá compartilhar o nome, endereço de email, e foto de perfil de sua conta do Google com %1$s. Feedback não disponível no momento - Um app no seu dispositivo está tentando fazer login numa conta do Google. Se isso for intencional, use o botão Iniciar sessão para se conectar a página de login do Google, se não, toque em Cancelar para voltar para o aplicativo que fez com que este diálogo aparecesse. + Um app no seu dispositivo está tentando fazer login numa conta do Google.\n\nSe isso for intencional, use o botão Iniciar sessão para se conectar a página de login do Google, se não, toque em Cancelar para voltar para o aplicativo que fez com que este diálogo aparecesse. provisionar serviços microG SafetyNet do Google Serviços do Google @@ -263,4 +263,21 @@ Isso pode demorar alguns minutos." Serviços Limitados microG Você está usando os Serviços Limitados microG. Diferente dos Serviços microG comum, esta variante só funciona com apps que usam as bibliotecas do microG, não aquelas do Google Play. Isto significa que a maioria dos aplicativos irão ignorar estes serviços. Eu entendo + Complete os seguintes passos para poder usar sua conta Google %s neste dispositivo. + Permitir Cloud Messaging para o microG + Configurar bloqueio de tela seguro + Termine de configurar sua conta Google + Alertas da conta Google + Notifica quando uma das suas contas Google precisa de configuração adicional antes que seja usada ou quando uma conta é incompatível com o microG. + Ação de conta necessária + Sua conta Google precisa de configuração adicional. + Ativar registro do dispositivo + Seu dispositivo precisa registrar-se ao Google pelo menos uma vez.\n\nVocê pode desativar o registro do dispositivo Google após a configuração da conta estar completa. + Ativar o Cloud Messaging + Você pode desativar o Cloud Messaging (também chamado de notificações push) depois que configuração da conta esteja completa. + De acordo com suas preferências, o microG precisa de permissão de você antes que possa se registrar para o Cloud Messaging. + Sua conta Google é gerenciada pelo seu lugar de trabalho ou instituição escolar. Seu administrador decidiu que dispositivos devem ter um bloqueio de tela seguro antes que possam acessar dados da conta.\n\nPor favor configure uma senha, PIN, ou um padrão como seu bloqueio de tela. + Clique para executar este passo + Terminar + Passo completo \ No newline at end of file From 0c6cd223989efdcb4b9457a9cb73c4938482c738 Mon Sep 17 00:00:00 2001 From: Sol Lam <18778938888q@gmail.com> Date: Mon, 4 Nov 2024 13:00:11 +0000 Subject: [PATCH 053/132] Translated using Weblate (Literary Chinese) Currently translated at 10.5% (2 of 19 strings) Translation: microG/vending-app Translate-URL: https://hosted.weblate.org/projects/microg/vending-app/lzh/ --- vending-app/src/main/res/values-lzh/strings.xml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/vending-app/src/main/res/values-lzh/strings.xml b/vending-app/src/main/res/values-lzh/strings.xml index a6b3daec93..955dcd8b5e 100644 --- a/vending-app/src/main/res/values-lzh/strings.xml +++ b/vending-app/src/main/res/values-lzh/strings.xml @@ -1,2 +1,5 @@ - \ No newline at end of file + + microG不可獨用,開啟時當引至microG服務設定界面。 + MicroG假商店 + \ No newline at end of file From bafdf4f366ee09169d63e8806e9d43c57467cee5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mustafa=20Bacaks=C4=B1z?= Date: Sun, 10 Nov 2024 16:29:29 +0000 Subject: [PATCH 054/132] Translated using Weblate (Turkish) Currently translated at 69.8% (109 of 156 strings) Translation: microG/play-services-core: permissions Translate-URL: https://hosted.weblate.org/projects/microg/play-services-core-permissions/tr/ --- .../src/main/res/values-tr/permissions.xml | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/play-services-core/src/main/res/values-tr/permissions.xml b/play-services-core/src/main/res/values-tr/permissions.xml index 0164053999..f4cd11b64d 100644 --- a/play-services-core/src/main/res/values-tr/permissions.xml +++ b/play-services-core/src/main/res/values-tr/permissions.xml @@ -78,4 +78,35 @@ Uygulamanın, ilgili herhangi bir Google hesabı aracılığıyla Kişiselleştirilmiş Konuşma Tanıma\'ya erişmesine izin verir. Uygulamanın, ilgili herhangi bir Google hesabı aracılığıyla YouTube\'a erişmesine izin verir. YouTube kullanıcı adları + AdSense barındırıcı verilerinizi ve ilişkili hesaplarınızı görüntüleyin ve yönetin + AdSense verilerinizi görüntüleyin + AdSense verilerinizi görüntüleyin ve yönetin + Google Analytics verilerinizi görüntüleyin + Google Analytics verilerinizi görüntüleyin ve yönetin + Google Play Android Geliştiricisine Erişim + Groups Migration API\'ına okuma ve yazma erişimi. + Bir Google Apps Grubunun ayarlarını görüntüleyin ve yönetin + Lisans Yöneticisi API\'sine okuma/yazma erişimi. + Satıcı yöneticileri ve kullanıcılar için, API\'nin korumalı alanında test yaparken okuma/yazma erişimi veya bir API işlemini doğrudan çağırırken okuma/yazma erişimi. + Blogger hesabınızı yönetin + Blogger hesabınızı görüntüleyin + Kitaplarınızı yönetin + Takvimlerinizi yönetin + Takvimlerinizi görüntüleyin + Google bulut yazdırma verilerinizi görüntüleyin ve yönetin + Google Compute Engine kaynaklarınızı görüntüleyin + Google Compute Engine kaynaklarınızı görüntüleyin ve yönetin + Google Koordinatör işlerinizi görüntüleyin + Google Haritalar Koordinatör işlerinizi görüntüleyin ve yönetin + Google Bulut Depolama\'daki verilerinizi ve izinlerinizi yönetin + Verilerinizi Google Bulut Depolama\'da yönetin + DoubleClick for Advertisers raporlarını görüntüleyin ve yönetin + Application Data klasörüne erişime izin verir + Google Drive uygulamalarınızı görüntüleyin + Bu uygulamayla açtığınız veya oluşturduğunuz Google Drive dosyalarını görüntüleyin ve yönetin + Google Drive\'ınızdaki dosya ve dokümanlara ilişkin meta verileri görüntüleyin + Google Drive\'ınızdaki dosya ve belgeleri görüntüleyin + Google Drive\'ınızdaki dosya ve belgeleri görüntüleyin ve yönetin + Verilerinizi Google Bulut Depolama\'da görüntüleyin + Kullanıcıların bir uygulamanın kurulumunu onaylamasına izin vermek için kullanılan özel kapsam \ No newline at end of file From 0902c75fe1bd6c67790e0cabeab549f053da7166 Mon Sep 17 00:00:00 2001 From: Marvin W Date: Wed, 13 Nov 2024 17:30:57 -0600 Subject: [PATCH 055/132] Huawei: Enable purchase free apps by default --- play-services-core/src/huawei/AndroidManifest.xml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/play-services-core/src/huawei/AndroidManifest.xml b/play-services-core/src/huawei/AndroidManifest.xml index cfe4c5bc51..9f4224aa37 100644 --- a/play-services-core/src/huawei/AndroidManifest.xml +++ b/play-services-core/src/huawei/AndroidManifest.xml @@ -34,5 +34,8 @@ + \ No newline at end of file From c9a4ebae62b8c229642f849ff9bcea7fdd5ad523 Mon Sep 17 00:00:00 2001 From: Marvin W Date: Wed, 13 Nov 2024 16:52:34 -0600 Subject: [PATCH 056/132] Location: Clear calling identity when handling requests from system fused location provider. --- .../provider/FusedLocationProviderService.kt | 25 +++++++++++-------- .../IntentLocationProviderPreTiramisu.kt | 8 +++--- .../provider/IntentLocationProviderService.kt | 4 +-- .../NetworkLocationProviderService.kt | 4 +-- 4 files changed, 23 insertions(+), 18 deletions(-) diff --git a/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/provider/FusedLocationProviderService.kt b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/provider/FusedLocationProviderService.kt index 445986a8a8..35d280d4a0 100644 --- a/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/provider/FusedLocationProviderService.kt +++ b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/provider/FusedLocationProviderService.kt @@ -9,6 +9,7 @@ import android.app.PendingIntent import android.content.Intent import android.location.Criteria import android.location.Location +import android.os.Binder import android.os.Build.VERSION.SDK_INT import android.util.Log import androidx.core.location.LocationRequestCompat @@ -23,29 +24,33 @@ import kotlin.math.max class FusedLocationProviderService : IntentLocationProviderService() { override fun extractLocation(intent: Intent): Location? = LocationResult.extractResult(intent)?.lastLocation - override fun requestIntentUpdated(currentRequest: ProviderRequestUnbundled?, pendingIntent: PendingIntent?) { - val intervalMillis = if (currentRequest?.reportLocation == true) { - max(currentRequest.interval, minIntervalMillis) - } else { - Long.MAX_VALUE - } + override fun requestIntentUpdated(currentRequest: ProviderRequestUnbundled?, pendingIntent: PendingIntent) { + val intervalMillis = max(currentRequest?.interval ?: Long.MAX_VALUE, minIntervalMillis) val request = LocationRequest.Builder(intervalMillis) if (SDK_INT >= 31 && currentRequest != null) { - request.setPriority(when(currentRequest.quality) { - LocationRequestCompat.QUALITY_LOW_POWER -> Priority.PRIORITY_LOW_POWER - LocationRequestCompat.QUALITY_HIGH_ACCURACY -> Priority.PRIORITY_HIGH_ACCURACY + request.setPriority(when { + currentRequest.interval == LocationRequestCompat.PASSIVE_INTERVAL -> Priority.PRIORITY_PASSIVE + currentRequest.quality == LocationRequestCompat.QUALITY_LOW_POWER -> Priority.PRIORITY_LOW_POWER + currentRequest.quality == LocationRequestCompat.QUALITY_HIGH_ACCURACY -> Priority.PRIORITY_HIGH_ACCURACY else -> Priority.PRIORITY_BALANCED_POWER_ACCURACY }) request.setMaxUpdateDelayMillis(currentRequest.maxUpdateDelayMillis) + } else { + request.setPriority(when { + currentRequest?.interval == LocationRequestCompat.PASSIVE_INTERVAL -> Priority.PRIORITY_PASSIVE + else -> Priority.PRIORITY_BALANCED_POWER_ACCURACY + }) } + val identity = Binder.clearCallingIdentity() try { LocationServices.getFusedLocationProviderClient(this).requestLocationUpdates(request.build(), pendingIntent) } catch (e: SecurityException) { Log.d(TAG, "Failed requesting location updated", e) } + Binder.restoreCallingIdentity(identity) } - override fun stopIntentUpdated(pendingIntent: PendingIntent?) { + override fun stopIntentUpdated(pendingIntent: PendingIntent) { LocationServices.getFusedLocationProviderClient(this).removeLocationUpdates(pendingIntent) } diff --git a/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/provider/IntentLocationProviderPreTiramisu.kt b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/provider/IntentLocationProviderPreTiramisu.kt index f5bb9c67e0..9e4e6c2c80 100644 --- a/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/provider/IntentLocationProviderPreTiramisu.kt +++ b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/provider/IntentLocationProviderPreTiramisu.kt @@ -46,8 +46,8 @@ class IntentLocationProviderPreTiramisu : AbstractLocationProviderPreTiramisu { private val reportAgainRunnable = Runnable { reportAgain() } private fun updateRequest() { - if (enabled) { - service.requestIntentUpdated(currentRequest, pendingIntent) + if (enabled && pendingIntent != null) { + service.requestIntentUpdated(currentRequest, pendingIntent!!) reportAgain() } } @@ -90,8 +90,8 @@ class IntentLocationProviderPreTiramisu : AbstractLocationProviderPreTiramisu { override fun disable() { synchronized(this) { - if (!enabled) throw IllegalStateException() - service.stopIntentUpdated(pendingIntent) + if (!enabled || pendingIntent == null) throw IllegalStateException() + service.stopIntentUpdated(pendingIntent!!) pendingIntent?.cancel() pendingIntent = null currentRequest = null diff --git a/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/provider/IntentLocationProviderService.kt b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/provider/IntentLocationProviderService.kt index 2761b9aa40..f0483d9dd7 100644 --- a/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/provider/IntentLocationProviderService.kt +++ b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/provider/IntentLocationProviderService.kt @@ -34,9 +34,9 @@ abstract class IntentLocationProviderService : Service() { handler = Handler(handlerThread.looper) } - abstract fun requestIntentUpdated(currentRequest: ProviderRequestUnbundled?, pendingIntent: PendingIntent?) + abstract fun requestIntentUpdated(currentRequest: ProviderRequestUnbundled?, pendingIntent: PendingIntent) - abstract fun stopIntentUpdated(pendingIntent: PendingIntent?) + abstract fun stopIntentUpdated(pendingIntent: PendingIntent) abstract fun extractLocation(intent: Intent): Location? diff --git a/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/provider/NetworkLocationProviderService.kt b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/provider/NetworkLocationProviderService.kt index 2c96eeb79d..4231a2f03d 100644 --- a/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/provider/NetworkLocationProviderService.kt +++ b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/provider/NetworkLocationProviderService.kt @@ -22,7 +22,7 @@ class NetworkLocationProviderService : IntentLocationProviderService() { extras?.remove(LOCATION_EXTRA_PRECISION) } - override fun requestIntentUpdated(currentRequest: ProviderRequestUnbundled?, pendingIntent: PendingIntent?) { + override fun requestIntentUpdated(currentRequest: ProviderRequestUnbundled?, pendingIntent: PendingIntent) { val forceNow: Boolean val intervalMillis: Long if (currentRequest?.reportLocation == true) { @@ -48,7 +48,7 @@ class NetworkLocationProviderService : IntentLocationProviderService() { startService(intent) } - override fun stopIntentUpdated(pendingIntent: PendingIntent?) { + override fun stopIntentUpdated(pendingIntent: PendingIntent) { val intent = Intent(ACTION_NETWORK_LOCATION_SERVICE) intent.`package` = packageName intent.putExtra(EXTRA_PENDING_INTENT, pendingIntent) From e3b08423ae9c47957e15e07a056933b2605da974 Mon Sep 17 00:00:00 2001 From: Marvin W Date: Wed, 13 Nov 2024 17:29:34 -0600 Subject: [PATCH 057/132] Maps: Handle apps not invoking onStart/onStop And turn on/off location request when paused/resumed. --- .../gms/maps/mapbox/GoogleLocationEngine.kt | 2 +- .../org/microg/gms/maps/mapbox/GoogleMap.kt | 51 ++++++++++++++----- 2 files changed, 38 insertions(+), 15 deletions(-) diff --git a/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/GoogleLocationEngine.kt b/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/GoogleLocationEngine.kt index 654a1407ba..43a6f53b2b 100644 --- a/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/GoogleLocationEngine.kt +++ b/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/GoogleLocationEngine.kt @@ -44,7 +44,7 @@ class GoogleLocationEngine(context: Context) : LocationEngine { .setMinUpdateDistanceMeters(request.displacement) .setMinUpdateIntervalMillis(request.fastestInterval) .setMaxUpdateDelayMillis(request.maxWaitTime) - .build(), listenerMap[callback], looper + .build(), listenerMap[callback]!!, looper ) } diff --git a/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/GoogleMap.kt b/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/GoogleMap.kt index 129eec77b3..08cd716551 100644 --- a/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/GoogleMap.kt +++ b/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/GoogleMap.kt @@ -448,17 +448,14 @@ class GoogleMapImpl(context: Context, var options: GoogleMapOptions) : AbstractG } private fun updateLocationEngineListener(myLocation: Boolean) { - val locationComponent = map?.locationComponent ?: return - if (locationComponent.isLocationComponentActivated) { - locationComponent.isLocationComponentEnabled = myLocation - if (myLocation) { - locationComponent.locationEngine?.requestLocationUpdates( - locationComponent.locationEngineRequest, - locationEngineCallback, - null - ) - } else { - locationComponent.locationEngine?.removeLocationUpdates(locationEngineCallback) + map?.locationComponent?.let { + if (it.isLocationComponentActivated) { + it.isLocationComponentEnabled = myLocation + if (myLocation) { + it.locationEngine?.requestLocationUpdates(it.locationEngineRequest, locationEngineCallback, Looper.getMainLooper()) + } else { + it.locationEngine?.removeLocationUpdates(locationEngineCallback) + } } } } @@ -829,10 +826,34 @@ class GoogleMapImpl(context: Context, var options: GoogleMapOptions) : AbstractG } } - override fun onResume() = mapView?.onResume() ?: Unit - override fun onPause() = mapView?.onPause() ?: Unit + override fun onResume() { + Log.d(TAG, "onResume") + if (!isStarted) { + // onStart was not called, invoke mapView.onStart() now + mapView?.onStart() + } + mapView?.onResume() + map?.locationComponent?.let { + if (it.isLocationComponentEnabled) { + it.locationEngine?.requestLocationUpdates(it.locationEngineRequest, locationEngineCallback, Looper.getMainLooper()) + } + } + } + override fun onPause() { + Log.d(TAG, "onPause") + map?.locationComponent?.let { + if (it.isLocationComponentEnabled) { + it.locationEngine?.removeLocationUpdates(locationEngineCallback) + } + } + mapView?.onPause() + if (!isStarted) { + // onStart was not called, invoke mapView.onStop() now + mapView?.onStop() + } + } override fun onDestroy() { - Log.d(TAG, "destroy"); + Log.d(TAG, "onDestroy"); userOnInitializedCallbackList.clear() lineManager?.onDestroy() lineManager = null @@ -858,11 +879,13 @@ class GoogleMapImpl(context: Context, var options: GoogleMapOptions) : AbstractG } override fun onStart() { + Log.d(TAG, "onStart") isStarted = true mapView?.onStart() } override fun onStop() { + Log.d(TAG, "onStop") isStarted = false mapView?.onStop() } From 5ed17740037b95e37a0ed12e2b93d69b31c78516 Mon Sep 17 00:00:00 2001 From: Marvin W Date: Wed, 13 Nov 2024 17:27:28 -0600 Subject: [PATCH 058/132] Location: Fix handing of null Looper Also put some nullability annotations --- .../location/FusedLocationProviderClient.java | 27 ++++++----- .../FusedLocationProviderApiImpl.java | 6 ++- .../FusedLocationProviderClientImpl.java | 48 +++++++++++++------ .../gms/location/LocationClientImpl.java | 5 +- 4 files changed, 55 insertions(+), 31 deletions(-) diff --git a/play-services-location/src/main/java/com/google/android/gms/location/FusedLocationProviderClient.java b/play-services-location/src/main/java/com/google/android/gms/location/FusedLocationProviderClient.java index 033eaa669b..0be024e85f 100644 --- a/play-services-location/src/main/java/com/google/android/gms/location/FusedLocationProviderClient.java +++ b/play-services-location/src/main/java/com/google/android/gms/location/FusedLocationProviderClient.java @@ -17,6 +17,7 @@ import android.provider.Settings; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.annotation.RequiresPermission; import com.google.android.gms.common.api.Api; import com.google.android.gms.common.api.GoogleApi; @@ -109,7 +110,7 @@ protected FusedLocationProviderClient(Context context) { */ @NonNull @RequiresPermission(anyOf = {ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION}) - public abstract Task getCurrentLocation(int priority, CancellationToken cancellationToken); + public abstract Task getCurrentLocation(int priority, @Nullable CancellationToken cancellationToken); /** * Returns a single location fix representing the best estimate of the current location of the device. This may return a historical location if a recent @@ -122,7 +123,7 @@ protected FusedLocationProviderClient(Context context) { */ @NonNull @RequiresPermission(anyOf = {ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION}) - public abstract Task getCurrentLocation(CurrentLocationRequest request, CancellationToken cancellationToken); + public abstract Task getCurrentLocation(@NonNull CurrentLocationRequest request, @Nullable CancellationToken cancellationToken); /** * Returns the most recent historical location currently available according to the given request. Will return null if no matching historical location is @@ -130,7 +131,7 @@ protected FusedLocationProviderClient(Context context) { */ @NonNull @RequiresPermission(anyOf = {ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION}) - public abstract Task getLastLocation(LastLocationRequest request); + public abstract Task getLastLocation(@NonNull LastLocationRequest request); /** * Returns the most recent historical location currently available. Will return null if no historical location is available. The historical location may @@ -154,19 +155,19 @@ protected FusedLocationProviderClient(Context context) { * Removes all location updates for the given listener. */ @NonNull - public abstract Task removeLocationUpdates(LocationListener listener); + public abstract Task removeLocationUpdates(@NonNull LocationListener listener); /** * Removes all location updates for the given callback. */ @NonNull - public abstract Task removeLocationUpdates(LocationCallback callback); + public abstract Task removeLocationUpdates(@NonNull LocationCallback callback); /** * Removes all location updates for the given pending intent. */ @NonNull - public abstract Task removeLocationUpdates(PendingIntent pendingIntent); + public abstract Task removeLocationUpdates(@NonNull PendingIntent pendingIntent); /** * Requests location updates with the given request and results delivered to the given listener on the specified {@link Looper}. A @@ -184,7 +185,7 @@ protected FusedLocationProviderClient(Context context) { */ @NonNull @RequiresPermission(anyOf = {ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION}) - public abstract Task requestLocationUpdates(LocationRequest request, LocationListener listener, Looper looper); + public abstract Task requestLocationUpdates(@NonNull LocationRequest request, @NonNull LocationListener listener, @Nullable Looper looper); /** * Requests location updates with the given request and results delivered to the given callback on the specified {@link Executor}. @@ -199,7 +200,7 @@ protected FusedLocationProviderClient(Context context) { */ @NonNull @RequiresPermission(anyOf = {ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION}) - public abstract Task requestLocationUpdates(LocationRequest request, Executor executor, LocationCallback callback); + public abstract Task requestLocationUpdates(@NonNull LocationRequest request, @NonNull Executor executor, @NonNull LocationCallback callback); /** * Requests location updates with the given request and results delivered to the given listener on the specified {@link Executor}. @@ -208,7 +209,7 @@ protected FusedLocationProviderClient(Context context) { */ @NonNull @RequiresPermission(anyOf = {ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION}) - public abstract Task requestLocationUpdates(LocationRequest request, Executor executor, LocationListener listener); + public abstract Task requestLocationUpdates(@NonNull LocationRequest request, @NonNull Executor executor, @NonNull LocationListener listener); /** * Requests location updates with the given request and results delivered to the given callback on the specified @@ -229,7 +230,7 @@ protected FusedLocationProviderClient(Context context) { */ @NonNull @RequiresPermission(anyOf = {ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION}) - public abstract Task requestLocationUpdates(LocationRequest request, LocationCallback callback, Looper looper); + public abstract Task requestLocationUpdates(@NonNull LocationRequest request, @NonNull LocationCallback callback, @Nullable Looper looper); /** * Requests location updates with the given request and results delivered via the specified {@link PendingIntent}. A previous @@ -256,7 +257,7 @@ protected FusedLocationProviderClient(Context context) { */ @NonNull @RequiresPermission(anyOf = {ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION}) - public abstract Task requestLocationUpdates(LocationRequest request, PendingIntent pendingIntent); + public abstract Task requestLocationUpdates(@NonNull LocationRequest request, @NonNull PendingIntent pendingIntent); /** * Sets the mock location of the Fused Location Provider. @@ -272,7 +273,8 @@ protected FusedLocationProviderClient(Context context) { * @param location valid location to set as the next FLP location * @throws SecurityException if security requirements are not met */ - public abstract Task setMockLocation(Location location); + @NonNull + public abstract Task setMockLocation(@NonNull Location location); /** * Sets whether or not the Fused Location Provider is in mock mode. @@ -289,5 +291,6 @@ protected FusedLocationProviderClient(Context context) { * @param mockMode the mock mode state to set for the Fused Location Provider APIs * @throws SecurityException if security requirements are not met */ + @NonNull public abstract Task setMockMode(boolean mockMode); } diff --git a/play-services-location/src/main/java/org/microg/gms/location/FusedLocationProviderApiImpl.java b/play-services-location/src/main/java/org/microg/gms/location/FusedLocationProviderApiImpl.java index 3ebffee547..eac50bb816 100644 --- a/play-services-location/src/main/java/org/microg/gms/location/FusedLocationProviderApiImpl.java +++ b/play-services-location/src/main/java/org/microg/gms/location/FusedLocationProviderApiImpl.java @@ -76,10 +76,11 @@ public void run(LocationClientImpl client) throws RemoteException { @Override public PendingResult requestLocationUpdates(GoogleApiClient client, LocationRequest request, LocationCallback callback, Looper looper) { + Looper currentLooper = looper == null ? Looper.myLooper() : looper; return callVoid(client, new Runnable() { @Override public void run(LocationClientImpl client) throws RemoteException { - client.requestLocationUpdates(request, callback, looper); + client.requestLocationUpdates(request, callback, currentLooper); } }); } @@ -88,10 +89,11 @@ public void run(LocationClientImpl client) throws RemoteException { public PendingResult requestLocationUpdates(GoogleApiClient client, final LocationRequest request, final LocationListener listener, final Looper looper) { + Looper currentLooper = looper == null ? Looper.myLooper() : looper; return callVoid(client, new Runnable() { @Override public void run(LocationClientImpl client) throws RemoteException { - client.requestLocationUpdates(request, listener, looper); + client.requestLocationUpdates(request, listener, currentLooper); } }); } diff --git a/play-services-location/src/main/java/org/microg/gms/location/FusedLocationProviderClientImpl.java b/play-services-location/src/main/java/org/microg/gms/location/FusedLocationProviderClientImpl.java index cf00155a88..a557fceca4 100644 --- a/play-services-location/src/main/java/org/microg/gms/location/FusedLocationProviderClientImpl.java +++ b/play-services-location/src/main/java/org/microg/gms/location/FusedLocationProviderClientImpl.java @@ -10,7 +10,9 @@ import android.location.Location; import android.os.Looper; +import android.util.Log; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import com.google.android.gms.location.*; import com.google.android.gms.tasks.CancellationToken; import com.google.android.gms.tasks.Task; @@ -26,6 +28,7 @@ public FusedLocationProviderClientImpl(Context context) { super(context); } + @NonNull public Task flushLocations() { return scheduleTask((ReturningGoogleApiCall) (client) -> null); } @@ -38,16 +41,17 @@ public Task getCurrentLocation(int priority, CancellationToken cancell @NonNull @Override - public Task getCurrentLocation(CurrentLocationRequest request, CancellationToken cancellationToken) { + public Task getCurrentLocation(@NonNull CurrentLocationRequest request, CancellationToken cancellationToken) { return null; } @NonNull @Override - public Task getLastLocation(LastLocationRequest request) { + public Task getLastLocation(@NonNull LastLocationRequest request) { return null; } + @NonNull public Task getLastLocation() { return scheduleTask((ReturningGoogleApiCall) LocationClientImpl::getLastLocation); } @@ -58,53 +62,67 @@ public Task getLocationAvailability() { return scheduleTask((ReturningGoogleApiCall) LocationClientImpl::getLocationAvailability); } + @NonNull @Override - public Task removeLocationUpdates(LocationListener listener) { + public Task removeLocationUpdates(@NonNull LocationListener listener) { return scheduleTask((VoidReturningGoogleApiCall) (client) -> client.removeLocationUpdates(listener)); } + @NonNull @Override - public Task removeLocationUpdates(PendingIntent pendingIntent) { + public Task removeLocationUpdates(@NonNull PendingIntent pendingIntent) { return scheduleTask((VoidReturningGoogleApiCall) (client) -> client.removeLocationUpdates(pendingIntent)); } + @NonNull @Override - public Task requestLocationUpdates(LocationRequest request, LocationListener listener, Looper looper) { - return scheduleTask((VoidReturningGoogleApiCall) (client) -> client.requestLocationUpdates(request, listener, looper)); + public Task requestLocationUpdates(@NonNull LocationRequest request, @NonNull LocationListener listener, @Nullable Looper looper) { + Looper currentLooper = looper == null ? Looper.myLooper() : looper; + if (currentLooper == null) throw new IllegalStateException("looper is null and the calling thread has not called Looper.prepare()"); + return scheduleTask((VoidReturningGoogleApiCall) (client) -> client.requestLocationUpdates(request, listener, currentLooper)); } + @NonNull @Override - public Task requestLocationUpdates(LocationRequest request, Executor executor, LocationCallback callback) { + public Task requestLocationUpdates(@NonNull LocationRequest request, @NonNull Executor executor, @NonNull LocationCallback callback) { return scheduleTask((VoidReturningGoogleApiCall) (client) -> client.requestLocationUpdates(request, executor, callback)); } + @NonNull @Override - public Task requestLocationUpdates(LocationRequest request, Executor executor, LocationListener listener) { + public Task requestLocationUpdates(@NonNull LocationRequest request, @NonNull Executor executor, @NonNull LocationListener listener) { return scheduleTask((VoidReturningGoogleApiCall) (client) -> client.requestLocationUpdates(request, executor, listener)); } + @NonNull @Override - public Task requestLocationUpdates(LocationRequest request, LocationCallback callback, Looper looper) { - return scheduleTask((VoidReturningGoogleApiCall) (client) -> client.requestLocationUpdates(request, callback, looper)); + public Task requestLocationUpdates(@NonNull LocationRequest request, @NonNull LocationCallback callback, Looper looper) { + Looper currentLooper = looper == null ? Looper.myLooper() : looper; + if (currentLooper == null) throw new IllegalStateException("looper is null and the calling thread has not called Looper.prepare()"); + return scheduleTask((VoidReturningGoogleApiCall) (client) -> client.requestLocationUpdates(request, callback, currentLooper)); } + @NonNull @Override - public Task requestLocationUpdates(LocationRequest request, PendingIntent pendingIntent) { + public Task requestLocationUpdates(@NonNull LocationRequest request, @NonNull PendingIntent pendingIntent) { return scheduleTask((VoidReturningGoogleApiCall) (client) -> client.requestLocationUpdates(request, pendingIntent)); } + @NonNull @Override - public Task setMockLocation(Location location) { - return null; + public Task setMockLocation(@NonNull Location location) { + return scheduleTask((VoidReturningGoogleApiCall) (client) -> client.setMockLocation(location)); } + @NonNull @Override public Task setMockMode(boolean mockMode) { - return null; + return scheduleTask((VoidReturningGoogleApiCall) (client) -> client.setMockMode(mockMode)); } + @NonNull @Override - public Task removeLocationUpdates(LocationCallback callback) { + public Task removeLocationUpdates(@NonNull LocationCallback callback) { return scheduleTask((VoidReturningGoogleApiCall) (client) -> client.removeLocationUpdates(callback)); } } diff --git a/play-services-location/src/main/java/org/microg/gms/location/LocationClientImpl.java b/play-services-location/src/main/java/org/microg/gms/location/LocationClientImpl.java index 7b4b62ddf2..bb302fffc5 100644 --- a/play-services-location/src/main/java/org/microg/gms/location/LocationClientImpl.java +++ b/play-services-location/src/main/java/org/microg/gms/location/LocationClientImpl.java @@ -24,6 +24,7 @@ import android.os.RemoteException; import android.util.Log; +import androidx.annotation.NonNull; import com.google.android.gms.common.api.GoogleApiClient; import com.google.android.gms.location.GeofencingRequest; import com.google.android.gms.location.ILocationListener; @@ -113,7 +114,7 @@ public void requestLocationUpdates(LocationRequest request, PendingIntent pendin getServiceInterface().requestLocationUpdatesWithIntent(request, pendingIntent); } - public void requestLocationUpdates(LocationRequest request, LocationListener listener, Looper looper) throws RemoteException { + public void requestLocationUpdates(LocationRequest request, LocationListener listener, @NonNull Looper looper) throws RemoteException { final Handler handler = new Handler(looper); requestLocationUpdates(request, handler::post, listener); } @@ -135,7 +136,7 @@ public void cancel() throws RemoteException { getServiceInterface().requestLocationUpdatesWithPackage(request, listenerMap.get(listener), getContext().getPackageName()); } - public void requestLocationUpdates(LocationRequest request, LocationCallback callback, Looper looper) throws RemoteException { + public void requestLocationUpdates(LocationRequest request, LocationCallback callback, @NonNull Looper looper) throws RemoteException { final Handler handler = new Handler(looper); requestLocationUpdates(request, handler::post, callback); } From d18c6c18f2eb4c45f2712de4ab2e83187e6a0382 Mon Sep 17 00:00:00 2001 From: Marvin W Date: Wed, 13 Nov 2024 17:45:00 -0600 Subject: [PATCH 059/132] Location: Forward more information from fused provider --- .../provider/FusedLocationProviderService.kt | 7 +++++++ .../provider/IntentLocationProviderPreTiramisu.kt | 1 + .../microg/gms/location/manager/LocationManager.kt | 4 ++-- .../gms/location/manager/LocationRequestManager.kt | 12 ++++++------ .../org/microg/gms/location/manager/extensions.kt | 5 +++-- 5 files changed, 19 insertions(+), 10 deletions(-) diff --git a/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/provider/FusedLocationProviderService.kt b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/provider/FusedLocationProviderService.kt index 35d280d4a0..c95093e756 100644 --- a/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/provider/FusedLocationProviderService.kt +++ b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/provider/FusedLocationProviderService.kt @@ -5,6 +5,7 @@ package org.microg.gms.location.provider +import android.annotation.SuppressLint import android.app.PendingIntent import android.content.Intent import android.location.Criteria @@ -24,23 +25,29 @@ import kotlin.math.max class FusedLocationProviderService : IntentLocationProviderService() { override fun extractLocation(intent: Intent): Location? = LocationResult.extractResult(intent)?.lastLocation + @SuppressLint("MissingPermission") override fun requestIntentUpdated(currentRequest: ProviderRequestUnbundled?, pendingIntent: PendingIntent) { val intervalMillis = max(currentRequest?.interval ?: Long.MAX_VALUE, minIntervalMillis) val request = LocationRequest.Builder(intervalMillis) if (SDK_INT >= 31 && currentRequest != null) { request.setPriority(when { currentRequest.interval == LocationRequestCompat.PASSIVE_INTERVAL -> Priority.PRIORITY_PASSIVE + currentRequest.isLowPower -> Priority.PRIORITY_LOW_POWER currentRequest.quality == LocationRequestCompat.QUALITY_LOW_POWER -> Priority.PRIORITY_LOW_POWER currentRequest.quality == LocationRequestCompat.QUALITY_HIGH_ACCURACY -> Priority.PRIORITY_HIGH_ACCURACY else -> Priority.PRIORITY_BALANCED_POWER_ACCURACY }) request.setMaxUpdateDelayMillis(currentRequest.maxUpdateDelayMillis) + request.setWorkSource(currentRequest.workSource) } else { request.setPriority(when { currentRequest?.interval == LocationRequestCompat.PASSIVE_INTERVAL -> Priority.PRIORITY_PASSIVE else -> Priority.PRIORITY_BALANCED_POWER_ACCURACY }) } + if (SDK_INT >= 29 && currentRequest != null) { + request.setBypass(currentRequest.isLocationSettingsIgnored) + } val identity = Binder.clearCallingIdentity() try { LocationServices.getFusedLocationProviderClient(this).requestLocationUpdates(request.build(), pendingIntent) diff --git a/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/provider/IntentLocationProviderPreTiramisu.kt b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/provider/IntentLocationProviderPreTiramisu.kt index 9e4e6c2c80..39c8642601 100644 --- a/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/provider/IntentLocationProviderPreTiramisu.kt +++ b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/provider/IntentLocationProviderPreTiramisu.kt @@ -55,6 +55,7 @@ class IntentLocationProviderPreTiramisu : AbstractLocationProviderPreTiramisu { override fun dump(writer: PrintWriter) { writer.println("Enabled: $enabled") writer.println("Current request: $currentRequest") + if (SDK_INT >= 31) writer.println("Current work source: ${currentRequest?.workSource}") writer.println("Last reported: $lastReportedLocation") writer.println("Last report time: ${lastReportTime.formatRealtime()}") } diff --git a/play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/LocationManager.kt b/play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/LocationManager.kt index 36d1af5a4f..cbce54d560 100644 --- a/play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/LocationManager.kt +++ b/play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/LocationManager.kt @@ -80,7 +80,7 @@ class LocationManager(private val context: Context, override val lifecycle: Life } val permissionGranularity = context.granularityFromPermission(clientIdentity) var effectiveGranularity = getEffectiveGranularity(request.granularity, permissionGranularity) - if (effectiveGranularity == GRANULARITY_FINE && database.getForceCoarse(clientIdentity.packageName)) effectiveGranularity = GRANULARITY_COARSE + if (effectiveGranularity == GRANULARITY_FINE && database.getForceCoarse(clientIdentity.packageName) && !clientIdentity.isSelfUser()) effectiveGranularity = GRANULARITY_COARSE val returnedLocation = if (effectiveGranularity > permissionGranularity) { // No last location available at requested granularity due to lack of permission null @@ -98,7 +98,7 @@ class LocationManager(private val context: Context, override val lifecycle: Life processedLocation } } - database.noteAppLocation(clientIdentity.packageName, returnedLocation) + if (!clientIdentity.isSelfUser()) database.noteAppLocation(clientIdentity.packageName, returnedLocation) return returnedLocation?.let { Location(it).apply { provider = "fused" } } } diff --git a/play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/LocationRequestManager.kt b/play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/LocationRequestManager.kt index 5e25af8763..2d22bd381f 100644 --- a/play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/LocationRequestManager.kt +++ b/play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/LocationRequestManager.kt @@ -88,7 +88,7 @@ class LocationRequestManager(private val context: Context, override val lifecycl try { val startedHolder = holder?.update(callback, request) ?: LocationRequestHolder(context, clientIdentity, request, callback, null).start().also { var effectiveGranularity = it.effectiveGranularity - if (effectiveGranularity == GRANULARITY_FINE && database.getForceCoarse(it.clientIdentity.packageName)) effectiveGranularity = GRANULARITY_COARSE + if (effectiveGranularity == GRANULARITY_FINE && database.getForceCoarse(it.clientIdentity.packageName) && !clientIdentity.isSelfUser()) effectiveGranularity = GRANULARITY_COARSE val lastLocation = lastLocationCapsule.getLocation(effectiveGranularity, request.maxUpdateAgeMillis) if (lastLocation != null) it.processNewLocation(lastLocation) } @@ -121,7 +121,7 @@ class LocationRequestManager(private val context: Context, override val lifecycl pendingIntentRequests[pendingIntent] = LocationRequestHolder(context, clientIdentity, request, null, pendingIntent).start().also { cacheManager.add(it.asParcelable()) { it.pendingIntent == pendingIntent } var effectiveGranularity = it.effectiveGranularity - if (effectiveGranularity == GRANULARITY_FINE && database.getForceCoarse(it.clientIdentity.packageName)) effectiveGranularity = GRANULARITY_COARSE + if (effectiveGranularity == GRANULARITY_FINE && database.getForceCoarse(it.clientIdentity.packageName) && !clientIdentity.isSelfUser()) effectiveGranularity = GRANULARITY_COARSE val lastLocation = lastLocationCapsule.getLocation(effectiveGranularity, request.maxUpdateAgeMillis) if (lastLocation != null) it.processNewLocation(lastLocation) } @@ -147,11 +147,11 @@ class LocationRequestManager(private val context: Context, override val lifecycl for ((key, holder) in map) { try { var effectiveGranularity = holder.effectiveGranularity - if (effectiveGranularity == GRANULARITY_FINE && database.getForceCoarse(holder.clientIdentity.packageName)) effectiveGranularity = GRANULARITY_COARSE + if (effectiveGranularity == GRANULARITY_FINE && database.getForceCoarse(holder.clientIdentity.packageName) && !holder.clientIdentity.isSelfUser()) effectiveGranularity = GRANULARITY_COARSE val location = lastLocationCapsule.getLocation(effectiveGranularity, holder.maxUpdateDelayMillis) postProcessor.process(location, effectiveGranularity, holder.clientIdentity.isGoogle(context))?.let { if (holder.processNewLocation(it)) { - database.noteAppLocation(holder.clientIdentity.packageName, it) + if (!holder.clientIdentity.isSelfUser()) database.noteAppLocation(holder.clientIdentity.packageName, it) updated.add(key) } } @@ -420,7 +420,7 @@ class LocationRequestManager(private val context: Context, override val lifecycl get() = request.maxUpdates - updates val timePendingMillis: Long get() = request.durationMillis - (SystemClock.elapsedRealtime() - start) - var workSource: WorkSource = WorkSource(request.workSource).also { WorkSourceUtil.add(it, clientIdentity.uid, clientIdentity.packageName) } + var workSource: WorkSource = WorkSource(request.workSource).also { if (!clientIdentity.isSelfUser()) WorkSourceUtil.add(it, clientIdentity.uid, clientIdentity.packageName) } private set val effectiveHighPower: Boolean get() = request.intervalMillis < 60000 || effectivePriority == PRIORITY_HIGH_ACCURACY @@ -431,7 +431,7 @@ class LocationRequestManager(private val context: Context, override val lifecycl this.request = request this.start = SystemClock.elapsedRealtime() this.updates = 0 - this.workSource = WorkSource(request.workSource).also { WorkSourceUtil.add(it, clientIdentity.uid, clientIdentity.packageName) } + this.workSource = WorkSource(request.workSource).also { if (!clientIdentity.isSelfUser()) WorkSourceUtil.add(it, clientIdentity.uid, clientIdentity.packageName) } if (changedGranularity) { if (!context.checkAppOpForEffectiveGranularity(clientIdentity, effectiveGranularity)) throw RuntimeException("Lack of permission") } diff --git a/play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/extensions.kt b/play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/extensions.kt index 7799f5af26..3f51ce91d5 100644 --- a/play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/extensions.kt +++ b/play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/extensions.kt @@ -68,6 +68,7 @@ fun ILocationCallback.redirectCancel(fusedCallback: IFusedLocationProviderCallba fun ClientIdentity.isGoogle(context: Context) = PackageUtils.isGooglePackage(context, packageName) fun ClientIdentity.isSelfProcess() = pid == Process.myPid() +fun ClientIdentity.isSelfUser() = uid == Process.myUid() fun Context.granularityFromPermission(clientIdentity: ClientIdentity): @Granularity Int = when (PackageManager.PERMISSION_GRANTED) { packageManager.checkPermission(Manifest.permission.ACCESS_FINE_LOCATION, clientIdentity.packageName) -> Granularity.GRANULARITY_FINE @@ -77,13 +78,13 @@ fun Context.granularityFromPermission(clientIdentity: ClientIdentity): @Granular fun LocationRequest.verify(context: Context, clientIdentity: ClientIdentity) { GranularityUtil.checkValidGranularity(granularity) - if (isBypass) { + if (isBypass && !clientIdentity.isSelfUser()) { val permission = if (SDK_INT >= 33) "android.permission.LOCATION_BYPASS" else Manifest.permission.WRITE_SECURE_SETTINGS if (context.checkPermission(permission, clientIdentity.pid, clientIdentity.uid) != PackageManager.PERMISSION_GRANTED) { throw SecurityException("Caller must hold $permission for location bypass") } } - if (impersonation != null) { + if (impersonation != null && !clientIdentity.isSelfUser()) { Log.w(TAG, "${clientIdentity.packageName} wants to impersonate ${impersonation!!.packageName}. Ignoring.") } From cd2c7289b74d43811b12ca08f6e09a77235cebbe Mon Sep 17 00:00:00 2001 From: Fynn Godau Date: Tue, 1 Oct 2024 19:27:54 +0200 Subject: [PATCH 060/132] Fix InfoWindow when provided view already has parent Since views cannot have two parents at the same time, we encounter a crash when a client provides us with a view to be used as an InfoWindow which already has a parent. The simple solution is to remove the parent. --- .../org/microg/gms/maps/mapbox/model/InfoWindow.kt | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/InfoWindow.kt b/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/InfoWindow.kt index 16f0471d8b..b9514a17d8 100644 --- a/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/InfoWindow.kt +++ b/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/InfoWindow.kt @@ -39,7 +39,12 @@ import kotlin.math.* */ fun IInfoWindowAdapter.getInfoWindowViewFor(marker: IMarkerDelegate, mapContext: MapContext): View? { - getInfoWindow(marker).unwrap()?.let { return it } + getInfoWindow(marker).unwrap()?.let { view -> + return view.apply { + // Remove any previous parents mistakenly added by the client + parent?.let { (it as ViewManager).removeView(this) } + } + } getInfoContents(marker).unwrap()?.let { view -> // Detach from previous BubbleLayout parent, if exists @@ -95,7 +100,7 @@ class InfoWindow internal constructor( /** * Close this [InfoWindow] if it is visible, otherwise calling this will do nothing. * - * @param silent `OnInfoWindowCloseListener` is only called if `silent` is not `false` + * @param silent `OnInfoWindowCloseListener` is only called if `silent` is `false` */ fun close(silent: Boolean = false) { if (isVisible) { From ec966bec0c77a2a73d836ebea4583686e5e82d8b Mon Sep 17 00:00:00 2001 From: lgjint <78463565+lgjint@users.noreply.github.com> Date: Mon, 11 Nov 2024 13:56:50 +0800 Subject: [PATCH 061/132] Fix microG settings icon not following system theme --- .../src/main/res/drawable-v21/ic_app_settings_system.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/play-services-core/src/main/res/drawable-v21/ic_app_settings_system.xml b/play-services-core/src/main/res/drawable-v21/ic_app_settings_system.xml index 3e10efc387..cfae14c650 100644 --- a/play-services-core/src/main/res/drawable-v21/ic_app_settings_system.xml +++ b/play-services-core/src/main/res/drawable-v21/ic_app_settings_system.xml @@ -7,7 +7,8 @@ android:width="24dp" android:height="24dp" android:viewportWidth="225.27" - android:viewportHeight="225.27"> + android:viewportHeight="225.27" + android:tint="?android:attr/colorControlNormal"> Date: Sun, 17 Nov 2024 06:06:23 +0800 Subject: [PATCH 062/132] HMS Maps: Optimize the phenomenon of Uber route drift. (#2608) --- .../org/microg/gms/maps/hms/GoogleMap.kt | 41 +++++++++++++------ 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/play-services-maps/core/hms/src/main/kotlin/org/microg/gms/maps/hms/GoogleMap.kt b/play-services-maps/core/hms/src/main/kotlin/org/microg/gms/maps/hms/GoogleMap.kt index aa5911f805..fe7704d2e4 100644 --- a/play-services-maps/core/hms/src/main/kotlin/org/microg/gms/maps/hms/GoogleMap.kt +++ b/play-services-maps/core/hms/src/main/kotlin/org/microg/gms/maps/hms/GoogleMap.kt @@ -18,8 +18,6 @@ import android.widget.LinearLayout import androidx.collection.LongSparseArray import com.google.android.gms.dynamic.IObjectWrapper import com.google.android.gms.dynamic.unwrap -import com.google.android.gms.maps.GoogleMap.MAP_TYPE_HYBRID -import com.google.android.gms.maps.GoogleMap.MAP_TYPE_SATELLITE import com.google.android.gms.maps.GoogleMap.MAP_TYPE_TERRAIN import com.google.android.gms.maps.GoogleMapOptions import com.google.android.gms.maps.internal.* @@ -87,7 +85,7 @@ class GoogleMapImpl(private val context: Context, var options: GoogleMapOptions) private var storedMapType: Int = options.mapType val waitingCameraUpdates = mutableListOf() - var locationEnabled: Boolean = false + private val controlLayerRun = Runnable { refreshContainerLayer(false) } private var markerId = 0L val markers = mutableMapOf() @@ -306,8 +304,9 @@ class GoogleMapImpl(private val context: Context, var options: GoogleMapOptions) internalOnInitializedCallbackList.add(it.getMapReadyCallback()) } - override fun getProjection(): IProjectionDelegate = - map?.projection?.let { ProjectionImpl(it) } ?: DummyProjection() + override fun getProjection(): IProjectionDelegate { + return map?.projection?.let { ProjectionImpl(it) } ?: DummyProjection() + } override fun setOnCameraChangeListener(listener: IOnCameraChangeListener?) = afterInitialize { Log.d(TAG, "setOnCameraChangeListener"); @@ -493,6 +492,7 @@ class GoogleMapImpl(private val context: Context, var options: GoogleMapOptions) cameraMoveStartedListener = listener hmap.setOnCameraMoveStartedListener { try { + Log.d(TAG, "setCameraMoveStartedListener: ") cameraMoveStartedListener?.onCameraMoveStarted(it) } catch (e: Exception) { Log.w(TAG, e) @@ -505,15 +505,12 @@ class GoogleMapImpl(private val context: Context, var options: GoogleMapOptions) cameraMoveListener = listener it.setOnCameraMoveListener { try { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - if(mapView != null){ - if(mapView!!.parent != null){ - mapView!!.parent.onDescendantInvalidated(mapView!!,mapView!!) - } - } - } + Log.d(TAG, "setOnCameraMoveListener: ") + view.removeCallbacks(controlLayerRun) + refreshContainerLayer(true) cameraMoveListener?.onCameraMove() cameraChangeListener?.onCameraChange(map?.cameraPosition?.toGms()) + view.postDelayed(controlLayerRun, 200) } catch (e: Exception) { Log.w(TAG, e) } @@ -525,6 +522,7 @@ class GoogleMapImpl(private val context: Context, var options: GoogleMapOptions) cameraMoveCanceledListener = listener it.setOnCameraMoveCanceledListener { try { + Log.d(TAG, "setOnCameraMoveCanceledListener: ") cameraMoveCanceledListener?.onCameraMoveCanceled() } catch (e: Exception) { Log.w(TAG, e) @@ -537,6 +535,7 @@ class GoogleMapImpl(private val context: Context, var options: GoogleMapOptions) cameraIdleListener = listener it.setOnCameraIdleListener { try { + Log.d(TAG, "setOnCameraIdleListener: ") cameraIdleListener?.onCameraIdle() } catch (e: Exception) { Log.w(TAG, e) @@ -804,6 +803,24 @@ class GoogleMapImpl(private val context: Context, var options: GoogleMapOptions) } } + private fun refreshContainerLayer(hide: Boolean = false) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + view.onDescendantInvalidated(mapView!!, mapView!!) + } + val parentView = view.parent?.parent + if (parentView != null) { + if (parentView is ViewGroup) { + for (i in 0 until parentView.childCount) { + val viewChild = parentView.getChildAt(i) + // Uber is prone to route drift, so here we hide the corresponding layer + if (viewChild::class.qualifiedName == "com.ubercab.android.map.fu") { + viewChild.visibility = if (hide) View.INVISIBLE else View.VISIBLE + } + } + } + } + } + override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean = if (super.onTransact(code, data, reply, flags)) { Log.d(TAG, "onTransact: $code, $data, $flags") From e1fa6609d16e17a15de87be56fe99ab4197420ac Mon Sep 17 00:00:00 2001 From: DaVinci9196 <150454414+DaVinci9196@users.noreply.github.com> Date: Sun, 17 Nov 2024 06:11:47 +0800 Subject: [PATCH 063/132] HMS Maps: Fix CameraPosition NullPointerException (#2593) --- .../hms/src/main/kotlin/org/microg/gms/maps/hms/GoogleMap.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/play-services-maps/core/hms/src/main/kotlin/org/microg/gms/maps/hms/GoogleMap.kt b/play-services-maps/core/hms/src/main/kotlin/org/microg/gms/maps/hms/GoogleMap.kt index fe7704d2e4..c8fd5051fb 100644 --- a/play-services-maps/core/hms/src/main/kotlin/org/microg/gms/maps/hms/GoogleMap.kt +++ b/play-services-maps/core/hms/src/main/kotlin/org/microg/gms/maps/hms/GoogleMap.kt @@ -100,7 +100,9 @@ class GoogleMapImpl(private val context: Context, var options: GoogleMapOptions) this.view = object : FrameLayout(mapContext) {} } - override fun getCameraPosition(): CameraPosition? = map?.cameraPosition?.toGms() + override fun getCameraPosition(): CameraPosition { + return map?.cameraPosition?.toGms() ?: CameraPosition(LatLng(0.0, 0.0), 0f, 0f, 0f) + } override fun getMaxZoomLevel(): Float = toHmsZoom(map?.maxZoomLevel ?: 18.toFloat()) override fun getMinZoomLevel(): Float = toHmsZoom(map?.minZoomLevel ?: 3.toFloat()) From df2f4be715be19d8f6021af23c621a28870c23ef Mon Sep 17 00:00:00 2001 From: DaVinci9196 <150454414+DaVinci9196@users.noreply.github.com> Date: Sun, 17 Nov 2024 06:13:08 +0800 Subject: [PATCH 064/132] Auth: Fix sign-in page in landscape rotation (#2585) --- .../gms/auth/signin/AuthSignInActivity.kt | 16 ++- .../main/res/layout-w600dp/signin_picker.xml | 108 ++++++++++++++++++ 2 files changed, 119 insertions(+), 5 deletions(-) create mode 100644 play-services-core/src/main/res/layout-w600dp/signin_picker.xml diff --git a/play-services-core/src/main/kotlin/org/microg/gms/auth/signin/AuthSignInActivity.kt b/play-services-core/src/main/kotlin/org/microg/gms/auth/signin/AuthSignInActivity.kt index edf566356c..4ddfd56bd1 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/auth/signin/AuthSignInActivity.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/auth/signin/AuthSignInActivity.kt @@ -8,6 +8,7 @@ package org.microg.gms.auth.signin import android.accounts.Account import android.accounts.AccountManager import android.content.Intent +import android.content.res.Configuration import android.graphics.Bitmap import android.os.Bundle import android.util.Log @@ -24,7 +25,6 @@ import com.google.android.gms.auth.api.identity.SignInCredential import com.google.android.gms.auth.api.signin.GoogleSignInAccount import com.google.android.gms.auth.api.signin.SignInAccount import com.google.android.gms.auth.api.signin.internal.SignInConfiguration -import com.google.android.gms.common.Scopes import com.google.android.gms.common.api.CommonStatusCodes import com.google.android.gms.common.api.Status import com.google.android.gms.common.internal.safeparcel.SafeParcelableSerializer @@ -36,7 +36,6 @@ import org.microg.gms.auth.AuthConstants import org.microg.gms.auth.AuthConstants.DEFAULT_ACCOUNT import org.microg.gms.auth.AuthConstants.DEFAULT_ACCOUNT_TYPE import org.microg.gms.auth.login.LoginActivity -import org.microg.gms.people.DatabaseHelper import org.microg.gms.people.PeopleManager import org.microg.gms.utils.getApplicationLabel @@ -53,8 +52,6 @@ class AuthSignInActivity : AppCompatActivity() { intent?.extras?.also { it.classLoader = SignInConfiguration::class.java.classLoader }?.getParcelable("config") }.getOrNull() - private val Int.px: Int get() = (this * resources.displayMetrics.density).toInt() - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setResult(CommonStatusCodes.CANCELED) @@ -64,8 +61,12 @@ class AuthSignInActivity : AppCompatActivity() { val packageName = config?.packageName if (packageName == null || (packageName != callingActivity?.packageName && callingActivity?.packageName != this.packageName)) return finishResult(CommonStatusCodes.DEVELOPER_ERROR, "package name mismatch") - val accountManager = getSystemService() ?: return finishResult(CommonStatusCodes.INTERNAL_ERROR, "No account manager") + initView() + } + + private fun initView() { + val accountManager = getSystemService() ?: return finishResult(CommonStatusCodes.INTERNAL_ERROR, "No account manager") val accounts = accountManager.getAccountsByType(DEFAULT_ACCOUNT_TYPE) if (accounts.isNotEmpty()) { val account = config?.options?.account @@ -230,4 +231,9 @@ class AuthSignInActivity : AppCompatActivity() { } } } + + override fun onConfigurationChanged(newConfig: Configuration) { + super.onConfigurationChanged(newConfig) + initView() + } } \ No newline at end of file diff --git a/play-services-core/src/main/res/layout-w600dp/signin_picker.xml b/play-services-core/src/main/res/layout-w600dp/signin_picker.xml new file mode 100644 index 0000000000..87a2038abb --- /dev/null +++ b/play-services-core/src/main/res/layout-w600dp/signin_picker.xml @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From 247f1dbaac5f396b77f0d6c170a608af368c8068 Mon Sep 17 00:00:00 2001 From: davinci9196 Date: Wed, 16 Oct 2024 10:03:30 +0800 Subject: [PATCH 065/132] Fixed the issue that when creating a child account in Family Link, clicking Continue does not respond --- .../org/microg/gms/common/KnownGooglePackages.kt | 6 ++++++ .../microg/gms/accountsettings/ui/MainActivity.kt | 13 +++++++++++-- .../org/microg/gms/accountsettings/ui/extensions.kt | 1 + 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/play-services-base/core/src/main/kotlin/org/microg/gms/common/KnownGooglePackages.kt b/play-services-base/core/src/main/kotlin/org/microg/gms/common/KnownGooglePackages.kt index e0a644d762..47e812eb9f 100644 --- a/play-services-base/core/src/main/kotlin/org/microg/gms/common/KnownGooglePackages.kt +++ b/play-services-base/core/src/main/kotlin/org/microg/gms/common/KnownGooglePackages.kt @@ -179,6 +179,12 @@ private val KNOWN_GOOGLE_PACKAGES = mapOf( PackageAndCertHash("com.google.android.apps.tasks", SHA256, "99f6cc5308e6f3318a3bf168bf106d5b5defe2b4b9c561e5ddd7924a7a2ba1e2"), setOf(ACCOUNT, AUTH, OWNER) ), + + // Google familylink + Pair( + PackageAndCertHash("com.google.android.apps.kids.familylink", SHA256, "6b58bb84c1c6d081d950448ff5c051a34769d7fd8d415452c86efeb808716c0e"), + setOf(ACCOUNT, AUTH, OWNER) + ), ) fun isGooglePackage(pkg: PackageAndCertHash): Boolean { diff --git a/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/MainActivity.kt b/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/MainActivity.kt index 5ba02536c7..7040c9dbed 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/MainActivity.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/MainActivity.kt @@ -80,6 +80,7 @@ private val SCREEN_ID_TO_URL = hashMapOf( 530 to "https://fit.google.com/privacy/settings", 547 to "https://myactivity.google.com/product/search", 562 to "https://myaccount.google.com/yourdata/youtube", + 580 to "https://families.google.com/kidonboarding", 10003 to "https://myaccount.google.com/personal-info", 10004 to "https://myaccount.google.com/data-and-privacy", 10005 to "https://myaccount.google.com/people-and-sharing", @@ -112,7 +113,8 @@ private val ALLOWED_WEB_PREFIXES = setOf( "https://policies.google.com/", "https://fit.google.com/privacy/settings", "https://maps.google.com/maps/timeline", - "https://myadcenter.google.com/controls" + "https://myadcenter.google.com/controls", + "https://families.google.com/kidonboarding" ) private val ACTION_TO_SCREEN_ID = hashMapOf( @@ -134,6 +136,7 @@ class MainActivity : AppCompatActivity() { val screenId = intent?.getIntExtra(EXTRA_SCREEN_ID, -1).takeIf { it != -1 } ?: ACTION_TO_SCREEN_ID[intent.action] ?: 1 val product = intent?.getStringExtra(EXTRA_SCREEN_MY_ACTIVITY_PRODUCT) + val kidOnboardingParams = intent?.getStringExtra(EXTRA_SCREEN_KID_ONBOARDING_PARAMS) val screenOptions = intent.extras?.keySet().orEmpty() .filter { it.startsWith(EXTRA_SCREEN_OPTIONS_PREFIX) } @@ -154,7 +157,13 @@ class MainActivity : AppCompatActivity() { } if (screenId in SCREEN_ID_TO_URL) { - val screenUrl = SCREEN_ID_TO_URL[screenId]?.run { if (screenId == 547 && !product.isNullOrEmpty()) { replace("search", product) } else { this } } + val screenUrl = SCREEN_ID_TO_URL[screenId]?.run { + if (screenId == 547 && !product.isNullOrEmpty()) { + replace("search", product) + } else if (screenId == 580 && !kidOnboardingParams.isNullOrEmpty()){ + "$this?params=$kidOnboardingParams" + } else { this } + } val layout = RelativeLayout(this) layout.addView(ProgressBar(this).apply { layoutParams = RelativeLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT).apply { diff --git a/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/extensions.kt b/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/extensions.kt index ddf9f1fb60..30a3b01be9 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/extensions.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/extensions.kt @@ -20,5 +20,6 @@ const val EXTRA_FALLBACK_URL = "extra.fallbackUrl" const val EXTRA_FALLBACK_AUTH = "extra.fallbackAuth" const val EXTRA_THEME_CHOICE = "extra.themeChoice" const val EXTRA_SCREEN_MY_ACTIVITY_PRODUCT = "extra.screen.myactivityProduct" +const val EXTRA_SCREEN_KID_ONBOARDING_PARAMS = "extra.screen.kidOnboardingParams" const val OPTION_SCREEN_FLAVOR = "screenFlavor" \ No newline at end of file From 94ba8987f14c80e300db525bfc748cec0e18b2ca Mon Sep 17 00:00:00 2001 From: DaVinci9196 <150454414+DaVinci9196@users.noreply.github.com> Date: Sun, 17 Nov 2024 06:40:10 +0800 Subject: [PATCH 066/132] HMS Maps: Fix NullPointerException caused by idle listener (#2603) Co-authored-by: Marvin W --- .../main/kotlin/org/microg/gms/maps/hms/GoogleMap.kt | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/play-services-maps/core/hms/src/main/kotlin/org/microg/gms/maps/hms/GoogleMap.kt b/play-services-maps/core/hms/src/main/kotlin/org/microg/gms/maps/hms/GoogleMap.kt index c8fd5051fb..aef3dd8e36 100644 --- a/play-services-maps/core/hms/src/main/kotlin/org/microg/gms/maps/hms/GoogleMap.kt +++ b/play-services-maps/core/hms/src/main/kotlin/org/microg/gms/maps/hms/GoogleMap.kt @@ -535,14 +535,6 @@ class GoogleMapImpl(private val context: Context, var options: GoogleMapOptions) override fun setCameraIdleListener(listener: IOnCameraIdleListener?) = afterInitialize { Log.d(TAG, "onCameraIdle: successful") cameraIdleListener = listener - it.setOnCameraIdleListener { - try { - Log.d(TAG, "setOnCameraIdleListener: ") - cameraIdleListener?.onCameraIdle() - } catch (e: Exception) { - Log.w(TAG, e) - } - } } override fun getTestingHelper(): IObjectWrapper? { @@ -620,14 +612,14 @@ class GoogleMapImpl(private val context: Context, var options: GoogleMapOptions) } catch (e: Exception) { Log.w(TAG, e) } - } - map.setOnCameraIdleListener { + try { cameraIdleListener?.onCameraIdle() } catch (e: Exception) { Log.w(TAG, e) } } + map.setOnCameraMoveListener { try { cameraMoveListener?.onCameraMove() From 187910b0782720967e3f7c6d6c39dcc549846a5e Mon Sep 17 00:00:00 2001 From: DaVinci9196 <150454414+DaVinci9196@users.noreply.github.com> Date: Sun, 17 Nov 2024 06:43:53 +0800 Subject: [PATCH 067/132] Auth: Make login activity single task (#2578) --- play-services-core/src/main/AndroidManifest.xml | 1 + .../main/java/org/microg/gms/auth/login/LoginActivity.java | 7 ++++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/play-services-core/src/main/AndroidManifest.xml b/play-services-core/src/main/AndroidManifest.xml index d444183602..0c606960c5 100644 --- a/play-services-core/src/main/AndroidManifest.xml +++ b/play-services-core/src/main/AndroidManifest.xml @@ -434,6 +434,7 @@ android:configChanges="keyboardHidden|keyboard|orientation|screenSize" android:exported="true" android:process=":ui" + android:launchMode="singleTask" android:theme="@style/Theme.LoginBlue"> diff --git a/play-services-core/src/main/java/org/microg/gms/auth/login/LoginActivity.java b/play-services-core/src/main/java/org/microg/gms/auth/login/LoginActivity.java index dd9a1be6ac..831c4751f4 100644 --- a/play-services-core/src/main/java/org/microg/gms/auth/login/LoginActivity.java +++ b/play-services-core/src/main/java/org/microg/gms/auth/login/LoginActivity.java @@ -186,11 +186,12 @@ protected void onBackButtonClicked() { } public void loginCanceled() { + Log.d(TAG, "loginCanceled: "); setResult(RESULT_CANCELED); if (response != null) { response.onError(AccountManager.ERROR_CODE_CANCELED, "Canceled"); } - finish(); + if (SDK_INT >= LOLLIPOP) { finishAndRemoveTask(); } else finish(); } @Override @@ -383,7 +384,7 @@ public void onResponse(AuthResponse response) { } checkin(true); returnSuccessResponse(account); - finish(); + if (SDK_INT >= LOLLIPOP) { finishAndRemoveTask(); } else finish(); } @Override @@ -669,7 +670,7 @@ public final void showView() { @JavascriptInterface public final void skipLogin() { Log.d(TAG, "JSBridge: skipLogin"); - finish(); + loginCanceled(); } @JavascriptInterface From 35ab8af3adba5cc023a775a49fc69851fab87d8c Mon Sep 17 00:00:00 2001 From: DaVinci9196 <150454414+DaVinci9196@users.noreply.github.com> Date: Sun, 17 Nov 2024 06:54:35 +0800 Subject: [PATCH 068/132] Add support for DynamicLinkData on app invite links. (#2519) --- firebase-dynamic-links/build.gradle | 1 + .../internal/DynamicLinkData.java | 3 +++ .../gms/appinivite/AppInviteActivity.kt | 24 ++++++++++++++++--- 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/firebase-dynamic-links/build.gradle b/firebase-dynamic-links/build.gradle index 76f298c8e6..7e10a722e7 100644 --- a/firebase-dynamic-links/build.gradle +++ b/firebase-dynamic-links/build.gradle @@ -42,6 +42,7 @@ dependencies { // api project(':firebase-common-ktx') // api project(':firebase-components') api 'org.jetbrains.kotlin:kotlin-stdlib:1.7.10' + annotationProcessor project(':safe-parcel-processor') } apply from: '../gradle/publish-android.gradle' diff --git a/firebase-dynamic-links/src/main/java/com/google/firebase/dynamiclinks/internal/DynamicLinkData.java b/firebase-dynamic-links/src/main/java/com/google/firebase/dynamiclinks/internal/DynamicLinkData.java index 8ae5d431f6..a2b0e42dbf 100644 --- a/firebase-dynamic-links/src/main/java/com/google/firebase/dynamiclinks/internal/DynamicLinkData.java +++ b/firebase-dynamic-links/src/main/java/com/google/firebase/dynamiclinks/internal/DynamicLinkData.java @@ -10,12 +10,14 @@ import android.os.Parcel; import androidx.annotation.NonNull; import com.google.android.gms.common.internal.safeparcel.AbstractSafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelable; import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter; import android.os.Bundle; import android.net.Uri; import org.microg.gms.utils.ToStringHelper; +@SafeParcelable.Class public class DynamicLinkData extends AbstractSafeParcelable { @Field(1) public final String dynamicLink; @@ -35,6 +37,7 @@ public class DynamicLinkData extends AbstractSafeParcelable { @Field(6) public final Uri redirectUrl; + @Constructor public DynamicLinkData(@Param(1) String dynamicLink, @Param(2) String deepLink, @Param(3) int minVersion, @Param(4) long clickTimestamp, @Param(5) Bundle extensionBundle, @Param(6) Uri redirectUrl) { this.dynamicLink = dynamicLink; this.deepLink = deepLink; diff --git a/play-services-appinvite/core/src/main/kotlin/org/microg/gms/appinivite/AppInviteActivity.kt b/play-services-appinvite/core/src/main/kotlin/org/microg/gms/appinivite/AppInviteActivity.kt index 86b3f86f4e..074a891262 100644 --- a/play-services-appinvite/core/src/main/kotlin/org/microg/gms/appinivite/AppInviteActivity.kt +++ b/play-services-appinvite/core/src/main/kotlin/org/microg/gms/appinivite/AppInviteActivity.kt @@ -21,6 +21,8 @@ import androidx.core.view.setPadding import androidx.lifecycle.lifecycleScope import com.android.volley.* import com.android.volley.toolbox.Volley +import com.google.android.gms.common.internal.safeparcel.SafeParcelableSerializer +import com.google.firebase.dynamiclinks.internal.DynamicLinkData import com.squareup.wire.Message import com.squareup.wire.ProtoAdapter import kotlinx.coroutines.CompletableDeferred @@ -28,7 +30,6 @@ import okio.ByteString.Companion.decodeHex import org.microg.gms.appinvite.* import org.microg.gms.common.Constants import org.microg.gms.utils.singleInstanceOf -import org.microg.gms.utils.toBase64 import java.util.* private const val TAG = "AppInviteActivity" @@ -37,6 +38,7 @@ private const val APPINVITE_DEEP_LINK = "com.google.android.gms.appinvite.DEEP_L private const val APPINVITE_INVITATION_ID = "com.google.android.gms.appinvite.INVITATION_ID" private const val APPINVITE_OPENED_FROM_PLAY_STORE = "com.google.android.gms.appinvite.OPENED_FROM_PLAY_STORE" private const val APPINVITE_REFERRAL_BUNDLE = "com.google.android.gms.appinvite.REFERRAL_BUNDLE" +private const val DYNAMIC_LINK_DATA = "com.google.firebase.dynamiclinks.DYNAMIC_LINK_DATA" class AppInviteActivity : AppCompatActivity() { private val queue by lazy { singleInstanceOf { Volley.newRequestQueue(applicationContext) } } @@ -71,6 +73,8 @@ class AppInviteActivity : AppCompatActivity() { } private fun open(appInviteLink: MutateAppInviteLinkResponse) { + val dynamicLinkData = DynamicLinkData(appInviteLink.metadata?.info?.url, appInviteLink.data_?.intentData, + (appInviteLink.data_?.app?.minAppVersion ?: 0).toInt(), System.currentTimeMillis(), null, null) val intent = Intent(Intent.ACTION_VIEW).apply { addCategory(Intent.CATEGORY_DEFAULT) data = appInviteLink.data_?.intentData?.let { Uri.parse(it) } @@ -83,17 +87,31 @@ class AppInviteActivity : AppCompatActivity() { APPINVITE_OPENED_FROM_PLAY_STORE to false ) ) + putExtra(DYNAMIC_LINK_DATA, SafeParcelableSerializer.serializeToBytes(dynamicLinkData)) } val fallbackIntent = Intent(Intent.ACTION_VIEW).apply { addCategory(Intent.CATEGORY_DEFAULT) data = appInviteLink.data_?.fallbackUrl?.let { Uri.parse(it) } } val installedVersionCode = runCatching { - intent.resolveActivity(packageManager)?.let { - PackageInfoCompat.getLongVersionCode(packageManager.getPackageInfo(it.packageName, 0)) + if (appInviteLink.data_?.packageName != null) { + PackageInfoCompat.getLongVersionCode(packageManager.getPackageInfo(appInviteLink.data_.packageName, 0)) + } else { + null } }.getOrNull() if (installedVersionCode != null && (appInviteLink.data_?.app?.minAppVersion == null || installedVersionCode >= appInviteLink.data_.app.minAppVersion)) { + val componentName = intent.resolveActivity(packageManager) + if (componentName == null) { + Log.w(TAG, "open resolve activity is null") + if (appInviteLink.data_?.packageName != null) { + val intentLaunch = + packageManager.getLaunchIntentForPackage(appInviteLink.data_.packageName) + if (intentLaunch != null) { + intent.setComponent(intentLaunch.component) + } + } + } startActivity(intent) finish() } else { From 564a08d63357445f64b38632fa878f437da3a532 Mon Sep 17 00:00:00 2001 From: DaVinci9196 <150454414+DaVinci9196@users.noreply.github.com> Date: Sun, 17 Nov 2024 06:56:53 +0800 Subject: [PATCH 069/132] Add more dummies to TapAndPay service (#2577) --- .../microg/gms/tapandpay/TapAndPayService.kt | 28 +++++++++++++++++-- .../tapandpay/internal/ITapAndPayService.aidl | 1 + .../internal/ITapAndPayServiceCallbacks.aidl | 3 +- .../gms/tapandpay/issuer/TokenInfo.aidl | 3 ++ .../gms/tapandpay/issuer/TokenInfo.java | 22 +++++++++++++++ 5 files changed, 54 insertions(+), 3 deletions(-) create mode 100644 play-services-tapandpay/src/main/aidl/com/google/android/gms/tapandpay/issuer/TokenInfo.aidl create mode 100644 play-services-tapandpay/src/main/java/com/google/android/gms/tapandpay/issuer/TokenInfo.java diff --git a/play-services-tapandpay/core/src/main/kotlin/org/microg/gms/tapandpay/TapAndPayService.kt b/play-services-tapandpay/core/src/main/kotlin/org/microg/gms/tapandpay/TapAndPayService.kt index 965f9e3db7..b8858a6ab1 100644 --- a/play-services-tapandpay/core/src/main/kotlin/org/microg/gms/tapandpay/TapAndPayService.kt +++ b/play-services-tapandpay/core/src/main/kotlin/org/microg/gms/tapandpay/TapAndPayService.kt @@ -5,7 +5,6 @@ package org.microg.gms.tapandpay import android.os.Parcel -import android.os.RemoteException import android.util.Log import android.util.SparseArray import com.google.android.gms.common.Feature @@ -36,20 +35,38 @@ class TapAndPayService : BaseService(TAG, GmsService.TAP_AND_PAY) { features = arrayOf( Feature("tapandpay", 1), Feature("tapandpay_account_linking", 1), + Feature("tapandpay_add_service_listener", 1), Feature("tapandpay_block_payment_cards", 1), Feature("tapandpay_check_contactless_eligibility", 1), Feature("tapandpay_dismiss_quick_access_wallet", 1), + Feature("tapandpay_enable_secure_keyguard", 1), + Feature("tapandpay_felica_tos", 1), + Feature("tapandpay_get_active_wallet_infos", 1L), Feature("tapandpay_get_all_cards_for_account", 1), Feature("tapandpay_get_contactless_setup_configuration", 1), + Feature("tapandpay_get_environment", 1L), Feature("tapandpay_get_last_attestation_result", 1), - Feature("tapandpay_get_token_pan", 1), + Feature("tapandpay_get_stable_hardware_id", 1L), + Feature("tapandpay_get_token_details", 1L), + Feature("tapandpay_get_token_status", 1L), Feature("tapandpay_global_actions", 1), + Feature("tapandpay_has_eligible_tokenization_target", 1L), Feature("tapandpay_issuer_api", 2), Feature("tapandpay_perform_tokenization_operation", 1), Feature("tapandpay_push_tokenize", 1), + Feature("tapandpay_override_payment_network", 3L), + Feature("tapandpay_get_parental_consent_intent", 1L), + Feature("tapandpay_perform_secure_element_management_operation", 1L), + Feature("tapandpay_perform_tokenization_operation", 1L), Feature("tapandpay_push_tokenize_session", 6), + Feature("tapandpay_push_tokenize", 1L), Feature("tapandpay_quick_access_wallet", 1), + Feature("tapandpay_report_unlock", 1L), + Feature("tapandpay_request_delete_token", 1L), + Feature("tapandpay_request_select_token", 1L), Feature("tapandpay_secureelement", 1), + Feature("tapandpay_settings", 2L), + Feature("tapandpay_token_listing_with_request", 1L), Feature("tapandpay_show_wear_card_management_view", 1), Feature("tapandpay_send_wear_request_to_phone", 1), Feature("tapandpay_sync_device_info", 1), @@ -58,6 +75,8 @@ class TapAndPayService : BaseService(TAG, GmsService.TAP_AND_PAY) { Feature("tapandpay_tokenize_pan", 1), Feature("tapandpay_transmission_event", 1), Feature("tapandpay_token_listing", 3), + Feature("tapandpay_wallet_ui_shown_status", 1L), + Feature("tapandpay_wallet_set_tap_doodle_enabled", 1L), Feature("tapandpay_wallet_feedback_psd", 1) ) }) @@ -101,6 +120,11 @@ class TapAndPayImpl : ITapAndPayService.Stub() { callbacks.onRefreshSeCardsResponse(Status.SUCCESS, RefreshSeCardsResponse()) } + override fun getListTokens(callbacks: ITapAndPayServiceCallbacks) { + Log.d(TAG, "getListTokensRequest: ") + callbacks.onListTokensRetrieved(Status.SUCCESS, emptyArray()) + } + override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean = warnOnTransactionIssues(code, reply, flags, TAG) { super.onTransact(code, data, reply, flags) } } diff --git a/play-services-tapandpay/src/main/aidl/com/google/android/gms/tapandpay/internal/ITapAndPayService.aidl b/play-services-tapandpay/src/main/aidl/com/google/android/gms/tapandpay/internal/ITapAndPayService.aidl index 2468d490e8..7c8b1785a7 100644 --- a/play-services-tapandpay/src/main/aidl/com/google/android/gms/tapandpay/internal/ITapAndPayService.aidl +++ b/play-services-tapandpay/src/main/aidl/com/google/android/gms/tapandpay/internal/ITapAndPayService.aidl @@ -47,4 +47,5 @@ interface ITapAndPayService { void refreshSeCards(in RefreshSeCardsRequest request, ITapAndPayServiceCallbacks callbacks) = 56; // void tokenizeAccount(in TokenizeAccountRequest request, ITapAndPayServiceCallbacks callbacks) = 57; // void syncDeviceInfo(in SyncDeviceInfoRequest request, ITapAndPayServiceCallbacks callbacks) = 64; + void getListTokens(ITapAndPayServiceCallbacks callbacks) = 73; } diff --git a/play-services-tapandpay/src/main/aidl/com/google/android/gms/tapandpay/internal/ITapAndPayServiceCallbacks.aidl b/play-services-tapandpay/src/main/aidl/com/google/android/gms/tapandpay/internal/ITapAndPayServiceCallbacks.aidl index b23c21c08c..5bbd2aaf2a 100644 --- a/play-services-tapandpay/src/main/aidl/com/google/android/gms/tapandpay/internal/ITapAndPayServiceCallbacks.aidl +++ b/play-services-tapandpay/src/main/aidl/com/google/android/gms/tapandpay/internal/ITapAndPayServiceCallbacks.aidl @@ -5,6 +5,7 @@ import com.google.android.gms.tapandpay.firstparty.GetActiveAccountResponse; import com.google.android.gms.tapandpay.firstparty.GetAllCardsResponse; import com.google.android.gms.tapandpay.firstparty.RefreshSeCardsResponse; import com.google.android.gms.tapandpay.issuer.TokenStatus; +import com.google.android.gms.tapandpay.issuer.TokenInfo; interface ITapAndPayServiceCallbacks { void onTokenSelected(in Status status) = 1; @@ -50,7 +51,7 @@ interface ITapAndPayServiceCallbacks { // void onQuickAccessWalletConfig(in Status status, in QuickAccessWalletConfig config) = 46; // void onContactlessSetupStatusRetrieved(in Status status, in GetContactlessSetupStatusResponse response) = 47; void onIsTokenizedRetrieved(in Status status, boolean isTokenized) = 48; -// void onListTokensRetrieved(in Status status, in TokenInfo[] tokens) = 49; + void onListTokensRetrieved(in Status status, in TokenInfo[] tokens) = 49; // void onContactlessEligibilityRetrieved(in Status status, in CheckContactlessEligibilityResponse response) = 50; void onProto(in Status status, in byte[] proto) = 51; // void onPushProvisionSessionContextRetrieved(in Status status, in PushProvisionSessionContext context) = 52; diff --git a/play-services-tapandpay/src/main/aidl/com/google/android/gms/tapandpay/issuer/TokenInfo.aidl b/play-services-tapandpay/src/main/aidl/com/google/android/gms/tapandpay/issuer/TokenInfo.aidl new file mode 100644 index 0000000000..a5eb709410 --- /dev/null +++ b/play-services-tapandpay/src/main/aidl/com/google/android/gms/tapandpay/issuer/TokenInfo.aidl @@ -0,0 +1,3 @@ +package com.google.android.gms.tapandpay.issuer; + +parcelable TokenInfo; diff --git a/play-services-tapandpay/src/main/java/com/google/android/gms/tapandpay/issuer/TokenInfo.java b/play-services-tapandpay/src/main/java/com/google/android/gms/tapandpay/issuer/TokenInfo.java new file mode 100644 index 0000000000..d8e4e74e3c --- /dev/null +++ b/play-services-tapandpay/src/main/java/com/google/android/gms/tapandpay/issuer/TokenInfo.java @@ -0,0 +1,22 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ +package com.google.android.gms.tapandpay.issuer; + +import android.os.Parcel; + +import androidx.annotation.NonNull; + +import com.google.android.gms.common.internal.safeparcel.AbstractSafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter; + +@SafeParcelable.Class +public class TokenInfo extends AbstractSafeParcelable { + public static final SafeParcelableCreatorAndWriter CREATOR = findCreator(TokenInfo.class); + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + CREATOR.writeToParcel(this, dest, flags); + } +} From 743aecc5ec61a41109f4e1db86e4c1afdc87df53 Mon Sep 17 00:00:00 2001 From: DaVinci9196 <150454414+DaVinci9196@users.noreply.github.com> Date: Sun, 17 Nov 2024 07:48:02 +0800 Subject: [PATCH 070/132] Fixed bugs related to Kids Space (#2580) Co-authored-by: Marvin W --- .../kotlin/org/microg/gms/common/KnownGooglePackages.kt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/play-services-base/core/src/main/kotlin/org/microg/gms/common/KnownGooglePackages.kt b/play-services-base/core/src/main/kotlin/org/microg/gms/common/KnownGooglePackages.kt index 47e812eb9f..ff727e157b 100644 --- a/play-services-base/core/src/main/kotlin/org/microg/gms/common/KnownGooglePackages.kt +++ b/play-services-base/core/src/main/kotlin/org/microg/gms/common/KnownGooglePackages.kt @@ -185,6 +185,12 @@ private val KNOWN_GOOGLE_PACKAGES = mapOf( PackageAndCertHash("com.google.android.apps.kids.familylink", SHA256, "6b58bb84c1c6d081d950448ff5c051a34769d7fd8d415452c86efeb808716c0e"), setOf(ACCOUNT, AUTH, OWNER) ), + + // Google Kids home + Pair( + PackageAndCertHash("com.google.android.apps.kids.home", SHA256, "8f7bd4c5c0273a1a0dd6b3bfa8cc8e9f980a25108adcfd7be9962e8ae9feeb6f"), + setOf(ACCOUNT, AUTH, OWNER) + ), ) fun isGooglePackage(pkg: PackageAndCertHash): Boolean { From bc58a0ffbdc48184f791904f9c81191215884780 Mon Sep 17 00:00:00 2001 From: DaVinci9196 <150454414+DaVinci9196@users.noreply.github.com> Date: Mon, 18 Nov 2024 21:07:08 +0800 Subject: [PATCH 071/132] Vending: Fix build info being uninitialized when invoked too early (#2571) --- .../org/microg/gms/profile/ProfileManager.kt | 11 +++++++++-- .../main/java/org/microg/vending/billing/Utils.kt | 15 ++++++++------- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/play-services-base/core/src/main/kotlin/org/microg/gms/profile/ProfileManager.kt b/play-services-base/core/src/main/kotlin/org/microg/gms/profile/ProfileManager.kt index 5744f80ee9..78fde4870f 100644 --- a/play-services-base/core/src/main/kotlin/org/microg/gms/profile/ProfileManager.kt +++ b/play-services-base/core/src/main/kotlin/org/microg/gms/profile/ProfileManager.kt @@ -325,8 +325,10 @@ object ProfileManager { Log.v(TAG, "") } } - applyProfileData(profileData) - activeProfile = PROFILE_REMOTE + if (profileData.isNotEmpty()) { + applyProfileData(profileData) + activeProfile = PROFILE_REMOTE + } } fun getProfileName(context: Context, profile: String): String? = getProfileName { getProfileXml(context, profile) } @@ -377,6 +379,11 @@ object ProfileManager { } } + @JvmStatic + fun resetActiveProfile() { + activeProfile = null + } + @JvmStatic fun ensureInitialized(context: Context) { val metaData = runCatching { context.packageManager.getApplicationInfo(context.packageName, PackageManager.GET_META_DATA).metaData }.getOrNull() ?: Bundle.EMPTY diff --git a/vending-app/src/main/java/org/microg/vending/billing/Utils.kt b/vending-app/src/main/java/org/microg/vending/billing/Utils.kt index 831dd014cc..af89548c88 100644 --- a/vending-app/src/main/java/org/microg/vending/billing/Utils.kt +++ b/vending-app/src/main/java/org/microg/vending/billing/Utils.kt @@ -25,6 +25,7 @@ import androidx.core.app.ActivityCompat import androidx.core.os.bundleOf import com.android.billingclient.api.BillingClient.BillingResponseCode import org.microg.gms.profile.Build +import org.microg.gms.profile.ProfileManager import org.microg.gms.utils.digest import org.microg.gms.utils.getExtendedPackageInfo import org.microg.gms.utils.toBase64 @@ -314,15 +315,15 @@ fun createDeviceEnvInfo(context: Context): DeviceEnvInfo? { gpLastUpdateTime = packageInfo.lastUpdateTime, gpFirstInstallTime = packageInfo.firstInstallTime, gpSourceDir = packageInfo.applicationInfo.sourceDir!!, - device = Build.DEVICE!!, + device = Build.DEVICE ?: "", displayMetrics = getDisplayInfo(context), telephonyData = getTelephonyData(context), - product = Build.PRODUCT!!, - model = Build.MODEL!!, - manufacturer = Build.MANUFACTURER!!, - fingerprint = Build.FINGERPRINT!!, - release = Build.VERSION.RELEASE!!, - brand = Build.BRAND!!, + product = Build.PRODUCT ?: "", + model = Build.MODEL ?: "", + manufacturer = Build.MANUFACTURER ?: "", + fingerprint = Build.FINGERPRINT ?: "", + release = Build.VERSION.RELEASE ?: "", + brand = Build.BRAND ?: "", batteryLevel = getBatteryLevel(context), timeZoneOffset = if (SDK_INT >= 24) TimeZone.getDefault().rawOffset.toLong() else 0, locationData = getLocationData(context), From c938921792968e57a35df81839da237194cb79ca Mon Sep 17 00:00:00 2001 From: Marvin W Date: Wed, 13 Nov 2024 18:26:30 -0600 Subject: [PATCH 072/132] Location: Allow faster updates through fused provider API --- .../gms/location/provider/FusedLocationProviderService.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/provider/FusedLocationProviderService.kt b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/provider/FusedLocationProviderService.kt index c95093e756..188b596c3a 100644 --- a/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/provider/FusedLocationProviderService.kt +++ b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/provider/FusedLocationProviderService.kt @@ -27,7 +27,7 @@ class FusedLocationProviderService : IntentLocationProviderService() { @SuppressLint("MissingPermission") override fun requestIntentUpdated(currentRequest: ProviderRequestUnbundled?, pendingIntent: PendingIntent) { - val intervalMillis = max(currentRequest?.interval ?: Long.MAX_VALUE, minIntervalMillis) + val intervalMillis = currentRequest?.interval ?: Long.MAX_VALUE val request = LocationRequest.Builder(intervalMillis) if (SDK_INT >= 31 && currentRequest != null) { request.setPriority(when { @@ -71,7 +71,7 @@ class FusedLocationProviderService : IntentLocationProviderService() { get() = "fused" companion object { - private const val MIN_INTERVAL_MILLIS = 20000L + private const val MIN_INTERVAL_MILLIS = 1000L private const val MIN_REPORT_MILLIS = 1000L private val PROPERTIES = ProviderPropertiesUnbundled.create(false, false, false, false, true, true, true, Criteria.POWER_LOW, Criteria.ACCURACY_COARSE) } From 10a5259d9dc813f6722dd1de4b93f69982ebaa11 Mon Sep 17 00:00:00 2001 From: Marvin W Date: Sat, 16 Nov 2024 15:56:42 -0600 Subject: [PATCH 073/132] Location: Remove passive location listener Fetching the last location once we get a request is already a good start and won't trigger any location indicators --- .../microg/gms/settings/SettingsContract.kt | 2 + .../microg/gms/settings/SettingsProvider.kt | 7 +++- .../microg/gms/location/LocationSettings.kt | 5 ++- play-services-location/core/build.gradle | 3 -- .../network/NetworkLocationService.kt | 39 ++++++++++++++----- .../provider/FusedLocationProviderService.kt | 2 +- .../manager/DeviceOrientationManager.kt | 1 + .../gms/location/manager/LocationManager.kt | 26 ++++++++----- .../manager/LocationRequestManager.kt | 2 +- .../ui/LocationPreferencesFragment.kt | 3 +- 10 files changed, 61 insertions(+), 29 deletions(-) diff --git a/play-services-base/core/src/main/kotlin/org/microg/gms/settings/SettingsContract.kt b/play-services-base/core/src/main/kotlin/org/microg/gms/settings/SettingsContract.kt index 6c9355986d..5a0ee849ba 100644 --- a/play-services-base/core/src/main/kotlin/org/microg/gms/settings/SettingsContract.kt +++ b/play-services-base/core/src/main/kotlin/org/microg/gms/settings/SettingsContract.kt @@ -173,6 +173,7 @@ object SettingsContract { const val GEOCODER_NOMINATIM = "location_geocoder_nominatim" const val ICHNAEA_ENDPOINT = "location_ichnaea_endpoint" const val ONLINE_SOURCE = "location_online_source" + const val ICHNAEA_CONTRIBUTE = "location_ichnaea_contribute" val PROJECTION = arrayOf( WIFI_ICHNAEA, @@ -185,6 +186,7 @@ object SettingsContract { GEOCODER_NOMINATIM, ICHNAEA_ENDPOINT, ONLINE_SOURCE, + ICHNAEA_CONTRIBUTE, ) } diff --git a/play-services-base/core/src/main/kotlin/org/microg/gms/settings/SettingsProvider.kt b/play-services-base/core/src/main/kotlin/org/microg/gms/settings/SettingsProvider.kt index 1b98a6d4a0..124932d270 100644 --- a/play-services-base/core/src/main/kotlin/org/microg/gms/settings/SettingsProvider.kt +++ b/play-services-base/core/src/main/kotlin/org/microg/gms/settings/SettingsProvider.kt @@ -10,12 +10,12 @@ import android.content.ContentValues import android.content.Context import android.content.Context.MODE_PRIVATE import android.content.SharedPreferences +import android.content.pm.ApplicationInfo import android.database.Cursor import android.database.MatrixCursor import android.net.Uri import android.os.Build.VERSION.SDK_INT import android.preference.PreferenceManager -import org.microg.gms.base.core.BuildConfig import org.microg.gms.common.PackageUtils.warnIfNotMainProcess import org.microg.gms.settings.SettingsContract.Auth import org.microg.gms.settings.SettingsContract.CheckIn @@ -23,12 +23,13 @@ import org.microg.gms.settings.SettingsContract.DroidGuard import org.microg.gms.settings.SettingsContract.Exposure import org.microg.gms.settings.SettingsContract.Gcm import org.microg.gms.settings.SettingsContract.Location -import org.microg.gms.settings.SettingsContract.Vending import org.microg.gms.settings.SettingsContract.Profile import org.microg.gms.settings.SettingsContract.SafetyNet +import org.microg.gms.settings.SettingsContract.Vending import org.microg.gms.settings.SettingsContract.getAuthority import java.io.File + private const val SETTINGS_PREFIX = "org.microg.gms.settings." /** @@ -321,6 +322,7 @@ class SettingsProvider : ContentProvider() { Location.GEOCODER_NOMINATIM -> getSettingsBoolean(key, hasUnifiedNlpGeocoderBackend("org.microg.nlp.backend.nominatim") ) Location.ICHNAEA_ENDPOINT -> getSettingsString(key, null) Location.ONLINE_SOURCE -> getSettingsString(key, null) + Location.ICHNAEA_CONTRIBUTE -> getSettingsBoolean(key, false) else -> throw IllegalArgumentException("Unknown key: $key") } } @@ -341,6 +343,7 @@ class SettingsProvider : ContentProvider() { Location.GEOCODER_NOMINATIM -> editor.putBoolean(key, value as Boolean) Location.ICHNAEA_ENDPOINT -> (value as String).let { if (it.isBlank()) editor.remove(key) else editor.putString(key, it) } Location.ONLINE_SOURCE -> (value as? String?).let { if (it.isNullOrBlank()) editor.remove(key) else editor.putString(key, it) } + Location.ICHNAEA_CONTRIBUTE -> editor.putBoolean(key, value as Boolean) else -> throw IllegalArgumentException("Unknown key: $key") } } diff --git a/play-services-location/core/base/src/main/kotlin/org/microg/gms/location/LocationSettings.kt b/play-services-location/core/base/src/main/kotlin/org/microg/gms/location/LocationSettings.kt index fec222c200..e8c29cfe33 100644 --- a/play-services-location/core/base/src/main/kotlin/org/microg/gms/location/LocationSettings.kt +++ b/play-services-location/core/base/src/main/kotlin/org/microg/gms/location/LocationSettings.kt @@ -8,6 +8,7 @@ package org.microg.gms.location import android.content.ContentValues import android.content.Context import android.database.Cursor +import org.microg.gms.location.base.BuildConfig import org.microg.gms.settings.SettingsContract private const val PATH_GEOLOCATE = "/v1/geolocate" @@ -96,6 +97,6 @@ class LocationSettings(private val context: Context) { set(value) = setSettings { put(SettingsContract.Location.ONLINE_SOURCE, value) } var ichnaeaContribute: Boolean - get() = false - set(value) = Unit + get() = getSettings(SettingsContract.Location.ICHNAEA_CONTRIBUTE) { c -> c.getInt(0) != 0 } + set(value) = setSettings { put(SettingsContract.Location.ICHNAEA_CONTRIBUTE, value) } } \ No newline at end of file diff --git a/play-services-location/core/build.gradle b/play-services-location/core/build.gradle index bdcd04fe99..c86d0d6a5d 100644 --- a/play-services-location/core/build.gradle +++ b/play-services-location/core/build.gradle @@ -43,7 +43,6 @@ android { targetSdkVersion androidTargetSdk buildConfigField "String", "FORCE_SHOW_BACKGROUND_PERMISSION", "\"\"" buildConfigField "boolean", "SHOW_NOTIFICATION_WHEN_NOT_PERMITTED", "false" - buildConfigField "boolean", "ALWAYS_LISTEN_GPS_PASSIVE", "true" } lintOptions { @@ -59,13 +58,11 @@ android { dimension 'target' buildConfigField "String", "FORCE_SHOW_BACKGROUND_PERMISSION", "\"com.huawei.permission.sec.MDM.v2\"" buildConfigField "boolean", "SHOW_NOTIFICATION_WHEN_NOT_PERMITTED", "true" - buildConfigField "boolean", "ALWAYS_LISTEN_GPS_PASSIVE", "false" } "huaweilh" { dimension 'target' buildConfigField "String", "FORCE_SHOW_BACKGROUND_PERMISSION", "\"com.huawei.permission.sec.MDM.v2\"" buildConfigField "boolean", "SHOW_NOTIFICATION_WHEN_NOT_PERMITTED", "true" - buildConfigField "boolean", "ALWAYS_LISTEN_GPS_PASSIVE", "false" } } diff --git a/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/NetworkLocationService.kt b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/NetworkLocationService.kt index 60082080ee..8a1036943c 100644 --- a/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/NetworkLocationService.kt +++ b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/NetworkLocationService.kt @@ -64,10 +64,11 @@ class NetworkLocationService : LifecycleService(), WifiDetailsCallback, CellDeta private var lastCellLocation: Location? = null private var lastLocation: Location? = null - private val gpsLocationListener by lazy { LocationListenerCompat { onNewGpsLocation(it) } } + private val passiveLocationListener by lazy { LocationListenerCompat { onNewPassiveLocation(it) } } @GuardedBy("gpsLocationBuffer") private val gpsLocationBuffer = LinkedList() + private var passiveListenerActive = false private var currentLocalMovingWifi: WifiDetails? = null private var lastLocalMovingWifiLocationCandidate: Location? = null @@ -88,15 +89,31 @@ class NetworkLocationService : LifecycleService(), WifiDetailsCallback, CellDeta putExtra(EXTRA_CONFIGURATION, CONFIGURATION_FIELD_ONLINE_SOURCE) }) } + } + + private fun updatePassiveGpsListenerRegistration() { try { getSystemService()?.let { locationManager -> - LocationManagerCompat.requestLocationUpdates( - locationManager, - LocationManager.GPS_PROVIDER, - LocationRequestCompat.Builder(LocationRequestCompat.PASSIVE_INTERVAL).setMinUpdateIntervalMillis(GPS_PASSIVE_INTERVAL).build(), - gpsLocationListener, - handlerThread.looper - ) + if ((settings.cellLearning || settings.wifiLearning) && (highPowerIntervalMillis != Long.MAX_VALUE)) { + if (!passiveListenerActive) { + LocationManagerCompat.requestLocationUpdates( + locationManager, + LocationManager.PASSIVE_PROVIDER, + LocationRequestCompat.Builder(LocationRequestCompat.PASSIVE_INTERVAL) + .setQuality(LocationRequestCompat.QUALITY_LOW_POWER) + .setMinUpdateIntervalMillis(GPS_PASSIVE_INTERVAL) + .build(), + passiveLocationListener, + handlerThread.looper + ) + passiveListenerActive = true + } + } else { + if (passiveListenerActive) { + LocationManagerCompat.removeUpdates(locationManager, passiveLocationListener) + passiveListenerActive = false + } + } } } catch (e: SecurityException) { Log.d(TAG, "GPS location retriever not initialized due to lack of permission") @@ -181,6 +198,8 @@ class NetworkLocationService : LifecycleService(), WifiDetailsCallback, CellDeta handler.postDelayed(highPowerScanRunnable, nextHighPowerRequestIn) } } + + updatePassiveGpsListenerRegistration() } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { @@ -539,8 +558,8 @@ class NetworkLocationService : LifecycleService(), WifiDetailsCallback, CellDeta } } - private fun onNewGpsLocation(location: Location) { - if (location.accuracy > GPS_PASSIVE_MIN_ACCURACY) return + private fun onNewPassiveLocation(location: Location) { + if (location.provider != LocationManager.GPS_PROVIDER || location.accuracy > GPS_PASSIVE_MIN_ACCURACY) return synchronized(gpsLocationBuffer) { if (gpsLocationBuffer.isNotEmpty() && gpsLocationBuffer.last.elapsedMillis < SystemClock.elapsedRealtime() - GPS_BUFFER_SIZE * GPS_PASSIVE_INTERVAL) { gpsLocationBuffer.clear() diff --git a/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/provider/FusedLocationProviderService.kt b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/provider/FusedLocationProviderService.kt index 188b596c3a..c9b4a10999 100644 --- a/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/provider/FusedLocationProviderService.kt +++ b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/provider/FusedLocationProviderService.kt @@ -27,7 +27,7 @@ class FusedLocationProviderService : IntentLocationProviderService() { @SuppressLint("MissingPermission") override fun requestIntentUpdated(currentRequest: ProviderRequestUnbundled?, pendingIntent: PendingIntent) { - val intervalMillis = currentRequest?.interval ?: Long.MAX_VALUE + val intervalMillis = max(currentRequest?.interval ?: Long.MAX_VALUE, minIntervalMillis) val request = LocationRequest.Builder(intervalMillis) if (SDK_INT >= 31 && currentRequest != null) { request.setPriority(when { diff --git a/play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/DeviceOrientationManager.kt b/play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/DeviceOrientationManager.kt index eb98ca0f49..1cb966b42a 100644 --- a/play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/DeviceOrientationManager.kt +++ b/play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/DeviceOrientationManager.kt @@ -118,6 +118,7 @@ class DeviceOrientationManager(private val context: Context, override val lifecy synchronized(appOpsLock) { val newAppOps = mutableSetOf() for (request in requests.values) { + if (request.clientIdentity.isSelfUser()) continue newAppOps.add(request.clientIdentity) } Log.d(TAG, "Updating app ops for device orientation, change attribution to: ${newAppOps.map { it.packageName }.joinToString().takeIf { it.isNotEmpty() } ?: "none"}") diff --git a/play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/LocationManager.kt b/play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/LocationManager.kt index cbce54d560..7b671113f5 100644 --- a/play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/LocationManager.kt +++ b/play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/LocationManager.kt @@ -13,8 +13,7 @@ import android.content.Context import android.content.Intent import android.content.pm.PackageManager import android.location.Location -import android.location.LocationManager.GPS_PROVIDER -import android.location.LocationManager.NETWORK_PROVIDER +import android.location.LocationManager.* import android.os.* import android.os.Build.VERSION.SDK_INT import android.util.Log @@ -53,10 +52,12 @@ class LocationManager(private val context: Context, override val lifecycle: Life private val requestManager by lazy { LocationRequestManager(context, lifecycle, postProcessor, database) { updateLocationRequests() } } private val gpsLocationListener by lazy { LocationListenerCompat { updateGpsLocation(it) } } private val networkLocationListener by lazy { LocationListenerCompat { updateNetworkLocation(it) } } + private val settings by lazy { LocationSettings(context) } private var boundToSystemNetworkLocation: Boolean = false private val activePermissionRequestLock = Mutex() private var activePermissionRequest: Deferred? = null private var lastGpsLocation: Location? = null + private var lastNetworkLocation: Location? = null private var currentGpsInterval: Long = -1 private var currentNetworkInterval: Long = -1 @@ -194,7 +195,7 @@ class LocationManager(private val context: Context, override val lifecycle: Life private fun updateLocationRequests() { val gpsInterval = when { - !BuildConfig.ALWAYS_LISTEN_GPS_PASSIVE && deviceOrientationManager.isActive -> min(requestManager.intervalMillis, DEVICE_ORIENTATION_INTERVAL) + deviceOrientationManager.isActive -> min(requestManager.intervalMillis, DEVICE_ORIENTATION_INTERVAL) requestManager.priority == PRIORITY_HIGH_ACCURACY && requestManager.granularity == GRANULARITY_FINE -> requestManager.intervalMillis else -> Long.MAX_VALUE } @@ -207,7 +208,7 @@ class LocationManager(private val context: Context, override val lifecycle: Life deviceOrientationManager.isActive -> DEVICE_ORIENTATION_INTERVAL else -> Long.MAX_VALUE } - val lowPower = requestManager.granularity <= GRANULARITY_COARSE || requestManager.priority >= Priority.PRIORITY_LOW_POWER + val lowPower = requestManager.granularity <= GRANULARITY_COARSE || requestManager.priority >= Priority.PRIORITY_LOW_POWER || (requestManager.priority >= Priority.PRIORITY_BALANCED_POWER_ACCURACY && requestManager.intervalMillis >= BALANCE_LOW_POWER_INTERVAL) if (context.hasNetworkLocationServiceBuiltIn() && currentNetworkInterval != networkInterval) { val intent = Intent(ACTION_NETWORK_LOCATION_SERVICE) @@ -243,6 +244,14 @@ class LocationManager(private val context: Context, override val lifecycle: Life } if (!context.hasNetworkLocationServiceBuiltIn() && LocationManagerCompat.hasProvider(locationManager, NETWORK_PROVIDER) && currentNetworkInterval != networkInterval) { boundToSystemNetworkLocation = true + if (networkInterval == Long.MAX_VALUE) { + // Fetch last location from GPS, just to make sure we already considered it + try { + locationManager.getLastKnownLocation(NETWORK_PROVIDER)?.let { updateNetworkLocation(it) } + } catch (e: SecurityException) { + // Ignore + } + } try { val quality = if (lowPower) QUALITY_LOW_POWER else QUALITY_BALANCED_POWER_ACCURACY locationManager.requestSystemProviderUpdates(NETWORK_PROVIDER, networkInterval, quality, networkLocationListener) @@ -258,9 +267,6 @@ class LocationManager(private val context: Context, override val lifecycle: Life if (interval != Long.MAX_VALUE) { Log.d(TAG, "Request updates for $provider at interval ${interval}ms") LocationManagerCompat.requestLocationUpdates(this, provider, Builder(interval).setQuality(quality).build(), listener, context.mainLooper) - } else if (BuildConfig.ALWAYS_LISTEN_GPS_PASSIVE && provider == GPS_PROVIDER) { - Log.d(TAG, "Request updates for $provider passively") - LocationManagerCompat.requestLocationUpdates(this, provider, Builder(PASSIVE_INTERVAL).setQuality(QUALITY_LOW_POWER).setMinUpdateIntervalMillis(MAX_FINE_UPDATE_INTERVAL).build(), listener, context.mainLooper) } else { Log.d(TAG, "Remove updates for $provider") LocationManagerCompat.removeUpdates(this, listener) @@ -288,14 +294,15 @@ class LocationManager(private val context: Context, override val lifecycle: Life } } - fun updateGpsLocation(location: Location) { + private fun updateGpsLocation(location: Location) { + if (location.provider != GPS_PROVIDER) return lastGpsLocation = location lastLocationCapsule.updateFineLocation(location) sendNewLocation() updateLocationRequests() } - fun sendNewLocation() { + private fun sendNewLocation() { lifecycleScope.launchWhenStarted { requestManager.processNewLocation(lastLocationCapsule) } @@ -382,5 +389,6 @@ class LocationManager(private val context: Context, override val lifecycle: Life const val DEVICE_ORIENTATION_INTERVAL = 10_000L const val NETWORK_OFF_GPS_AGE = 5000L const val NETWORK_OFF_GPS_ACCURACY = 10f + const val BALANCE_LOW_POWER_INTERVAL = 30_000L } } \ No newline at end of file diff --git a/play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/LocationRequestManager.kt b/play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/LocationRequestManager.kt index 2d22bd381f..ca770689d1 100644 --- a/play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/LocationRequestManager.kt +++ b/play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/LocationRequestManager.kt @@ -214,7 +214,7 @@ class LocationRequestManager(private val context: Context, override val lifecycl val newAppOps = mutableMapOf() val merged = binderRequests.values + pendingIntentRequests.values for (request in merged) { - if (request.effectivePriority >= PRIORITY_PASSIVE) continue + if (request.effectivePriority >= PRIORITY_PASSIVE || request.clientIdentity.isSelfUser()) continue if (!newAppOps.containsKey(request.clientIdentity)) { newAppOps[request.clientIdentity] = request.effectiveHighPower } else if (request.effectiveHighPower) { diff --git a/play-services-location/core/src/main/kotlin/org/microg/gms/location/ui/LocationPreferencesFragment.kt b/play-services-location/core/src/main/kotlin/org/microg/gms/location/ui/LocationPreferencesFragment.kt index b0fd7305e5..41eb4c269a 100644 --- a/play-services-location/core/src/main/kotlin/org/microg/gms/location/ui/LocationPreferencesFragment.kt +++ b/play-services-location/core/src/main/kotlin/org/microg/gms/location/ui/LocationPreferencesFragment.kt @@ -36,6 +36,7 @@ import org.microg.gms.location.core.R import org.microg.gms.location.manager.LocationAppsDatabase import org.microg.gms.location.network.OnlineSource import org.microg.gms.location.network.effectiveEndpoint +import org.microg.gms.location.network.onlineSource import org.microg.gms.ui.AppIconPreference import org.microg.gms.ui.buildAlertDialog import org.microg.gms.ui.getApplicationInfoIfExists @@ -189,7 +190,7 @@ class LocationPreferencesFragment : PreferenceFragmentCompat() { view.setPadding(0, 16.dp, 0, 0) view.orientation = LinearLayout.VERTICAL val settings = LocationSettings(requireContext()) - val currentSourceId = settings.onlineSourceId + val currentSourceId = settings.onlineSource?.id val unselectHandlerMap = mutableMapOf Unit>() var selectedSourceId = currentSourceId val customView = layoutInflater.inflate(R.layout.preference_location_custom_url, null) From 5e642704286768493b1c3d8234a5bf1288effc54 Mon Sep 17 00:00:00 2001 From: davinci9196 Date: Thu, 22 Aug 2024 22:19:37 +0800 Subject: [PATCH 074/132] Implement AssetModuleService --- .../protocol/IAssetModuleServiceCallback.aidl | 26 +- .../AssetModuleService.java | 103 ------ .../microg/vending/billing/core/HttpClient.kt | 39 ++- .../assetmoduleservice/AssetModuleService.kt | 271 ++++++++++++++++ .../finsky/assetmoduleservice/ModuleData.kt | 66 ++++ .../com/google/android/finsky/extensions.kt | 295 ++++++++++++++++++ vending-app/src/main/proto/AssetModule.proto | 100 ++++++ 7 files changed, 781 insertions(+), 119 deletions(-) delete mode 100644 vending-app/src/main/java/com/google/android/finsky/assetmoduleservice/AssetModuleService.java create mode 100644 vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/AssetModuleService.kt create mode 100644 vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/ModuleData.kt create mode 100644 vending-app/src/main/kotlin/com/google/android/finsky/extensions.kt create mode 100644 vending-app/src/main/proto/AssetModule.proto diff --git a/vending-app/src/main/aidl/com/google/android/play/core/assetpacks/protocol/IAssetModuleServiceCallback.aidl b/vending-app/src/main/aidl/com/google/android/play/core/assetpacks/protocol/IAssetModuleServiceCallback.aidl index 8e484fee36..da775df785 100644 --- a/vending-app/src/main/aidl/com/google/android/play/core/assetpacks/protocol/IAssetModuleServiceCallback.aidl +++ b/vending-app/src/main/aidl/com/google/android/play/core/assetpacks/protocol/IAssetModuleServiceCallback.aidl @@ -6,17 +6,17 @@ package com.google.android.play.core.assetpacks.protocol; interface IAssetModuleServiceCallback { - void onStartDownload(int status, in Bundle bundle) = 1; - void onCancelDownload(int status) = 2; - void onGetSession(int status) = 3; - void onGetSessionStates(in List list) = 4; - void onNotifyChunkTransferred(in Bundle bundle) = 5; - void onError(in Bundle bundle) = 6; - void onNotifyModuleCompleted(in Bundle bundle) = 7; - void onNotifySessionFailed(in Bundle bundle) = 9; - void onKeepAlive(in Bundle bundle, in Bundle bundle2) = 10; - void onGetChunkFileDescriptor(in Bundle bundle, in Bundle bundle2) = 11; - void onRequestDownloadInfo(in Bundle bundle, in Bundle bundle2) = 12; - void onRemoveModule() = 13; - void onCancelDownloads() = 14; + oneway void onStartDownload(int status, in Bundle bundle) = 1; + oneway void onCancelDownload(int status) = 2; + oneway void onGetSession(int status) = 3; + oneway void onGetSessionStates(in List list) = 4; + oneway void onNotifyChunkTransferred(in Bundle bundle,in Bundle bundle2) = 5; + oneway void onError(in Bundle bundle) = 6; + oneway void onNotifyModuleCompleted(in Bundle bundle,in Bundle bundle2) = 7; + oneway void onNotifySessionFailed(in Bundle bundle) = 9; + oneway void onKeepAlive(in Bundle bundle, in Bundle bundle2) = 10; + oneway void onGetChunkFileDescriptor(in Bundle bundle, in Bundle bundle2) = 11; + oneway void onRequestDownloadInfo(in Bundle bundle, in Bundle bundle2) = 12; + oneway void onRemoveModule() = 13; + oneway void onCancelDownloads() = 14; } \ No newline at end of file diff --git a/vending-app/src/main/java/com/google/android/finsky/assetmoduleservice/AssetModuleService.java b/vending-app/src/main/java/com/google/android/finsky/assetmoduleservice/AssetModuleService.java deleted file mode 100644 index 19ce4f2021..0000000000 --- a/vending-app/src/main/java/com/google/android/finsky/assetmoduleservice/AssetModuleService.java +++ /dev/null @@ -1,103 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2023 microG Project Team - * SPDX-License-Identifier: Apache-2.0 - */ - -package com.google.android.finsky.assetmoduleservice; - -import android.app.Service; -import android.content.Intent; -import android.os.Bundle; -import android.os.IBinder; -import android.os.RemoteException; -import android.util.Log; - -import com.google.android.play.core.assetpacks.protocol.IAssetModuleService; -import com.google.android.play.core.assetpacks.protocol.IAssetModuleServiceCallback; - -import java.util.ArrayList; -import java.util.List; - -public class AssetModuleService extends Service { - private static final String TAG = "AssetModuleService"; - - private final List requested = new ArrayList<>(); - - private final IAssetModuleService.Stub service = new IAssetModuleService.Stub() { - - @Override - public void startDownload(String packageName, List list, Bundle bundle, IAssetModuleServiceCallback callback) throws RemoteException { - Log.d(TAG, "Method (startDownload) called by packageName -> " + packageName); - Bundle result = new Bundle(); - result.putStringArrayList("pack_names", new ArrayList<>()); - callback.onStartDownload(-1, result); - } - - @Override - public void getSessionStates(String packageName, Bundle bundle, IAssetModuleServiceCallback callback) { - Log.d(TAG, "Method (getSessionStates) called but not implement by packageName -> " + packageName); - } - - @Override - public void notifyChunkTransferred(String packageName, Bundle bundle, Bundle bundle2, IAssetModuleServiceCallback callback) { - Log.d(TAG, "Method (notifyChunkTransferred) called but not implement by packageName -> " + packageName); - } - - @Override - public void notifyModuleCompleted(String packageName, Bundle bundle, Bundle bundle2, IAssetModuleServiceCallback callback) { - Log.d(TAG, "Method (notifyModuleCompleted) called but not implement by packageName -> " + packageName); - } - - @Override - public void notifySessionFailed(String packageName, Bundle bundle, Bundle bundle2, IAssetModuleServiceCallback callback) { - Log.d(TAG, "Method (notifySessionFailed) called but not implement by packageName -> " + packageName); - } - - @Override - public void keepAlive(String packageName, Bundle bundle, IAssetModuleServiceCallback callback) { - Log.d(TAG, "Method (keepAlive) called but not implement by packageName -> " + packageName); - } - - @Override - public void getChunkFileDescriptor(String packageName, Bundle bundle, Bundle bundle2, IAssetModuleServiceCallback callback) { - Log.d(TAG, "Method (getChunkFileDescriptor) called but not implement by packageName -> " + packageName); - } - - @Override - public void requestDownloadInfo(String packageName, List list, Bundle bundle, IAssetModuleServiceCallback callback) throws RemoteException { - Log.d(TAG, "Method (requestDownloadInfo) called by packageName -> " + packageName); - Bundle result = new Bundle(); - if (requested.contains(packageName)) { - result.putInt("error_code", -5); - callback.onError(result); - return; - } - requested.add(packageName); - result.putStringArrayList("pack_names", new ArrayList<>()); - callback.onRequestDownloadInfo(result, result); - } - - @Override - public void removeModule(String packageName, Bundle bundle, Bundle bundle2, IAssetModuleServiceCallback callback) { - Log.d(TAG, "Method (removeModule) called but not implement by packageName -> " + packageName); - } - - @Override - public void cancelDownloads(String packageName, List list, Bundle bundle, IAssetModuleServiceCallback callback) { - Log.d(TAG, "Method (cancelDownloads) called but not implement by packageName -> " + packageName); - } - }; - - @Override - public IBinder onBind(Intent intent) { - Log.d(TAG, "onBind"); - return service.asBinder(); - } - - @Override - public boolean onUnbind(Intent intent) { - Log.d(TAG, "onUnbind"); - requested.clear(); - return super.onUnbind(intent); - } -} \ No newline at end of file diff --git a/vending-app/src/main/java/org/microg/vending/billing/core/HttpClient.kt b/vending-app/src/main/java/org/microg/vending/billing/core/HttpClient.kt index 9835e0e8db..49d849312a 100644 --- a/vending-app/src/main/java/org/microg/vending/billing/core/HttpClient.kt +++ b/vending-app/src/main/java/org/microg/vending/billing/core/HttpClient.kt @@ -10,6 +10,9 @@ import com.squareup.wire.Message import com.squareup.wire.ProtoAdapter import org.json.JSONObject import org.microg.gms.utils.singleInstanceOf +import java.io.File +import java.io.FileOutputStream +import java.io.IOException import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException import kotlin.coroutines.suspendCoroutine @@ -17,7 +20,39 @@ import kotlin.coroutines.suspendCoroutine private const val POST_TIMEOUT = 8000 class HttpClient(context: Context) { - private val requestQueue = singleInstanceOf { Volley.newRequestQueue(context.applicationContext) } + + val requestQueue = singleInstanceOf { Volley.newRequestQueue(context.applicationContext) } + + suspend fun download( + url: String, downloadFile: File, tag: String + ): String = suspendCoroutine { continuation -> + val uriBuilder = Uri.parse(url).buildUpon() + requestQueue.add(object : Request(Method.GET, uriBuilder.build().toString(), null) { + override fun parseNetworkResponse(response: NetworkResponse): Response { + if (response.statusCode != 200) throw VolleyError(response) + return try { + val parentDir = downloadFile.getParentFile() + if (parentDir != null && !parentDir.exists() && !parentDir.mkdirs()) { + throw IOException("Failed to create directories: ${parentDir.absolutePath}") + } + val fos = FileOutputStream(downloadFile) + fos.write(response.data) + fos.close() + Response.success(downloadFile.absolutePath, HttpHeaderParser.parseCacheHeaders(response)) + } catch (e: Exception) { + Response.error(VolleyError(e)) + } + } + + override fun deliverResponse(response: String) { + continuation.resume(response) + } + + override fun deliverError(error: VolleyError) { + continuation.resumeWithException(error) + } + }.setShouldCache(false).setTag(tag)) + } suspend fun get( url: String, @@ -48,8 +83,6 @@ class HttpClient(context: Context) { }.setShouldCache(cache)) } - - suspend fun , O> post( url: String, headers: Map = emptyMap(), diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/AssetModuleService.kt b/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/AssetModuleService.kt new file mode 100644 index 0000000000..9157756eb5 --- /dev/null +++ b/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/AssetModuleService.kt @@ -0,0 +1,271 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ +package com.google.android.finsky.assetmoduleservice + +import android.accounts.Account +import android.accounts.AccountManager +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.os.IBinder +import android.os.ParcelFileDescriptor +import android.util.Log +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LifecycleService +import androidx.lifecycle.lifecycleScope +import com.android.vending.licensing.AUTH_TOKEN_SCOPE +import com.android.vending.licensing.getAuthToken +import com.google.android.finsky.AssetModuleDeliveryRequest +import com.google.android.finsky.AssetModuleInfo +import com.google.android.finsky.CallerInfo +import com.google.android.finsky.CallerState +import com.google.android.finsky.KEY_CHUNK_FILE_DESCRIPTOR +import com.google.android.finsky.KEY_CHUNK_NUMBER +import com.google.android.finsky.KEY_ERROR_CODE +import com.google.android.finsky.KEY_MODULE_NAME +import com.google.android.finsky.KEY_PACK_NAMES +import com.google.android.finsky.KEY_PLAY_CORE_VERSION_CODE +import com.google.android.finsky.KEY_RESOURCE_PACKAGE_NAME +import com.google.android.finsky.KEY_SESSION_ID +import com.google.android.finsky.KEY_SLICE_ID +import com.google.android.finsky.PageSource +import com.google.android.finsky.STATUS_COMPLETED +import com.google.android.finsky.STATUS_DOWNLOADING +import com.google.android.finsky.STATUS_NOT_INSTALLED +import com.google.android.finsky.STATUS_TRANSFERRING +import com.google.android.finsky.TAG_REQUEST +import com.google.android.finsky.buildDownloadBundle +import com.google.android.finsky.downloadFile +import com.google.android.finsky.getAppVersionCode +import com.google.android.finsky.initModuleDownloadInfo +import com.google.android.finsky.requestAssetModule +import com.google.android.finsky.sendBroadcastForExistingFile +import com.google.android.play.core.assetpacks.protocol.IAssetModuleService +import com.google.android.play.core.assetpacks.protocol.IAssetModuleServiceCallback +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.collectLatest +import okhttp3.internal.filterList +import org.microg.gms.auth.AuthConstants +import org.microg.gms.profile.ProfileManager +import org.microg.vending.billing.core.HttpClient +import java.io.File + +private const val TAG = "AssetModuleService" + +class AssetModuleService : LifecycleService() { + + private lateinit var httpClient: HttpClient + private lateinit var accountManager: AccountManager + + override fun onBind(intent: Intent): IBinder { + super.onBind(intent) + Log.d(TAG, "onBind: ") + ProfileManager.ensureInitialized(this) + accountManager = AccountManager.get(this) + httpClient = HttpClient(this) + return AssetModuleServiceImpl(this, lifecycle, httpClient, accountManager).asBinder() + } + + override fun onUnbind(intent: Intent?): Boolean { + Log.d(TAG, "onUnbind: ") + httpClient.requestQueue.cancelAll(TAG_REQUEST) + return super.onUnbind(intent) + } +} + +class AssetModuleServiceImpl( + val context: Context, override val lifecycle: Lifecycle, private val httpClient: HttpClient, private val accountManager: AccountManager +) : IAssetModuleService.Stub(), LifecycleOwner { + private val sharedModuleDataFlow = MutableSharedFlow() + private var moduleData: ModuleData? = null + + override fun startDownload(packageName: String?, list: MutableList?, bundle: Bundle?, callback: IAssetModuleServiceCallback?) { + Log.d(TAG, "Method (startDownload) called by packageName -> $packageName") + if (packageName == null || list == null || bundle == null) { + Log.d(TAG, "startDownload: params invalid ") + callback?.onError(Bundle().apply { putInt(KEY_ERROR_CODE, -5) }) + return + } + if (moduleData == null && moduleErrorRequested.contains(packageName)) { + Log.d(TAG, "startDownload: moduleData request error") + val result = Bundle().apply { putStringArrayList(KEY_PACK_NAMES, arrayListOf()) } + callback?.onStartDownload(-1, result) + return + } + suspend fun prepare(data: ModuleData) { + list.forEach { + val moduleName = it.getString(KEY_MODULE_NAME) + if (moduleName != null) { + callback?.onStartDownload(-1, buildDownloadBundle(moduleName, data, true)) + val packData = data.getPackData(moduleName) + if (packData?.status != STATUS_NOT_INSTALLED) { + Log.w(TAG, "startDownload: packData?.status is ${packData?.status}") + return@forEach + } + data.updateDownloadStatus(moduleName, STATUS_DOWNLOADING) + data.updateModuleDownloadStatus(STATUS_DOWNLOADING) + packData.bundleList?.forEach { download -> + if (moduleName == download.getString(KEY_RESOURCE_PACKAGE_NAME)) { + httpClient.downloadFile(context, moduleName, data, download) + } + } + } + } + } + lifecycleScope.launchWhenStarted { + if (moduleData == null) { + sharedModuleDataFlow.collectLatest { prepare(it) } + return@launchWhenStarted + } + prepare(moduleData!!) + } + } + + override fun getSessionStates(packageName: String?, bundle: Bundle?, callback: IAssetModuleServiceCallback?) { + Log.d(TAG, "Method (getSessionStates) called by packageName -> $packageName") + } + + override fun notifyChunkTransferred(packageName: String?, bundle: Bundle?, bundle2: Bundle?, callback: IAssetModuleServiceCallback?) { + Log.d(TAG, "Method (notifyChunkTransferred) called but not implemented by packageName -> $packageName") + callback?.onNotifyChunkTransferred(bundle, Bundle()) + } + + override fun notifyModuleCompleted(packageName: String?, bundle: Bundle?, bundle2: Bundle?, callback: IAssetModuleServiceCallback?) { + Log.d(TAG, "Method (notifyModuleCompleted) called but not implemented by packageName -> $packageName") + val moduleName = bundle?.getString(KEY_MODULE_NAME) + if (moduleName.isNullOrEmpty()) { + Log.d(TAG, "notifyModuleCompleted: params invalid ") + callback?.onError(Bundle().apply { putInt(KEY_ERROR_CODE, -5) }) + return + } + fun notify(data: ModuleData) { + callback?.onNotifyModuleCompleted(bundle, bundle) + val notify = data.packNames?.all { data.getPackData(it)?.status == STATUS_TRANSFERRING } ?: false + if (notify) { + data.packNames?.forEach { moduleData?.updateDownloadStatus(it, STATUS_COMPLETED) } + data.updateModuleDownloadStatus(STATUS_COMPLETED) + sendBroadcastForExistingFile(context, moduleData!!, moduleName, null, null) + } + } + lifecycleScope.launchWhenStarted { + if (moduleData == null) { + sharedModuleDataFlow.collectLatest { notify(it) } + return@launchWhenStarted + } + notify(moduleData!!) + } + } + + override fun notifySessionFailed(packageName: String?, bundle: Bundle?, bundle2: Bundle?, callback: IAssetModuleServiceCallback?) { + Log.d(TAG, "Method (notifySessionFailed) called but not implemented by packageName -> $packageName") + } + + override fun keepAlive(packageName: String?, bundle: Bundle?, callback: IAssetModuleServiceCallback?) { + Log.d(TAG, "Method (keepAlive) called but not implemented by packageName -> $packageName") + } + + override fun getChunkFileDescriptor(packageName: String, bundle: Bundle, bundle2: Bundle, callback: IAssetModuleServiceCallback?) { + Log.d(TAG, "Method (getChunkFileDescriptor) called but not implemented by packageName -> $packageName") + val moduleName = bundle.getString(KEY_MODULE_NAME) + if (moduleName.isNullOrEmpty()) { + Log.d(TAG, "getChunkFileDescriptor: params invalid ") + callback?.onError(Bundle().apply { putInt(KEY_ERROR_CODE, -5) }) + return + } + val parcelFileDescriptor = runCatching { + val sessionId = bundle.getInt(KEY_SESSION_ID) + val sliceId = bundle.getString(KEY_SLICE_ID) + val chunkNumber = bundle.getInt(KEY_CHUNK_NUMBER) + val downLoadFile = "${context.filesDir.absolutePath}/assetpacks/$sessionId/$moduleName/$sliceId/$chunkNumber" + val filePath = Uri.parse(downLoadFile).path?.let { File(it) } + ParcelFileDescriptor.open(filePath, ParcelFileDescriptor.MODE_READ_ONLY) + }.getOrNull() + callback?.onGetChunkFileDescriptor(Bundle().apply { putParcelable(KEY_CHUNK_FILE_DESCRIPTOR, parcelFileDescriptor) }, Bundle()) + } + + override fun requestDownloadInfo(packageName: String?, list: MutableList?, bundle: Bundle?, callback: IAssetModuleServiceCallback?) { + Log.d(TAG, "Method (requestDownloadInfo) called by packageName -> $packageName") + if (packageName == null || list == null || bundle == null) { + Log.w(TAG, "requestDownloadInfo: params invalid ") + callback?.onError(Bundle().apply { putInt(KEY_ERROR_CODE, -5) }) + return + } + lifecycleScope.launchWhenStarted { + val requestedAssetModuleNames = arrayListOf() + for (data in list) { + val moduleName = data.getString(KEY_MODULE_NAME) + if (!moduleName.isNullOrEmpty()) { + requestedAssetModuleNames.add(moduleName) + } + } + if (!moduleData?.packNames.isNullOrEmpty()) { + val bundleData = Bundle().apply { + val isPack = moduleData?.packNames?.size != requestedAssetModuleNames.size + requestedAssetModuleNames.forEach { + putAll(buildDownloadBundle(it, moduleData!!, isPack, packNames = requestedAssetModuleNames)) + } + } + callback?.onRequestDownloadInfo(bundleData, bundleData) + return@launchWhenStarted + } + val accounts = accountManager.getAccountsByType(AuthConstants.DEFAULT_ACCOUNT_TYPE) + var oauthToken: String? = null + if (accounts.isEmpty()) { + callback?.onError(Bundle().apply { putInt(KEY_ERROR_CODE, -5) }) + return@launchWhenStarted + } else for (account: Account in accounts) { + oauthToken = accountManager.getAuthToken(account, AUTH_TOKEN_SCOPE, false).getString(AccountManager.KEY_AUTHTOKEN) + if (oauthToken != null) { + break + } + } + val requestPayload = + AssetModuleDeliveryRequest.Builder().callerInfo(CallerInfo(getAppVersionCode(context, packageName)?.toInt())).packageName(packageName) + .playCoreVersion(bundle.getInt(KEY_PLAY_CORE_VERSION_CODE)) + .pageSource(listOf(PageSource.UNKNOWN_SEARCH_TRAFFIC_SOURCE, PageSource.BOOKS_HOME_PAGE)) + .callerState(listOf(CallerState.CALLER_APP_REQUEST, CallerState.CALLER_APP_DEBUGGABLE)).moduleInfo(ArrayList().apply { + list.filterList { !getString(KEY_MODULE_NAME).isNullOrEmpty() }.forEach { + add(AssetModuleInfo.Builder().name(it.getString(KEY_MODULE_NAME)).build()) + } + }).build() + val moduleDeliveryInfo = httpClient.requestAssetModule(context, oauthToken!!, requestPayload) + if (moduleDeliveryInfo == null || moduleDeliveryInfo.status != null) { + if (moduleErrorRequested.contains(packageName)) { + callback?.onError(Bundle().apply { putInt(KEY_ERROR_CODE, -5) }) + return@launchWhenStarted + } + moduleErrorRequested.add(packageName) + val result = Bundle().apply { putStringArrayList(KEY_PACK_NAMES, arrayListOf()) } + callback?.onRequestDownloadInfo(result, result) + return@launchWhenStarted + } + moduleErrorRequested.remove(packageName) + Log.d(TAG, "requestDownloadInfo: moduleDeliveryInfo-> $moduleDeliveryInfo") + moduleData = initModuleDownloadInfo(context, packageName, moduleDeliveryInfo) + val bundleData = Bundle().apply { + val isPack = moduleData?.packNames?.size != requestedAssetModuleNames.size + requestedAssetModuleNames.forEach { + putAll(buildDownloadBundle(it, moduleData!!, isPack, packNames = requestedAssetModuleNames)) + } + } + callback?.onRequestDownloadInfo(bundleData, bundleData) + sharedModuleDataFlow.emit(moduleData!!) + } + } + + override fun removeModule(packageName: String?, bundle: Bundle?, bundle2: Bundle?, callback: IAssetModuleServiceCallback?) { + Log.d(TAG, "Method (removeModule) called but not implemented by packageName -> $packageName") + } + + override fun cancelDownloads(packageName: String?, list: MutableList?, bundle: Bundle?, callback: IAssetModuleServiceCallback?) { + Log.d(TAG, "Method (cancelDownloads) called but not implemented by packageName -> $packageName") + } + + companion object { + private val moduleErrorRequested = arrayListOf() + } +} \ No newline at end of file diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/ModuleData.kt b/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/ModuleData.kt new file mode 100644 index 0000000000..064fb20c93 --- /dev/null +++ b/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/ModuleData.kt @@ -0,0 +1,66 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.finsky.assetmoduleservice + +import android.os.Bundle +import androidx.collection.ArrayMap + +data class ModuleData( + var packageName: String? = null, + var errorCode: Int = 0, + var sessionIds: ArrayMap? = null, + var bytesDownloaded: Long = 0, + var status: Int = 0, + var packNames: ArrayList? = null, + var appVersionCode: Int = 0, + var totalBytesToDownload: Long = 0 +) { + private var mPackData = emptyMap() + + fun setPackData(packData: Map) { + this.mPackData = packData + } + + fun getPackData(packName: String): PackData? { + return mPackData[packName] + } + + fun incrementPackBytesDownloaded(packName: String, bytes: Long) { + mPackData[packName]?.incrementBytesDownloaded(bytes) + } + + fun incrementBytesDownloaded(packName: String) { + bytesDownloaded += getPackData(packName)?.bytesDownloaded ?: 0 + } + + fun updateDownloadStatus(packName: String, statusCode: Int) { + getPackData(packName)?.status = statusCode + getPackData(packName)?.sessionId = statusCode + } + + fun updateModuleDownloadStatus(statusCode: Int) { + this.status = statusCode + } +} + +data class PackData( + var packVersion: Int = 0, + var packBaseVersion: Int = 0, + var sessionId: Int = 0, + var errorCode: Int = 0, + var status: Int = 0, + var bytesDownloaded: Long = 0, + var totalBytesToDownload: Long = 0, + var packVersionTag: String? = null, + var bundleList: ArrayList? = null, + var totalSumOfSubcontractedModules: Int = 0, + var subcontractingBaseUnit: Int = 0, + var listOfSubcontractNames: ArrayList? = null +) { + fun incrementBytesDownloaded(bytes: Long) { + bytesDownloaded += bytes + } +} diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/extensions.kt b/vending-app/src/main/kotlin/com/google/android/finsky/extensions.kt new file mode 100644 index 0000000000..2aa9687639 --- /dev/null +++ b/vending-app/src/main/kotlin/com/google/android/finsky/extensions.kt @@ -0,0 +1,295 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.finsky + +import android.content.Context +import android.content.Intent +import android.database.Cursor +import android.net.Uri +import android.os.Bundle +import android.util.Log +import androidx.collection.arrayMapOf +import androidx.collection.arraySetOf +import com.android.vending.licensing.getLicenseRequestHeaders +import com.google.android.finsky.assetmoduleservice.ModuleData +import com.google.android.finsky.assetmoduleservice.PackData +import org.microg.gms.settings.SettingsContract +import org.microg.vending.billing.core.HttpClient +import java.io.File +import java.util.Collections + +const val STATUS_NOT_INSTALLED = 8 +const val STATUS_COMPLETED = 4 +const val STATUS_TRANSFERRING = 3 +const val STATUS_DOWNLOADING = 2 +const val STATUS_SUCCESS = 0 + +const val KEY_ERROR_CODE = "error_code" +const val KEY_MODULE_NAME = "module_name" +const val KEY_RESOURCE_PACKAGE_NAME = "resourcePackageName" + +const val KEY_SESSION_ID = "session_id" +const val KEY_STATUS = "status" +const val KEY_PACK_VERSION = "pack_version" +const val KEY_PACK_BASE_VERSION = "pack_base_version" +const val KEY_BYTES_DOWNLOADED = "bytes_downloaded" +const val KEY_TOTAL_BYTES_TO_DOWNLOAD = "total_bytes_to_download" +const val KEY_PACK_NAMES = "pack_names" +const val KEY_APP_VERSION_CODE = "app_version_code" + +const val KEY_CACHE_DIR = "CacheDir" +const val KEY_INDEX = "index" +const val KEY_CHUNK_NAME = "chunkName" +const val KEY_CHUNK_INTENT = "chunk_intents" +const val KEY_CHUNK_NUMBER = "chunk_number" +const val KEY_CHUNK_FILE_DESCRIPTOR = "chunk_file_descriptor" +const val KEY_RESOURCE_LINK = "resourceLink" +const val KEY_BYTE_LENGTH = "byteLength" +const val KEY_RESOURCE_BLOCK_NAME = "resourceBlockName" +const val KEY_UNCOMPRESSED_SIZE = "uncompressed_size" +const val KEY_UNCOMPRESSED_HASH_SHA256 = "uncompressed_hash_sha256" +const val KEY_NUMBER_OF_SUBCONTRACTORS = "numberOfSubcontractors" + +const val KEY_USING_EXTRACTOR_STREAM = "usingExtractorStream" +const val KEY_COMPRESSION_FORMAT = "compression_format" +const val KEY_SLICE_IDS = "slice_ids" +const val KEY_SLICE_ID = "slice_id" +const val KEY_PLAY_CORE_VERSION_CODE = "playcore_version_code" + +const val ACTION_VIEW = "android.intent.action.VIEW" +const val TAG_REQUEST = "asset_module" + +private const val ACTION_SESSION_UPDATE = "com.google.android.play.core.assetpacks.receiver.ACTION_SESSION_UPDATE" +private const val EXTRA_SESSION_STATE = "com.google.android.play.core.assetpacks.receiver.EXTRA_SESSION_STATE" +private const val FLAGS = "com.google.android.play.core.FLAGS" + +private const val ASSET_MODULE_DELIVERY_URL = "https://play-fe.googleapis.com/fdfe/assetModuleDelivery" + +private const val TAG = "AssetModuleRequest" + +fun combineModule(key: String, vararg moduleNames: String?): String { + return moduleNames.joinToString(separator = ":", prefix = "$key:") +} + +fun getAppVersionCode(context: Context, packageName: String): String? { + return runCatching { context.packageManager.getPackageInfo(packageName, 0).versionCode.toString() }.getOrNull() +} + +suspend fun HttpClient.requestAssetModule(context: Context, auth: String, requestPayload: AssetModuleDeliveryRequest) = runCatching { + val androidId = SettingsContract.getSettings( + context, SettingsContract.CheckIn.getContentUri(context), arrayOf(SettingsContract.CheckIn.ANDROID_ID) + ) { cursor: Cursor -> cursor.getLong(0) } + Log.d(TAG, "auth->$auth") + Log.d(TAG, "androidId->$androidId") + Log.d(TAG, "requestPayload->$requestPayload") + post( + url = ASSET_MODULE_DELIVERY_URL, headers = getLicenseRequestHeaders(auth, 1), payload = requestPayload, adapter = AssetModuleDeliveryResponse.ADAPTER + ).wrapper?.deliveryInfo +}.onFailure { + Log.d(TAG, "requestAssetModule: ", it) +}.getOrNull() + +suspend fun HttpClient.downloadFile(context: Context, moduleName: String, moduleData: ModuleData, bundle: Bundle) { + val resourcePackageName: String? = bundle.getString(KEY_RESOURCE_PACKAGE_NAME) + val chunkName: String? = bundle.getString(KEY_CHUNK_NAME) + val resourceLink: String? = bundle.getString(KEY_RESOURCE_LINK) + val byteLength: Long = bundle.getLong(KEY_BYTE_LENGTH) + val index: Int = bundle.getInt(KEY_INDEX) + val resourceBlockName: String? = bundle.getString(KEY_RESOURCE_BLOCK_NAME) + if (resourcePackageName == null || chunkName == null || resourceLink == null || resourceBlockName == null) { + Log.d(TAG, "downloadFile: params invalid ") + return + } + val filesDir = "${context.filesDir}/assetpacks/$index/$resourcePackageName/$chunkName/" + val destination = File(filesDir, resourceBlockName) + if (destination.exists() && destination.length() == byteLength) { + sendBroadcastForExistingFile(context, moduleData, moduleName, bundle, destination) + return + } + if (destination.exists()) { + destination.delete() + } + val path = runCatching { download(resourceLink, destination, TAG_REQUEST) }.onFailure { Log.w(TAG, "downloadFile: ", it) }.getOrNull() + if (path != null) { + val file = File(path) + if (file.exists() && file.length() == byteLength) { + moduleData.updateDownloadStatus(moduleName, STATUS_TRANSFERRING) + moduleData.incrementPackBytesDownloaded(moduleName, byteLength) + moduleData.incrementBytesDownloaded(moduleName) + sendBroadcastForExistingFile(context, moduleData, moduleName, bundle, destination) + } + } +} + +fun initModuleDownloadInfo(context: Context, packageName: String, deliveryInfo: ModuleDeliveryInfo): ModuleData { + val packNames: ArrayList = arrayListOf() + var moduleDownloadByteLength = 0L + var appVersionCode = 0 + val sessionIds = arrayMapOf() + val packDataList = arrayMapOf() + for (deliveryIndex in deliveryInfo.res.indices) { + val resource: ModuleResource = deliveryInfo.res[deliveryIndex] + appVersionCode = resource.versionCode?.toInt() ?: 0 + val resourceList: List = resource.packResource + val resourcePackageName: String = resource.packName ?: continue + var packDownloadByteLength = 0L + packNames.add(resourcePackageName) + sessionIds[resourcePackageName] = deliveryIndex + 7 + var totalSumOfSubcontractedModules = 0 + val listOfSubcontractNames: ArrayList = ArrayList() + val bundlePackageName: ArrayList = arrayListOf() + for (resIndex in resourceList.indices) { + val packResource: PackResource = resourceList[resIndex] + if (packResource.downloadInfo == null || packResource.chunkInfo == null) { + continue + } + val downloadList = packResource.downloadInfo.download + val numberOfSubcontractors = downloadList.size + val uncompressedSize = packResource.downloadInfo.uncompressedSize + val uncompressedHashSha256 = packResource.downloadInfo.uncompressedHashCode + val chunkName = packResource.chunkInfo.chunkName?.also { listOfSubcontractNames.add(it) } + var resDownloadByteLength = 0L + for (downIndex in downloadList.indices) { + val dResource: Download = downloadList[downIndex] + resDownloadByteLength += dResource.byteLength!! + totalSumOfSubcontractedModules += 1 + val bundle = Bundle() + bundle.putString(KEY_CACHE_DIR, context.cacheDir.toString()) + bundle.putInt(KEY_INDEX, deliveryIndex + 7) + bundle.putString(KEY_RESOURCE_PACKAGE_NAME, resourcePackageName) + bundle.putString(KEY_CHUNK_NAME, chunkName) + bundle.putString(KEY_RESOURCE_LINK, dResource.resourceLink) + bundle.putLong(KEY_BYTE_LENGTH, dResource.byteLength) + bundle.putString(KEY_RESOURCE_BLOCK_NAME, downIndex.toString()) + bundle.putLong(KEY_UNCOMPRESSED_SIZE, uncompressedSize ?: 0) + bundle.putString(KEY_UNCOMPRESSED_HASH_SHA256, uncompressedHashSha256) + bundle.putInt(KEY_NUMBER_OF_SUBCONTRACTORS, numberOfSubcontractors) + bundlePackageName.add(bundle) + } + packDownloadByteLength += resDownloadByteLength + } + val packData = PackData( + packVersion = appVersionCode, + packBaseVersion = 0, + sessionId = STATUS_NOT_INSTALLED, + errorCode = STATUS_SUCCESS, + status = STATUS_NOT_INSTALLED, + bytesDownloaded = 0, + totalBytesToDownload = packDownloadByteLength, + bundleList = bundlePackageName, + totalSumOfSubcontractedModules = totalSumOfSubcontractedModules, + listOfSubcontractNames = listOfSubcontractNames + ) + moduleDownloadByteLength += packDownloadByteLength + packDataList[resourcePackageName] = packData + } + return ModuleData( + packageName = packageName, + errorCode = 0, + sessionIds = sessionIds, + bytesDownloaded = 0, + status = STATUS_NOT_INSTALLED, + packNames = packNames, + appVersionCode = appVersionCode, + totalBytesToDownload = moduleDownloadByteLength + ).apply { + setPackData(packDataList) + } +} + +fun buildDownloadBundle(packName: String, moduleData: ModuleData, isPack: Boolean = false, packNames: ArrayList? = null) = Bundle().apply { + val packData = moduleData.getPackData(packName) + packData?.run { + putInt(combineModule(KEY_SESSION_ID, packName), sessionId) + putInt(combineModule(KEY_STATUS, packName), status) + putInt(combineModule(KEY_ERROR_CODE, packName), errorCode) + putInt(combineModule(KEY_PACK_VERSION, packName), packVersion) + putInt(combineModule(KEY_PACK_BASE_VERSION, packName), packBaseVersion) + putLong(combineModule(KEY_BYTES_DOWNLOADED, packName), bytesDownloaded) + putLong(combineModule(KEY_TOTAL_BYTES_TO_DOWNLOAD, packName), totalBytesToDownload) + + putStringArrayList(KEY_PACK_NAMES, packNames ?: if (isPack) arrayListOf(packName) else moduleData.packNames) + putInt(KEY_STATUS, moduleData.status) + putInt(KEY_APP_VERSION_CODE, moduleData.appVersionCode) + putLong(KEY_TOTAL_BYTES_TO_DOWNLOAD, if (isPack) totalBytesToDownload else moduleData.totalBytesToDownload) + putInt(KEY_ERROR_CODE, if (isPack) errorCode else moduleData.errorCode) + putInt(KEY_SESSION_ID, moduleData.sessionIds?.get(packName) ?: sessionId) + putLong(KEY_BYTES_DOWNLOADED, if (isPack) bytesDownloaded else moduleData.bytesDownloaded) + } +} + +fun sendBroadcastForExistingFile(context: Context, moduleData: ModuleData, moduleName: String, bundle: Bundle?, destination: File?) { + val packData = moduleData.getPackData(moduleName) ?: return + try { + val downloadBundle = Bundle() + downloadBundle.putInt(KEY_APP_VERSION_CODE, moduleData.appVersionCode) + downloadBundle.putInt(KEY_ERROR_CODE, STATUS_SUCCESS) + downloadBundle.putInt(KEY_SESSION_ID, moduleData.sessionIds?.get(moduleName) ?: moduleData.status) + downloadBundle.putInt(KEY_STATUS, moduleData.status) + downloadBundle.putStringArrayList(KEY_PACK_NAMES, arrayListOf(moduleName)) + downloadBundle.putLong(KEY_BYTES_DOWNLOADED, packData.bytesDownloaded) + downloadBundle.putLong(KEY_TOTAL_BYTES_TO_DOWNLOAD, packData.totalBytesToDownload) + downloadBundle.putLong(combineModule(KEY_TOTAL_BYTES_TO_DOWNLOAD, moduleName), packData.totalBytesToDownload) + downloadBundle.putInt(combineModule(KEY_PACK_VERSION, moduleName), packData.packVersion) + downloadBundle.putInt(combineModule(KEY_STATUS, moduleName), packData.status) + downloadBundle.putInt(combineModule(KEY_ERROR_CODE, moduleName), STATUS_SUCCESS) + downloadBundle.putLong(combineModule(KEY_BYTES_DOWNLOADED, moduleName), packData.bytesDownloaded) + downloadBundle.putInt(combineModule(KEY_PACK_BASE_VERSION, moduleName), packData.packBaseVersion) + val resultList = arraySetOf() + packData.bundleList?.forEach { + val result = Bundle() + result.putString(KEY_CHUNK_NAME, it.getString(KEY_CHUNK_NAME)) + result.putLong(KEY_UNCOMPRESSED_SIZE, it.getLong(KEY_UNCOMPRESSED_SIZE)) + result.putString(KEY_UNCOMPRESSED_HASH_SHA256, it.getString(KEY_UNCOMPRESSED_HASH_SHA256)) + result.putInt(KEY_NUMBER_OF_SUBCONTRACTORS, it.getInt(KEY_NUMBER_OF_SUBCONTRACTORS)) + result.putLong(KEY_BYTE_LENGTH, it.getLong(KEY_BYTE_LENGTH)) + resultList.add(result) + } + resultList.forEach { + val chunkName = it.getString(KEY_CHUNK_NAME) + val uncompressedSize = it.getLong(KEY_UNCOMPRESSED_SIZE) + val uncompressedHashSha256 = it.getString(KEY_UNCOMPRESSED_HASH_SHA256) + val numberOfSubcontractors = it.getInt(KEY_NUMBER_OF_SUBCONTRACTORS) + val chunkIntents: ArrayList + if (destination == null) { + chunkIntents = ArrayList(Collections.nCopies(numberOfSubcontractors, null)) + } else { + val uFile = Uri.parse(destination.absolutePath).path?.let { path -> File(path) } + chunkIntents = ArrayList(Collections.nCopies(numberOfSubcontractors, null)) + val uri = Uri.fromFile(uFile) + context.grantUriPermission(moduleName, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION) + val intent = Intent(ACTION_VIEW) + intent.setDataAndType(uri, context.contentResolver.getType(uri)) + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + val resourceBlockIndex = bundle?.getString(KEY_RESOURCE_BLOCK_NAME)?.toInt() + if (uFile?.exists() == true && bundle?.getString(KEY_CHUNK_NAME) == chunkName && resourceBlockIndex != null) { + if (chunkIntents[resourceBlockIndex] == null) { + chunkIntents[resourceBlockIndex] = intent + } + } + } + downloadBundle.putParcelableArrayList(combineModule(KEY_CHUNK_INTENT, moduleName, chunkName), chunkIntents) + downloadBundle.putLong(combineModule(KEY_UNCOMPRESSED_SIZE, moduleName, chunkName), uncompressedSize) + downloadBundle.putInt(combineModule(KEY_COMPRESSION_FORMAT, moduleName, chunkName), 1) + downloadBundle.putString(combineModule(KEY_UNCOMPRESSED_HASH_SHA256, moduleName, chunkName), uncompressedHashSha256) + } + downloadBundle.putStringArrayList(combineModule(KEY_SLICE_IDS, moduleName), packData.listOfSubcontractNames) + Log.d(TAG, "sendBroadcastForExistingFile: $downloadBundle") + sendBroadCast(context, moduleData, downloadBundle) + } catch (e: Exception) { + Log.w(TAG, "sendBroadcastForExistingFile error:" + e.message) + } +} + +private fun sendBroadCast(context: Context, moduleData: ModuleData, result: Bundle) { + val intent = Intent() + intent.setAction(ACTION_SESSION_UPDATE) + intent.putExtra(EXTRA_SESSION_STATE, result) + intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY) + intent.putExtra(FLAGS, Bundle().apply { putBoolean(KEY_USING_EXTRACTOR_STREAM, true) }) + intent.setPackage(moduleData.packageName) + context.sendBroadcast(intent) +} diff --git a/vending-app/src/main/proto/AssetModule.proto b/vending-app/src/main/proto/AssetModule.proto new file mode 100644 index 0000000000..31f2db45dd --- /dev/null +++ b/vending-app/src/main/proto/AssetModule.proto @@ -0,0 +1,100 @@ +option java_package = "com.google.android.finsky"; +option java_multiple_files = true; + +import "Timestamp.proto"; + +message AssetModuleDeliveryRequest { + optional string packageName = 1; + optional CallerInfo callerInfo = 2; + optional uint32 playCoreVersion = 3; + repeated PageSource pageSource = 4; + repeated CallerState callerState = 5; + repeated AssetModuleInfo moduleInfo = 6; +} + +message CallerInfo { + oneof Version { + int32 versionCode = 1; + string versionName = 3; + } +} + +message AssetModuleInfo { + optional string name = 1; + optional int64 type = 2; +} + +enum PageSource { + UNKNOWN_SEARCH_TRAFFIC_SOURCE = 0; + GAMES_HOME_PAGE = 1; + APPS_HOME_PAGE = 2; + BOOKS_HOME_PAGE = 3; + FAMILY_HOME_PAGE = 4; + KIDS_HOME_PAGE = 5; + PLAY_PASS_HOME_PAGE = 6; + NOW_HOME_PAGE = 7; + DEALS_HOME_PAGE = 8; + SEARCH_HOME_PAGE = 9; + DETAILS_PAGE = 10; + APPS_SEARCH_PAGE = 11; + EBOOKS_SEARCH_PAGE = 12; + DEEP_LINK_SEARCH = 13; + TABBED_BROWSE_FRAGMENT_HOME_PAGE = 14; + DETAILS_PAGE_TOOLBAR = 15; + UNKNOWN_SEARCH_TRAFFIC_SOURCE_CAR = 16; + LOYALTY_HOME_PAGE = 17; +} + +enum CallerState { + UNKNOWN_SECURITY_CHECK_TYPE = 0; + CALLER_APP_REQUEST = 1; + CALLER_APP_DEBUGGABLE = 2; + CALLER_APP_INTENT_HANDLING_PACKAGE_MISSING = 3; + CALLER_APP_INSTALLER_PACKAGE_NAME_UNKNOWN = 4; + CALLER_APP_INSTALLER_PACKAGE_NAME_3P = 5; + CALLER_APP_INSTALLED_CERTIFICATE_MISSING = 6; + CALLER_APP_CERTIFICATE_UNMATCHED = 7; +} + +message AssetModuleDeliveryResponse { + optional ModuleDeliveryWrapper wrapper = 1; +} + +message ModuleDeliveryWrapper { + optional ModuleDeliveryInfo deliveryInfo = 151; +} + +message ModuleDeliveryInfo { + repeated ModuleResource res = 3; + optional int32 status = 4; +} + +message ModuleResource { + optional string packName = 1; + optional int64 versionCode = 2; + repeated PackResource packResource = 3; + optional int64 unknownInt4 = 4; + optional string unknownString5 = 5; +} + +message PackResource { + optional ChunkInfo chunkInfo = 1; + optional DownloadInfo downloadInfo = 2; +} + +message ChunkInfo { + optional string chunkName = 1; +} + +message DownloadInfo { + optional int64 uncompressedSize = 1; + optional string uncompressedHashCode = 2; + optional PageSource state = 3; + repeated Download download = 4; +} + +message Download { + optional int64 byteLength = 1; + optional string uncompressed = 2; + optional string resourceLink = 3; +} From 78192070b6d2675ed40f44ec538820a7f21afdf4 Mon Sep 17 00:00:00 2001 From: davinci9196 Date: Sat, 12 Oct 2024 09:40:10 +0800 Subject: [PATCH 075/132] Asset Modules: Change the Volley download request process to HttpURLConnection. It is necessary to send progress change broadcasts in real time, otherwise the progress bar will be stuck. --- .../microg/vending/billing/core/HttpClient.kt | 40 +--- .../assetmoduleservice/AssetModuleService.kt | 200 +++++++++--------- .../finsky/assetmoduleservice/ModuleData.kt | 25 ++- .../com/google/android/finsky/extensions.kt | 157 ++++++++++---- 4 files changed, 235 insertions(+), 187 deletions(-) diff --git a/vending-app/src/main/java/org/microg/vending/billing/core/HttpClient.kt b/vending-app/src/main/java/org/microg/vending/billing/core/HttpClient.kt index 49d849312a..3d66ae1669 100644 --- a/vending-app/src/main/java/org/microg/vending/billing/core/HttpClient.kt +++ b/vending-app/src/main/java/org/microg/vending/billing/core/HttpClient.kt @@ -2,7 +2,11 @@ package org.microg.vending.billing.core import android.content.Context import android.net.Uri -import com.android.volley.* +import com.android.volley.DefaultRetryPolicy +import com.android.volley.NetworkResponse +import com.android.volley.Request +import com.android.volley.Response +import com.android.volley.VolleyError import com.android.volley.toolbox.HttpHeaderParser import com.android.volley.toolbox.JsonObjectRequest import com.android.volley.toolbox.Volley @@ -10,9 +14,6 @@ import com.squareup.wire.Message import com.squareup.wire.ProtoAdapter import org.json.JSONObject import org.microg.gms.utils.singleInstanceOf -import java.io.File -import java.io.FileOutputStream -import java.io.IOException import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException import kotlin.coroutines.suspendCoroutine @@ -23,37 +24,6 @@ class HttpClient(context: Context) { val requestQueue = singleInstanceOf { Volley.newRequestQueue(context.applicationContext) } - suspend fun download( - url: String, downloadFile: File, tag: String - ): String = suspendCoroutine { continuation -> - val uriBuilder = Uri.parse(url).buildUpon() - requestQueue.add(object : Request(Method.GET, uriBuilder.build().toString(), null) { - override fun parseNetworkResponse(response: NetworkResponse): Response { - if (response.statusCode != 200) throw VolleyError(response) - return try { - val parentDir = downloadFile.getParentFile() - if (parentDir != null && !parentDir.exists() && !parentDir.mkdirs()) { - throw IOException("Failed to create directories: ${parentDir.absolutePath}") - } - val fos = FileOutputStream(downloadFile) - fos.write(response.data) - fos.close() - Response.success(downloadFile.absolutePath, HttpHeaderParser.parseCacheHeaders(response)) - } catch (e: Exception) { - Response.error(VolleyError(e)) - } - } - - override fun deliverResponse(response: String) { - continuation.resume(response) - } - - override fun deliverError(error: VolleyError) { - continuation.resumeWithException(error) - } - }.setShouldCache(false).setTag(tag)) - } - suspend fun get( url: String, headers: Map = emptyMap(), diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/AssetModuleService.kt b/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/AssetModuleService.kt index 9157756eb5..39cd1245d3 100644 --- a/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/AssetModuleService.kt +++ b/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/AssetModuleService.kt @@ -4,7 +4,6 @@ */ package com.google.android.finsky.assetmoduleservice -import android.accounts.Account import android.accounts.AccountManager import android.content.Context import android.content.Intent @@ -17,39 +16,33 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleService import androidx.lifecycle.lifecycleScope -import com.android.vending.licensing.AUTH_TOKEN_SCOPE -import com.android.vending.licensing.getAuthToken -import com.google.android.finsky.AssetModuleDeliveryRequest -import com.google.android.finsky.AssetModuleInfo -import com.google.android.finsky.CallerInfo -import com.google.android.finsky.CallerState +import com.google.android.finsky.ERROR_CODE_FAIL +import com.google.android.finsky.KEY_APP_VERSION_CODE +import com.google.android.finsky.KEY_BYTES_DOWNLOADED import com.google.android.finsky.KEY_CHUNK_FILE_DESCRIPTOR import com.google.android.finsky.KEY_CHUNK_NUMBER import com.google.android.finsky.KEY_ERROR_CODE import com.google.android.finsky.KEY_MODULE_NAME +import com.google.android.finsky.KEY_PACK_BASE_VERSION import com.google.android.finsky.KEY_PACK_NAMES +import com.google.android.finsky.KEY_PACK_VERSION import com.google.android.finsky.KEY_PLAY_CORE_VERSION_CODE import com.google.android.finsky.KEY_RESOURCE_PACKAGE_NAME import com.google.android.finsky.KEY_SESSION_ID import com.google.android.finsky.KEY_SLICE_ID -import com.google.android.finsky.PageSource +import com.google.android.finsky.KEY_STATUS +import com.google.android.finsky.KEY_TOTAL_BYTES_TO_DOWNLOAD import com.google.android.finsky.STATUS_COMPLETED import com.google.android.finsky.STATUS_DOWNLOADING -import com.google.android.finsky.STATUS_NOT_INSTALLED -import com.google.android.finsky.STATUS_TRANSFERRING +import com.google.android.finsky.STATUS_INITIAL_STATE import com.google.android.finsky.TAG_REQUEST import com.google.android.finsky.buildDownloadBundle +import com.google.android.finsky.combineModule import com.google.android.finsky.downloadFile -import com.google.android.finsky.getAppVersionCode -import com.google.android.finsky.initModuleDownloadInfo -import com.google.android.finsky.requestAssetModule +import com.google.android.finsky.initAssertModuleData import com.google.android.finsky.sendBroadcastForExistingFile import com.google.android.play.core.assetpacks.protocol.IAssetModuleService import com.google.android.play.core.assetpacks.protocol.IAssetModuleServiceCallback -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.collectLatest -import okhttp3.internal.filterList -import org.microg.gms.auth.AuthConstants import org.microg.gms.profile.ProfileManager import org.microg.vending.billing.core.HttpClient import java.io.File @@ -80,8 +73,9 @@ class AssetModuleService : LifecycleService() { class AssetModuleServiceImpl( val context: Context, override val lifecycle: Lifecycle, private val httpClient: HttpClient, private val accountManager: AccountManager ) : IAssetModuleService.Stub(), LifecycleOwner { - private val sharedModuleDataFlow = MutableSharedFlow() + @Volatile private var moduleData: ModuleData? = null + private val fileDescriptorMap = mutableMapOf() override fun startDownload(packageName: String?, list: MutableList?, bundle: Bundle?, callback: IAssetModuleServiceCallback?) { Log.d(TAG, "Method (startDownload) called by packageName -> $packageName") @@ -90,38 +84,63 @@ class AssetModuleServiceImpl( callback?.onError(Bundle().apply { putInt(KEY_ERROR_CODE, -5) }) return } - if (moduleData == null && moduleErrorRequested.contains(packageName)) { - Log.d(TAG, "startDownload: moduleData request error") - val result = Bundle().apply { putStringArrayList(KEY_PACK_NAMES, arrayListOf()) } - callback?.onStartDownload(-1, result) - return - } - suspend fun prepare(data: ModuleData) { - list.forEach { - val moduleName = it.getString(KEY_MODULE_NAME) - if (moduleName != null) { - callback?.onStartDownload(-1, buildDownloadBundle(moduleName, data, true)) - val packData = data.getPackData(moduleName) - if (packData?.status != STATUS_NOT_INSTALLED) { - Log.w(TAG, "startDownload: packData?.status is ${packData?.status}") - return@forEach - } - data.updateDownloadStatus(moduleName, STATUS_DOWNLOADING) - data.updateModuleDownloadStatus(STATUS_DOWNLOADING) - packData.bundleList?.forEach { download -> - if (moduleName == download.getString(KEY_RESOURCE_PACKAGE_NAME)) { - httpClient.downloadFile(context, moduleName, data, download) + lifecycleScope.launchWhenStarted { + if (moduleErrorRequested.contains(packageName)) { + Log.d(TAG, "startDownload: moduleData request error") + val result = Bundle().apply { putStringArrayList(KEY_PACK_NAMES, arrayListOf()) } + Log.d(TAG, "prepare: ${result.keySet()}") + callback?.onStartDownload(-1, result) + return@launchWhenStarted + } + if (moduleData?.status != STATUS_DOWNLOADING) { + val requestedAssetModuleNames = list.map { it.getString(KEY_MODULE_NAME) }.filter { !it.isNullOrEmpty() } + if (moduleData == null) { + val playCoreVersionCode = bundle.getInt(KEY_PLAY_CORE_VERSION_CODE) + moduleData = httpClient.initAssertModuleData(context, packageName, accountManager, requestedAssetModuleNames, playCoreVersionCode) + } + list.forEach { + val moduleName = it.getString(KEY_MODULE_NAME) + if (moduleName != null) { + val packData = moduleData!!.getPackData(moduleName) + callback?.onStartDownload(-1, buildDownloadBundle(moduleName, moduleData!!, true)) + if (packData?.status != STATUS_INITIAL_STATE) { + Log.w(TAG, "startDownload: packData?.status is ${packData?.status}") + return@forEach + } + moduleData?.updateDownloadStatus(moduleName, STATUS_DOWNLOADING) + moduleData?.updateModuleDownloadStatus(STATUS_DOWNLOADING) + packData.bundleList?.forEach { download -> + if (moduleName == download.getString(KEY_RESOURCE_PACKAGE_NAME)) { + downloadFile(context, moduleName, moduleData!!, download) + } } } } - } - } - lifecycleScope.launchWhenStarted { - if (moduleData == null) { - sharedModuleDataFlow.collectLatest { prepare(it) } return@launchWhenStarted } - prepare(moduleData!!) + val result = Bundle() + val arrayList = arrayListOf() + result.putInt(KEY_STATUS, moduleData!!.status) + result.putLong(KEY_APP_VERSION_CODE, moduleData!!.appVersionCode) + result.putLong(KEY_BYTES_DOWNLOADED, moduleData!!.bytesDownloaded) + result.putInt(KEY_ERROR_CODE, moduleData!!.errorCode) + result.putInt(KEY_SESSION_ID, 6) + result.putLong(KEY_TOTAL_BYTES_TO_DOWNLOAD, moduleData!!.totalBytesToDownload) + list.forEach { + val moduleName = it.getString(KEY_MODULE_NAME) + arrayList.add(moduleName!!) + val packData = moduleData!!.getPackData(moduleName) + result.putInt(combineModule(KEY_SESSION_ID, moduleName), packData!!.sessionId) + result.putInt(combineModule(KEY_STATUS, moduleName), packData.status) + result.putInt(combineModule(KEY_ERROR_CODE, moduleName), packData.errorCode) + result.putLong(combineModule(KEY_PACK_VERSION, moduleName), packData.packVersion) + result.putLong(combineModule(KEY_PACK_BASE_VERSION, moduleName), packData.packBaseVersion) + result.putLong(combineModule(KEY_BYTES_DOWNLOADED, moduleName), packData.bytesDownloaded) + result.putLong(combineModule(KEY_TOTAL_BYTES_TO_DOWNLOAD, moduleName), packData.totalBytesToDownload) + sendBroadcastForExistingFile(context, moduleData!!, moduleName, null, null) + } + result.putStringArrayList(KEY_PACK_NAMES, arrayList) + callback?.onStartDownload(-1, result) } } @@ -130,8 +149,20 @@ class AssetModuleServiceImpl( } override fun notifyChunkTransferred(packageName: String?, bundle: Bundle?, bundle2: Bundle?, callback: IAssetModuleServiceCallback?) { - Log.d(TAG, "Method (notifyChunkTransferred) called but not implemented by packageName -> $packageName") - callback?.onNotifyChunkTransferred(bundle, Bundle()) + Log.d(TAG, "Method (notifyChunkTransferred) called by packageName -> $packageName") + val moduleName = bundle?.getString(KEY_MODULE_NAME) + if (moduleName.isNullOrEmpty()) { + Log.d(TAG, "notifyChunkTransferred: params invalid ") + callback?.onError(Bundle().apply { putInt(KEY_ERROR_CODE, -5) }) + return + } + val sessionId = bundle.getInt(KEY_SESSION_ID) + val sliceId = bundle.getString(KEY_SLICE_ID) + val chunkNumber = bundle.getInt(KEY_CHUNK_NUMBER) + val downLoadFile = "${context.filesDir.absolutePath}/assetpacks/$sessionId/$moduleName/$sliceId/$chunkNumber" + fileDescriptorMap[downLoadFile]?.close() + fileDescriptorMap.remove(downLoadFile) + callback?.onNotifyChunkTransferred(bundle, Bundle().apply { putInt(KEY_ERROR_CODE, 0) }) } override fun notifyModuleCompleted(packageName: String?, bundle: Bundle?, bundle2: Bundle?, callback: IAssetModuleServiceCallback?) { @@ -142,21 +173,16 @@ class AssetModuleServiceImpl( callback?.onError(Bundle().apply { putInt(KEY_ERROR_CODE, -5) }) return } - fun notify(data: ModuleData) { - callback?.onNotifyModuleCompleted(bundle, bundle) - val notify = data.packNames?.all { data.getPackData(it)?.status == STATUS_TRANSFERRING } ?: false - if (notify) { - data.packNames?.forEach { moduleData?.updateDownloadStatus(it, STATUS_COMPLETED) } - data.updateModuleDownloadStatus(STATUS_COMPLETED) - sendBroadcastForExistingFile(context, moduleData!!, moduleName, null, null) - } - } lifecycleScope.launchWhenStarted { - if (moduleData == null) { - sharedModuleDataFlow.collectLatest { notify(it) } - return@launchWhenStarted + Log.d(TAG, "notify: moduleName: $moduleName packNames: ${moduleData?.packNames}") + moduleData?.packNames?.find { it == moduleName }?.let { + moduleData?.updateDownloadStatus(it, STATUS_COMPLETED) + if (moduleData?.packNames?.all { pack -> moduleData?.getPackData(pack)?.status == STATUS_COMPLETED } == true) { + moduleData?.updateModuleDownloadStatus(STATUS_COMPLETED) + } + sendBroadcastForExistingFile(context, moduleData!!, moduleName, null, null) } - notify(moduleData!!) + callback?.onNotifyModuleCompleted(bundle, bundle) } } @@ -169,7 +195,7 @@ class AssetModuleServiceImpl( } override fun getChunkFileDescriptor(packageName: String, bundle: Bundle, bundle2: Bundle, callback: IAssetModuleServiceCallback?) { - Log.d(TAG, "Method (getChunkFileDescriptor) called but not implemented by packageName -> $packageName") + Log.d(TAG, "Method (getChunkFileDescriptor) called by packageName -> $packageName") val moduleName = bundle.getString(KEY_MODULE_NAME) if (moduleName.isNullOrEmpty()) { Log.d(TAG, "getChunkFileDescriptor: params invalid ") @@ -182,7 +208,9 @@ class AssetModuleServiceImpl( val chunkNumber = bundle.getInt(KEY_CHUNK_NUMBER) val downLoadFile = "${context.filesDir.absolutePath}/assetpacks/$sessionId/$moduleName/$sliceId/$chunkNumber" val filePath = Uri.parse(downLoadFile).path?.let { File(it) } - ParcelFileDescriptor.open(filePath, ParcelFileDescriptor.MODE_READ_ONLY) + ParcelFileDescriptor.open(filePath, ParcelFileDescriptor.MODE_READ_ONLY).also { + fileDescriptorMap[downLoadFile] = it + } }.getOrNull() callback?.onGetChunkFileDescriptor(Bundle().apply { putParcelable(KEY_CHUNK_FILE_DESCRIPTOR, parcelFileDescriptor) }, Bundle()) } @@ -195,45 +223,12 @@ class AssetModuleServiceImpl( return } lifecycleScope.launchWhenStarted { - val requestedAssetModuleNames = arrayListOf() - for (data in list) { - val moduleName = data.getString(KEY_MODULE_NAME) - if (!moduleName.isNullOrEmpty()) { - requestedAssetModuleNames.add(moduleName) - } - } - if (!moduleData?.packNames.isNullOrEmpty()) { - val bundleData = Bundle().apply { - val isPack = moduleData?.packNames?.size != requestedAssetModuleNames.size - requestedAssetModuleNames.forEach { - putAll(buildDownloadBundle(it, moduleData!!, isPack, packNames = requestedAssetModuleNames)) - } - } - callback?.onRequestDownloadInfo(bundleData, bundleData) - return@launchWhenStarted - } - val accounts = accountManager.getAccountsByType(AuthConstants.DEFAULT_ACCOUNT_TYPE) - var oauthToken: String? = null - if (accounts.isEmpty()) { - callback?.onError(Bundle().apply { putInt(KEY_ERROR_CODE, -5) }) - return@launchWhenStarted - } else for (account: Account in accounts) { - oauthToken = accountManager.getAuthToken(account, AUTH_TOKEN_SCOPE, false).getString(AccountManager.KEY_AUTHTOKEN) - if (oauthToken != null) { - break - } + val requestedAssetModuleNames = list.map { it.getString(KEY_MODULE_NAME) }.filter { !it.isNullOrEmpty() } + if (moduleData == null) { + val playCoreVersionCode = bundle.getInt(KEY_PLAY_CORE_VERSION_CODE) + moduleData = httpClient.initAssertModuleData(context, packageName, accountManager, requestedAssetModuleNames, playCoreVersionCode) } - val requestPayload = - AssetModuleDeliveryRequest.Builder().callerInfo(CallerInfo(getAppVersionCode(context, packageName)?.toInt())).packageName(packageName) - .playCoreVersion(bundle.getInt(KEY_PLAY_CORE_VERSION_CODE)) - .pageSource(listOf(PageSource.UNKNOWN_SEARCH_TRAFFIC_SOURCE, PageSource.BOOKS_HOME_PAGE)) - .callerState(listOf(CallerState.CALLER_APP_REQUEST, CallerState.CALLER_APP_DEBUGGABLE)).moduleInfo(ArrayList().apply { - list.filterList { !getString(KEY_MODULE_NAME).isNullOrEmpty() }.forEach { - add(AssetModuleInfo.Builder().name(it.getString(KEY_MODULE_NAME)).build()) - } - }).build() - val moduleDeliveryInfo = httpClient.requestAssetModule(context, oauthToken!!, requestPayload) - if (moduleDeliveryInfo == null || moduleDeliveryInfo.status != null) { + if (moduleData?.errorCode == ERROR_CODE_FAIL) { if (moduleErrorRequested.contains(packageName)) { callback?.onError(Bundle().apply { putInt(KEY_ERROR_CODE, -5) }) return@launchWhenStarted @@ -244,16 +239,13 @@ class AssetModuleServiceImpl( return@launchWhenStarted } moduleErrorRequested.remove(packageName) - Log.d(TAG, "requestDownloadInfo: moduleDeliveryInfo-> $moduleDeliveryInfo") - moduleData = initModuleDownloadInfo(context, packageName, moduleDeliveryInfo) val bundleData = Bundle().apply { val isPack = moduleData?.packNames?.size != requestedAssetModuleNames.size requestedAssetModuleNames.forEach { - putAll(buildDownloadBundle(it, moduleData!!, isPack, packNames = requestedAssetModuleNames)) + putAll(buildDownloadBundle(it!!, moduleData!!, isPack, packNames = requestedAssetModuleNames)) } } callback?.onRequestDownloadInfo(bundleData, bundleData) - sharedModuleDataFlow.emit(moduleData!!) } } diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/ModuleData.kt b/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/ModuleData.kt index 064fb20c93..e52919b5d6 100644 --- a/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/ModuleData.kt +++ b/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/ModuleData.kt @@ -5,8 +5,10 @@ package com.google.android.finsky.assetmoduleservice +import android.content.Context import android.os.Bundle import androidx.collection.ArrayMap +import com.google.android.finsky.sendBroadcastForExistingFile data class ModuleData( var packageName: String? = null, @@ -15,8 +17,8 @@ data class ModuleData( var bytesDownloaded: Long = 0, var status: Int = 0, var packNames: ArrayList? = null, - var appVersionCode: Int = 0, - var totalBytesToDownload: Long = 0 + var appVersionCode: Long = 0, + var totalBytesToDownload: Long = 0, ) { private var mPackData = emptyMap() @@ -28,12 +30,9 @@ data class ModuleData( return mPackData[packName] } - fun incrementPackBytesDownloaded(packName: String, bytes: Long) { + fun incrementPackBytesDownloaded(context: Context, packName: String, bytes: Long) { mPackData[packName]?.incrementBytesDownloaded(bytes) - } - - fun incrementBytesDownloaded(packName: String) { - bytesDownloaded += getPackData(packName)?.bytesDownloaded ?: 0 + bytesDownloaded += bytes } fun updateDownloadStatus(packName: String, statusCode: Int) { @@ -44,11 +43,15 @@ data class ModuleData( fun updateModuleDownloadStatus(statusCode: Int) { this.status = statusCode } + + override fun toString(): String { + return "ModuleData(packageName=$packageName, errorCode=$errorCode, sessionIds=$sessionIds, bytesDownloaded=$bytesDownloaded, status=$status, packNames=$packNames, appVersionCode=$appVersionCode, totalBytesToDownload=$totalBytesToDownload, mPackData=$mPackData)" + } } data class PackData( - var packVersion: Int = 0, - var packBaseVersion: Int = 0, + var packVersion: Long = 0, + var packBaseVersion: Long = 0, var sessionId: Int = 0, var errorCode: Int = 0, var status: Int = 0, @@ -63,4 +66,8 @@ data class PackData( fun incrementBytesDownloaded(bytes: Long) { bytesDownloaded += bytes } + + override fun toString(): String { + return "PackData(packVersion=$packVersion, packBaseVersion=$packBaseVersion, sessionId=$sessionId, errorCode=$errorCode, status=$status, bytesDownloaded=$bytesDownloaded, totalBytesToDownload=$totalBytesToDownload, packVersionTag=$packVersionTag, bundleList=$bundleList, totalSumOfSubcontractedModules=$totalSumOfSubcontractedModules, subcontractingBaseUnit=$subcontractingBaseUnit, listOfSubcontractNames=$listOfSubcontractNames)" + } } diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/extensions.kt b/vending-app/src/main/kotlin/com/google/android/finsky/extensions.kt index 2aa9687639..a1511cce93 100644 --- a/vending-app/src/main/kotlin/com/google/android/finsky/extensions.kt +++ b/vending-app/src/main/kotlin/com/google/android/finsky/extensions.kt @@ -5,27 +5,38 @@ package com.google.android.finsky +import android.accounts.Account +import android.accounts.AccountManager import android.content.Context import android.content.Intent -import android.database.Cursor import android.net.Uri import android.os.Bundle import android.util.Log import androidx.collection.arrayMapOf import androidx.collection.arraySetOf +import com.android.vending.licensing.AUTH_TOKEN_SCOPE +import com.android.vending.licensing.getAuthToken import com.android.vending.licensing.getLicenseRequestHeaders import com.google.android.finsky.assetmoduleservice.ModuleData import com.google.android.finsky.assetmoduleservice.PackData -import org.microg.gms.settings.SettingsContract +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.microg.gms.auth.AuthConstants import org.microg.vending.billing.core.HttpClient import java.io.File +import java.io.FileOutputStream +import java.io.IOException +import java.net.HttpURLConnection +import java.net.URL import java.util.Collections const val STATUS_NOT_INSTALLED = 8 const val STATUS_COMPLETED = 4 -const val STATUS_TRANSFERRING = 3 const val STATUS_DOWNLOADING = 2 -const val STATUS_SUCCESS = 0 +const val STATUS_INITIAL_STATE = 1 + +const val ERROR_CODE_SUCCESS = 0 +const val ERROR_CODE_FAIL = -5 const val KEY_ERROR_CODE = "error_code" const val KEY_MODULE_NAME = "module_name" @@ -78,21 +89,47 @@ fun getAppVersionCode(context: Context, packageName: String): String? { return runCatching { context.packageManager.getPackageInfo(packageName, 0).versionCode.toString() }.getOrNull() } -suspend fun HttpClient.requestAssetModule(context: Context, auth: String, requestPayload: AssetModuleDeliveryRequest) = runCatching { - val androidId = SettingsContract.getSettings( - context, SettingsContract.CheckIn.getContentUri(context), arrayOf(SettingsContract.CheckIn.ANDROID_ID) - ) { cursor: Cursor -> cursor.getLong(0) } - Log.d(TAG, "auth->$auth") - Log.d(TAG, "androidId->$androidId") - Log.d(TAG, "requestPayload->$requestPayload") - post( - url = ASSET_MODULE_DELIVERY_URL, headers = getLicenseRequestHeaders(auth, 1), payload = requestPayload, adapter = AssetModuleDeliveryResponse.ADAPTER - ).wrapper?.deliveryInfo -}.onFailure { - Log.d(TAG, "requestAssetModule: ", it) -}.getOrNull() +suspend fun HttpClient.initAssertModuleData( + context: Context, + packageName: String, + accountManager: AccountManager, + requestedAssetModuleNames: List, + playCoreVersionCode: Int, +): ModuleData { + Log.d(TAG, "initAssertModuleData: requestedAssetModuleNames: $requestedAssetModuleNames") + val accounts = accountManager.getAccountsByType(AuthConstants.DEFAULT_ACCOUNT_TYPE) + var oauthToken: String? = null + if (accounts.isEmpty()) { + return ModuleData(errorCode = ERROR_CODE_FAIL) + } else for (account: Account in accounts) { + oauthToken = accountManager.getAuthToken(account, AUTH_TOKEN_SCOPE, false).getString(AccountManager.KEY_AUTHTOKEN) + if (oauthToken != null) { + break + } + } + if (oauthToken == null) { + return ModuleData(errorCode = ERROR_CODE_FAIL) + } + val requestPayload = AssetModuleDeliveryRequest.Builder().callerInfo(CallerInfo(getAppVersionCode(context, packageName)?.toInt())).packageName(packageName) + .playCoreVersion(playCoreVersionCode).pageSource(listOf(PageSource.UNKNOWN_SEARCH_TRAFFIC_SOURCE, PageSource.BOOKS_HOME_PAGE)) + .callerState(listOf(CallerState.CALLER_APP_REQUEST, CallerState.CALLER_APP_DEBUGGABLE)).moduleInfo(ArrayList().apply { + requestedAssetModuleNames.forEach { add(AssetModuleInfo.Builder().name(it).build()) } + }).build() + val moduleDeliveryInfo = runCatching { + post( + url = ASSET_MODULE_DELIVERY_URL, + headers = getLicenseRequestHeaders(oauthToken, 1), + payload = requestPayload, + adapter = AssetModuleDeliveryResponse.ADAPTER + ).wrapper?.deliveryInfo + }.onFailure { + Log.d(TAG, "initAssertModuleData: ", it) + }.getOrNull() + Log.d(TAG, "initAssertModuleData: moduleDeliveryInfo-> $moduleDeliveryInfo") + return initModuleDownloadInfo(context, packageName, moduleDeliveryInfo) +} -suspend fun HttpClient.downloadFile(context: Context, moduleName: String, moduleData: ModuleData, bundle: Bundle) { +suspend fun downloadFile(context: Context, moduleName: String, moduleData: ModuleData, bundle: Bundle) { val resourcePackageName: String? = bundle.getString(KEY_RESOURCE_PACKAGE_NAME) val chunkName: String? = bundle.getString(KEY_CHUNK_NAME) val resourceLink: String? = bundle.getString(KEY_RESOURCE_LINK) @@ -112,27 +149,69 @@ suspend fun HttpClient.downloadFile(context: Context, moduleName: String, module if (destination.exists()) { destination.delete() } - val path = runCatching { download(resourceLink, destination, TAG_REQUEST) }.onFailure { Log.w(TAG, "downloadFile: ", it) }.getOrNull() + val path = runCatching { download(context, resourceLink, destination, moduleName, moduleData) }.onFailure { Log.w(TAG, "downloadFile: ", it) }.getOrNull() if (path != null) { val file = File(path) if (file.exists() && file.length() == byteLength) { - moduleData.updateDownloadStatus(moduleName, STATUS_TRANSFERRING) - moduleData.incrementPackBytesDownloaded(moduleName, byteLength) - moduleData.incrementBytesDownloaded(moduleName) sendBroadcastForExistingFile(context, moduleData, moduleName, bundle, destination) } } } -fun initModuleDownloadInfo(context: Context, packageName: String, deliveryInfo: ModuleDeliveryInfo): ModuleData { +suspend fun download( + context: Context, + url: String, + destinationFile: File, + moduleName: String, + moduleData: ModuleData, +): String = withContext(Dispatchers.IO) { + val uri = Uri.parse(url).toString() + val connection = URL(uri).openConnection() as HttpURLConnection + var bytebit:Long = 0 + try { + connection.requestMethod = "GET" + connection.connectTimeout = 10000 + connection.readTimeout = 10000 + connection.connect() + if (connection.responseCode != HttpURLConnection.HTTP_OK) { + throw IOException("Failed to download file: HTTP response code ${connection.responseCode}") + } + destinationFile.parentFile?.mkdirs() + connection.inputStream.use { input -> + FileOutputStream(destinationFile).use { output -> + val buffer = ByteArray(4096) + var bytesRead: Int + while (input.read(buffer).also { bytesRead = it } != -1) { + output.write(buffer, 0, bytesRead) + moduleData.incrementPackBytesDownloaded(context, moduleName, bytesRead.toLong()) + bytebit += bytesRead + if (bytebit >= 1048576){ + sendBroadcastForExistingFile(context, moduleData, moduleName, null, null) + bytebit = 0 + } + } + } + } + destinationFile.absolutePath + } catch (e: Exception) { + throw IOException("Download failed: ${e.message}", e) + } finally { + connection.disconnect() + } +} + +fun initModuleDownloadInfo(context: Context, packageName: String, deliveryInfo: ModuleDeliveryInfo?): ModuleData { + if (deliveryInfo == null || deliveryInfo.status != null) { + return ModuleData(errorCode = ERROR_CODE_FAIL) + } val packNames: ArrayList = arrayListOf() var moduleDownloadByteLength = 0L - var appVersionCode = 0 + var appVersionCode = 0L val sessionIds = arrayMapOf() val packDataList = arrayMapOf() for (deliveryIndex in deliveryInfo.res.indices) { val resource: ModuleResource = deliveryInfo.res[deliveryIndex] - appVersionCode = resource.versionCode?.toInt() ?: 0 + appVersionCode = resource.versionCode ?: 0 val resourceList: List = resource.packResource val resourcePackageName: String = resource.packName ?: continue var packDownloadByteLength = 0L @@ -175,8 +254,8 @@ fun initModuleDownloadInfo(context: Context, packageName: String, deliveryInfo: packVersion = appVersionCode, packBaseVersion = 0, sessionId = STATUS_NOT_INSTALLED, - errorCode = STATUS_SUCCESS, - status = STATUS_NOT_INSTALLED, + errorCode = ERROR_CODE_SUCCESS, + status = STATUS_INITIAL_STATE, bytesDownloaded = 0, totalBytesToDownload = packDownloadByteLength, bundleList = bundlePackageName, @@ -188,10 +267,10 @@ fun initModuleDownloadInfo(context: Context, packageName: String, deliveryInfo: } return ModuleData( packageName = packageName, - errorCode = 0, + errorCode = ERROR_CODE_SUCCESS, sessionIds = sessionIds, bytesDownloaded = 0, - status = STATUS_NOT_INSTALLED, + status = STATUS_INITIAL_STATE, packNames = packNames, appVersionCode = appVersionCode, totalBytesToDownload = moduleDownloadByteLength @@ -200,20 +279,20 @@ fun initModuleDownloadInfo(context: Context, packageName: String, deliveryInfo: } } -fun buildDownloadBundle(packName: String, moduleData: ModuleData, isPack: Boolean = false, packNames: ArrayList? = null) = Bundle().apply { +fun buildDownloadBundle(packName: String, moduleData: ModuleData, isPack: Boolean = false, packNames: List? = null) = Bundle().apply { val packData = moduleData.getPackData(packName) packData?.run { putInt(combineModule(KEY_SESSION_ID, packName), sessionId) putInt(combineModule(KEY_STATUS, packName), status) putInt(combineModule(KEY_ERROR_CODE, packName), errorCode) - putInt(combineModule(KEY_PACK_VERSION, packName), packVersion) - putInt(combineModule(KEY_PACK_BASE_VERSION, packName), packBaseVersion) + putLong(combineModule(KEY_PACK_VERSION, packName), packVersion) + putLong(combineModule(KEY_PACK_BASE_VERSION, packName), packBaseVersion) putLong(combineModule(KEY_BYTES_DOWNLOADED, packName), bytesDownloaded) putLong(combineModule(KEY_TOTAL_BYTES_TO_DOWNLOAD, packName), totalBytesToDownload) - putStringArrayList(KEY_PACK_NAMES, packNames ?: if (isPack) arrayListOf(packName) else moduleData.packNames) + putStringArrayList(KEY_PACK_NAMES, packNames?.let { ArrayList(it) } ?: if (isPack) arrayListOf(packName) else moduleData.packNames) putInt(KEY_STATUS, moduleData.status) - putInt(KEY_APP_VERSION_CODE, moduleData.appVersionCode) + putInt(KEY_APP_VERSION_CODE, moduleData.appVersionCode.toInt()) putLong(KEY_TOTAL_BYTES_TO_DOWNLOAD, if (isPack) totalBytesToDownload else moduleData.totalBytesToDownload) putInt(KEY_ERROR_CODE, if (isPack) errorCode else moduleData.errorCode) putInt(KEY_SESSION_ID, moduleData.sessionIds?.get(packName) ?: sessionId) @@ -225,19 +304,19 @@ fun sendBroadcastForExistingFile(context: Context, moduleData: ModuleData, modul val packData = moduleData.getPackData(moduleName) ?: return try { val downloadBundle = Bundle() - downloadBundle.putInt(KEY_APP_VERSION_CODE, moduleData.appVersionCode) - downloadBundle.putInt(KEY_ERROR_CODE, STATUS_SUCCESS) + downloadBundle.putInt(KEY_APP_VERSION_CODE, moduleData.appVersionCode.toInt()) + downloadBundle.putInt(KEY_ERROR_CODE, ERROR_CODE_SUCCESS) downloadBundle.putInt(KEY_SESSION_ID, moduleData.sessionIds?.get(moduleName) ?: moduleData.status) downloadBundle.putInt(KEY_STATUS, moduleData.status) downloadBundle.putStringArrayList(KEY_PACK_NAMES, arrayListOf(moduleName)) downloadBundle.putLong(KEY_BYTES_DOWNLOADED, packData.bytesDownloaded) downloadBundle.putLong(KEY_TOTAL_BYTES_TO_DOWNLOAD, packData.totalBytesToDownload) downloadBundle.putLong(combineModule(KEY_TOTAL_BYTES_TO_DOWNLOAD, moduleName), packData.totalBytesToDownload) - downloadBundle.putInt(combineModule(KEY_PACK_VERSION, moduleName), packData.packVersion) + downloadBundle.putLong(combineModule(KEY_PACK_VERSION, moduleName), packData.packVersion) downloadBundle.putInt(combineModule(KEY_STATUS, moduleName), packData.status) - downloadBundle.putInt(combineModule(KEY_ERROR_CODE, moduleName), STATUS_SUCCESS) + downloadBundle.putInt(combineModule(KEY_ERROR_CODE, moduleName), ERROR_CODE_SUCCESS) downloadBundle.putLong(combineModule(KEY_BYTES_DOWNLOADED, moduleName), packData.bytesDownloaded) - downloadBundle.putInt(combineModule(KEY_PACK_BASE_VERSION, moduleName), packData.packBaseVersion) + downloadBundle.putLong(combineModule(KEY_PACK_BASE_VERSION, moduleName), packData.packBaseVersion) val resultList = arraySetOf() packData.bundleList?.forEach { val result = Bundle() From 0358d9dfffb948df840566f51b29bc5a8558e92f Mon Sep 17 00:00:00 2001 From: davinci9196 Date: Tue, 15 Oct 2024 10:55:05 +0800 Subject: [PATCH 076/132] Asset Modules: Fixed the issue where League of Legends Mobile pops up an error message --- .../android/finsky/assetmoduleservice/AssetModuleService.kt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/AssetModuleService.kt b/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/AssetModuleService.kt index 39cd1245d3..d6555609e3 100644 --- a/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/AssetModuleService.kt +++ b/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/AssetModuleService.kt @@ -85,6 +85,12 @@ class AssetModuleServiceImpl( return } lifecycleScope.launchWhenStarted { + if (list.all { it.getString(KEY_MODULE_NAME) == null }) { + Log.d(TAG, "startDownload: module name is null") + val result = Bundle().apply { putStringArrayList(KEY_PACK_NAMES, arrayListOf()) } + callback?.onStartDownload(-1, result) + return@launchWhenStarted + } if (moduleErrorRequested.contains(packageName)) { Log.d(TAG, "startDownload: moduleData request error") val result = Bundle().apply { putStringArrayList(KEY_PACK_NAMES, arrayListOf()) } From a29147712a29c8d34717aaaab3b5a256f8a10ac5 Mon Sep 17 00:00:00 2001 From: davinci9196 Date: Fri, 1 Nov 2024 17:00:24 +0800 Subject: [PATCH 077/132] Asset Modules: Adjust to background resource download and display download progress in notification bar. --- .../google/android/finsky/DownloadManager.kt | 270 ++++++++++++++++++ .../assetmoduleservice/AssetModuleService.kt | 125 +++----- .../finsky/assetmoduleservice/DownloadData.kt | 58 ++++ .../finsky/assetmoduleservice/ModuleData.kt | 73 ----- .../com/google/android/finsky/extensions.kt | 233 +++++---------- .../res/drawable/download_progress_bar.xml | 17 ++ .../layout/layout_download_notification.xml | 51 ++++ .../src/main/res/values-zh-rCN/strings.xml | 2 + vending-app/src/main/res/values/colors.xml | 4 + vending-app/src/main/res/values/strings.xml | 5 + 10 files changed, 525 insertions(+), 313 deletions(-) create mode 100644 vending-app/src/main/kotlin/com/google/android/finsky/DownloadManager.kt create mode 100644 vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/DownloadData.kt delete mode 100644 vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/ModuleData.kt create mode 100644 vending-app/src/main/res/drawable/download_progress_bar.xml create mode 100644 vending-app/src/main/res/layout/layout_download_notification.xml diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/DownloadManager.kt b/vending-app/src/main/kotlin/com/google/android/finsky/DownloadManager.kt new file mode 100644 index 0000000000..c2135823a1 --- /dev/null +++ b/vending-app/src/main/kotlin/com/google/android/finsky/DownloadManager.kt @@ -0,0 +1,270 @@ +/** + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.finsky + +import android.annotation.SuppressLint +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.content.pm.PackageManager +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.drawable.BitmapDrawable +import android.net.Uri +import android.os.Build +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.os.Message +import android.util.Log +import android.widget.RemoteViews +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import androidx.core.content.ContextCompat +import com.android.vending.R +import com.google.android.finsky.assetmoduleservice.DownloadData +import java.io.File +import java.io.FileOutputStream +import java.io.IOException +import java.net.HttpURLConnection +import java.net.URL +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.Future +import java.util.concurrent.LinkedBlockingQueue +import java.util.concurrent.ThreadPoolExecutor +import java.util.concurrent.TimeUnit + +private const val corePoolSize = 0 +private const val maximumPoolSize = 1 +private const val keepAliveTime = 30L +private const val progressDelayTime = 1000L + +private const val PACK_DOWNLOADING = 0 +private const val PACK_DOWNLOADED = 1 +private const val DOWNLOAD_PREPARE = 2 + +private const val CHANNEL_ID = "progress_notification_channel" +private const val NOTIFICATION_ID = 1 +private const val CANCEL_ACTION = "CANCEL_DOWNLOAD" + +private const val TAG = "DownloadManager" + +class DownloadManager(private val context: Context) { + + private lateinit var notifyBuilder: NotificationCompat.Builder + private lateinit var notificationLayout: RemoteViews + private val downloadingRecord = ConcurrentHashMap>() + + init { + createNotificationChannel() + } + + private fun createNotificationChannel() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val name = "Download Progress" + val descriptionText = "Shows download progress" + val importance = NotificationManager.IMPORTANCE_LOW + val channel = NotificationChannel(CHANNEL_ID, name, importance).apply { + description = descriptionText + } + val notificationManager: NotificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + notificationManager.createNotificationChannel(channel) + } + } + + private val executor by lazy { + ThreadPoolExecutor( + corePoolSize, maximumPoolSize, keepAliveTime, TimeUnit.SECONDS, LinkedBlockingQueue() + ) { r -> Thread(r).apply { name = "DownloadThread" } } + } + + private val mHandler = object : Handler(Looper.getMainLooper()) { + + override fun handleMessage(msg: Message) { + when (msg.what) { + PACK_DOWNLOADING -> { + val bundle = msg.obj as Bundle + val moduleName = bundle.getString(KEY_MODULE_NAME)!! + val downloadData = bundle.getSerializable(KEY_DOWNLOAD_DATA) as DownloadData + updateProgress((downloadData.bytesDownloaded * 100 / downloadData.totalBytesToDownload).toInt()) + sendBroadcastForExistingFile(context, downloadData, moduleName, null, null) + sendMessageDelayed(obtainMessage(PACK_DOWNLOADING).apply { obj = bundle }, progressDelayTime) + } + + PACK_DOWNLOADED -> { + val bundle = msg.obj as Bundle + val moduleName = bundle.getString(KEY_MODULE_NAME)!! + val dataBundle = bundle.getBundle(KEY_DOWNLOAD_PARK_BUNDLE) + val destinationFile = bundle.getString(KEY_FILE_PATH)?.let { File(it) } + val downloadData = bundle.getSerializable(KEY_DOWNLOAD_DATA) as DownloadData + sendBroadcastForExistingFile(context, downloadData, moduleName, dataBundle, destinationFile) + } + + DOWNLOAD_PREPARE -> { + val downloadData = msg.obj as DownloadData + initNotification(downloadData.packageName) + context.registerReceiver(cancelReceiver, IntentFilter(CANCEL_ACTION)) + } + } + } + } + + private fun initNotification(packageName: String) { + val cancelIntent = Intent(CANCEL_ACTION) + val cancelPendingIntent = PendingIntent.getBroadcast(context, 0, cancelIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) + val packageManager: PackageManager = context.packageManager + val applicationInfo = packageManager.getApplicationInfo(packageName, 0) + val appName = packageManager.getApplicationLabel(applicationInfo).toString() + val appIcon = packageManager.getApplicationIcon(applicationInfo) + val bitmap = if (appIcon is BitmapDrawable) { + appIcon.bitmap + } else { + val bitmapTemp = Bitmap.createBitmap(appIcon.intrinsicWidth, appIcon.intrinsicHeight, Bitmap.Config.ARGB_8888) + val canvas = Canvas(bitmapTemp) + appIcon.setBounds(0, 0, canvas.width, canvas.height) + appIcon.draw(canvas) + bitmapTemp + } + + notificationLayout = RemoteViews(context.packageName, R.layout.layout_download_notification) + notificationLayout.setTextViewText(R.id.notification_title, context.getString(R.string.download_notification_attachment_file, appName)) + notificationLayout.setTextViewText(R.id.notification_text, context.getString(R.string.download_notification_tips)) + notificationLayout.setProgressBar(R.id.progress_bar, 100, 0, false) + notificationLayout.setImageViewBitmap(R.id.app_icon, bitmap) + notificationLayout.setOnClickPendingIntent(R.id.cancel_button, cancelPendingIntent) + + notifyBuilder = + NotificationCompat.Builder(context, CHANNEL_ID).setSmallIcon(R.drawable.ic_app_foreground).setStyle(NotificationCompat.DecoratedCustomViewStyle()) + .setCustomContentView(notificationLayout).setPriority(NotificationCompat.PRIORITY_LOW).setOngoing(true).setOnlyAlertOnce(true) + .setColor(ContextCompat.getColor(context, R.color.notification_color)).setColorized(true) + NotificationManagerCompat.from(context).notify(NOTIFICATION_ID, notifyBuilder.build()) + } + + fun updateProgress(progress: Int) { + notificationLayout.setProgressBar(R.id.progress_bar, 100, progress, false) + notifyBuilder.setCustomContentView(notificationLayout) + NotificationManagerCompat.from(context).notify(NOTIFICATION_ID, notifyBuilder.build()) + } + + private val cancelReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + cleanup() + } + } + + fun cleanup() { + mHandler.removeCallbacksAndMessages(null) + NotificationManagerCompat.from(context).cancel(NOTIFICATION_ID) + runCatching { context.unregisterReceiver(cancelReceiver) } + } + + @Synchronized + fun prepareDownload(downloadData: DownloadData) { + Log.d(TAG, "prepareDownload: ${downloadData.packageName}") + val callingPackageName = downloadData.packageName + if (downloadingRecord.containsKey(callingPackageName) && downloadingRecord[callingPackageName]?.isDone == false) { + return + } + if (downloadingRecord.isNotEmpty() && !downloadingRecord.containsKey(callingPackageName)) { + downloadingRecord.values.forEach { it.cancel(true) } + cleanup() + downloadingRecord.clear() + } + Log.d(TAG, "prepareDownload: ${downloadData.packageName} start") + val future = executor.submit { + mHandler.sendMessage(mHandler.obtainMessage(DOWNLOAD_PREPARE).apply { obj = downloadData }) + downloadData.moduleNames.forEach { moduleName -> + mHandler.sendMessage(mHandler.obtainMessage(PACK_DOWNLOADING).apply { + obj = Bundle().apply { + putString(KEY_MODULE_NAME, moduleName) + putSerializable(KEY_DOWNLOAD_DATA, downloadData) + } + }) + val packData = downloadData.getModuleData(moduleName) + for (dataBundle in packData.packBundleList) { + val resourcePackageName: String? = dataBundle.getString(KEY_RESOURCE_PACKAGE_NAME) + val chunkName: String? = dataBundle.getString(KEY_CHUNK_NAME) + val resourceLink: String? = dataBundle.getString(KEY_RESOURCE_LINK) + val index: Int = dataBundle.getInt(KEY_INDEX) + val resourceBlockName: String? = dataBundle.getString(KEY_RESOURCE_BLOCK_NAME) + if (resourcePackageName == null || chunkName == null || resourceLink == null || resourceBlockName == null) { + continue + } + val filesDir = "${context.filesDir}/assetpacks/$index/$resourcePackageName/$chunkName/" + val destination = File(filesDir, resourceBlockName) + startDownload(moduleName, resourceLink, destination, dataBundle, downloadData) ?: return@forEach + } + mHandler.removeMessages(PACK_DOWNLOADING) + } + cleanup() + } + downloadingRecord[callingPackageName] = future + } + + @Synchronized + private fun startDownload(moduleName: String, downloadLink: String, destinationFile: File, dataBundle: Bundle, downloadData: DownloadData): String? { + val uri = Uri.parse(downloadLink).toString() + var retryCount = 0 + while (retryCount < 3) { + val connection = URL(uri).openConnection() as HttpURLConnection + try { + connection.requestMethod = "GET" + connection.connectTimeout = 10000 + connection.readTimeout = 10000 + connection.connect() + if (connection.responseCode != HttpURLConnection.HTTP_OK) { + throw IOException("Failed to download file: HTTP response code ${connection.responseCode}") + } + if (destinationFile.exists()) { + destinationFile.delete() + } else destinationFile.parentFile?.mkdirs() + connection.inputStream.use { input -> + FileOutputStream(destinationFile).use { output -> + val buffer = ByteArray(4096) + var bytesRead: Int + while (input.read(buffer).also { bytesRead = it } != -1) { + output.write(buffer, 0, bytesRead) + downloadData.incrementModuleBytesDownloaded(moduleName, bytesRead.toLong()) + } + } + } + mHandler.sendMessage(mHandler.obtainMessage(PACK_DOWNLOADED).apply { + obj = Bundle().apply { + putString(KEY_MODULE_NAME, moduleName) + putString(KEY_FILE_PATH, destinationFile.absolutePath) + putBundle(KEY_DOWNLOAD_PARK_BUNDLE, dataBundle) + putSerializable(KEY_DOWNLOAD_DATA, downloadData) + } + }) + return destinationFile.absolutePath + } catch (e: Exception) { + Log.e(TAG, "prepareDownload: startDownload error ", e) + retryCount++ + if (retryCount >= 3) { + return null + } + } finally { + connection.disconnect() + } + } + return null + } + + companion object { + @SuppressLint("StaticFieldLeak") + @Volatile + private var instance: DownloadManager? = null + fun get(context: Context): DownloadManager { + return instance ?: synchronized(this) { + instance ?: DownloadManager(context.applicationContext).also { instance = it } + } + } + } +} diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/AssetModuleService.kt b/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/AssetModuleService.kt index d6555609e3..b4f01ac234 100644 --- a/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/AssetModuleService.kt +++ b/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/AssetModuleService.kt @@ -16,29 +16,22 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleService import androidx.lifecycle.lifecycleScope +import com.google.android.finsky.DownloadManager import com.google.android.finsky.ERROR_CODE_FAIL -import com.google.android.finsky.KEY_APP_VERSION_CODE -import com.google.android.finsky.KEY_BYTES_DOWNLOADED import com.google.android.finsky.KEY_CHUNK_FILE_DESCRIPTOR import com.google.android.finsky.KEY_CHUNK_NUMBER import com.google.android.finsky.KEY_ERROR_CODE import com.google.android.finsky.KEY_MODULE_NAME -import com.google.android.finsky.KEY_PACK_BASE_VERSION import com.google.android.finsky.KEY_PACK_NAMES -import com.google.android.finsky.KEY_PACK_VERSION import com.google.android.finsky.KEY_PLAY_CORE_VERSION_CODE -import com.google.android.finsky.KEY_RESOURCE_PACKAGE_NAME import com.google.android.finsky.KEY_SESSION_ID import com.google.android.finsky.KEY_SLICE_ID -import com.google.android.finsky.KEY_STATUS -import com.google.android.finsky.KEY_TOTAL_BYTES_TO_DOWNLOAD import com.google.android.finsky.STATUS_COMPLETED import com.google.android.finsky.STATUS_DOWNLOADING import com.google.android.finsky.STATUS_INITIAL_STATE +import com.google.android.finsky.STATUS_NOT_INSTALLED import com.google.android.finsky.TAG_REQUEST import com.google.android.finsky.buildDownloadBundle -import com.google.android.finsky.combineModule -import com.google.android.finsky.downloadFile import com.google.android.finsky.initAssertModuleData import com.google.android.finsky.sendBroadcastForExistingFile import com.google.android.play.core.assetpacks.protocol.IAssetModuleService @@ -73,8 +66,6 @@ class AssetModuleService : LifecycleService() { class AssetModuleServiceImpl( val context: Context, override val lifecycle: Lifecycle, private val httpClient: HttpClient, private val accountManager: AccountManager ) : IAssetModuleService.Stub(), LifecycleOwner { - @Volatile - private var moduleData: ModuleData? = null private val fileDescriptorMap = mutableMapOf() override fun startDownload(packageName: String?, list: MutableList?, bundle: Bundle?, callback: IAssetModuleServiceCallback?) { @@ -85,68 +76,35 @@ class AssetModuleServiceImpl( return } lifecycleScope.launchWhenStarted { - if (list.all { it.getString(KEY_MODULE_NAME) == null }) { - Log.d(TAG, "startDownload: module name is null") - val result = Bundle().apply { putStringArrayList(KEY_PACK_NAMES, arrayListOf()) } - callback?.onStartDownload(-1, result) - return@launchWhenStarted - } - if (moduleErrorRequested.contains(packageName)) { + val requestedAssetModuleNames = list.map { it.getString(KEY_MODULE_NAME) }.filter { !it.isNullOrEmpty() } + if (requestedAssetModuleNames.isEmpty() || moduleErrorRequested[packageName]?.contains(requestedAssetModuleNames.joinToString()) == true) { Log.d(TAG, "startDownload: moduleData request error") val result = Bundle().apply { putStringArrayList(KEY_PACK_NAMES, arrayListOf()) } - Log.d(TAG, "prepare: ${result.keySet()}") callback?.onStartDownload(-1, result) return@launchWhenStarted } - if (moduleData?.status != STATUS_DOWNLOADING) { - val requestedAssetModuleNames = list.map { it.getString(KEY_MODULE_NAME) }.filter { !it.isNullOrEmpty() } - if (moduleData == null) { - val playCoreVersionCode = bundle.getInt(KEY_PLAY_CORE_VERSION_CODE) - moduleData = httpClient.initAssertModuleData(context, packageName, accountManager, requestedAssetModuleNames, playCoreVersionCode) + if (downloadData == null || downloadData?.packageName != packageName) { + val playCoreVersionCode = bundle.getInt(KEY_PLAY_CORE_VERSION_CODE) + downloadData = httpClient.initAssertModuleData(context, packageName, accountManager, requestedAssetModuleNames, playCoreVersionCode) + } + if (downloadData?.status == STATUS_NOT_INSTALLED) { + downloadData?.moduleNames?.forEach{ + downloadData?.updateDownloadStatus(it, STATUS_INITIAL_STATE) } - list.forEach { - val moduleName = it.getString(KEY_MODULE_NAME) - if (moduleName != null) { - val packData = moduleData!!.getPackData(moduleName) - callback?.onStartDownload(-1, buildDownloadBundle(moduleName, moduleData!!, true)) - if (packData?.status != STATUS_INITIAL_STATE) { - Log.w(TAG, "startDownload: packData?.status is ${packData?.status}") - return@forEach - } - moduleData?.updateDownloadStatus(moduleName, STATUS_DOWNLOADING) - moduleData?.updateModuleDownloadStatus(STATUS_DOWNLOADING) - packData.bundleList?.forEach { download -> - if (moduleName == download.getString(KEY_RESOURCE_PACKAGE_NAME)) { - downloadFile(context, moduleName, moduleData!!, download) - } - } - } + downloadData?.updateModuleDownloadStatus(STATUS_INITIAL_STATE) + val bundleData = buildDownloadBundle(downloadData!!,list) + DownloadManager.get(context).prepareDownload(downloadData!!) + Log.d(TAG, "startDownload---1${bundleData}") + callback?.onStartDownload(-1, bundleData) + downloadData?.moduleNames?.forEach{ + downloadData?.updateDownloadStatus(it, STATUS_DOWNLOADING) } + downloadData?.updateModuleDownloadStatus(STATUS_DOWNLOADING) return@launchWhenStarted } - val result = Bundle() - val arrayList = arrayListOf() - result.putInt(KEY_STATUS, moduleData!!.status) - result.putLong(KEY_APP_VERSION_CODE, moduleData!!.appVersionCode) - result.putLong(KEY_BYTES_DOWNLOADED, moduleData!!.bytesDownloaded) - result.putInt(KEY_ERROR_CODE, moduleData!!.errorCode) - result.putInt(KEY_SESSION_ID, 6) - result.putLong(KEY_TOTAL_BYTES_TO_DOWNLOAD, moduleData!!.totalBytesToDownload) - list.forEach { - val moduleName = it.getString(KEY_MODULE_NAME) - arrayList.add(moduleName!!) - val packData = moduleData!!.getPackData(moduleName) - result.putInt(combineModule(KEY_SESSION_ID, moduleName), packData!!.sessionId) - result.putInt(combineModule(KEY_STATUS, moduleName), packData.status) - result.putInt(combineModule(KEY_ERROR_CODE, moduleName), packData.errorCode) - result.putLong(combineModule(KEY_PACK_VERSION, moduleName), packData.packVersion) - result.putLong(combineModule(KEY_PACK_BASE_VERSION, moduleName), packData.packBaseVersion) - result.putLong(combineModule(KEY_BYTES_DOWNLOADED, moduleName), packData.bytesDownloaded) - result.putLong(combineModule(KEY_TOTAL_BYTES_TO_DOWNLOAD, moduleName), packData.totalBytesToDownload) - sendBroadcastForExistingFile(context, moduleData!!, moduleName, null, null) - } - result.putStringArrayList(KEY_PACK_NAMES, arrayList) - callback?.onStartDownload(-1, result) + val bundleData = buildDownloadBundle(downloadData!!,list) + Log.d(TAG, "startDownload---2${bundleData}") + callback?.onStartDownload(-1, bundleData) } } @@ -180,14 +138,9 @@ class AssetModuleServiceImpl( return } lifecycleScope.launchWhenStarted { - Log.d(TAG, "notify: moduleName: $moduleName packNames: ${moduleData?.packNames}") - moduleData?.packNames?.find { it == moduleName }?.let { - moduleData?.updateDownloadStatus(it, STATUS_COMPLETED) - if (moduleData?.packNames?.all { pack -> moduleData?.getPackData(pack)?.status == STATUS_COMPLETED } == true) { - moduleData?.updateModuleDownloadStatus(STATUS_COMPLETED) - } - sendBroadcastForExistingFile(context, moduleData!!, moduleName, null, null) - } + Log.d(TAG, "notify: moduleName: $moduleName packNames: ${downloadData?.moduleNames}") + downloadData?.updateDownloadStatus(moduleName, STATUS_COMPLETED) + sendBroadcastForExistingFile(context, downloadData!!, moduleName, null, null) callback?.onNotifyModuleCompleted(bundle, bundle) } } @@ -230,27 +183,27 @@ class AssetModuleServiceImpl( } lifecycleScope.launchWhenStarted { val requestedAssetModuleNames = list.map { it.getString(KEY_MODULE_NAME) }.filter { !it.isNullOrEmpty() } - if (moduleData == null) { + if (downloadData == null || downloadData?.packageName != packageName) { val playCoreVersionCode = bundle.getInt(KEY_PLAY_CORE_VERSION_CODE) - moduleData = httpClient.initAssertModuleData(context, packageName, accountManager, requestedAssetModuleNames, playCoreVersionCode) + downloadData = httpClient.initAssertModuleData(context, packageName, accountManager, requestedAssetModuleNames, playCoreVersionCode) } - if (moduleData?.errorCode == ERROR_CODE_FAIL) { - if (moduleErrorRequested.contains(packageName)) { + Log.d(TAG, "requestDownloadInfo: $requestedAssetModuleNames ") + if (downloadData?.errorCode == ERROR_CODE_FAIL) { + val errorModule = moduleErrorRequested[packageName] + if (!errorModule.isNullOrEmpty() && !errorModule.contains(requestedAssetModuleNames.joinToString())) { callback?.onError(Bundle().apply { putInt(KEY_ERROR_CODE, -5) }) return@launchWhenStarted } - moduleErrorRequested.add(packageName) - val result = Bundle().apply { putStringArrayList(KEY_PACK_NAMES, arrayListOf()) } + Log.d(TAG, "requestDownloadInfo: error by $packageName ") + moduleErrorRequested[packageName] = moduleErrorRequested[packageName]?.apply { add(requestedAssetModuleNames.joinToString()) } + ?: arrayListOf(requestedAssetModuleNames.joinToString()) + val result = Bundle().apply { putStringArrayList(KEY_PACK_NAMES, ArrayList(requestedAssetModuleNames)) } callback?.onRequestDownloadInfo(result, result) return@launchWhenStarted } moduleErrorRequested.remove(packageName) - val bundleData = Bundle().apply { - val isPack = moduleData?.packNames?.size != requestedAssetModuleNames.size - requestedAssetModuleNames.forEach { - putAll(buildDownloadBundle(it!!, moduleData!!, isPack, packNames = requestedAssetModuleNames)) - } - } + val bundleData = buildDownloadBundle(downloadData!!,list) + Log.d(TAG, "requestDownloadInfo---${bundleData}") callback?.onRequestDownloadInfo(bundleData, bundleData) } } @@ -264,6 +217,8 @@ class AssetModuleServiceImpl( } companion object { - private val moduleErrorRequested = arrayListOf() + @Volatile + private var downloadData: DownloadData? = null + private val moduleErrorRequested = HashMap>() } } \ No newline at end of file diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/DownloadData.kt b/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/DownloadData.kt new file mode 100644 index 0000000000..68423bb9f3 --- /dev/null +++ b/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/DownloadData.kt @@ -0,0 +1,58 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.finsky.assetmoduleservice + +import android.os.Bundle +import java.io.Serializable + +data class DownloadData( + var packageName: String = "", + var errorCode: Int = 0, + var sessionIds: Map = emptyMap(), + var bytesDownloaded: Long = 0, + var status: Int = 0, + var moduleNames: Set = emptySet(), + var appVersionCode: Long = 0, + var totalBytesToDownload: Long = 0, + var moduleDataList: Map = emptyMap() +) : Serializable { + + fun getModuleData(packName: String): ModuleData { + return moduleDataList[packName] ?: throw IllegalArgumentException("ModuleData for packName '$packName' not found.") + } + + fun incrementModuleBytesDownloaded(packName: String, bytes: Long) { + getModuleData(packName).incrementBytesDownloaded(bytes) + bytesDownloaded += bytes + } + + fun updateDownloadStatus(packName: String, statusCode: Int) { + getModuleData(packName).apply { + status = statusCode + sessionId = statusCode + } + } + + fun updateModuleDownloadStatus(statusCode: Int) { + this.status = statusCode + } +} + +data class ModuleData( + var appVersionCode: Long = 0, + var moduleVersion: Long = 0, + var sessionId: Int = 0, + var errorCode: Int = 0, + var status: Int = 0, + var bytesDownloaded: Long = 0, + var totalBytesToDownload: Long = 0, + var packBundleList: List = emptyList(), + var listOfSubcontractNames: ArrayList? = null +) : Serializable { + fun incrementBytesDownloaded(bytes: Long) { + bytesDownloaded += bytes + } +} \ No newline at end of file diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/ModuleData.kt b/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/ModuleData.kt deleted file mode 100644 index e52919b5d6..0000000000 --- a/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/ModuleData.kt +++ /dev/null @@ -1,73 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2023 microG Project Team - * SPDX-License-Identifier: Apache-2.0 - */ - -package com.google.android.finsky.assetmoduleservice - -import android.content.Context -import android.os.Bundle -import androidx.collection.ArrayMap -import com.google.android.finsky.sendBroadcastForExistingFile - -data class ModuleData( - var packageName: String? = null, - var errorCode: Int = 0, - var sessionIds: ArrayMap? = null, - var bytesDownloaded: Long = 0, - var status: Int = 0, - var packNames: ArrayList? = null, - var appVersionCode: Long = 0, - var totalBytesToDownload: Long = 0, -) { - private var mPackData = emptyMap() - - fun setPackData(packData: Map) { - this.mPackData = packData - } - - fun getPackData(packName: String): PackData? { - return mPackData[packName] - } - - fun incrementPackBytesDownloaded(context: Context, packName: String, bytes: Long) { - mPackData[packName]?.incrementBytesDownloaded(bytes) - bytesDownloaded += bytes - } - - fun updateDownloadStatus(packName: String, statusCode: Int) { - getPackData(packName)?.status = statusCode - getPackData(packName)?.sessionId = statusCode - } - - fun updateModuleDownloadStatus(statusCode: Int) { - this.status = statusCode - } - - override fun toString(): String { - return "ModuleData(packageName=$packageName, errorCode=$errorCode, sessionIds=$sessionIds, bytesDownloaded=$bytesDownloaded, status=$status, packNames=$packNames, appVersionCode=$appVersionCode, totalBytesToDownload=$totalBytesToDownload, mPackData=$mPackData)" - } -} - -data class PackData( - var packVersion: Long = 0, - var packBaseVersion: Long = 0, - var sessionId: Int = 0, - var errorCode: Int = 0, - var status: Int = 0, - var bytesDownloaded: Long = 0, - var totalBytesToDownload: Long = 0, - var packVersionTag: String? = null, - var bundleList: ArrayList? = null, - var totalSumOfSubcontractedModules: Int = 0, - var subcontractingBaseUnit: Int = 0, - var listOfSubcontractNames: ArrayList? = null -) { - fun incrementBytesDownloaded(bytes: Long) { - bytesDownloaded += bytes - } - - override fun toString(): String { - return "PackData(packVersion=$packVersion, packBaseVersion=$packBaseVersion, sessionId=$sessionId, errorCode=$errorCode, status=$status, bytesDownloaded=$bytesDownloaded, totalBytesToDownload=$totalBytesToDownload, packVersionTag=$packVersionTag, bundleList=$bundleList, totalSumOfSubcontractedModules=$totalSumOfSubcontractedModules, subcontractingBaseUnit=$subcontractingBaseUnit, listOfSubcontractNames=$listOfSubcontractNames)" - } -} diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/extensions.kt b/vending-app/src/main/kotlin/com/google/android/finsky/extensions.kt index a1511cce93..c20a762294 100644 --- a/vending-app/src/main/kotlin/com/google/android/finsky/extensions.kt +++ b/vending-app/src/main/kotlin/com/google/android/finsky/extensions.kt @@ -12,22 +12,20 @@ import android.content.Intent import android.net.Uri import android.os.Bundle import android.util.Log +import androidx.collection.ArraySet import androidx.collection.arrayMapOf import androidx.collection.arraySetOf import com.android.vending.licensing.AUTH_TOKEN_SCOPE +import com.android.vending.licensing.LicensingService import com.android.vending.licensing.getAuthToken import com.android.vending.licensing.getLicenseRequestHeaders +import com.google.android.finsky.assetmoduleservice.DownloadData import com.google.android.finsky.assetmoduleservice.ModuleData -import com.google.android.finsky.assetmoduleservice.PackData -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext +import kotlinx.coroutines.withTimeoutOrNull import org.microg.gms.auth.AuthConstants +import org.microg.vending.billing.GServices import org.microg.vending.billing.core.HttpClient import java.io.File -import java.io.FileOutputStream -import java.io.IOException -import java.net.HttpURLConnection -import java.net.URL import java.util.Collections const val STATUS_NOT_INSTALLED = 8 @@ -51,6 +49,10 @@ const val KEY_TOTAL_BYTES_TO_DOWNLOAD = "total_bytes_to_download" const val KEY_PACK_NAMES = "pack_names" const val KEY_APP_VERSION_CODE = "app_version_code" +const val KEY_DOWNLOAD_DATA = "download_data" +const val KEY_FILE_PATH = "file_path" +const val KEY_DOWNLOAD_PARK_BUNDLE = "download_park_bundle" + const val KEY_CACHE_DIR = "CacheDir" const val KEY_INDEX = "index" const val KEY_CHUNK_NAME = "chunkName" @@ -78,6 +80,7 @@ private const val EXTRA_SESSION_STATE = "com.google.android.play.core.assetpacks private const val FLAGS = "com.google.android.play.core.FLAGS" private const val ASSET_MODULE_DELIVERY_URL = "https://play-fe.googleapis.com/fdfe/assetModuleDelivery" +private val CHECKIN_SETTINGS_PROVIDER = Uri.parse("content://com.google.android.gms.microg.settings/check-in") private const val TAG = "AssetModuleRequest" @@ -90,37 +93,39 @@ fun getAppVersionCode(context: Context, packageName: String): String? { } suspend fun HttpClient.initAssertModuleData( - context: Context, - packageName: String, - accountManager: AccountManager, - requestedAssetModuleNames: List, - playCoreVersionCode: Int, -): ModuleData { + context: Context, + packageName: String, + accountManager: AccountManager, + requestedAssetModuleNames: List, + playCoreVersionCode: Int, +): DownloadData { Log.d(TAG, "initAssertModuleData: requestedAssetModuleNames: $requestedAssetModuleNames") val accounts = accountManager.getAccountsByType(AuthConstants.DEFAULT_ACCOUNT_TYPE) var oauthToken: String? = null if (accounts.isEmpty()) { - return ModuleData(errorCode = ERROR_CODE_FAIL) + return DownloadData(errorCode = ERROR_CODE_FAIL) } else for (account: Account in accounts) { oauthToken = accountManager.getAuthToken(account, AUTH_TOKEN_SCOPE, false).getString(AccountManager.KEY_AUTHTOKEN) if (oauthToken != null) { break } } + Log.d(TAG, "initAssertModuleData: oauthToken -> $oauthToken") if (oauthToken == null) { - return ModuleData(errorCode = ERROR_CODE_FAIL) + return DownloadData(errorCode = ERROR_CODE_FAIL) } val requestPayload = AssetModuleDeliveryRequest.Builder().callerInfo(CallerInfo(getAppVersionCode(context, packageName)?.toInt())).packageName(packageName) - .playCoreVersion(playCoreVersionCode).pageSource(listOf(PageSource.UNKNOWN_SEARCH_TRAFFIC_SOURCE, PageSource.BOOKS_HOME_PAGE)) - .callerState(listOf(CallerState.CALLER_APP_REQUEST, CallerState.CALLER_APP_DEBUGGABLE)).moduleInfo(ArrayList().apply { - requestedAssetModuleNames.forEach { add(AssetModuleInfo.Builder().name(it).build()) } - }).build() + .playCoreVersion(playCoreVersionCode).pageSource(listOf(PageSource.UNKNOWN_SEARCH_TRAFFIC_SOURCE, PageSource.BOOKS_HOME_PAGE)) + .callerState(listOf(CallerState.CALLER_APP_REQUEST, CallerState.CALLER_APP_DEBUGGABLE)).moduleInfo(ArrayList().apply { + requestedAssetModuleNames.forEach { add(AssetModuleInfo.Builder().name(it).build()) } + }).build() + val moduleDeliveryInfo = runCatching { post( - url = ASSET_MODULE_DELIVERY_URL, - headers = getLicenseRequestHeaders(oauthToken, 1), - payload = requestPayload, - adapter = AssetModuleDeliveryResponse.ADAPTER + url = ASSET_MODULE_DELIVERY_URL, + headers = getLicenseRequestHeaders(oauthToken, 1), + payload = requestPayload, + adapter = AssetModuleDeliveryResponse.ADAPTER ).wrapper?.deliveryInfo }.onFailure { Log.d(TAG, "initAssertModuleData: ", it) @@ -129,86 +134,15 @@ suspend fun HttpClient.initAssertModuleData( return initModuleDownloadInfo(context, packageName, moduleDeliveryInfo) } -suspend fun downloadFile(context: Context, moduleName: String, moduleData: ModuleData, bundle: Bundle) { - val resourcePackageName: String? = bundle.getString(KEY_RESOURCE_PACKAGE_NAME) - val chunkName: String? = bundle.getString(KEY_CHUNK_NAME) - val resourceLink: String? = bundle.getString(KEY_RESOURCE_LINK) - val byteLength: Long = bundle.getLong(KEY_BYTE_LENGTH) - val index: Int = bundle.getInt(KEY_INDEX) - val resourceBlockName: String? = bundle.getString(KEY_RESOURCE_BLOCK_NAME) - if (resourcePackageName == null || chunkName == null || resourceLink == null || resourceBlockName == null) { - Log.d(TAG, "downloadFile: params invalid ") - return - } - val filesDir = "${context.filesDir}/assetpacks/$index/$resourcePackageName/$chunkName/" - val destination = File(filesDir, resourceBlockName) - if (destination.exists() && destination.length() == byteLength) { - sendBroadcastForExistingFile(context, moduleData, moduleName, bundle, destination) - return - } - if (destination.exists()) { - destination.delete() - } - val path = runCatching { download(context, resourceLink, destination, moduleName, moduleData) }.onFailure { Log.w(TAG, "downloadFile: ", it) }.getOrNull() - if (path != null) { - val file = File(path) - if (file.exists() && file.length() == byteLength) { - sendBroadcastForExistingFile(context, moduleData, moduleName, bundle, destination) - } - } -} - -suspend fun download( - context: Context, - url: String, - destinationFile: File, - moduleName: String, - moduleData: ModuleData, -): String = withContext(Dispatchers.IO) { - val uri = Uri.parse(url).toString() - val connection = URL(uri).openConnection() as HttpURLConnection - var bytebit:Long = 0 - try { - connection.requestMethod = "GET" - connection.connectTimeout = 10000 - connection.readTimeout = 10000 - connection.connect() - if (connection.responseCode != HttpURLConnection.HTTP_OK) { - throw IOException("Failed to download file: HTTP response code ${connection.responseCode}") - } - destinationFile.parentFile?.mkdirs() - connection.inputStream.use { input -> - FileOutputStream(destinationFile).use { output -> - val buffer = ByteArray(4096) - var bytesRead: Int - while (input.read(buffer).also { bytesRead = it } != -1) { - output.write(buffer, 0, bytesRead) - moduleData.incrementPackBytesDownloaded(context, moduleName, bytesRead.toLong()) - bytebit += bytesRead - if (bytebit >= 1048576){ - sendBroadcastForExistingFile(context, moduleData, moduleName, null, null) - bytebit = 0 - } - } - } - } - destinationFile.absolutePath - } catch (e: Exception) { - throw IOException("Download failed: ${e.message}", e) - } finally { - connection.disconnect() - } -} - -fun initModuleDownloadInfo(context: Context, packageName: String, deliveryInfo: ModuleDeliveryInfo?): ModuleData { +fun initModuleDownloadInfo(context: Context, packageName: String, deliveryInfo: ModuleDeliveryInfo?): DownloadData { if (deliveryInfo == null || deliveryInfo.status != null) { - return ModuleData(errorCode = ERROR_CODE_FAIL) + return DownloadData(errorCode = ERROR_CODE_FAIL) } - val packNames: ArrayList = arrayListOf() + val packNames: ArraySet = arraySetOf() var moduleDownloadByteLength = 0L var appVersionCode = 0L val sessionIds = arrayMapOf() - val packDataList = arrayMapOf() + val moduleDataList = arrayMapOf() for (deliveryIndex in deliveryInfo.res.indices) { val resource: ModuleResource = deliveryInfo.res[deliveryIndex] appVersionCode = resource.versionCode ?: 0 @@ -216,10 +150,10 @@ fun initModuleDownloadInfo(context: Context, packageName: String, deliveryInfo: val resourcePackageName: String = resource.packName ?: continue var packDownloadByteLength = 0L packNames.add(resourcePackageName) - sessionIds[resourcePackageName] = deliveryIndex + 7 + sessionIds[resourcePackageName] = deliveryIndex + 10 var totalSumOfSubcontractedModules = 0 val listOfSubcontractNames: ArrayList = ArrayList() - val bundlePackageName: ArrayList = arrayListOf() + val dataBundle: ArrayList = arrayListOf() for (resIndex in resourceList.indices) { val packResource: PackResource = resourceList[resIndex] if (packResource.downloadInfo == null || packResource.chunkInfo == null) { @@ -237,7 +171,7 @@ fun initModuleDownloadInfo(context: Context, packageName: String, deliveryInfo: totalSumOfSubcontractedModules += 1 val bundle = Bundle() bundle.putString(KEY_CACHE_DIR, context.cacheDir.toString()) - bundle.putInt(KEY_INDEX, deliveryIndex + 7) + bundle.putInt(KEY_INDEX, deliveryIndex + 10) bundle.putString(KEY_RESOURCE_PACKAGE_NAME, resourcePackageName) bundle.putString(KEY_CHUNK_NAME, chunkName) bundle.putString(KEY_RESOURCE_LINK, dResource.resourceLink) @@ -246,79 +180,68 @@ fun initModuleDownloadInfo(context: Context, packageName: String, deliveryInfo: bundle.putLong(KEY_UNCOMPRESSED_SIZE, uncompressedSize ?: 0) bundle.putString(KEY_UNCOMPRESSED_HASH_SHA256, uncompressedHashSha256) bundle.putInt(KEY_NUMBER_OF_SUBCONTRACTORS, numberOfSubcontractors) - bundlePackageName.add(bundle) + dataBundle.add(bundle) } packDownloadByteLength += resDownloadByteLength } - val packData = PackData( - packVersion = appVersionCode, - packBaseVersion = 0, - sessionId = STATUS_NOT_INSTALLED, - errorCode = ERROR_CODE_SUCCESS, - status = STATUS_INITIAL_STATE, - bytesDownloaded = 0, - totalBytesToDownload = packDownloadByteLength, - bundleList = bundlePackageName, - totalSumOfSubcontractedModules = totalSumOfSubcontractedModules, - listOfSubcontractNames = listOfSubcontractNames - ) + val moduleData = ModuleData(appVersionCode = appVersionCode, moduleVersion = 0, sessionId = STATUS_NOT_INSTALLED, errorCode = ERROR_CODE_SUCCESS, status = STATUS_NOT_INSTALLED, bytesDownloaded = 0, totalBytesToDownload = packDownloadByteLength, packBundleList = dataBundle, listOfSubcontractNames = listOfSubcontractNames) moduleDownloadByteLength += packDownloadByteLength - packDataList[resourcePackageName] = packData - } - return ModuleData( - packageName = packageName, - errorCode = ERROR_CODE_SUCCESS, - sessionIds = sessionIds, - bytesDownloaded = 0, - status = STATUS_INITIAL_STATE, - packNames = packNames, - appVersionCode = appVersionCode, - totalBytesToDownload = moduleDownloadByteLength - ).apply { - setPackData(packDataList) + moduleDataList[resourcePackageName] = moduleData } + return DownloadData(packageName = packageName, errorCode = ERROR_CODE_SUCCESS, sessionIds = sessionIds, bytesDownloaded = 0, status = STATUS_NOT_INSTALLED, moduleNames = packNames, appVersionCode = appVersionCode, totalBytesToDownload = moduleDownloadByteLength, moduleDataList) } -fun buildDownloadBundle(packName: String, moduleData: ModuleData, isPack: Boolean = false, packNames: List? = null) = Bundle().apply { - val packData = moduleData.getPackData(packName) - packData?.run { - putInt(combineModule(KEY_SESSION_ID, packName), sessionId) - putInt(combineModule(KEY_STATUS, packName), status) - putInt(combineModule(KEY_ERROR_CODE, packName), errorCode) - putLong(combineModule(KEY_PACK_VERSION, packName), packVersion) - putLong(combineModule(KEY_PACK_BASE_VERSION, packName), packBaseVersion) - putLong(combineModule(KEY_BYTES_DOWNLOADED, packName), bytesDownloaded) - putLong(combineModule(KEY_TOTAL_BYTES_TO_DOWNLOAD, packName), totalBytesToDownload) +fun buildDownloadBundle(downloadData: DownloadData, list: List? = null): Bundle { + val bundleData = Bundle() + val arrayList = arrayListOf() + var totalBytesToDownload = 0L + var bytesDownloaded = 0L - putStringArrayList(KEY_PACK_NAMES, packNames?.let { ArrayList(it) } ?: if (isPack) arrayListOf(packName) else moduleData.packNames) - putInt(KEY_STATUS, moduleData.status) - putInt(KEY_APP_VERSION_CODE, moduleData.appVersionCode.toInt()) - putLong(KEY_TOTAL_BYTES_TO_DOWNLOAD, if (isPack) totalBytesToDownload else moduleData.totalBytesToDownload) - putInt(KEY_ERROR_CODE, if (isPack) errorCode else moduleData.errorCode) - putInt(KEY_SESSION_ID, moduleData.sessionIds?.get(packName) ?: sessionId) - putLong(KEY_BYTES_DOWNLOADED, if (isPack) bytesDownloaded else moduleData.bytesDownloaded) + list?.forEach { + val moduleName = it?.getString(KEY_MODULE_NAME) + val packData = downloadData.getModuleData(moduleName!!) + bundleData.putInt(KEY_STATUS, packData.status) + downloadData.sessionIds[moduleName]?.let { sessionId -> bundleData.putInt(KEY_SESSION_ID, sessionId) } + bundleData.putInt(combineModule(KEY_SESSION_ID, moduleName), packData.sessionId) + bundleData.putInt(combineModule(KEY_STATUS, moduleName), packData.status) + bundleData.putInt(combineModule(KEY_ERROR_CODE, moduleName), packData.errorCode) + bundleData.putInt(combineModule(KEY_PACK_VERSION, moduleName), packData.appVersionCode.toInt()) + bundleData.putLong(combineModule(KEY_PACK_BASE_VERSION, moduleName), packData.moduleVersion) + bundleData.putLong(combineModule(KEY_BYTES_DOWNLOADED, moduleName), packData.bytesDownloaded) + bundleData.putLong(combineModule(KEY_TOTAL_BYTES_TO_DOWNLOAD, moduleName), packData.totalBytesToDownload) + arrayList.add(moduleName) + totalBytesToDownload += packData.totalBytesToDownload + bytesDownloaded += packData.bytesDownloaded } + + bundleData.putStringArrayList(KEY_PACK_NAMES, arrayList) + bundleData.putLong(KEY_TOTAL_BYTES_TO_DOWNLOAD, totalBytesToDownload) + bundleData.putLong(KEY_BYTES_DOWNLOADED, bytesDownloaded) + + return bundleData } -fun sendBroadcastForExistingFile(context: Context, moduleData: ModuleData, moduleName: String, bundle: Bundle?, destination: File?) { - val packData = moduleData.getPackData(moduleName) ?: return + +fun sendBroadcastForExistingFile(context: Context, downloadData: DownloadData, moduleName: String, bundle: Bundle?, destination: File?) { + val packData = downloadData.getModuleData(moduleName) try { val downloadBundle = Bundle() - downloadBundle.putInt(KEY_APP_VERSION_CODE, moduleData.appVersionCode.toInt()) + downloadBundle.putInt(KEY_APP_VERSION_CODE, downloadData.appVersionCode.toInt()) downloadBundle.putInt(KEY_ERROR_CODE, ERROR_CODE_SUCCESS) - downloadBundle.putInt(KEY_SESSION_ID, moduleData.sessionIds?.get(moduleName) ?: moduleData.status) - downloadBundle.putInt(KEY_STATUS, moduleData.status) + downloadBundle.putInt(KEY_SESSION_ID, downloadData.sessionIds[moduleName] + ?: downloadData.status) + downloadBundle.putInt(KEY_STATUS, packData.status) downloadBundle.putStringArrayList(KEY_PACK_NAMES, arrayListOf(moduleName)) downloadBundle.putLong(KEY_BYTES_DOWNLOADED, packData.bytesDownloaded) downloadBundle.putLong(KEY_TOTAL_BYTES_TO_DOWNLOAD, packData.totalBytesToDownload) downloadBundle.putLong(combineModule(KEY_TOTAL_BYTES_TO_DOWNLOAD, moduleName), packData.totalBytesToDownload) - downloadBundle.putLong(combineModule(KEY_PACK_VERSION, moduleName), packData.packVersion) + downloadBundle.putLong(combineModule(KEY_PACK_VERSION, moduleName), packData.appVersionCode) downloadBundle.putInt(combineModule(KEY_STATUS, moduleName), packData.status) downloadBundle.putInt(combineModule(KEY_ERROR_CODE, moduleName), ERROR_CODE_SUCCESS) downloadBundle.putLong(combineModule(KEY_BYTES_DOWNLOADED, moduleName), packData.bytesDownloaded) - downloadBundle.putLong(combineModule(KEY_PACK_BASE_VERSION, moduleName), packData.packBaseVersion) + downloadBundle.putLong(combineModule(KEY_PACK_BASE_VERSION, moduleName), packData.moduleVersion) val resultList = arraySetOf() - packData.bundleList?.forEach { + packData.packBundleList.forEach { val result = Bundle() result.putString(KEY_CHUNK_NAME, it.getString(KEY_CHUNK_NAME)) result.putLong(KEY_UNCOMPRESSED_SIZE, it.getLong(KEY_UNCOMPRESSED_SIZE)) @@ -357,18 +280,18 @@ fun sendBroadcastForExistingFile(context: Context, moduleData: ModuleData, modul } downloadBundle.putStringArrayList(combineModule(KEY_SLICE_IDS, moduleName), packData.listOfSubcontractNames) Log.d(TAG, "sendBroadcastForExistingFile: $downloadBundle") - sendBroadCast(context, moduleData, downloadBundle) + sendBroadCast(context, downloadData, downloadBundle) } catch (e: Exception) { Log.w(TAG, "sendBroadcastForExistingFile error:" + e.message) } } -private fun sendBroadCast(context: Context, moduleData: ModuleData, result: Bundle) { +private fun sendBroadCast(context: Context, downloadData: DownloadData, result: Bundle) { val intent = Intent() intent.setAction(ACTION_SESSION_UPDATE) intent.putExtra(EXTRA_SESSION_STATE, result) intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY) intent.putExtra(FLAGS, Bundle().apply { putBoolean(KEY_USING_EXTRACTOR_STREAM, true) }) - intent.setPackage(moduleData.packageName) + intent.setPackage(downloadData.packageName) context.sendBroadcast(intent) } diff --git a/vending-app/src/main/res/drawable/download_progress_bar.xml b/vending-app/src/main/res/drawable/download_progress_bar.xml new file mode 100644 index 0000000000..e121118550 --- /dev/null +++ b/vending-app/src/main/res/drawable/download_progress_bar.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vending-app/src/main/res/layout/layout_download_notification.xml b/vending-app/src/main/res/layout/layout_download_notification.xml new file mode 100644 index 0000000000..58edb0b4bb --- /dev/null +++ b/vending-app/src/main/res/layout/layout_download_notification.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/vending-app/src/main/res/values-zh-rCN/strings.xml b/vending-app/src/main/res/values-zh-rCN/strings.xml index 89db7dd662..1353d852d2 100644 --- a/vending-app/src/main/res/values-zh-rCN/strings.xml +++ b/vending-app/src/main/res/values-zh-rCN/strings.xml @@ -19,4 +19,6 @@ 登录 忽略 microG Companion + %s的附件文件 + 文件下载中 \ No newline at end of file diff --git a/vending-app/src/main/res/values/colors.xml b/vending-app/src/main/res/values/colors.xml index 4ad8d9df03..dbe602e2c6 100644 --- a/vending-app/src/main/res/values/colors.xml +++ b/vending-app/src/main/res/values/colors.xml @@ -2,4 +2,8 @@ #01875f @android:color/white + #FFFFFF + #000000 + #555555 + #20FF2A \ No newline at end of file diff --git a/vending-app/src/main/res/values/strings.xml b/vending-app/src/main/res/values/strings.xml index d505d6fdfd..6b9a242f61 100644 --- a/vending-app/src/main/res/values/strings.xml +++ b/vending-app/src/main/res/values/strings.xml @@ -28,4 +28,9 @@ Forget password? Learn more Verify + + Cancel + Additional files for %s + Downloading + From 7b714156a2b625e5483a09ea4d2a0650d36ffe60 Mon Sep 17 00:00:00 2001 From: davinci9196 Date: Tue, 5 Nov 2024 19:40:55 +0800 Subject: [PATCH 078/132] Asset Modules: Added a new device information synchronization interface Solve the problem that some games cannot obtain sub-packaged downloaded content. --- vending-app/src/main/AndroidManifest.xml | 3 + .../google/android/finsky/DownloadManager.kt | 259 ++++---- .../assetmoduleservice/AssetModuleService.kt | 122 +++- .../com/google/android/finsky/extensions.kt | 47 +- .../android/finsky/model/DeviceSyncInfo.kt | 611 ++++++++++++++++++ vending-app/src/main/proto/SyncRequest.proto | 188 ++++++ 6 files changed, 1045 insertions(+), 185 deletions(-) create mode 100644 vending-app/src/main/kotlin/com/google/android/finsky/model/DeviceSyncInfo.kt create mode 100644 vending-app/src/main/proto/SyncRequest.proto diff --git a/vending-app/src/main/AndroidManifest.xml b/vending-app/src/main/AndroidManifest.xml index eb12ae7378..754c7ddc1b 100644 --- a/vending-app/src/main/AndroidManifest.xml +++ b/vending-app/src/main/AndroidManifest.xml @@ -19,6 +19,9 @@ + + () + private val notificationLayoutMap = ConcurrentHashMap() private val downloadingRecord = ConcurrentHashMap>() + @Volatile + private var shouldStop = false + + private val cancelReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + val moduleName = intent.getStringExtra(KEY_MODULE_NAME) + if (moduleName != null) { + cancelDownload(moduleName) + } + } + } + init { createNotificationChannel() + val filter = IntentFilter(CANCEL_ACTION) + context.registerReceiver(cancelReceiver, filter) } private fun createNotificationChannel() { @@ -81,44 +87,17 @@ class DownloadManager(private val context: Context) { private val executor by lazy { ThreadPoolExecutor( - corePoolSize, maximumPoolSize, keepAliveTime, TimeUnit.SECONDS, LinkedBlockingQueue() + corePoolSize, maximumPoolSize, keepAliveTime, TimeUnit.SECONDS, LinkedBlockingQueue() ) { r -> Thread(r).apply { name = "DownloadThread" } } } - private val mHandler = object : Handler(Looper.getMainLooper()) { - - override fun handleMessage(msg: Message) { - when (msg.what) { - PACK_DOWNLOADING -> { - val bundle = msg.obj as Bundle - val moduleName = bundle.getString(KEY_MODULE_NAME)!! - val downloadData = bundle.getSerializable(KEY_DOWNLOAD_DATA) as DownloadData - updateProgress((downloadData.bytesDownloaded * 100 / downloadData.totalBytesToDownload).toInt()) - sendBroadcastForExistingFile(context, downloadData, moduleName, null, null) - sendMessageDelayed(obtainMessage(PACK_DOWNLOADING).apply { obj = bundle }, progressDelayTime) - } - - PACK_DOWNLOADED -> { - val bundle = msg.obj as Bundle - val moduleName = bundle.getString(KEY_MODULE_NAME)!! - val dataBundle = bundle.getBundle(KEY_DOWNLOAD_PARK_BUNDLE) - val destinationFile = bundle.getString(KEY_FILE_PATH)?.let { File(it) } - val downloadData = bundle.getSerializable(KEY_DOWNLOAD_DATA) as DownloadData - sendBroadcastForExistingFile(context, downloadData, moduleName, dataBundle, destinationFile) - } - - DOWNLOAD_PREPARE -> { - val downloadData = msg.obj as DownloadData - initNotification(downloadData.packageName) - context.registerReceiver(cancelReceiver, IntentFilter(CANCEL_ACTION)) - } - } + private fun initNotification(moduleName: String, packageName: String) { + val cancelIntent = Intent(CANCEL_ACTION).apply { + putExtra(KEY_MODULE_NAME, moduleName) } - } - - private fun initNotification(packageName: String) { - val cancelIntent = Intent(CANCEL_ACTION) - val cancelPendingIntent = PendingIntent.getBroadcast(context, 0, cancelIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) + val cancelPendingIntent = PendingIntent.getBroadcast( + context, 0, cancelIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) val packageManager: PackageManager = context.packageManager val applicationInfo = packageManager.getApplicationInfo(packageName, 0) val appName = packageManager.getApplicationLabel(applicationInfo).toString() @@ -126,135 +105,129 @@ class DownloadManager(private val context: Context) { val bitmap = if (appIcon is BitmapDrawable) { appIcon.bitmap } else { - val bitmapTemp = Bitmap.createBitmap(appIcon.intrinsicWidth, appIcon.intrinsicHeight, Bitmap.Config.ARGB_8888) + val bitmapTemp = Bitmap.createBitmap( + appIcon.intrinsicWidth, appIcon.intrinsicHeight, Bitmap.Config.ARGB_8888 + ) val canvas = Canvas(bitmapTemp) appIcon.setBounds(0, 0, canvas.width, canvas.height) appIcon.draw(canvas) bitmapTemp } - notificationLayout = RemoteViews(context.packageName, R.layout.layout_download_notification) - notificationLayout.setTextViewText(R.id.notification_title, context.getString(R.string.download_notification_attachment_file, appName)) - notificationLayout.setTextViewText(R.id.notification_text, context.getString(R.string.download_notification_tips)) + val notificationLayout = RemoteViews(context.packageName, R.layout.layout_download_notification) + notificationLayout.setTextViewText( + R.id.notification_title, context.getString(R.string.download_notification_attachment_file, appName) + ) + notificationLayout.setTextViewText( + R.id.notification_text, context.getString(R.string.download_notification_tips) + ) notificationLayout.setProgressBar(R.id.progress_bar, 100, 0, false) notificationLayout.setImageViewBitmap(R.id.app_icon, bitmap) notificationLayout.setOnClickPendingIntent(R.id.cancel_button, cancelPendingIntent) - notifyBuilder = - NotificationCompat.Builder(context, CHANNEL_ID).setSmallIcon(R.drawable.ic_app_foreground).setStyle(NotificationCompat.DecoratedCustomViewStyle()) - .setCustomContentView(notificationLayout).setPriority(NotificationCompat.PRIORITY_LOW).setOngoing(true).setOnlyAlertOnce(true) - .setColor(ContextCompat.getColor(context, R.color.notification_color)).setColorized(true) - NotificationManagerCompat.from(context).notify(NOTIFICATION_ID, notifyBuilder.build()) - } + val notifyBuilder = + NotificationCompat.Builder(context, CHANNEL_ID).setSmallIcon(R.drawable.ic_app_foreground).setStyle(NotificationCompat.DecoratedCustomViewStyle()) + .setCustomContentView(notificationLayout).setPriority(NotificationCompat.PRIORITY_LOW).setOngoing(true).setOnlyAlertOnce(true) + .setColor(ContextCompat.getColor(context, R.color.notification_color)).setColorized(true) + notifyBuilderMap[moduleName] = notifyBuilder + notificationLayoutMap[moduleName] = notificationLayout - fun updateProgress(progress: Int) { - notificationLayout.setProgressBar(R.id.progress_bar, 100, progress, false) - notifyBuilder.setCustomContentView(notificationLayout) - NotificationManagerCompat.from(context).notify(NOTIFICATION_ID, notifyBuilder.build()) + NotificationManagerCompat.from(context).notify(NOTIFICATION_ID, notifyBuilder.setCustomContentView(notificationLayout).build()) } - private val cancelReceiver = object : BroadcastReceiver() { - override fun onReceive(context: Context?, intent: Intent?) { - cleanup() - } - } - fun cleanup() { - mHandler.removeCallbacksAndMessages(null) - NotificationManagerCompat.from(context).cancel(NOTIFICATION_ID) - runCatching { context.unregisterReceiver(cancelReceiver) } + private fun updateProgress(moduleName: String, progress: Int) { + val notificationLayout = notificationLayoutMap[moduleName] ?: return + val notifyBuilder = notifyBuilderMap[moduleName] ?: return + + notificationLayout.setProgressBar(R.id.progress_bar, 100, progress, false) + notifyBuilder.setCustomContentView(notificationLayout) + NotificationManagerCompat.from(context).notify(NOTIFICATION_ID, notifyBuilder.setCustomContentView(notificationLayout).build()) } @Synchronized - fun prepareDownload(downloadData: DownloadData) { + fun prepareDownload(downloadData: DownloadData, moduleName: String) { Log.d(TAG, "prepareDownload: ${downloadData.packageName}") - val callingPackageName = downloadData.packageName - if (downloadingRecord.containsKey(callingPackageName) && downloadingRecord[callingPackageName]?.isDone == false) { - return - } - if (downloadingRecord.isNotEmpty() && !downloadingRecord.containsKey(callingPackageName)) { - downloadingRecord.values.forEach { it.cancel(true) } - cleanup() - downloadingRecord.clear() - } - Log.d(TAG, "prepareDownload: ${downloadData.packageName} start") + shouldStop = false + downloadData.updateDownloadStatus(moduleName, STATUS_DOWNLOADING) + initNotification(moduleName, downloadData.packageName) val future = executor.submit { - mHandler.sendMessage(mHandler.obtainMessage(DOWNLOAD_PREPARE).apply { obj = downloadData }) - downloadData.moduleNames.forEach { moduleName -> - mHandler.sendMessage(mHandler.obtainMessage(PACK_DOWNLOADING).apply { - obj = Bundle().apply { - putString(KEY_MODULE_NAME, moduleName) - putSerializable(KEY_DOWNLOAD_DATA, downloadData) - } - }) - val packData = downloadData.getModuleData(moduleName) - for (dataBundle in packData.packBundleList) { - val resourcePackageName: String? = dataBundle.getString(KEY_RESOURCE_PACKAGE_NAME) - val chunkName: String? = dataBundle.getString(KEY_CHUNK_NAME) - val resourceLink: String? = dataBundle.getString(KEY_RESOURCE_LINK) - val index: Int = dataBundle.getInt(KEY_INDEX) - val resourceBlockName: String? = dataBundle.getString(KEY_RESOURCE_BLOCK_NAME) - if (resourcePackageName == null || chunkName == null || resourceLink == null || resourceBlockName == null) { - continue - } - val filesDir = "${context.filesDir}/assetpacks/$index/$resourcePackageName/$chunkName/" - val destination = File(filesDir, resourceBlockName) - startDownload(moduleName, resourceLink, destination, dataBundle, downloadData) ?: return@forEach + val packData = downloadData.getModuleData(moduleName) + for (dataBundle in packData.packBundleList) { + val resourcePackageName: String? = dataBundle.getString(KEY_RESOURCE_PACKAGE_NAME) + val chunkName: String? = dataBundle.getString(KEY_CHUNK_NAME) + val resourceLink: String? = dataBundle.getString(KEY_RESOURCE_LINK) + val index: Int = dataBundle.getInt(KEY_INDEX) + val resourceBlockName: String? = dataBundle.getString(KEY_RESOURCE_BLOCK_NAME) + if (resourcePackageName == null || chunkName == null || resourceLink == null || resourceBlockName == null) { + continue } - mHandler.removeMessages(PACK_DOWNLOADING) + val filesDir = "${context.filesDir}/assetpacks/$index/$resourcePackageName/$chunkName/" + val destination = File(filesDir, resourceBlockName) + startDownload(moduleName, resourceLink, destination, downloadData) + sendBroadcastForExistingFile(context, downloadData, moduleName, dataBundle, destination) } - cleanup() + updateProgress(moduleName, 100) + notifyBuilderMap[moduleName]?.setOngoing(false) + NotificationManagerCompat.from(context).cancel(NOTIFICATION_ID) } - downloadingRecord[callingPackageName] = future + downloadingRecord[moduleName] = future } @Synchronized - private fun startDownload(moduleName: String, downloadLink: String, destinationFile: File, dataBundle: Bundle, downloadData: DownloadData): String? { + private fun cancelDownload(moduleName: String) { + Log.d(TAG, "Download for module $moduleName has been canceled.") + downloadingRecord[moduleName]?.cancel(true) + shouldStop = true + notifyBuilderMap[moduleName]?.setOngoing(false) + NotificationManagerCompat.from(context).cancel(NOTIFICATION_ID) + } + + private fun startDownload(moduleName: String, downloadLink: String, destinationFile: File, downloadData: DownloadData) { + val packData = downloadData.getModuleData(moduleName) val uri = Uri.parse(downloadLink).toString() - var retryCount = 0 - while (retryCount < 3) { - val connection = URL(uri).openConnection() as HttpURLConnection - try { - connection.requestMethod = "GET" - connection.connectTimeout = 10000 - connection.readTimeout = 10000 - connection.connect() - if (connection.responseCode != HttpURLConnection.HTTP_OK) { - throw IOException("Failed to download file: HTTP response code ${connection.responseCode}") - } - if (destinationFile.exists()) { - destinationFile.delete() - } else destinationFile.parentFile?.mkdirs() - connection.inputStream.use { input -> - FileOutputStream(destinationFile).use { output -> - val buffer = ByteArray(4096) - var bytesRead: Int - while (input.read(buffer).also { bytesRead = it } != -1) { - output.write(buffer, 0, bytesRead) - downloadData.incrementModuleBytesDownloaded(moduleName, bytesRead.toLong()) + val connection = URL(uri).openConnection() as HttpURLConnection + var bytes: Long = 0 + try { + connection.requestMethod = "GET" + connection.connectTimeout = 20000 + connection.readTimeout = 20000 + connection.connect() + if (connection.responseCode != HttpURLConnection.HTTP_OK) { + throw IOException("Failed to download file: HTTP response code ${connection.responseCode}") + } + if (destinationFile.exists()) { + destinationFile.delete() + } else destinationFile.parentFile?.mkdirs() + + connection.inputStream.use { input -> + FileOutputStream(destinationFile).use { output -> + val buffer = ByteArray(4096) + var bytesRead: Int + while (input.read(buffer).also { bytesRead = it } != -1) { + if (shouldStop) { + Log.d(TAG, "Download interrupted for module: $moduleName") + downloadData.updateDownloadStatus(moduleName, CANCELED) + return + } + output.write(buffer, 0, bytesRead) + bytes += bytesRead.toLong() + downloadData.incrementModuleBytesDownloaded(moduleName, bytesRead.toLong()) + if (bytes >= 1048576) { + val progress = ((packData.bytesDownloaded.toDouble() / packData.totalBytesToDownload.toDouble()) * 100).toInt() + updateProgress(moduleName, progress) + sendBroadcastForExistingFile(context, downloadData, moduleName, null, null) + bytes = 0 } } } - mHandler.sendMessage(mHandler.obtainMessage(PACK_DOWNLOADED).apply { - obj = Bundle().apply { - putString(KEY_MODULE_NAME, moduleName) - putString(KEY_FILE_PATH, destinationFile.absolutePath) - putBundle(KEY_DOWNLOAD_PARK_BUNDLE, dataBundle) - putSerializable(KEY_DOWNLOAD_DATA, downloadData) - } - }) - return destinationFile.absolutePath - } catch (e: Exception) { - Log.e(TAG, "prepareDownload: startDownload error ", e) - retryCount++ - if (retryCount >= 3) { - return null - } - } finally { - connection.disconnect() } + } catch (e: Exception) { + Log.e(TAG, "prepareDownload: startDownload error ", e) + downloadData.updateDownloadStatus(moduleName, STATUS_FAILED) + } finally { + connection.disconnect() } - return null } companion object { @@ -267,4 +240,4 @@ class DownloadManager(private val context: Context) { } } } -} +} \ No newline at end of file diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/AssetModuleService.kt b/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/AssetModuleService.kt index b4f01ac234..a006f47510 100644 --- a/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/AssetModuleService.kt +++ b/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/AssetModuleService.kt @@ -18,12 +18,19 @@ import androidx.lifecycle.LifecycleService import androidx.lifecycle.lifecycleScope import com.google.android.finsky.DownloadManager import com.google.android.finsky.ERROR_CODE_FAIL +import com.google.android.finsky.KEY_BYTE_LENGTH import com.google.android.finsky.KEY_CHUNK_FILE_DESCRIPTOR +import com.google.android.finsky.KEY_CHUNK_NAME import com.google.android.finsky.KEY_CHUNK_NUMBER import com.google.android.finsky.KEY_ERROR_CODE +import com.google.android.finsky.KEY_INDEX +import com.google.android.finsky.KEY_INSTALLED_ASSET_MODULE import com.google.android.finsky.KEY_MODULE_NAME import com.google.android.finsky.KEY_PACK_NAMES import com.google.android.finsky.KEY_PLAY_CORE_VERSION_CODE +import com.google.android.finsky.KEY_RESOURCE_BLOCK_NAME +import com.google.android.finsky.KEY_RESOURCE_LINK +import com.google.android.finsky.KEY_RESOURCE_PACKAGE_NAME import com.google.android.finsky.KEY_SESSION_ID import com.google.android.finsky.KEY_SLICE_ID import com.google.android.finsky.STATUS_COMPLETED @@ -76,42 +83,103 @@ class AssetModuleServiceImpl( return } lifecycleScope.launchWhenStarted { - val requestedAssetModuleNames = list.map { it.getString(KEY_MODULE_NAME) }.filter { !it.isNullOrEmpty() } - if (requestedAssetModuleNames.isEmpty() || moduleErrorRequested[packageName]?.contains(requestedAssetModuleNames.joinToString()) == true) { + if (downloadData == null || downloadData?.packageName != packageName) { + val requestedAssetModuleNames = list.map { it.getString(KEY_MODULE_NAME) }.filter { !it.isNullOrEmpty() } + val playCoreVersionCode = bundle.getInt(KEY_PLAY_CORE_VERSION_CODE) + downloadData = httpClient.initAssertModuleData(context, packageName, accountManager, requestedAssetModuleNames, playCoreVersionCode) + } + if (list.all { it.getString(KEY_MODULE_NAME) == null } || moduleErrorRequested.contains(packageName)) { Log.d(TAG, "startDownload: moduleData request error") val result = Bundle().apply { putStringArrayList(KEY_PACK_NAMES, arrayListOf()) } callback?.onStartDownload(-1, result) return@launchWhenStarted } - if (downloadData == null || downloadData?.packageName != packageName) { - val playCoreVersionCode = bundle.getInt(KEY_PLAY_CORE_VERSION_CODE) - downloadData = httpClient.initAssertModuleData(context, packageName, accountManager, requestedAssetModuleNames, playCoreVersionCode) - } - if (downloadData?.status == STATUS_NOT_INSTALLED) { - downloadData?.moduleNames?.forEach{ - downloadData?.updateDownloadStatus(it, STATUS_INITIAL_STATE) - } - downloadData?.updateModuleDownloadStatus(STATUS_INITIAL_STATE) - val bundleData = buildDownloadBundle(downloadData!!,list) - DownloadManager.get(context).prepareDownload(downloadData!!) - Log.d(TAG, "startDownload---1${bundleData}") - callback?.onStartDownload(-1, bundleData) - downloadData?.moduleNames?.forEach{ - downloadData?.updateDownloadStatus(it, STATUS_DOWNLOADING) + list.forEach { + val moduleName = it.getString(KEY_MODULE_NAME) + val packData = downloadData?.getModuleData(moduleName!!) + if (packData?.status != STATUS_DOWNLOADING){ + downloadData?.updateDownloadStatus(moduleName!!, STATUS_INITIAL_STATE) } - downloadData?.updateModuleDownloadStatus(STATUS_DOWNLOADING) - return@launchWhenStarted } val bundleData = buildDownloadBundle(downloadData!!,list) - Log.d(TAG, "startDownload---2${bundleData}") + Log.d(TAG, "startDownload---${bundleData}") callback?.onStartDownload(-1, bundleData) + list.forEach { + val moduleName = it.getString(KEY_MODULE_NAME) + val packData = downloadData?.getModuleData(moduleName!!) + if (packData?.status != STATUS_DOWNLOADING){ + DownloadManager.get(context).prepareDownload(downloadData!!, moduleName!!) + } + } } } override fun getSessionStates(packageName: String?, bundle: Bundle?, callback: IAssetModuleServiceCallback?) { Log.d(TAG, "Method (getSessionStates) called by packageName -> $packageName") + val installedAssetModuleNames = mutableListOf() + bundle?.keySet()?.forEach { key -> + val value = bundle.get(key) + if (key == KEY_INSTALLED_ASSET_MODULE) { + when (value) { + is Bundle -> { + value.keySet().forEach { subKey -> + val item = value.get(subKey) + if (item is String) { + installedAssetModuleNames.add(item) + Log.d(TAG, "installed_asset_module Bundle Value: $item") + } + } + } + is ArrayList<*> -> { + value.forEachIndexed { index, item -> + if (item is Bundle) { + Log.d(TAG, "installed_asset_module Bundle at index $index") + item.keySet().forEach { subKey -> + val subItem = item.get(subKey) + if (subItem is String) { + installedAssetModuleNames.add(subItem) + Log.d(TAG, "installed_asset_module[$index] Value: $subItem") + } + } + } + } + } + else -> { + Log.d(TAG, "installed_asset_module: - ${value?.javaClass?.name}") + } + } + } else { + Log.d(TAG, "Bundle Key: $key, Value: $value") + } + } + + Log.d(TAG, "getSessionStates installedAssetModuleNames: $installedAssetModuleNames") + + if (packageName == downloadData?.packageName) { + downloadData?.moduleNames?.forEach { moduleName -> + if (installedAssetModuleNames.contains(moduleName)) { return@forEach } + val packData = downloadData?.getModuleData(moduleName) + for (dataBundle in packData!!.packBundleList) { + val resourcePackageName: String? = dataBundle.getString(KEY_RESOURCE_PACKAGE_NAME) + val chunkName: String? = dataBundle.getString(KEY_CHUNK_NAME) + val resourceLink: String? = dataBundle.getString(KEY_RESOURCE_LINK) + val index: Int = dataBundle.getInt(KEY_INDEX) + val resourceBlockName: String? = dataBundle.getString(KEY_RESOURCE_BLOCK_NAME) + if (resourcePackageName == null || chunkName == null || resourceLink == null || resourceBlockName == null) { + continue + } + val filesDir = "${context.filesDir}/assetpacks/$index/$resourcePackageName/$chunkName/" + val destination = File(filesDir, resourceBlockName) + val byteLength = dataBundle.getLong(KEY_BYTE_LENGTH) + if (destination.exists() && destination.length() == byteLength) { + sendBroadcastForExistingFile(context, downloadData!!, moduleName, dataBundle, destination) + } + } + } + } } + override fun notifyChunkTransferred(packageName: String?, bundle: Bundle?, bundle2: Bundle?, callback: IAssetModuleServiceCallback?) { Log.d(TAG, "Method (notifyChunkTransferred) called by packageName -> $packageName") val moduleName = bundle?.getString(KEY_MODULE_NAME) @@ -182,22 +250,18 @@ class AssetModuleServiceImpl( return } lifecycleScope.launchWhenStarted { - val requestedAssetModuleNames = list.map { it.getString(KEY_MODULE_NAME) }.filter { !it.isNullOrEmpty() } if (downloadData == null || downloadData?.packageName != packageName) { + val requestedAssetModuleNames = list.map { it.getString(KEY_MODULE_NAME) }.filter { !it.isNullOrEmpty() } val playCoreVersionCode = bundle.getInt(KEY_PLAY_CORE_VERSION_CODE) downloadData = httpClient.initAssertModuleData(context, packageName, accountManager, requestedAssetModuleNames, playCoreVersionCode) } - Log.d(TAG, "requestDownloadInfo: $requestedAssetModuleNames ") if (downloadData?.errorCode == ERROR_CODE_FAIL) { - val errorModule = moduleErrorRequested[packageName] - if (!errorModule.isNullOrEmpty() && !errorModule.contains(requestedAssetModuleNames.joinToString())) { + if (moduleErrorRequested.contains(packageName)) { callback?.onError(Bundle().apply { putInt(KEY_ERROR_CODE, -5) }) return@launchWhenStarted } - Log.d(TAG, "requestDownloadInfo: error by $packageName ") - moduleErrorRequested[packageName] = moduleErrorRequested[packageName]?.apply { add(requestedAssetModuleNames.joinToString()) } - ?: arrayListOf(requestedAssetModuleNames.joinToString()) - val result = Bundle().apply { putStringArrayList(KEY_PACK_NAMES, ArrayList(requestedAssetModuleNames)) } + moduleErrorRequested.add(packageName) + val result = Bundle().apply { putStringArrayList(KEY_PACK_NAMES, arrayListOf()) } callback?.onRequestDownloadInfo(result, result) return@launchWhenStarted } @@ -219,6 +283,6 @@ class AssetModuleServiceImpl( companion object { @Volatile private var downloadData: DownloadData? = null - private val moduleErrorRequested = HashMap>() + private val moduleErrorRequested = arrayListOf() } } \ No newline at end of file diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/extensions.kt b/vending-app/src/main/kotlin/com/google/android/finsky/extensions.kt index c20a762294..0aa525aee3 100644 --- a/vending-app/src/main/kotlin/com/google/android/finsky/extensions.kt +++ b/vending-app/src/main/kotlin/com/google/android/finsky/extensions.kt @@ -16,12 +16,11 @@ import androidx.collection.ArraySet import androidx.collection.arrayMapOf import androidx.collection.arraySetOf import com.android.vending.licensing.AUTH_TOKEN_SCOPE -import com.android.vending.licensing.LicensingService import com.android.vending.licensing.getAuthToken import com.android.vending.licensing.getLicenseRequestHeaders import com.google.android.finsky.assetmoduleservice.DownloadData import com.google.android.finsky.assetmoduleservice.ModuleData -import kotlinx.coroutines.withTimeoutOrNull +import com.google.android.finsky.model.DeviceSyncInfo import org.microg.gms.auth.AuthConstants import org.microg.vending.billing.GServices import org.microg.vending.billing.core.HttpClient @@ -29,6 +28,8 @@ import java.io.File import java.util.Collections const val STATUS_NOT_INSTALLED = 8 +const val CANCELED = 6 +const val STATUS_FAILED = 5 const val STATUS_COMPLETED = 4 const val STATUS_DOWNLOADING = 2 const val STATUS_INITIAL_STATE = 1 @@ -48,10 +49,7 @@ const val KEY_BYTES_DOWNLOADED = "bytes_downloaded" const val KEY_TOTAL_BYTES_TO_DOWNLOAD = "total_bytes_to_download" const val KEY_PACK_NAMES = "pack_names" const val KEY_APP_VERSION_CODE = "app_version_code" - -const val KEY_DOWNLOAD_DATA = "download_data" -const val KEY_FILE_PATH = "file_path" -const val KEY_DOWNLOAD_PARK_BUNDLE = "download_park_bundle" +const val KEY_INSTALLED_ASSET_MODULE = "installed_asset_module" const val KEY_CACHE_DIR = "CacheDir" const val KEY_INDEX = "index" @@ -80,7 +78,7 @@ private const val EXTRA_SESSION_STATE = "com.google.android.play.core.assetpacks private const val FLAGS = "com.google.android.play.core.FLAGS" private const val ASSET_MODULE_DELIVERY_URL = "https://play-fe.googleapis.com/fdfe/assetModuleDelivery" -private val CHECKIN_SETTINGS_PROVIDER = Uri.parse("content://com.google.android.gms.microg.settings/check-in") +private const val SYNC_NOCACHE_QOS = "https://play-fe.googleapis.com/fdfe/sync?nocache_qos=lt" private const val TAG = "AssetModuleRequest" @@ -119,13 +117,37 @@ suspend fun HttpClient.initAssertModuleData( .callerState(listOf(CallerState.CALLER_APP_REQUEST, CallerState.CALLER_APP_DEBUGGABLE)).moduleInfo(ArrayList().apply { requestedAssetModuleNames.forEach { add(AssetModuleInfo.Builder().name(it).build()) } }).build() + val androidId = GServices.getString(context.contentResolver, "android_id", "0")?.toLong()?:1 + var moduleDeliveryInfo = runCatching { + post( + url = ASSET_MODULE_DELIVERY_URL, + headers = getLicenseRequestHeaders(oauthToken, androidId), + payload = requestPayload, + adapter = AssetModuleDeliveryResponse.ADAPTER + ).wrapper?.deliveryInfo + }.onFailure { + Log.d(TAG, "initAssertModuleData: ", it) + }.getOrNull() - val moduleDeliveryInfo = runCatching { + if (moduleDeliveryInfo?.status == 2) { + runCatching { + post( + url = SYNC_NOCACHE_QOS, + headers = getLicenseRequestHeaders(oauthToken, androidId), + payload = DeviceSyncInfo.buildSyncRequest(context, androidId.toString(), accounts.first()), + adapter = SyncResponse.ADAPTER + ) + }.onFailure { + Log.d(TAG, "initAssertModuleData: sync -> ", it) + } + } + + moduleDeliveryInfo = runCatching { post( - url = ASSET_MODULE_DELIVERY_URL, - headers = getLicenseRequestHeaders(oauthToken, 1), - payload = requestPayload, - adapter = AssetModuleDeliveryResponse.ADAPTER + url = ASSET_MODULE_DELIVERY_URL, + headers = getLicenseRequestHeaders(oauthToken, androidId), + payload = requestPayload, + adapter = AssetModuleDeliveryResponse.ADAPTER ).wrapper?.deliveryInfo }.onFailure { Log.d(TAG, "initAssertModuleData: ", it) @@ -221,7 +243,6 @@ fun buildDownloadBundle(downloadData: DownloadData, list: List? = null) return bundleData } - fun sendBroadcastForExistingFile(context: Context, downloadData: DownloadData, moduleName: String, bundle: Bundle?, destination: File?) { val packData = downloadData.getModuleData(moduleName) try { diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/model/DeviceSyncInfo.kt b/vending-app/src/main/kotlin/com/google/android/finsky/model/DeviceSyncInfo.kt new file mode 100644 index 0000000000..790b4fc8d4 --- /dev/null +++ b/vending-app/src/main/kotlin/com/google/android/finsky/model/DeviceSyncInfo.kt @@ -0,0 +1,611 @@ +/** + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.finsky.model + +import android.accounts.Account +import android.accounts.AccountManager +import android.annotation.SuppressLint +import android.app.ActivityManager +import android.app.admin.DevicePolicyManager +import android.content.Context +import android.content.pm.PackageInfo +import android.content.pm.PackageManager +import android.content.res.Configuration +import android.graphics.Point +import android.opengl.GLES10 +import android.os.Build +import android.telephony.TelephonyManager +import android.text.TextUtils +import android.util.Base64 +import android.util.DisplayMetrics +import android.util.Log +import android.view.WindowManager +import com.google.android.finsky.AccountAssValue +import com.google.android.finsky.AccountAssociationPayload +import com.google.android.finsky.CarrierPropertiesPayload +import com.google.android.finsky.DeviceAccountsPayload +import com.google.android.finsky.DeviceCapabilitiesPayload +import com.google.android.finsky.DeviceInfoCollect +import com.google.android.finsky.DeviceInputPropertiesPayload +import com.google.android.finsky.DeviceModelPayload +import com.google.android.finsky.EnterprisePropertiesPayload +import com.google.android.finsky.FeatureInfoProto +import com.google.android.finsky.GpuInfoWrapper +import com.google.android.finsky.GpuPayload +import com.google.android.finsky.HardwareIdentifierPayload +import com.google.android.finsky.HardwarePropertiesPayload +import com.google.android.finsky.LocalePropertiesPayload +import com.google.android.finsky.MangedScope +import com.google.android.finsky.PlayPartnerPropertiesPayload +import com.google.android.finsky.PlayPropertiesPayload +import com.google.android.finsky.ProfileInfo +import com.google.android.finsky.ScreenPropertiesPayload +import com.google.android.finsky.SyncReqWrapper +import com.google.android.finsky.SyncRequest +import com.google.android.finsky.SystemPropertiesPayload +import com.google.android.finsky.TelephonyInfo +import com.google.android.finsky.TelephonyStateWrapper +import java.security.MessageDigest +import java.security.NoSuchAlgorithmException +import java.util.Arrays +import java.util.Objects +import java.util.Random +import java.util.TimeZone +import java.util.regex.Pattern +import java.util.stream.Collectors +import javax.microedition.khronos.egl.EGL10 +import javax.microedition.khronos.egl.EGLConfig +import javax.microedition.khronos.egl.EGLContext +import javax.microedition.khronos.egl.EGLDisplay +import javax.microedition.khronos.egl.EGLSurface +import kotlin.math.abs + +object DeviceSyncInfo { + + private const val TAG = "DeviceSyncInfo" + private val glInfoList = ArrayList() + + fun buildSyncRequest(context: Context, androidId: String, account: Account): SyncReqWrapper { + Log.d(TAG, "cachePayload: ") + val builder = SyncReqWrapper.Builder() + val payloads = buildPayloads(context, androidId, account) + val syncRequests = builder.request.toMutableList() + for (payload in payloads) { + payload?.run { syncRequests.add(this) } + } + builder.request = syncRequests + return builder.build() + } + + private fun buildPayloads(context: Context, androidId: String, account: Account): Array { + val fetchedGlStrings: ArrayList = fetchGLInfo() + //---------------------------------------GPU info-------------------------------------------------------------------- + val accountSha256 = accountSha256(androidId, account) + val accountAssValue = AccountAssValue.Builder().value_(accountSha256).build() + val accountAssociationPayload = AccountAssociationPayload.Builder().accountAss(accountAssValue).build() + val accountAssociationPayloadRequest = SyncRequest.Builder().accountAssociationPayload(accountAssociationPayload).build() + //-------------------------------------------------------------------------------------------------------------------- + val carrierPropertiesPayloadRequest = createCarrierPropertiesPayloadRequest(context) + val deviceAccountsPayloadRequest = createDeviceAccountsPayloadRequest(context, androidId) + val deviceInfoCollect = createDeviceInfoCollect(context, fetchedGlStrings.toList()) + val deviceCapabilitiesPayloadRequest = createDeviceCapabilitiesPayloadRequest(deviceInfoCollect) + val deviceInputPropertiesPayloadRequest = createDeviceInputPropertiesPayloadRequest(deviceInfoCollect) + val deviceModelPayloadRequest = createDeviceModelPayloadRequest() + val enterprisePropertiesPayloadRequest = createEnterprisePropertiesPayloadRequest(context) + val hardwareIdentifierPayloadRequest = createHardwareIdentifierPayloadRequest(context) + val hardwarePropertiesPayloadRequest = createHardwarePropertiesPayloadRequest(deviceInfoCollect) + val localePropertiesPayloadRequest = createLocalePropertiesPayloadRequest() + val playPartnerPropertiesPayloadRequest = createPlayPartnerPropertiesPayloadRequest() + val playPropertiesPayloadRequest = createPlayPropertiesPayload(context) + val screenPropertiesPayloadRequest = createScreenPropertiesPayloadRequest(deviceInfoCollect) + val systemPropertiesPayloadRequest = createSystemPropertiesPayloadRequest(deviceInfoCollect) + val gpuPayloadRequest = createGpuPayloadRequest(fetchedGlStrings.toList()) + return arrayOf(accountAssociationPayloadRequest, carrierPropertiesPayloadRequest, deviceAccountsPayloadRequest, + deviceCapabilitiesPayloadRequest, deviceInputPropertiesPayloadRequest, deviceModelPayloadRequest, + enterprisePropertiesPayloadRequest, hardwareIdentifierPayloadRequest, hardwarePropertiesPayloadRequest, + localePropertiesPayloadRequest, playPartnerPropertiesPayloadRequest, playPropertiesPayloadRequest, + screenPropertiesPayloadRequest, systemPropertiesPayloadRequest, gpuPayloadRequest) + } + + private fun createDeviceInfoCollect(context: Context, gpuInfoList: List): DeviceInfoCollect { + val builder = DeviceInfoCollect.Builder() + .reqTouchScreen(0) + .reqKeyboardType(0) + .reqNavigation(0) + .deviceStablePoint(0) + .reqInputFeaturesV1(false) + .reqInputFeaturesV2(false) + .deviceStable(0) + .reqGlEsVersion(0) + val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager + val configurationInfo = activityManager.deviceConfigurationInfo + if (configurationInfo != null) { + if (configurationInfo.reqTouchScreen != Configuration.TOUCHSCREEN_UNDEFINED) { + builder.reqTouchScreen(configurationInfo.reqTouchScreen) + } + if (configurationInfo.reqKeyboardType != Configuration.KEYBOARD_UNDEFINED) { + builder.reqKeyboardType(configurationInfo.reqKeyboardType) + } + if (configurationInfo.reqNavigation != Configuration.NAVIGATION_UNDEFINED) { + builder.reqNavigation(configurationInfo.reqNavigation) + } + builder.reqGlEsVersion(configurationInfo.reqGlEsVersion) + builder.reqInputFeaturesV1((configurationInfo.reqInputFeatures and 1) == 1) + .reqInputFeaturesV2((configurationInfo.reqInputFeatures and 2) > 0) + } + val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager + val size = Point() + windowManager.defaultDisplay.getSize(size) + builder.displayX(size.x).displayY(size.y) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + builder.deviceStable(DisplayMetrics.DENSITY_DEVICE_STABLE) + .deviceStablePoint(calculatePoint(size, DisplayMetrics.DENSITY_DEVICE_STABLE)) + } + val configuration = context.resources.configuration + builder.screenLayout(configuration.screenLayout) + .smallestScreenWidthDp(configuration.smallestScreenWidthDp) + .systemSharedLibraryNames(listOf(*Objects.requireNonNull(context.packageManager.systemSharedLibraryNames))) + .locales(listOf(*context.assets.locales)) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + builder.glExtensions(gpuInfoList.stream() + .flatMap { fetchedGlStrings: FetchedGlStrings -> fetchedGlStrings.glExtensions?.let { Arrays.stream(it.toTypedArray()) } } + .collect(Collectors.toList())) + .isLowRamDevice(activityManager.isLowRamDevice) + } + val memoryInfo = ActivityManager.MemoryInfo() + activityManager.getMemoryInfo(memoryInfo) + builder.totalMem(memoryInfo.totalMem) + .availableProcessors(Runtime.getRuntime().availableProcessors()) + val systemAvailableFeatures = context.packageManager.systemAvailableFeatures + for (featureInfo in systemAvailableFeatures) { + if (!TextUtils.isEmpty(featureInfo.name)) { + var featureInfoProto = FeatureInfoProto.Builder().build() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + featureInfoProto = FeatureInfoProto.Builder().name(featureInfo.name).version(featureInfo.version).build() + } + builder.featureInfoList = builder.featureInfoList.toMutableList().apply { + add(featureInfoProto) + } + builder.featureNames = builder.featureNames.toMutableList().apply { + add(featureInfoProto.name!!) + } + } + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + builder.supportedAbi(listOf(*Build.SUPPORTED_ABIS)) + } + var prop = getSystemProperty("ro.oem.key1", "") + if (!TextUtils.isEmpty(prop)) { + builder.oemKey(prop) + } + builder.buildCodeName(Build.VERSION.CODENAME) + prop = getSystemProperty("ro.build.version.preview_sdk_fingerprint", "") + if (!TextUtils.isEmpty(prop)) { + builder.previewSdkFingerprint(prop) + } + return builder.build() + } + + private fun createGpuPayloadRequest(glStringsList: List): SyncRequest? { + var gpuPayloadRequest: SyncRequest? = null + try { + var infos = glStringsList + var gpuPayloads = emptyList() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + infos = infos.stream() + .filter { fetchedGlStrings: FetchedGlStrings -> + fetchedGlStrings.glRenderer!!.isNotEmpty() || fetchedGlStrings.glVendor!!.isNotEmpty() || fetchedGlStrings.glVersion!!.isNotEmpty() + }.collect(Collectors.toList()) + val maxVersion = infos.stream() + .max(Comparator.comparingInt { + fetchedGlStrings: FetchedGlStrings -> + fetchedGlStrings.contextClientVersion + }).map { obj: FetchedGlStrings -> + obj.contextClientVersion + } + if (maxVersion.isPresent) { + infos = infos.stream() + .filter { fetchedGlStrings: FetchedGlStrings -> + fetchedGlStrings.contextClientVersion == maxVersion.get() + }.collect(Collectors.toList()) + } + gpuPayloads = infos.stream().map { fetchedGlStrings: FetchedGlStrings -> + val gpuInfoWrapper = GpuInfoWrapper.Builder() + if (!TextUtils.isEmpty(fetchedGlStrings.glRenderer)) gpuInfoWrapper.glRenderer(fetchedGlStrings.glRenderer) + if (!TextUtils.isEmpty(fetchedGlStrings.glVendor)) gpuInfoWrapper.glVendor(fetchedGlStrings.glVendor) + if (!TextUtils.isEmpty(fetchedGlStrings.glVersion)) gpuInfoWrapper.glVersion(fetchedGlStrings.glVersion) + GpuPayload.Builder().gpuInfo(gpuInfoWrapper.build()).build() + }.distinct().collect(Collectors.toList()) + } + gpuPayloadRequest = SyncRequest.Builder().gpuPayload(if (gpuPayloads.isEmpty()) GpuPayload.Builder().build() else gpuPayloads[0]).build() + } catch (e: Exception) { + Log.w(TAG, "createGpuPayloadRequest error", e) + } + return gpuPayloadRequest + } + + private fun createHardwarePropertiesPayloadRequest(deviceInfoCollect: DeviceInfoCollect): SyncRequest { + val hardwarePropertiesPayload = HardwarePropertiesPayload.Builder() + .isLowRamDevice(deviceInfoCollect.isLowRamDevice) + .totalMem(deviceInfoCollect.totalMem) + .availableProcessors(deviceInfoCollect.availableProcessors) + .supportedAbi(deviceInfoCollect.supportedAbi) + .build() + return SyncRequest.Builder().hardwarePropertiesPayload(hardwarePropertiesPayload).build() + } + + private fun createLocalePropertiesPayloadRequest(): SyncRequest { + val timeZone = TimeZone.getDefault() + val gmtFormat = String.format( + "GMT%+d:%02d", + timeZone.rawOffset / (60 * 60 * 1000), + abs(timeZone.rawOffset / (60 * 1000) % 60) + ) + val localePropertiesPayload = LocalePropertiesPayload.Builder() + .locale(gmtFormat) + .build() + return SyncRequest.Builder().localePropertiesPayload(localePropertiesPayload).build() + } + + private fun createPlayPartnerPropertiesPayloadRequest(): SyncRequest { + val playPartnerPropertiesPayload = PlayPartnerPropertiesPayload.Builder() + .marketId("am-google") + .partnerIdMs("play-ms-android-google") + .partnerIdAd("play-ad-ms-android-google") + .build() + return SyncRequest.Builder().playPartnerPropertiesPayload(playPartnerPropertiesPayload).build() + } + + private fun createPlayPropertiesPayload(context: Context): SyncRequest { + var version = 0 + try { + version = context.packageManager.getPackageInfo("com.android.vending", 0).versionCode + } catch (exception: PackageManager.NameNotFoundException) { + Log.w(TAG, "[DAS] Could not find our package", exception) + } + val playPropertiesPayload = PlayPropertiesPayload.Builder().playVersion(version).build() + return SyncRequest.Builder().playPropertiesPayload(playPropertiesPayload).build() + } + + private fun createScreenPropertiesPayloadRequest(deviceInfoCollect: DeviceInfoCollect): SyncRequest { + val screenPropertiesPayload = ScreenPropertiesPayload.Builder() + .reqTouchScreen(deviceInfoCollect.reqTouchScreen) + .displayX(deviceInfoCollect.displayX) + .displayY(deviceInfoCollect.displayY) + .deviceStablePoint(deviceInfoCollect.deviceStablePoint) + .deviceStable(deviceInfoCollect.deviceStable) + .build() + return SyncRequest.Builder().screenPropertiesPayload(screenPropertiesPayload).build() + } + + private fun createSystemPropertiesPayloadRequest(deviceInfoCollect: DeviceInfoCollect): SyncRequest { + val systemPropertiesPayload = SystemPropertiesPayload.Builder() + .fingerprint(Build.FINGERPRINT) + .sdkInt(Build.VERSION.SDK_INT.toLong()) + .previewSdkFingerprint(deviceInfoCollect.previewSdkFingerprint) + .buildCodeName(deviceInfoCollect.buildCodeName) + .oemKey(deviceInfoCollect.oemKey) + .reqGlEsVersion(deviceInfoCollect.reqGlEsVersion) + .build() + return SyncRequest.Builder().systemPropertiesPayload(systemPropertiesPayload).build() + } + + private fun createHardwareIdentifierPayloadRequest(context: Context): SyncRequest? { + var hardwareIdentifierPayloadRequest: SyncRequest? = null + try { + val builder = HardwareIdentifierPayload.Builder() + val telephonyManager = context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager + val randomIMEI = generateRandomIMEI() + var imeid: Long = if (TextUtils.isEmpty(randomIMEI) || !Pattern.compile("^[0-9]{15}$").matcher(randomIMEI).matches()) 0L else randomIMEI.toLong(10) or 0x1000000000000000L + if (imeid == 0L) { + var meid = "" + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + meid = telephonyManager.meid + } + if (!TextUtils.isEmpty(meid) && Pattern.compile("^[0-9a-fA-F]{14}$").matcher(meid).matches()) { + imeid = meid.toLong(16) or 0x1100000000000000L + if (imeid == 0L) { + if (context.packageManager.checkPermission("android.permission.READ_PRIVILEGED_PHONE_STATE", "com.android.vending") == PackageManager.PERMISSION_GRANTED) { + var serial = "" + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + serial = Build.getSerial() + } + if (TextUtils.isEmpty(serial) && serial != "unknown") { + try { + val serialShaByte = MessageDigest.getInstance("SHA1").digest(serial.toByteArray()) + imeid = ((serialShaByte[0].toLong()) and 0xFFL) shl 0x30 or 0x1400000000000000L or (((serialShaByte[1].toLong()) and 0xFFL) shl 40) or (((serialShaByte[2].toLong()) and 0xFFL) shl 0x20) or (((serialShaByte[3].toLong()) and 0xFFL) shl 24) or (((serialShaByte[4].toLong()) and 0xFFL) shl 16) or (((serialShaByte[5].toLong()) and 0xFFL) shl 8) or ((serialShaByte[6].toLong()) and 0xFFL) + } catch (noSuchAlgorithmException0: NoSuchAlgorithmException) { + Log.w(TAG, "No support for sha1?") + } + } + } + } + } + } + builder.imeId(imeid) + hardwareIdentifierPayloadRequest = SyncRequest.Builder().hardwareIdentifierPayload(builder.build()).build() + }catch (e:Exception){ + Log.w(TAG, "createHardwareIdentifierPayloadRequest error", e) + } + return hardwareIdentifierPayloadRequest + } + + private fun createEnterprisePropertiesPayloadRequest(context: Context): SyncRequest? { + var enterprisePropertiesPayloadRequest: SyncRequest? = null + try { + val enterprisePropertiesPayload = EnterprisePropertiesPayload.Builder() + val devicePolicyManager = context.getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager + val activeAdmins = devicePolicyManager.activeAdmins + if (activeAdmins != null) { + for (componentName in activeAdmins) { + val packageName = componentName.packageName + val packageInfo: PackageInfo? = context.packageManager.getPackageInfo(packageName, PackageManager.GET_SIGNATURES) + val isDeviceOwner = devicePolicyManager.isDeviceOwnerApp(packageName) + var isProfileOwner = false + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + isProfileOwner = devicePolicyManager.isProfileOwnerApp(packageName) + } + val policyType = if (isDeviceOwner) MangedScope.MANAGED_DEVICES else if (isProfileOwner) MangedScope.MANAGED_PROFILES else MangedScope.LEGACY_DEVICE_ADMINS + val profileInfo = ProfileInfo.Builder() + .pkgName(componentName.packageName) + .policyType(policyType) + .pkgSHA1(calculateSHA(packageInfo!!.signatures[0].toByteArray(), "SHA1")) + .pkgSHA256(calculateSHA(packageInfo.signatures[0].toByteArray(), "SHA256")).build() + if (isProfileOwner) { + enterprisePropertiesPayload.profileOwner(profileInfo) + } + enterprisePropertiesPayload.default = enterprisePropertiesPayload.default.toMutableList() + .apply { add(profileInfo) } + } + } + enterprisePropertiesPayloadRequest = SyncRequest.Builder().enterprisePropertiesPayload(enterprisePropertiesPayload.build()).build() + }catch (e:Exception){ + Log.w(TAG, "createEnterprisePropertiesPayloadRequest error", e) + } + return enterprisePropertiesPayloadRequest + } + + private fun createDeviceInputPropertiesPayloadRequest(deviceInfoCollect: DeviceInfoCollect): SyncRequest { + val builder = DeviceInputPropertiesPayload.Builder() + .reqInputFeatures(deviceInfoCollect.reqInputFeaturesV1) + .reqKeyboardType(deviceInfoCollect.reqKeyboardType) + .reqNavigation(deviceInfoCollect.reqNavigation) + return SyncRequest.Builder().deviceInputPropertiesPayload(builder.build()).build() + } + + private fun createDeviceModelPayloadRequest(): SyncRequest { + val builder = DeviceModelPayload.Builder() + .manufacturer(Build.MANUFACTURER) + .model(Build.MODEL) + .device(Build.DEVICE) + .product(Build.PRODUCT) + .brand(Build.BRAND) + return SyncRequest.Builder().deviceModelPayload(builder.build()).build() + } + + private fun createDeviceCapabilitiesPayloadRequest(deviceInfoCollect: DeviceInfoCollect): SyncRequest { + val builder = DeviceCapabilitiesPayload.Builder() + builder.glExtensions(deviceInfoCollect.glExtensions) + val featureInfoList = builder.featureInfo.toMutableList() + for (featureInfoProto in deviceInfoCollect.featureInfoList) { + featureInfoList.add(FeatureInfoProto.Builder() + .name(featureInfoProto.name) + .version(featureInfoProto.version) + .build()) + } + builder.featureInfo = featureInfoList + builder.systemSharedLibraryNames(deviceInfoCollect.systemSharedLibraryNames) + .locales(deviceInfoCollect.locales) + .unknownFlag(false) + return SyncRequest.Builder().deviceCapabilitiesPayload(builder.build()).build() + } + + private fun createDeviceAccountsPayloadRequest(context: Context, androidId: String): SyncRequest? { + var deviceAccountsPayloadRequest: SyncRequest? = null + try { + val accountManager = context.getSystemService(Context.ACCOUNT_SERVICE) as AccountManager + val accounts = accountManager.accounts + val builder = DeviceAccountsPayload.Builder() + val accountAssValues = builder.accountAss.toMutableList() + for (account in accounts) { + accountAssValues.add(AccountAssValue.Builder().value_(accountSha256(androidId, account)).build()) + } + builder.accountAss = accountAssValues + deviceAccountsPayloadRequest = SyncRequest.Builder().deviceAccountsPayload(builder.build()).build() + } catch (e: Exception) { + Log.w(TAG, "createDeviceAccountsPayloadRequest error", e) + } + return deviceAccountsPayloadRequest + } + + @SuppressLint("HardwareIds") + private fun createCarrierPropertiesPayloadRequest(context: Context): SyncRequest? { + var carrierPropertiesPayloadRequest: SyncRequest? = null + try { + val telephonyManager = context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager + var simCardId = 0 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + simCardId = telephonyManager.simCarrierId + } + var carrierIdFromSimMccMnc = 0 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + carrierIdFromSimMccMnc = telephonyManager.carrierIdFromSimMccMnc + } + val telephonyInfo = TelephonyInfo.Builder().subscriberId(((telephonyManager.subscriberId.toLong() / 100000L).toString() + "00000").toLong()) + .operatorName(telephonyManager.simOperatorName).groupIdLevel(telephonyManager.groupIdLevel1).simCardId(simCardId) + .carrierIdFromSimMccMnc(carrierIdFromSimMccMnc).build() + val telephonyStateWrapper = TelephonyStateWrapper.Builder().telephonyInfo(telephonyInfo).build() + val carrierPropertiesPayload = + CarrierPropertiesPayload.Builder().telephonyStateValue(telephonyStateWrapper).simOperator(telephonyManager.simOperator).build() + carrierPropertiesPayloadRequest = SyncRequest.Builder().carrierPropertiesPayload(carrierPropertiesPayload).build() + } catch (securityException: SecurityException) { + Log.w(TAG, "SecurityException when reading IMSI.", securityException) + } catch (stateException: IllegalStateException) { + Log.w(TAG, "IllegalStateException when reading IMSI. This is a known SDK 31 Samsung bug.", stateException) + } + return carrierPropertiesPayloadRequest + } + + private fun accountSha256(androidId: String, account: Account): String? { + return try { + val androidIdAcc = (androidId + "-" + account.name).toByteArray() + val messageDigest0 = MessageDigest.getInstance("SHA256") + messageDigest0.update(androidIdAcc, 0, androidIdAcc.size) + Base64.encodeToString(messageDigest0.digest(), 11) + } catch (ignored: Exception) { + null + } + } + + private fun generateRandomIMEI(): String { + val random = Random() + val imeiBuilder = StringBuilder() + for (i in 0..13) { + val digit = random.nextInt(10) + imeiBuilder.append(digit) + } + val imei = imeiBuilder.toString() + val checkDigit = calculateCheckDigit(imei) + imeiBuilder.append(checkDigit) + return imeiBuilder.toString() + } + + private fun calculateCheckDigit(imei: String): Int { + var sum = 0 + for (i in imei.indices) { + var digit = Character.getNumericValue(imei[i]) + if (i % 2 == 1) { + digit *= 2 + } + if (digit > 9) { + digit -= 9 + } + sum += digit + } + return (10 - (sum % 10)) % 10 + } + + private fun calculateSHA(data: ByteArray, algorithm: String?): String? { + val messageDigest0: MessageDigest + try { + messageDigest0 = algorithm?.let { MessageDigest.getInstance(it) }!! + } catch (noSuchAlgorithmException0: NoSuchAlgorithmException) { + Log.w(TAG, "[DC] No support for %s?", noSuchAlgorithmException0) + return null + } + messageDigest0.update(data, 0, data.size) + return Base64.encodeToString(messageDigest0.digest(), 11) + } + + private fun fetchGLInfo(): ArrayList { + if(glInfoList.isNotEmpty()) return glInfoList + try { + val eGL100 = EGLContext.getEGL() as? EGL10 + val result = ArrayList() + val egl10Instance = eGL100?.let { EGL10Wrapper(it) } + val eglDisplay = eGL100!!.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY) + eGL100.eglInitialize(eglDisplay, IntArray(2)) + val ints = IntArray(1) + val configCount = if (eGL100.eglGetConfigs(eglDisplay, null, 0, ints)) ints[0] else 0 + val arrEglConfig = arrayOfNulls(configCount) + val eglConfigs = if (eGL100.eglGetConfigs(eglDisplay, arrEglConfig, configCount, IntArray(1))) arrEglConfig else null + val arrV1 = intArrayOf(0x3057, 1, 0x3056, 1, 0x3038) + for (v1 in 0 until configCount) { + if (egl10Instance?.eglGetConfigAttrib(eglDisplay, eglConfigs?.get(v1), 0x3027) != 0x3050 + && (egl10Instance?.eglGetConfigAttrib(eglDisplay, eglConfigs?.get(v1), 0x3033)?.and(1)) != 0) { + val v2 = egl10Instance?.eglGetConfigAttrib(eglDisplay, eglConfigs?.get(v1), 0x3040) + if ((v2?.and(1)) != 0) { + egl10Instance?.let { wrapper -> buildGLStrings(wrapper, eglDisplay, eglConfigs?.get(v1), arrV1, null)?.let { result.add(it) } } + } + if ((v2?.and(4)) != 0) { + egl10Instance?.let { wrapper -> buildGLStrings(wrapper, eglDisplay, eglConfigs?.get(v1), arrV1, intArrayOf(0x3098, 2, 0x3038))?.let { result.add(it) } } + } + } + } + egl10Instance?.instance?.eglTerminate(eglDisplay) + return result.also { glInfoList.addAll(it) } + }catch (e:Exception){ + Log.d(TAG, "fetchGLInfo: error", e) + } + return ArrayList() + } + + private fun buildGLStrings(egl10Tools: EGL10Wrapper, eglDisplay: EGLDisplay, eglConfig: EGLConfig?, arrV: IntArray, arrV1: IntArray?): FetchedGlStrings? { + val eglContext = egl10Tools.instance.eglCreateContext(eglDisplay, eglConfig, EGL10.EGL_NO_CONTEXT, arrV1) + if (eglContext != EGL10.EGL_NO_CONTEXT) { + val eglSurface = egl10Tools.instance.eglCreatePbufferSurface(eglDisplay, eglConfig, arrV) + if (eglSurface == EGL10.EGL_NO_SURFACE) { + egl10Tools.eglDestroyContext(eglDisplay, eglContext) + return null + } + egl10Tools.eglMakeCurrent(eglDisplay, eglSurface, eglSurface, eglContext) + val result = FetchedGlStrings(0, null, null, null, null) + val glExtensions = GLES10.glGetString(GLES10.GL_EXTENSIONS) + if (!TextUtils.isEmpty(glExtensions)) { + result.glExtensions = glExtensions.split(" ".toRegex()).dropLastWhile { it.isEmpty() } + } + result.glRenderer = GLES10.glGetString(GLES10.GL_RENDERER) + result.glVendor = GLES10.glGetString(GLES10.GL_VENDOR) + result.glVersion = GLES10.glGetString(GLES10.GL_VERSION) + if (result.glExtensions != null) { + egl10Tools.eglMakeCurrent(eglDisplay, EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_CONTEXT) + egl10Tools.instance.eglDestroySurface(eglDisplay, eglSurface) + egl10Tools.eglDestroyContext(eglDisplay, eglContext) + return result + } + throw IllegalStateException("Missing required properties ") + } + return null + } + + private fun getSystemProperty(key: String?, defaultValue: String?): String? { + var value = defaultValue + try { + @SuppressLint("PrivateApi") val systemPropertiesClass = Class.forName("android.os.SystemProperties") + val getMethod = systemPropertiesClass.getMethod("get", String::class.java, String::class.java) + value = getMethod.invoke(null, key, defaultValue) as String + } catch (e: Exception) { + Log.w(TAG, "Unable to retrieve system property", e) + } + return value + } + + private fun calculatePoint(point: Point, v: Int): Int { + val f = point.x.toFloat() + val v1 = ((point.y.toFloat()) * (160.0f / (v.toFloat()))).toInt() + if (v1 < 470) { + return 17 + } + val v2 = (f * (160.0f / (v.toFloat()))).toInt() + if (v1 >= 960 && v2 >= 720) { + return if (v1 * 3 / 5 < v2 - 1) 20 else 4 + } + val v3 = if (v1 < 640 || v2 < 480) 2 else 3 + return if (v1 * 3 / 5 < v2 - 1) v3 or 16 else v3 + } + + internal class EGL10Wrapper(val instance: EGL10) { + fun eglGetConfigAttrib(eglDisplay: EGLDisplay?, eglConfig: EGLConfig?, v: Int): Int { + val value = IntArray(1) + instance.eglGetConfigAttrib(eglDisplay, eglConfig, v, value) + return value[0] + } + + fun eglDestroyContext(eglDisplay: EGLDisplay?, eglContext: EGLContext?) { + instance.eglDestroyContext(eglDisplay, eglContext) + } + + fun eglMakeCurrent(eglDisplay: EGLDisplay?, draw: EGLSurface?, read: EGLSurface?, eglContext: EGLContext?) { + instance.eglMakeCurrent(eglDisplay, draw, read, eglContext) + } + } + + internal class FetchedGlStrings(var contextClientVersion: Int, + var glExtensions: List?, + var glRenderer: String?, + var glVendor: String?, + var glVersion: String?) +} \ No newline at end of file diff --git a/vending-app/src/main/proto/SyncRequest.proto b/vending-app/src/main/proto/SyncRequest.proto new file mode 100644 index 0000000000..546b390380 --- /dev/null +++ b/vending-app/src/main/proto/SyncRequest.proto @@ -0,0 +1,188 @@ +option java_package = "com.google.android.finsky"; +option java_multiple_files = true; + +message SyncReqWrapper { + repeated SyncRequest request = 1; +} + +message SyncRequest { + oneof payload { + AccountAssociationPayload accountAssociationPayload = 7; + DeviceAccountsPayload deviceAccountsPayload = 8; + CarrierPropertiesPayload carrierPropertiesPayload = 9; + DeviceCapabilitiesPayload deviceCapabilitiesPayload = 10; + DeviceInputPropertiesPayload deviceInputPropertiesPayload = 11; + DeviceModelPayload deviceModelPayload = 12; + EnterprisePropertiesPayload enterprisePropertiesPayload = 13; + HardwareIdentifierPayload hardwareIdentifierPayload = 14; + HardwarePropertiesPayload hardwarePropertiesPayload = 15; + LocalePropertiesPayload localePropertiesPayload = 16; + NotificationRoutingInfoPayload notificationRoutingInfoPayload = 17; + PlayPartnerPropertiesPayload playPartnerPropertiesPayload = 18; + PlayPropertiesPayload playPropertiesPayload = 19; + ScreenPropertiesPayload screenPropertiesPayload = 20; + SystemPropertiesPayload systemPropertiesPayload = 21; + GpuPayload gpuPayload = 24; + } +} + +message AccountAssociationPayload { + optional AccountAssValue accountAss = 1; +} + +message AccountAssValue { + optional string value = 1; +} + +message DeviceAccountsPayload { + repeated AccountAssValue accountAss = 1; +} + +message CarrierPropertiesPayload { + optional string simOperator = 1; + optional TelephonyStateWrapper telephonyStateValue = 2; +} + +message TelephonyStateWrapper { + optional TelephonyInfo telephonyInfo = 1; +} + +message TelephonyInfo { + optional int64 subscriberId = 1; + optional string operatorName = 2; + optional string groupIdLevel = 3; + optional int32 simCardId = 6; + optional int32 carrierIdFromSimMccMnc = 7; +} + +message DeviceCapabilitiesPayload { + repeated FeatureInfoProto featureInfo = 1; + repeated string systemSharedLibraryNames = 2; + repeated string locales = 3; + repeated string glExtensions = 4; + optional bool unknownFlag = 5; +} + +message DeviceInputPropertiesPayload { + optional int32 reqKeyboardType = 1; + optional bool reqInputFeatures = 2; + optional int32 reqNavigation = 3; +} + +message DeviceModelPayload { + optional string manufacturer = 1; + optional string model = 2; + optional string device = 3; + optional string product = 4; + optional string brand = 5; +} + +message EnterprisePropertiesPayload { + optional ProfileInfo profileOwner = 1; + repeated ProfileInfo default = 2; +} + +message ProfileInfo { + optional string pkgName = 1; + optional string pkgSHA1 = 2; + optional string pkgSHA256 = 3; + optional MangedScope policyType = 4; +} + +enum MangedScope { + UNKNOWN_MANAGED_SCOPE = 0; + MANAGED_DEVICES = 1; + MANAGED_PROFILES = 2; + MANAGED_AVENGER = 3; + LEGACY_DEVICE_ADMINS = 4; +} + +message HardwareIdentifierPayload { + optional fixed64 imeId = 1; +} + +message HardwarePropertiesPayload { + optional bool isLowRamDevice = 1; + optional int64 totalMem = 2; + optional int32 availableProcessors = 3; + repeated string supportedAbi = 4; +} + +message LocalePropertiesPayload { + optional string locale = 1; +} + +message NotificationRoutingInfoPayload { + optional string info = 1; +} + +message PlayPartnerPropertiesPayload { + optional string marketId = 1; + optional string partnerIdMs = 2; + optional string partnerIdAd = 3; +} + +message PlayPropertiesPayload { + optional int32 playVersion = 2; +} + +message ScreenPropertiesPayload { + optional int32 reqTouchScreen = 1; + optional int32 displayX = 2; + optional int32 displayY = 3; + optional int32 deviceStablePoint = 4; + optional int32 deviceStable = 5; +} + +message SystemPropertiesPayload { + optional string fingerprint = 1; + optional int64 sdkInt = 2; + optional string previewSdkFingerprint = 3; + optional string buildCodeName = 4; + optional string oemKey = 5; + optional int32 reqGlEsVersion = 6; +} + +message GpuPayload { + optional GpuInfoWrapper gpuInfo = 1; +} + +message GpuInfoWrapper { + optional string glRenderer = 1; + optional string glVendor = 2; + optional string glVersion = 3; +} + +message DeviceInfoCollect { + optional int32 reqTouchScreen = 1; + optional int32 reqKeyboardType = 2; + optional int32 reqNavigation = 3; + optional int32 deviceStablePoint = 4; + optional bool reqInputFeaturesV1 = 5; + optional bool reqInputFeaturesV2 = 6; + optional int32 deviceStable = 7; + optional int32 reqGlEsVersion = 8; + repeated string systemSharedLibraryNames = 9; + repeated string featureNames = 10; + repeated string supportedAbi = 11; + optional int32 displayX = 12; + optional int32 displayY = 13; + repeated string locales = 14; + repeated string glExtensions = 15; + optional int32 smallestScreenWidthDp = 18; + optional bool isLowRamDevice = 19; + optional int64 totalMem = 20; + optional int32 availableProcessors = 21; + repeated FeatureInfoProto featureInfoList = 26; + optional int32 screenLayout = 27; + optional string oemKey = 29; + optional string buildCodeName = 30; + optional string previewSdkFingerprint = 31; +} + +message FeatureInfoProto { + optional string name = 1; + optional int32 version = 2; +} + +message SyncResponse {} From bbbddb07c4971901330a614fa8a505b17be02923 Mon Sep 17 00:00:00 2001 From: davinci9196 Date: Fri, 8 Nov 2024 17:59:05 +0800 Subject: [PATCH 079/132] Asset Modules: Optimize code logic --- .../google/android/finsky/DownloadManager.kt | 15 +- .../assetmoduleservice/AssetModuleService.kt | 136 ++++++------------ .../com/google/android/finsky/extensions.kt | 98 +++++++------ 3 files changed, 109 insertions(+), 140 deletions(-) diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/DownloadManager.kt b/vending-app/src/main/kotlin/com/google/android/finsky/DownloadManager.kt index 6e020ef85c..f81f23e3b1 100644 --- a/vending-app/src/main/kotlin/com/google/android/finsky/DownloadManager.kt +++ b/vending-app/src/main/kotlin/com/google/android/finsky/DownloadManager.kt @@ -19,7 +19,6 @@ import android.graphics.Canvas import android.graphics.drawable.BitmapDrawable import android.net.Uri import android.os.Build -import android.os.Bundle import android.util.Log import android.widget.RemoteViews import androidx.core.app.NotificationCompat @@ -55,7 +54,7 @@ class DownloadManager(private val context: Context) { private val downloadingRecord = ConcurrentHashMap>() @Volatile - private var shouldStop = false + private var shouldStops = false private val cancelReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { @@ -145,14 +144,18 @@ class DownloadManager(private val context: Context) { NotificationManagerCompat.from(context).notify(NOTIFICATION_ID, notifyBuilder.setCustomContentView(notificationLayout).build()) } + @Synchronized + fun shouldStop(shouldStop:Boolean){ + shouldStops = shouldStop + } + @Synchronized fun prepareDownload(downloadData: DownloadData, moduleName: String) { Log.d(TAG, "prepareDownload: ${downloadData.packageName}") - shouldStop = false - downloadData.updateDownloadStatus(moduleName, STATUS_DOWNLOADING) initNotification(moduleName, downloadData.packageName) val future = executor.submit { val packData = downloadData.getModuleData(moduleName) + downloadData.updateDownloadStatus(moduleName, STATUS_DOWNLOADING) for (dataBundle in packData.packBundleList) { val resourcePackageName: String? = dataBundle.getString(KEY_RESOURCE_PACKAGE_NAME) val chunkName: String? = dataBundle.getString(KEY_CHUNK_NAME) @@ -178,7 +181,7 @@ class DownloadManager(private val context: Context) { private fun cancelDownload(moduleName: String) { Log.d(TAG, "Download for module $moduleName has been canceled.") downloadingRecord[moduleName]?.cancel(true) - shouldStop = true + shouldStops = true notifyBuilderMap[moduleName]?.setOngoing(false) NotificationManagerCompat.from(context).cancel(NOTIFICATION_ID) } @@ -205,7 +208,7 @@ class DownloadManager(private val context: Context) { val buffer = ByteArray(4096) var bytesRead: Int while (input.read(buffer).also { bytesRead = it } != -1) { - if (shouldStop) { + if (shouldStops) { Log.d(TAG, "Download interrupted for module: $moduleName") downloadData.updateDownloadStatus(moduleName, CANCELED) return diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/AssetModuleService.kt b/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/AssetModuleService.kt index a006f47510..52660154bc 100644 --- a/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/AssetModuleService.kt +++ b/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/AssetModuleService.kt @@ -15,7 +15,6 @@ import android.util.Log import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleService -import androidx.lifecycle.lifecycleScope import com.google.android.finsky.DownloadManager import com.google.android.finsky.ERROR_CODE_FAIL import com.google.android.finsky.KEY_BYTE_LENGTH @@ -36,7 +35,6 @@ import com.google.android.finsky.KEY_SLICE_ID import com.google.android.finsky.STATUS_COMPLETED import com.google.android.finsky.STATUS_DOWNLOADING import com.google.android.finsky.STATUS_INITIAL_STATE -import com.google.android.finsky.STATUS_NOT_INSTALLED import com.google.android.finsky.TAG_REQUEST import com.google.android.finsky.buildDownloadBundle import com.google.android.finsky.initAssertModuleData @@ -82,34 +80,27 @@ class AssetModuleServiceImpl( callback?.onError(Bundle().apply { putInt(KEY_ERROR_CODE, -5) }) return } - lifecycleScope.launchWhenStarted { - if (downloadData == null || downloadData?.packageName != packageName) { - val requestedAssetModuleNames = list.map { it.getString(KEY_MODULE_NAME) }.filter { !it.isNullOrEmpty() } - val playCoreVersionCode = bundle.getInt(KEY_PLAY_CORE_VERSION_CODE) - downloadData = httpClient.initAssertModuleData(context, packageName, accountManager, requestedAssetModuleNames, playCoreVersionCode) - } - if (list.all { it.getString(KEY_MODULE_NAME) == null } || moduleErrorRequested.contains(packageName)) { - Log.d(TAG, "startDownload: moduleData request error") - val result = Bundle().apply { putStringArrayList(KEY_PACK_NAMES, arrayListOf()) } - callback?.onStartDownload(-1, result) - return@launchWhenStarted - } - list.forEach { - val moduleName = it.getString(KEY_MODULE_NAME) - val packData = downloadData?.getModuleData(moduleName!!) - if (packData?.status != STATUS_DOWNLOADING){ - downloadData?.updateDownloadStatus(moduleName!!, STATUS_INITIAL_STATE) - } + if (downloadData == null || downloadData?.packageName != packageName) { + val requestedAssetModuleNames = list.map { it.getString(KEY_MODULE_NAME) }.filter { !it.isNullOrEmpty() } + val playCoreVersionCode = bundle.getInt(KEY_PLAY_CORE_VERSION_CODE) + downloadData = httpClient.initAssertModuleData(context, packageName, accountManager, requestedAssetModuleNames, playCoreVersionCode) + } + list.forEach { + val moduleName = it.getString(KEY_MODULE_NAME) + val packData = downloadData?.getModuleData(moduleName!!) + if (packData?.status != STATUS_DOWNLOADING && packData?.status != STATUS_COMPLETED){ + downloadData?.updateDownloadStatus(moduleName!!, STATUS_INITIAL_STATE) } - val bundleData = buildDownloadBundle(downloadData!!,list) - Log.d(TAG, "startDownload---${bundleData}") - callback?.onStartDownload(-1, bundleData) - list.forEach { - val moduleName = it.getString(KEY_MODULE_NAME) - val packData = downloadData?.getModuleData(moduleName!!) - if (packData?.status != STATUS_DOWNLOADING){ - DownloadManager.get(context).prepareDownload(downloadData!!, moduleName!!) - } + } + val bundleData = buildDownloadBundle(downloadData!!,list) + Log.d(TAG, "startDownload: $bundleData") + callback?.onStartDownload(-1, bundleData) + list.forEach { + val moduleName = it.getString(KEY_MODULE_NAME) + val packData = downloadData?.getModuleData(moduleName!!) + if (packData?.status == STATUS_INITIAL_STATE){ + DownloadManager.get(context).shouldStop(false) + DownloadManager.get(context).prepareDownload(downloadData!!, moduleName!!) } } } @@ -117,44 +108,11 @@ class AssetModuleServiceImpl( override fun getSessionStates(packageName: String?, bundle: Bundle?, callback: IAssetModuleServiceCallback?) { Log.d(TAG, "Method (getSessionStates) called by packageName -> $packageName") val installedAssetModuleNames = mutableListOf() - bundle?.keySet()?.forEach { key -> - val value = bundle.get(key) - if (key == KEY_INSTALLED_ASSET_MODULE) { - when (value) { - is Bundle -> { - value.keySet().forEach { subKey -> - val item = value.get(subKey) - if (item is String) { - installedAssetModuleNames.add(item) - Log.d(TAG, "installed_asset_module Bundle Value: $item") - } - } - } - is ArrayList<*> -> { - value.forEachIndexed { index, item -> - if (item is Bundle) { - Log.d(TAG, "installed_asset_module Bundle at index $index") - item.keySet().forEach { subKey -> - val subItem = item.get(subKey) - if (subItem is String) { - installedAssetModuleNames.add(subItem) - Log.d(TAG, "installed_asset_module[$index] Value: $subItem") - } - } - } - } - } - else -> { - Log.d(TAG, "installed_asset_module: - ${value?.javaClass?.name}") - } - } - } else { - Log.d(TAG, "Bundle Key: $key, Value: $value") + bundle?.getParcelableArrayList(KEY_INSTALLED_ASSET_MODULE)?.forEach { item -> + item.keySet().forEach { subKey -> + (item.get(subKey) as? String)?.let { installedAssetModuleNames.add(it) } } } - - Log.d(TAG, "getSessionStates installedAssetModuleNames: $installedAssetModuleNames") - if (packageName == downloadData?.packageName) { downloadData?.moduleNames?.forEach { moduleName -> if (installedAssetModuleNames.contains(moduleName)) { return@forEach } @@ -179,7 +137,6 @@ class AssetModuleServiceImpl( } } - override fun notifyChunkTransferred(packageName: String?, bundle: Bundle?, bundle2: Bundle?, callback: IAssetModuleServiceCallback?) { Log.d(TAG, "Method (notifyChunkTransferred) called by packageName -> $packageName") val moduleName = bundle?.getString(KEY_MODULE_NAME) @@ -205,16 +162,15 @@ class AssetModuleServiceImpl( callback?.onError(Bundle().apply { putInt(KEY_ERROR_CODE, -5) }) return } - lifecycleScope.launchWhenStarted { - Log.d(TAG, "notify: moduleName: $moduleName packNames: ${downloadData?.moduleNames}") - downloadData?.updateDownloadStatus(moduleName, STATUS_COMPLETED) - sendBroadcastForExistingFile(context, downloadData!!, moduleName, null, null) - callback?.onNotifyModuleCompleted(bundle, bundle) - } + Log.d(TAG, "notify: moduleName: $moduleName packNames: ${downloadData?.moduleNames}") + downloadData?.updateDownloadStatus(moduleName, STATUS_COMPLETED) + sendBroadcastForExistingFile(context, downloadData!!, moduleName, null, null) + callback?.onNotifyModuleCompleted(bundle, bundle) } override fun notifySessionFailed(packageName: String?, bundle: Bundle?, bundle2: Bundle?, callback: IAssetModuleServiceCallback?) { Log.d(TAG, "Method (notifySessionFailed) called but not implemented by packageName -> $packageName") + callback?.onNotifySessionFailed(bundle) } override fun keepAlive(packageName: String?, bundle: Bundle?, callback: IAssetModuleServiceCallback?) { @@ -249,27 +205,25 @@ class AssetModuleServiceImpl( callback?.onError(Bundle().apply { putInt(KEY_ERROR_CODE, -5) }) return } - lifecycleScope.launchWhenStarted { - if (downloadData == null || downloadData?.packageName != packageName) { - val requestedAssetModuleNames = list.map { it.getString(KEY_MODULE_NAME) }.filter { !it.isNullOrEmpty() } - val playCoreVersionCode = bundle.getInt(KEY_PLAY_CORE_VERSION_CODE) - downloadData = httpClient.initAssertModuleData(context, packageName, accountManager, requestedAssetModuleNames, playCoreVersionCode) - } - if (downloadData?.errorCode == ERROR_CODE_FAIL) { - if (moduleErrorRequested.contains(packageName)) { - callback?.onError(Bundle().apply { putInt(KEY_ERROR_CODE, -5) }) - return@launchWhenStarted - } - moduleErrorRequested.add(packageName) - val result = Bundle().apply { putStringArrayList(KEY_PACK_NAMES, arrayListOf()) } - callback?.onRequestDownloadInfo(result, result) - return@launchWhenStarted + if (downloadData == null || downloadData?.packageName != packageName) { + val requestedAssetModuleNames = list.map { it.getString(KEY_MODULE_NAME) }.filter { !it.isNullOrEmpty() } + val playCoreVersionCode = bundle.getInt(KEY_PLAY_CORE_VERSION_CODE) + downloadData = httpClient.initAssertModuleData(context, packageName, accountManager, requestedAssetModuleNames, playCoreVersionCode) + } + if (downloadData?.errorCode == ERROR_CODE_FAIL) { + if (moduleErrorRequested.contains(packageName)) { + callback?.onError(Bundle().apply { putInt(KEY_ERROR_CODE, -5) }) + return } - moduleErrorRequested.remove(packageName) - val bundleData = buildDownloadBundle(downloadData!!,list) - Log.d(TAG, "requestDownloadInfo---${bundleData}") - callback?.onRequestDownloadInfo(bundleData, bundleData) + moduleErrorRequested.add(packageName) + val result = Bundle().apply { putStringArrayList(KEY_PACK_NAMES, arrayListOf()) } + callback?.onRequestDownloadInfo(result, result) + return } + moduleErrorRequested.remove(packageName) + val bundleData = buildDownloadBundle(downloadData!!,list) + Log.d(TAG, "requestDownloadInfo: $bundleData") + callback?.onRequestDownloadInfo(bundleData, bundleData) } override fun removeModule(packageName: String?, bundle: Bundle?, bundle2: Bundle?, callback: IAssetModuleServiceCallback?) { diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/extensions.kt b/vending-app/src/main/kotlin/com/google/android/finsky/extensions.kt index 0aa525aee3..9315462deb 100644 --- a/vending-app/src/main/kotlin/com/google/android/finsky/extensions.kt +++ b/vending-app/src/main/kotlin/com/google/android/finsky/extensions.kt @@ -26,6 +26,7 @@ import org.microg.vending.billing.GServices import org.microg.vending.billing.core.HttpClient import java.io.File import java.util.Collections +import kotlinx.coroutines.runBlocking const val STATUS_NOT_INSTALLED = 8 const val CANCELED = 6 @@ -90,46 +91,60 @@ fun getAppVersionCode(context: Context, packageName: String): String? { return runCatching { context.packageManager.getPackageInfo(packageName, 0).versionCode.toString() }.getOrNull() } -suspend fun HttpClient.initAssertModuleData( - context: Context, - packageName: String, - accountManager: AccountManager, - requestedAssetModuleNames: List, - playCoreVersionCode: Int, +fun HttpClient.initAssertModuleData( + context: Context, + packageName: String, + accountManager: AccountManager, + requestedAssetModuleNames: List, + playCoreVersionCode: Int, ): DownloadData { - Log.d(TAG, "initAssertModuleData: requestedAssetModuleNames: $requestedAssetModuleNames") val accounts = accountManager.getAccountsByType(AuthConstants.DEFAULT_ACCOUNT_TYPE) var oauthToken: String? = null if (accounts.isEmpty()) { return DownloadData(errorCode = ERROR_CODE_FAIL) - } else for (account: Account in accounts) { - oauthToken = accountManager.getAuthToken(account, AUTH_TOKEN_SCOPE, false).getString(AccountManager.KEY_AUTHTOKEN) - if (oauthToken != null) { - break + } else { + for (account: Account in accounts) { + oauthToken = runBlocking { + accountManager.getAuthToken(account, AUTH_TOKEN_SCOPE, false).getString(AccountManager.KEY_AUTHTOKEN) + } + if (oauthToken != null) { + break + } } } - Log.d(TAG, "initAssertModuleData: oauthToken -> $oauthToken") + if (oauthToken == null) { return DownloadData(errorCode = ERROR_CODE_FAIL) } - val requestPayload = AssetModuleDeliveryRequest.Builder().callerInfo(CallerInfo(getAppVersionCode(context, packageName)?.toInt())).packageName(packageName) - .playCoreVersion(playCoreVersionCode).pageSource(listOf(PageSource.UNKNOWN_SEARCH_TRAFFIC_SOURCE, PageSource.BOOKS_HOME_PAGE)) - .callerState(listOf(CallerState.CALLER_APP_REQUEST, CallerState.CALLER_APP_DEBUGGABLE)).moduleInfo(ArrayList().apply { - requestedAssetModuleNames.forEach { add(AssetModuleInfo.Builder().name(it).build()) } - }).build() - val androidId = GServices.getString(context.contentResolver, "android_id", "0")?.toLong()?:1 - var moduleDeliveryInfo = runCatching { - post( - url = ASSET_MODULE_DELIVERY_URL, - headers = getLicenseRequestHeaders(oauthToken, androidId), - payload = requestPayload, - adapter = AssetModuleDeliveryResponse.ADAPTER - ).wrapper?.deliveryInfo - }.onFailure { - Log.d(TAG, "initAssertModuleData: ", it) - }.getOrNull() - if (moduleDeliveryInfo?.status == 2) { + val requestPayload = AssetModuleDeliveryRequest.Builder() + .callerInfo(CallerInfo(getAppVersionCode(context, packageName)?.toInt())) + .packageName(packageName) + .playCoreVersion(playCoreVersionCode) + .pageSource(listOf(PageSource.UNKNOWN_SEARCH_TRAFFIC_SOURCE, PageSource.BOOKS_HOME_PAGE)) + .callerState(listOf(CallerState.CALLER_APP_REQUEST, CallerState.CALLER_APP_DEBUGGABLE)) + .moduleInfo(ArrayList().apply { + requestedAssetModuleNames.forEach { add(AssetModuleInfo.Builder().name(it).build()) } + }).build() + + val androidId = GServices.getString(context.contentResolver, "android_id", "0")?.toLong() ?: 1 + + var moduleDeliveryInfo = runBlocking { + runCatching { + post( + url = ASSET_MODULE_DELIVERY_URL, + headers = getLicenseRequestHeaders(oauthToken, androidId), + payload = requestPayload, + adapter = AssetModuleDeliveryResponse.ADAPTER + ).wrapper?.deliveryInfo + }.getOrNull() + } + + if (moduleDeliveryInfo?.status != 2) { + return initModuleDownloadInfo(context, packageName, moduleDeliveryInfo) + } + + runBlocking { runCatching { post( url = SYNC_NOCACHE_QOS, @@ -138,20 +153,20 @@ suspend fun HttpClient.initAssertModuleData( adapter = SyncResponse.ADAPTER ) }.onFailure { - Log.d(TAG, "initAssertModuleData: sync -> ", it) + Log.d(TAG, "initAssertModuleData: ", it) } } - moduleDeliveryInfo = runCatching { - post( - url = ASSET_MODULE_DELIVERY_URL, - headers = getLicenseRequestHeaders(oauthToken, androidId), - payload = requestPayload, - adapter = AssetModuleDeliveryResponse.ADAPTER - ).wrapper?.deliveryInfo - }.onFailure { - Log.d(TAG, "initAssertModuleData: ", it) - }.getOrNull() + moduleDeliveryInfo = runBlocking { + runCatching { + post( + url = ASSET_MODULE_DELIVERY_URL, + headers = getLicenseRequestHeaders(oauthToken, androidId), + payload = requestPayload, + adapter = AssetModuleDeliveryResponse.ADAPTER + ).wrapper?.deliveryInfo + }.getOrNull() + } Log.d(TAG, "initAssertModuleData: moduleDeliveryInfo-> $moduleDeliveryInfo") return initModuleDownloadInfo(context, packageName, moduleDeliveryInfo) } @@ -235,11 +250,9 @@ fun buildDownloadBundle(downloadData: DownloadData, list: List? = null) totalBytesToDownload += packData.totalBytesToDownload bytesDownloaded += packData.bytesDownloaded } - bundleData.putStringArrayList(KEY_PACK_NAMES, arrayList) bundleData.putLong(KEY_TOTAL_BYTES_TO_DOWNLOAD, totalBytesToDownload) bundleData.putLong(KEY_BYTES_DOWNLOADED, bytesDownloaded) - return bundleData } @@ -300,7 +313,6 @@ fun sendBroadcastForExistingFile(context: Context, downloadData: DownloadData, m downloadBundle.putString(combineModule(KEY_UNCOMPRESSED_HASH_SHA256, moduleName, chunkName), uncompressedHashSha256) } downloadBundle.putStringArrayList(combineModule(KEY_SLICE_IDS, moduleName), packData.listOfSubcontractNames) - Log.d(TAG, "sendBroadcastForExistingFile: $downloadBundle") sendBroadCast(context, downloadData, downloadBundle) } catch (e: Exception) { Log.w(TAG, "sendBroadcastForExistingFile error:" + e.message) From 3114a80a90bbf6bbb8cabf6424a207c9370028af Mon Sep 17 00:00:00 2001 From: davinci9196 Date: Tue, 12 Nov 2024 21:03:23 +0800 Subject: [PATCH 080/132] Asset Modules: Removed Device Sync request --- .../protocol/IAssetModuleServiceCallback.aidl | 10 +- .../google/android/finsky/DownloadManager.kt | 10 +- .../assetmoduleservice/AssetModuleService.kt | 75 ++- .../com/google/android/finsky/extensions.kt | 55 +- .../android/finsky/model/DeviceSyncInfo.kt | 611 ------------------ vending-app/src/main/proto/SyncRequest.proto | 188 ------ 6 files changed, 62 insertions(+), 887 deletions(-) delete mode 100644 vending-app/src/main/kotlin/com/google/android/finsky/model/DeviceSyncInfo.kt delete mode 100644 vending-app/src/main/proto/SyncRequest.proto diff --git a/vending-app/src/main/aidl/com/google/android/play/core/assetpacks/protocol/IAssetModuleServiceCallback.aidl b/vending-app/src/main/aidl/com/google/android/play/core/assetpacks/protocol/IAssetModuleServiceCallback.aidl index da775df785..f695429424 100644 --- a/vending-app/src/main/aidl/com/google/android/play/core/assetpacks/protocol/IAssetModuleServiceCallback.aidl +++ b/vending-app/src/main/aidl/com/google/android/play/core/assetpacks/protocol/IAssetModuleServiceCallback.aidl @@ -7,16 +7,16 @@ package com.google.android.play.core.assetpacks.protocol; interface IAssetModuleServiceCallback { oneway void onStartDownload(int status, in Bundle bundle) = 1; - oneway void onCancelDownload(int status) = 2; - oneway void onGetSession(int status) = 3; + oneway void onCancelDownload(int status, in Bundle bundle) = 2; + oneway void onGetSession(int status, in Bundle bundle) = 3; oneway void onGetSessionStates(in List list) = 4; oneway void onNotifyChunkTransferred(in Bundle bundle,in Bundle bundle2) = 5; oneway void onError(in Bundle bundle) = 6; oneway void onNotifyModuleCompleted(in Bundle bundle,in Bundle bundle2) = 7; - oneway void onNotifySessionFailed(in Bundle bundle) = 9; + oneway void onNotifySessionFailed(in Bundle bundle,in Bundle bundle2) = 9; oneway void onKeepAlive(in Bundle bundle, in Bundle bundle2) = 10; oneway void onGetChunkFileDescriptor(in Bundle bundle, in Bundle bundle2) = 11; oneway void onRequestDownloadInfo(in Bundle bundle, in Bundle bundle2) = 12; - oneway void onRemoveModule() = 13; - oneway void onCancelDownloads() = 14; + oneway void onRemoveModule(in Bundle bundle,in Bundle bundle2) = 13; + oneway void onCancelDownloads(in Bundle bundle) = 14; } \ No newline at end of file diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/DownloadManager.kt b/vending-app/src/main/kotlin/com/google/android/finsky/DownloadManager.kt index f81f23e3b1..5459bc9a7f 100644 --- a/vending-app/src/main/kotlin/com/google/android/finsky/DownloadManager.kt +++ b/vending-app/src/main/kotlin/com/google/android/finsky/DownloadManager.kt @@ -19,6 +19,7 @@ import android.graphics.Canvas import android.graphics.drawable.BitmapDrawable import android.net.Uri import android.os.Build +import android.os.Bundle import android.util.Log import android.widget.RemoteViews import androidx.core.app.NotificationCompat @@ -26,6 +27,7 @@ import androidx.core.app.NotificationManagerCompat import androidx.core.content.ContextCompat import com.android.vending.R import com.google.android.finsky.assetmoduleservice.DownloadData +import com.google.android.play.core.assetpacks.protocol.IAssetModuleServiceCallback import java.io.File import java.io.FileOutputStream import java.io.IOException @@ -150,7 +152,7 @@ class DownloadManager(private val context: Context) { } @Synchronized - fun prepareDownload(downloadData: DownloadData, moduleName: String) { + fun prepareDownload(downloadData: DownloadData, moduleName: String, callback: IAssetModuleServiceCallback?) { Log.d(TAG, "prepareDownload: ${downloadData.packageName}") initNotification(moduleName, downloadData.packageName) val future = executor.submit { @@ -167,7 +169,7 @@ class DownloadManager(private val context: Context) { } val filesDir = "${context.filesDir}/assetpacks/$index/$resourcePackageName/$chunkName/" val destination = File(filesDir, resourceBlockName) - startDownload(moduleName, resourceLink, destination, downloadData) + startDownload(moduleName, resourceLink, destination, downloadData,callback) sendBroadcastForExistingFile(context, downloadData, moduleName, dataBundle, destination) } updateProgress(moduleName, 100) @@ -186,7 +188,7 @@ class DownloadManager(private val context: Context) { NotificationManagerCompat.from(context).cancel(NOTIFICATION_ID) } - private fun startDownload(moduleName: String, downloadLink: String, destinationFile: File, downloadData: DownloadData) { + private fun startDownload(moduleName: String, downloadLink: String, destinationFile: File, downloadData: DownloadData, callback: IAssetModuleServiceCallback?) { val packData = downloadData.getModuleData(moduleName) val uri = Uri.parse(downloadLink).toString() val connection = URL(uri).openConnection() as HttpURLConnection @@ -228,6 +230,8 @@ class DownloadManager(private val context: Context) { } catch (e: Exception) { Log.e(TAG, "prepareDownload: startDownload error ", e) downloadData.updateDownloadStatus(moduleName, STATUS_FAILED) + cancelDownload(moduleName) + callback?.onError(Bundle().apply { putInt(KEY_ERROR_CODE, ACCESS_DENIED) }) } finally { connection.disconnect() } diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/AssetModuleService.kt b/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/AssetModuleService.kt index 52660154bc..64b396db3b 100644 --- a/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/AssetModuleService.kt +++ b/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/AssetModuleService.kt @@ -15,8 +15,8 @@ import android.util.Log import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleService +import com.google.android.finsky.API_NOT_AVAILABLE import com.google.android.finsky.DownloadManager -import com.google.android.finsky.ERROR_CODE_FAIL import com.google.android.finsky.KEY_BYTE_LENGTH import com.google.android.finsky.KEY_CHUNK_FILE_DESCRIPTOR import com.google.android.finsky.KEY_CHUNK_NAME @@ -28,10 +28,10 @@ import com.google.android.finsky.KEY_MODULE_NAME import com.google.android.finsky.KEY_PACK_NAMES import com.google.android.finsky.KEY_PLAY_CORE_VERSION_CODE import com.google.android.finsky.KEY_RESOURCE_BLOCK_NAME -import com.google.android.finsky.KEY_RESOURCE_LINK import com.google.android.finsky.KEY_RESOURCE_PACKAGE_NAME import com.google.android.finsky.KEY_SESSION_ID import com.google.android.finsky.KEY_SLICE_ID +import com.google.android.finsky.NO_ERROR import com.google.android.finsky.STATUS_COMPLETED import com.google.android.finsky.STATUS_DOWNLOADING import com.google.android.finsky.STATUS_INITIAL_STATE @@ -76,8 +76,7 @@ class AssetModuleServiceImpl( override fun startDownload(packageName: String?, list: MutableList?, bundle: Bundle?, callback: IAssetModuleServiceCallback?) { Log.d(TAG, "Method (startDownload) called by packageName -> $packageName") if (packageName == null || list == null || bundle == null) { - Log.d(TAG, "startDownload: params invalid ") - callback?.onError(Bundle().apply { putInt(KEY_ERROR_CODE, -5) }) + callback?.onError(Bundle().apply { putInt(KEY_ERROR_CODE, API_NOT_AVAILABLE) }) return } if (downloadData == null || downloadData?.packageName != packageName) { @@ -100,49 +99,49 @@ class AssetModuleServiceImpl( val packData = downloadData?.getModuleData(moduleName!!) if (packData?.status == STATUS_INITIAL_STATE){ DownloadManager.get(context).shouldStop(false) - DownloadManager.get(context).prepareDownload(downloadData!!, moduleName!!) + DownloadManager.get(context).prepareDownload(downloadData!!, moduleName!!,callback) } } } override fun getSessionStates(packageName: String?, bundle: Bundle?, callback: IAssetModuleServiceCallback?) { Log.d(TAG, "Method (getSessionStates) called by packageName -> $packageName") - val installedAssetModuleNames = mutableListOf() - bundle?.getParcelableArrayList(KEY_INSTALLED_ASSET_MODULE)?.forEach { item -> - item.keySet().forEach { subKey -> - (item.get(subKey) as? String)?.let { installedAssetModuleNames.add(it) } - } - } - if (packageName == downloadData?.packageName) { + val installedAssetModuleNames = bundle?.getParcelableArrayList(KEY_INSTALLED_ASSET_MODULE) + ?.flatMap { it.keySet().mapNotNull { subKey -> it.get(subKey) as? String } } + ?.toMutableList() ?: mutableListOf() + + val listBundleData: MutableList = mutableListOf() + + packageName.takeIf { it == downloadData?.packageName }?.let { downloadData?.moduleNames?.forEach { moduleName -> - if (installedAssetModuleNames.contains(moduleName)) { return@forEach } - val packData = downloadData?.getModuleData(moduleName) - for (dataBundle in packData!!.packBundleList) { - val resourcePackageName: String? = dataBundle.getString(KEY_RESOURCE_PACKAGE_NAME) - val chunkName: String? = dataBundle.getString(KEY_CHUNK_NAME) - val resourceLink: String? = dataBundle.getString(KEY_RESOURCE_LINK) - val index: Int = dataBundle.getInt(KEY_INDEX) - val resourceBlockName: String? = dataBundle.getString(KEY_RESOURCE_BLOCK_NAME) - if (resourcePackageName == null || chunkName == null || resourceLink == null || resourceBlockName == null) { - continue + if (moduleName in installedAssetModuleNames) return@forEach + + listBundleData.add(sendBroadcastForExistingFile(context, downloadData!!, moduleName, null, null)) + + downloadData?.getModuleData(moduleName)?.packBundleList?.forEach { dataBundle -> + val destination = dataBundle.run { + val resourcePackageName = getString(KEY_RESOURCE_PACKAGE_NAME) + val chunkName = getString(KEY_CHUNK_NAME) + val resourceBlockName = getString(KEY_RESOURCE_BLOCK_NAME) + if (resourcePackageName == null || chunkName == null || resourceBlockName == null) return@forEach + File("${context.filesDir}/assetpacks/${getInt(KEY_INDEX)}/$resourcePackageName/$chunkName/", resourceBlockName) } - val filesDir = "${context.filesDir}/assetpacks/$index/$resourcePackageName/$chunkName/" - val destination = File(filesDir, resourceBlockName) - val byteLength = dataBundle.getLong(KEY_BYTE_LENGTH) - if (destination.exists() && destination.length() == byteLength) { - sendBroadcastForExistingFile(context, downloadData!!, moduleName, dataBundle, destination) + + if (destination.exists() && destination.length() == dataBundle.getLong(KEY_BYTE_LENGTH)) { + listBundleData.add(sendBroadcastForExistingFile(context, downloadData!!, moduleName, dataBundle, destination)) } } } } + Log.d(TAG, "getSessionStates: $listBundleData") + callback?.onGetSessionStates(listBundleData) } override fun notifyChunkTransferred(packageName: String?, bundle: Bundle?, bundle2: Bundle?, callback: IAssetModuleServiceCallback?) { Log.d(TAG, "Method (notifyChunkTransferred) called by packageName -> $packageName") val moduleName = bundle?.getString(KEY_MODULE_NAME) if (moduleName.isNullOrEmpty()) { - Log.d(TAG, "notifyChunkTransferred: params invalid ") - callback?.onError(Bundle().apply { putInt(KEY_ERROR_CODE, -5) }) + callback?.onError(Bundle().apply { putInt(KEY_ERROR_CODE, API_NOT_AVAILABLE) }) return } val sessionId = bundle.getInt(KEY_SESSION_ID) @@ -151,15 +150,14 @@ class AssetModuleServiceImpl( val downLoadFile = "${context.filesDir.absolutePath}/assetpacks/$sessionId/$moduleName/$sliceId/$chunkNumber" fileDescriptorMap[downLoadFile]?.close() fileDescriptorMap.remove(downLoadFile) - callback?.onNotifyChunkTransferred(bundle, Bundle().apply { putInt(KEY_ERROR_CODE, 0) }) + callback?.onNotifyChunkTransferred(bundle, Bundle().apply { putInt(KEY_ERROR_CODE, NO_ERROR) }) } override fun notifyModuleCompleted(packageName: String?, bundle: Bundle?, bundle2: Bundle?, callback: IAssetModuleServiceCallback?) { Log.d(TAG, "Method (notifyModuleCompleted) called but not implemented by packageName -> $packageName") val moduleName = bundle?.getString(KEY_MODULE_NAME) if (moduleName.isNullOrEmpty()) { - Log.d(TAG, "notifyModuleCompleted: params invalid ") - callback?.onError(Bundle().apply { putInt(KEY_ERROR_CODE, -5) }) + callback?.onError(Bundle().apply { putInt(KEY_ERROR_CODE, API_NOT_AVAILABLE) }) return } Log.d(TAG, "notify: moduleName: $moduleName packNames: ${downloadData?.moduleNames}") @@ -170,7 +168,7 @@ class AssetModuleServiceImpl( override fun notifySessionFailed(packageName: String?, bundle: Bundle?, bundle2: Bundle?, callback: IAssetModuleServiceCallback?) { Log.d(TAG, "Method (notifySessionFailed) called but not implemented by packageName -> $packageName") - callback?.onNotifySessionFailed(bundle) + callback?.onNotifySessionFailed(Bundle(),Bundle()) } override fun keepAlive(packageName: String?, bundle: Bundle?, callback: IAssetModuleServiceCallback?) { @@ -181,8 +179,7 @@ class AssetModuleServiceImpl( Log.d(TAG, "Method (getChunkFileDescriptor) called by packageName -> $packageName") val moduleName = bundle.getString(KEY_MODULE_NAME) if (moduleName.isNullOrEmpty()) { - Log.d(TAG, "getChunkFileDescriptor: params invalid ") - callback?.onError(Bundle().apply { putInt(KEY_ERROR_CODE, -5) }) + callback?.onError(Bundle().apply { putInt(KEY_ERROR_CODE, API_NOT_AVAILABLE) }) return } val parcelFileDescriptor = runCatching { @@ -201,8 +198,7 @@ class AssetModuleServiceImpl( override fun requestDownloadInfo(packageName: String?, list: MutableList?, bundle: Bundle?, callback: IAssetModuleServiceCallback?) { Log.d(TAG, "Method (requestDownloadInfo) called by packageName -> $packageName") if (packageName == null || list == null || bundle == null) { - Log.w(TAG, "requestDownloadInfo: params invalid ") - callback?.onError(Bundle().apply { putInt(KEY_ERROR_CODE, -5) }) + callback?.onError(Bundle().apply { putInt(KEY_ERROR_CODE, API_NOT_AVAILABLE) }) return } if (downloadData == null || downloadData?.packageName != packageName) { @@ -210,9 +206,9 @@ class AssetModuleServiceImpl( val playCoreVersionCode = bundle.getInt(KEY_PLAY_CORE_VERSION_CODE) downloadData = httpClient.initAssertModuleData(context, packageName, accountManager, requestedAssetModuleNames, playCoreVersionCode) } - if (downloadData?.errorCode == ERROR_CODE_FAIL) { + if (downloadData?.errorCode == API_NOT_AVAILABLE) { if (moduleErrorRequested.contains(packageName)) { - callback?.onError(Bundle().apply { putInt(KEY_ERROR_CODE, -5) }) + callback?.onError(Bundle().apply { putInt(KEY_ERROR_CODE, API_NOT_AVAILABLE) }) return } moduleErrorRequested.add(packageName) @@ -232,6 +228,7 @@ class AssetModuleServiceImpl( override fun cancelDownloads(packageName: String?, list: MutableList?, bundle: Bundle?, callback: IAssetModuleServiceCallback?) { Log.d(TAG, "Method (cancelDownloads) called but not implemented by packageName -> $packageName") + callback?.onCancelDownloads(Bundle()) } companion object { diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/extensions.kt b/vending-app/src/main/kotlin/com/google/android/finsky/extensions.kt index 9315462deb..8710e1173c 100644 --- a/vending-app/src/main/kotlin/com/google/android/finsky/extensions.kt +++ b/vending-app/src/main/kotlin/com/google/android/finsky/extensions.kt @@ -20,7 +20,6 @@ import com.android.vending.licensing.getAuthToken import com.android.vending.licensing.getLicenseRequestHeaders import com.google.android.finsky.assetmoduleservice.DownloadData import com.google.android.finsky.assetmoduleservice.ModuleData -import com.google.android.finsky.model.DeviceSyncInfo import org.microg.gms.auth.AuthConstants import org.microg.vending.billing.GServices import org.microg.vending.billing.core.HttpClient @@ -35,8 +34,9 @@ const val STATUS_COMPLETED = 4 const val STATUS_DOWNLOADING = 2 const val STATUS_INITIAL_STATE = 1 -const val ERROR_CODE_SUCCESS = 0 -const val ERROR_CODE_FAIL = -5 +const val ACCESS_DENIED = -7 +const val API_NOT_AVAILABLE = -5 +const val NO_ERROR = 0 const val KEY_ERROR_CODE = "error_code" const val KEY_MODULE_NAME = "module_name" @@ -79,7 +79,6 @@ private const val EXTRA_SESSION_STATE = "com.google.android.play.core.assetpacks private const val FLAGS = "com.google.android.play.core.FLAGS" private const val ASSET_MODULE_DELIVERY_URL = "https://play-fe.googleapis.com/fdfe/assetModuleDelivery" -private const val SYNC_NOCACHE_QOS = "https://play-fe.googleapis.com/fdfe/sync?nocache_qos=lt" private const val TAG = "AssetModuleRequest" @@ -101,7 +100,7 @@ fun HttpClient.initAssertModuleData( val accounts = accountManager.getAccountsByType(AuthConstants.DEFAULT_ACCOUNT_TYPE) var oauthToken: String? = null if (accounts.isEmpty()) { - return DownloadData(errorCode = ERROR_CODE_FAIL) + return DownloadData(errorCode = API_NOT_AVAILABLE) } else { for (account: Account in accounts) { oauthToken = runBlocking { @@ -114,7 +113,7 @@ fun HttpClient.initAssertModuleData( } if (oauthToken == null) { - return DownloadData(errorCode = ERROR_CODE_FAIL) + return DownloadData(errorCode = API_NOT_AVAILABLE) } val requestPayload = AssetModuleDeliveryRequest.Builder() @@ -129,35 +128,7 @@ fun HttpClient.initAssertModuleData( val androidId = GServices.getString(context.contentResolver, "android_id", "0")?.toLong() ?: 1 - var moduleDeliveryInfo = runBlocking { - runCatching { - post( - url = ASSET_MODULE_DELIVERY_URL, - headers = getLicenseRequestHeaders(oauthToken, androidId), - payload = requestPayload, - adapter = AssetModuleDeliveryResponse.ADAPTER - ).wrapper?.deliveryInfo - }.getOrNull() - } - - if (moduleDeliveryInfo?.status != 2) { - return initModuleDownloadInfo(context, packageName, moduleDeliveryInfo) - } - - runBlocking { - runCatching { - post( - url = SYNC_NOCACHE_QOS, - headers = getLicenseRequestHeaders(oauthToken, androidId), - payload = DeviceSyncInfo.buildSyncRequest(context, androidId.toString(), accounts.first()), - adapter = SyncResponse.ADAPTER - ) - }.onFailure { - Log.d(TAG, "initAssertModuleData: ", it) - } - } - - moduleDeliveryInfo = runBlocking { + val moduleDeliveryInfo = runBlocking { runCatching { post( url = ASSET_MODULE_DELIVERY_URL, @@ -173,7 +144,7 @@ fun HttpClient.initAssertModuleData( fun initModuleDownloadInfo(context: Context, packageName: String, deliveryInfo: ModuleDeliveryInfo?): DownloadData { if (deliveryInfo == null || deliveryInfo.status != null) { - return DownloadData(errorCode = ERROR_CODE_FAIL) + return DownloadData(errorCode = API_NOT_AVAILABLE) } val packNames: ArraySet = arraySetOf() var moduleDownloadByteLength = 0L @@ -221,11 +192,11 @@ fun initModuleDownloadInfo(context: Context, packageName: String, deliveryInfo: } packDownloadByteLength += resDownloadByteLength } - val moduleData = ModuleData(appVersionCode = appVersionCode, moduleVersion = 0, sessionId = STATUS_NOT_INSTALLED, errorCode = ERROR_CODE_SUCCESS, status = STATUS_NOT_INSTALLED, bytesDownloaded = 0, totalBytesToDownload = packDownloadByteLength, packBundleList = dataBundle, listOfSubcontractNames = listOfSubcontractNames) + val moduleData = ModuleData(appVersionCode = appVersionCode, moduleVersion = 0, sessionId = STATUS_NOT_INSTALLED, errorCode = NO_ERROR, status = STATUS_NOT_INSTALLED, bytesDownloaded = 0, totalBytesToDownload = packDownloadByteLength, packBundleList = dataBundle, listOfSubcontractNames = listOfSubcontractNames) moduleDownloadByteLength += packDownloadByteLength moduleDataList[resourcePackageName] = moduleData } - return DownloadData(packageName = packageName, errorCode = ERROR_CODE_SUCCESS, sessionIds = sessionIds, bytesDownloaded = 0, status = STATUS_NOT_INSTALLED, moduleNames = packNames, appVersionCode = appVersionCode, totalBytesToDownload = moduleDownloadByteLength, moduleDataList) + return DownloadData(packageName = packageName, errorCode = NO_ERROR, sessionIds = sessionIds, bytesDownloaded = 0, status = STATUS_NOT_INSTALLED, moduleNames = packNames, appVersionCode = appVersionCode, totalBytesToDownload = moduleDownloadByteLength, moduleDataList) } fun buildDownloadBundle(downloadData: DownloadData, list: List? = null): Bundle { @@ -256,12 +227,12 @@ fun buildDownloadBundle(downloadData: DownloadData, list: List? = null) return bundleData } -fun sendBroadcastForExistingFile(context: Context, downloadData: DownloadData, moduleName: String, bundle: Bundle?, destination: File?) { +fun sendBroadcastForExistingFile(context: Context, downloadData: DownloadData, moduleName: String, bundle: Bundle?, destination: File?): Bundle { val packData = downloadData.getModuleData(moduleName) try { val downloadBundle = Bundle() downloadBundle.putInt(KEY_APP_VERSION_CODE, downloadData.appVersionCode.toInt()) - downloadBundle.putInt(KEY_ERROR_CODE, ERROR_CODE_SUCCESS) + downloadBundle.putInt(KEY_ERROR_CODE, NO_ERROR) downloadBundle.putInt(KEY_SESSION_ID, downloadData.sessionIds[moduleName] ?: downloadData.status) downloadBundle.putInt(KEY_STATUS, packData.status) @@ -271,7 +242,7 @@ fun sendBroadcastForExistingFile(context: Context, downloadData: DownloadData, m downloadBundle.putLong(combineModule(KEY_TOTAL_BYTES_TO_DOWNLOAD, moduleName), packData.totalBytesToDownload) downloadBundle.putLong(combineModule(KEY_PACK_VERSION, moduleName), packData.appVersionCode) downloadBundle.putInt(combineModule(KEY_STATUS, moduleName), packData.status) - downloadBundle.putInt(combineModule(KEY_ERROR_CODE, moduleName), ERROR_CODE_SUCCESS) + downloadBundle.putInt(combineModule(KEY_ERROR_CODE, moduleName), NO_ERROR) downloadBundle.putLong(combineModule(KEY_BYTES_DOWNLOADED, moduleName), packData.bytesDownloaded) downloadBundle.putLong(combineModule(KEY_PACK_BASE_VERSION, moduleName), packData.moduleVersion) val resultList = arraySetOf() @@ -314,8 +285,10 @@ fun sendBroadcastForExistingFile(context: Context, downloadData: DownloadData, m } downloadBundle.putStringArrayList(combineModule(KEY_SLICE_IDS, moduleName), packData.listOfSubcontractNames) sendBroadCast(context, downloadData, downloadBundle) + return downloadBundle } catch (e: Exception) { Log.w(TAG, "sendBroadcastForExistingFile error:" + e.message) + return Bundle(Bundle().apply { putInt(KEY_ERROR_CODE, API_NOT_AVAILABLE) }) } } diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/model/DeviceSyncInfo.kt b/vending-app/src/main/kotlin/com/google/android/finsky/model/DeviceSyncInfo.kt deleted file mode 100644 index 790b4fc8d4..0000000000 --- a/vending-app/src/main/kotlin/com/google/android/finsky/model/DeviceSyncInfo.kt +++ /dev/null @@ -1,611 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2024 microG Project Team - * SPDX-License-Identifier: Apache-2.0 - */ - -package com.google.android.finsky.model - -import android.accounts.Account -import android.accounts.AccountManager -import android.annotation.SuppressLint -import android.app.ActivityManager -import android.app.admin.DevicePolicyManager -import android.content.Context -import android.content.pm.PackageInfo -import android.content.pm.PackageManager -import android.content.res.Configuration -import android.graphics.Point -import android.opengl.GLES10 -import android.os.Build -import android.telephony.TelephonyManager -import android.text.TextUtils -import android.util.Base64 -import android.util.DisplayMetrics -import android.util.Log -import android.view.WindowManager -import com.google.android.finsky.AccountAssValue -import com.google.android.finsky.AccountAssociationPayload -import com.google.android.finsky.CarrierPropertiesPayload -import com.google.android.finsky.DeviceAccountsPayload -import com.google.android.finsky.DeviceCapabilitiesPayload -import com.google.android.finsky.DeviceInfoCollect -import com.google.android.finsky.DeviceInputPropertiesPayload -import com.google.android.finsky.DeviceModelPayload -import com.google.android.finsky.EnterprisePropertiesPayload -import com.google.android.finsky.FeatureInfoProto -import com.google.android.finsky.GpuInfoWrapper -import com.google.android.finsky.GpuPayload -import com.google.android.finsky.HardwareIdentifierPayload -import com.google.android.finsky.HardwarePropertiesPayload -import com.google.android.finsky.LocalePropertiesPayload -import com.google.android.finsky.MangedScope -import com.google.android.finsky.PlayPartnerPropertiesPayload -import com.google.android.finsky.PlayPropertiesPayload -import com.google.android.finsky.ProfileInfo -import com.google.android.finsky.ScreenPropertiesPayload -import com.google.android.finsky.SyncReqWrapper -import com.google.android.finsky.SyncRequest -import com.google.android.finsky.SystemPropertiesPayload -import com.google.android.finsky.TelephonyInfo -import com.google.android.finsky.TelephonyStateWrapper -import java.security.MessageDigest -import java.security.NoSuchAlgorithmException -import java.util.Arrays -import java.util.Objects -import java.util.Random -import java.util.TimeZone -import java.util.regex.Pattern -import java.util.stream.Collectors -import javax.microedition.khronos.egl.EGL10 -import javax.microedition.khronos.egl.EGLConfig -import javax.microedition.khronos.egl.EGLContext -import javax.microedition.khronos.egl.EGLDisplay -import javax.microedition.khronos.egl.EGLSurface -import kotlin.math.abs - -object DeviceSyncInfo { - - private const val TAG = "DeviceSyncInfo" - private val glInfoList = ArrayList() - - fun buildSyncRequest(context: Context, androidId: String, account: Account): SyncReqWrapper { - Log.d(TAG, "cachePayload: ") - val builder = SyncReqWrapper.Builder() - val payloads = buildPayloads(context, androidId, account) - val syncRequests = builder.request.toMutableList() - for (payload in payloads) { - payload?.run { syncRequests.add(this) } - } - builder.request = syncRequests - return builder.build() - } - - private fun buildPayloads(context: Context, androidId: String, account: Account): Array { - val fetchedGlStrings: ArrayList = fetchGLInfo() - //---------------------------------------GPU info-------------------------------------------------------------------- - val accountSha256 = accountSha256(androidId, account) - val accountAssValue = AccountAssValue.Builder().value_(accountSha256).build() - val accountAssociationPayload = AccountAssociationPayload.Builder().accountAss(accountAssValue).build() - val accountAssociationPayloadRequest = SyncRequest.Builder().accountAssociationPayload(accountAssociationPayload).build() - //-------------------------------------------------------------------------------------------------------------------- - val carrierPropertiesPayloadRequest = createCarrierPropertiesPayloadRequest(context) - val deviceAccountsPayloadRequest = createDeviceAccountsPayloadRequest(context, androidId) - val deviceInfoCollect = createDeviceInfoCollect(context, fetchedGlStrings.toList()) - val deviceCapabilitiesPayloadRequest = createDeviceCapabilitiesPayloadRequest(deviceInfoCollect) - val deviceInputPropertiesPayloadRequest = createDeviceInputPropertiesPayloadRequest(deviceInfoCollect) - val deviceModelPayloadRequest = createDeviceModelPayloadRequest() - val enterprisePropertiesPayloadRequest = createEnterprisePropertiesPayloadRequest(context) - val hardwareIdentifierPayloadRequest = createHardwareIdentifierPayloadRequest(context) - val hardwarePropertiesPayloadRequest = createHardwarePropertiesPayloadRequest(deviceInfoCollect) - val localePropertiesPayloadRequest = createLocalePropertiesPayloadRequest() - val playPartnerPropertiesPayloadRequest = createPlayPartnerPropertiesPayloadRequest() - val playPropertiesPayloadRequest = createPlayPropertiesPayload(context) - val screenPropertiesPayloadRequest = createScreenPropertiesPayloadRequest(deviceInfoCollect) - val systemPropertiesPayloadRequest = createSystemPropertiesPayloadRequest(deviceInfoCollect) - val gpuPayloadRequest = createGpuPayloadRequest(fetchedGlStrings.toList()) - return arrayOf(accountAssociationPayloadRequest, carrierPropertiesPayloadRequest, deviceAccountsPayloadRequest, - deviceCapabilitiesPayloadRequest, deviceInputPropertiesPayloadRequest, deviceModelPayloadRequest, - enterprisePropertiesPayloadRequest, hardwareIdentifierPayloadRequest, hardwarePropertiesPayloadRequest, - localePropertiesPayloadRequest, playPartnerPropertiesPayloadRequest, playPropertiesPayloadRequest, - screenPropertiesPayloadRequest, systemPropertiesPayloadRequest, gpuPayloadRequest) - } - - private fun createDeviceInfoCollect(context: Context, gpuInfoList: List): DeviceInfoCollect { - val builder = DeviceInfoCollect.Builder() - .reqTouchScreen(0) - .reqKeyboardType(0) - .reqNavigation(0) - .deviceStablePoint(0) - .reqInputFeaturesV1(false) - .reqInputFeaturesV2(false) - .deviceStable(0) - .reqGlEsVersion(0) - val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager - val configurationInfo = activityManager.deviceConfigurationInfo - if (configurationInfo != null) { - if (configurationInfo.reqTouchScreen != Configuration.TOUCHSCREEN_UNDEFINED) { - builder.reqTouchScreen(configurationInfo.reqTouchScreen) - } - if (configurationInfo.reqKeyboardType != Configuration.KEYBOARD_UNDEFINED) { - builder.reqKeyboardType(configurationInfo.reqKeyboardType) - } - if (configurationInfo.reqNavigation != Configuration.NAVIGATION_UNDEFINED) { - builder.reqNavigation(configurationInfo.reqNavigation) - } - builder.reqGlEsVersion(configurationInfo.reqGlEsVersion) - builder.reqInputFeaturesV1((configurationInfo.reqInputFeatures and 1) == 1) - .reqInputFeaturesV2((configurationInfo.reqInputFeatures and 2) > 0) - } - val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager - val size = Point() - windowManager.defaultDisplay.getSize(size) - builder.displayX(size.x).displayY(size.y) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - builder.deviceStable(DisplayMetrics.DENSITY_DEVICE_STABLE) - .deviceStablePoint(calculatePoint(size, DisplayMetrics.DENSITY_DEVICE_STABLE)) - } - val configuration = context.resources.configuration - builder.screenLayout(configuration.screenLayout) - .smallestScreenWidthDp(configuration.smallestScreenWidthDp) - .systemSharedLibraryNames(listOf(*Objects.requireNonNull(context.packageManager.systemSharedLibraryNames))) - .locales(listOf(*context.assets.locales)) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - builder.glExtensions(gpuInfoList.stream() - .flatMap { fetchedGlStrings: FetchedGlStrings -> fetchedGlStrings.glExtensions?.let { Arrays.stream(it.toTypedArray()) } } - .collect(Collectors.toList())) - .isLowRamDevice(activityManager.isLowRamDevice) - } - val memoryInfo = ActivityManager.MemoryInfo() - activityManager.getMemoryInfo(memoryInfo) - builder.totalMem(memoryInfo.totalMem) - .availableProcessors(Runtime.getRuntime().availableProcessors()) - val systemAvailableFeatures = context.packageManager.systemAvailableFeatures - for (featureInfo in systemAvailableFeatures) { - if (!TextUtils.isEmpty(featureInfo.name)) { - var featureInfoProto = FeatureInfoProto.Builder().build() - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - featureInfoProto = FeatureInfoProto.Builder().name(featureInfo.name).version(featureInfo.version).build() - } - builder.featureInfoList = builder.featureInfoList.toMutableList().apply { - add(featureInfoProto) - } - builder.featureNames = builder.featureNames.toMutableList().apply { - add(featureInfoProto.name!!) - } - } - } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - builder.supportedAbi(listOf(*Build.SUPPORTED_ABIS)) - } - var prop = getSystemProperty("ro.oem.key1", "") - if (!TextUtils.isEmpty(prop)) { - builder.oemKey(prop) - } - builder.buildCodeName(Build.VERSION.CODENAME) - prop = getSystemProperty("ro.build.version.preview_sdk_fingerprint", "") - if (!TextUtils.isEmpty(prop)) { - builder.previewSdkFingerprint(prop) - } - return builder.build() - } - - private fun createGpuPayloadRequest(glStringsList: List): SyncRequest? { - var gpuPayloadRequest: SyncRequest? = null - try { - var infos = glStringsList - var gpuPayloads = emptyList() - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - infos = infos.stream() - .filter { fetchedGlStrings: FetchedGlStrings -> - fetchedGlStrings.glRenderer!!.isNotEmpty() || fetchedGlStrings.glVendor!!.isNotEmpty() || fetchedGlStrings.glVersion!!.isNotEmpty() - }.collect(Collectors.toList()) - val maxVersion = infos.stream() - .max(Comparator.comparingInt { - fetchedGlStrings: FetchedGlStrings -> - fetchedGlStrings.contextClientVersion - }).map { obj: FetchedGlStrings -> - obj.contextClientVersion - } - if (maxVersion.isPresent) { - infos = infos.stream() - .filter { fetchedGlStrings: FetchedGlStrings -> - fetchedGlStrings.contextClientVersion == maxVersion.get() - }.collect(Collectors.toList()) - } - gpuPayloads = infos.stream().map { fetchedGlStrings: FetchedGlStrings -> - val gpuInfoWrapper = GpuInfoWrapper.Builder() - if (!TextUtils.isEmpty(fetchedGlStrings.glRenderer)) gpuInfoWrapper.glRenderer(fetchedGlStrings.glRenderer) - if (!TextUtils.isEmpty(fetchedGlStrings.glVendor)) gpuInfoWrapper.glVendor(fetchedGlStrings.glVendor) - if (!TextUtils.isEmpty(fetchedGlStrings.glVersion)) gpuInfoWrapper.glVersion(fetchedGlStrings.glVersion) - GpuPayload.Builder().gpuInfo(gpuInfoWrapper.build()).build() - }.distinct().collect(Collectors.toList()) - } - gpuPayloadRequest = SyncRequest.Builder().gpuPayload(if (gpuPayloads.isEmpty()) GpuPayload.Builder().build() else gpuPayloads[0]).build() - } catch (e: Exception) { - Log.w(TAG, "createGpuPayloadRequest error", e) - } - return gpuPayloadRequest - } - - private fun createHardwarePropertiesPayloadRequest(deviceInfoCollect: DeviceInfoCollect): SyncRequest { - val hardwarePropertiesPayload = HardwarePropertiesPayload.Builder() - .isLowRamDevice(deviceInfoCollect.isLowRamDevice) - .totalMem(deviceInfoCollect.totalMem) - .availableProcessors(deviceInfoCollect.availableProcessors) - .supportedAbi(deviceInfoCollect.supportedAbi) - .build() - return SyncRequest.Builder().hardwarePropertiesPayload(hardwarePropertiesPayload).build() - } - - private fun createLocalePropertiesPayloadRequest(): SyncRequest { - val timeZone = TimeZone.getDefault() - val gmtFormat = String.format( - "GMT%+d:%02d", - timeZone.rawOffset / (60 * 60 * 1000), - abs(timeZone.rawOffset / (60 * 1000) % 60) - ) - val localePropertiesPayload = LocalePropertiesPayload.Builder() - .locale(gmtFormat) - .build() - return SyncRequest.Builder().localePropertiesPayload(localePropertiesPayload).build() - } - - private fun createPlayPartnerPropertiesPayloadRequest(): SyncRequest { - val playPartnerPropertiesPayload = PlayPartnerPropertiesPayload.Builder() - .marketId("am-google") - .partnerIdMs("play-ms-android-google") - .partnerIdAd("play-ad-ms-android-google") - .build() - return SyncRequest.Builder().playPartnerPropertiesPayload(playPartnerPropertiesPayload).build() - } - - private fun createPlayPropertiesPayload(context: Context): SyncRequest { - var version = 0 - try { - version = context.packageManager.getPackageInfo("com.android.vending", 0).versionCode - } catch (exception: PackageManager.NameNotFoundException) { - Log.w(TAG, "[DAS] Could not find our package", exception) - } - val playPropertiesPayload = PlayPropertiesPayload.Builder().playVersion(version).build() - return SyncRequest.Builder().playPropertiesPayload(playPropertiesPayload).build() - } - - private fun createScreenPropertiesPayloadRequest(deviceInfoCollect: DeviceInfoCollect): SyncRequest { - val screenPropertiesPayload = ScreenPropertiesPayload.Builder() - .reqTouchScreen(deviceInfoCollect.reqTouchScreen) - .displayX(deviceInfoCollect.displayX) - .displayY(deviceInfoCollect.displayY) - .deviceStablePoint(deviceInfoCollect.deviceStablePoint) - .deviceStable(deviceInfoCollect.deviceStable) - .build() - return SyncRequest.Builder().screenPropertiesPayload(screenPropertiesPayload).build() - } - - private fun createSystemPropertiesPayloadRequest(deviceInfoCollect: DeviceInfoCollect): SyncRequest { - val systemPropertiesPayload = SystemPropertiesPayload.Builder() - .fingerprint(Build.FINGERPRINT) - .sdkInt(Build.VERSION.SDK_INT.toLong()) - .previewSdkFingerprint(deviceInfoCollect.previewSdkFingerprint) - .buildCodeName(deviceInfoCollect.buildCodeName) - .oemKey(deviceInfoCollect.oemKey) - .reqGlEsVersion(deviceInfoCollect.reqGlEsVersion) - .build() - return SyncRequest.Builder().systemPropertiesPayload(systemPropertiesPayload).build() - } - - private fun createHardwareIdentifierPayloadRequest(context: Context): SyncRequest? { - var hardwareIdentifierPayloadRequest: SyncRequest? = null - try { - val builder = HardwareIdentifierPayload.Builder() - val telephonyManager = context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager - val randomIMEI = generateRandomIMEI() - var imeid: Long = if (TextUtils.isEmpty(randomIMEI) || !Pattern.compile("^[0-9]{15}$").matcher(randomIMEI).matches()) 0L else randomIMEI.toLong(10) or 0x1000000000000000L - if (imeid == 0L) { - var meid = "" - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - meid = telephonyManager.meid - } - if (!TextUtils.isEmpty(meid) && Pattern.compile("^[0-9a-fA-F]{14}$").matcher(meid).matches()) { - imeid = meid.toLong(16) or 0x1100000000000000L - if (imeid == 0L) { - if (context.packageManager.checkPermission("android.permission.READ_PRIVILEGED_PHONE_STATE", "com.android.vending") == PackageManager.PERMISSION_GRANTED) { - var serial = "" - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - serial = Build.getSerial() - } - if (TextUtils.isEmpty(serial) && serial != "unknown") { - try { - val serialShaByte = MessageDigest.getInstance("SHA1").digest(serial.toByteArray()) - imeid = ((serialShaByte[0].toLong()) and 0xFFL) shl 0x30 or 0x1400000000000000L or (((serialShaByte[1].toLong()) and 0xFFL) shl 40) or (((serialShaByte[2].toLong()) and 0xFFL) shl 0x20) or (((serialShaByte[3].toLong()) and 0xFFL) shl 24) or (((serialShaByte[4].toLong()) and 0xFFL) shl 16) or (((serialShaByte[5].toLong()) and 0xFFL) shl 8) or ((serialShaByte[6].toLong()) and 0xFFL) - } catch (noSuchAlgorithmException0: NoSuchAlgorithmException) { - Log.w(TAG, "No support for sha1?") - } - } - } - } - } - } - builder.imeId(imeid) - hardwareIdentifierPayloadRequest = SyncRequest.Builder().hardwareIdentifierPayload(builder.build()).build() - }catch (e:Exception){ - Log.w(TAG, "createHardwareIdentifierPayloadRequest error", e) - } - return hardwareIdentifierPayloadRequest - } - - private fun createEnterprisePropertiesPayloadRequest(context: Context): SyncRequest? { - var enterprisePropertiesPayloadRequest: SyncRequest? = null - try { - val enterprisePropertiesPayload = EnterprisePropertiesPayload.Builder() - val devicePolicyManager = context.getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager - val activeAdmins = devicePolicyManager.activeAdmins - if (activeAdmins != null) { - for (componentName in activeAdmins) { - val packageName = componentName.packageName - val packageInfo: PackageInfo? = context.packageManager.getPackageInfo(packageName, PackageManager.GET_SIGNATURES) - val isDeviceOwner = devicePolicyManager.isDeviceOwnerApp(packageName) - var isProfileOwner = false - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - isProfileOwner = devicePolicyManager.isProfileOwnerApp(packageName) - } - val policyType = if (isDeviceOwner) MangedScope.MANAGED_DEVICES else if (isProfileOwner) MangedScope.MANAGED_PROFILES else MangedScope.LEGACY_DEVICE_ADMINS - val profileInfo = ProfileInfo.Builder() - .pkgName(componentName.packageName) - .policyType(policyType) - .pkgSHA1(calculateSHA(packageInfo!!.signatures[0].toByteArray(), "SHA1")) - .pkgSHA256(calculateSHA(packageInfo.signatures[0].toByteArray(), "SHA256")).build() - if (isProfileOwner) { - enterprisePropertiesPayload.profileOwner(profileInfo) - } - enterprisePropertiesPayload.default = enterprisePropertiesPayload.default.toMutableList() - .apply { add(profileInfo) } - } - } - enterprisePropertiesPayloadRequest = SyncRequest.Builder().enterprisePropertiesPayload(enterprisePropertiesPayload.build()).build() - }catch (e:Exception){ - Log.w(TAG, "createEnterprisePropertiesPayloadRequest error", e) - } - return enterprisePropertiesPayloadRequest - } - - private fun createDeviceInputPropertiesPayloadRequest(deviceInfoCollect: DeviceInfoCollect): SyncRequest { - val builder = DeviceInputPropertiesPayload.Builder() - .reqInputFeatures(deviceInfoCollect.reqInputFeaturesV1) - .reqKeyboardType(deviceInfoCollect.reqKeyboardType) - .reqNavigation(deviceInfoCollect.reqNavigation) - return SyncRequest.Builder().deviceInputPropertiesPayload(builder.build()).build() - } - - private fun createDeviceModelPayloadRequest(): SyncRequest { - val builder = DeviceModelPayload.Builder() - .manufacturer(Build.MANUFACTURER) - .model(Build.MODEL) - .device(Build.DEVICE) - .product(Build.PRODUCT) - .brand(Build.BRAND) - return SyncRequest.Builder().deviceModelPayload(builder.build()).build() - } - - private fun createDeviceCapabilitiesPayloadRequest(deviceInfoCollect: DeviceInfoCollect): SyncRequest { - val builder = DeviceCapabilitiesPayload.Builder() - builder.glExtensions(deviceInfoCollect.glExtensions) - val featureInfoList = builder.featureInfo.toMutableList() - for (featureInfoProto in deviceInfoCollect.featureInfoList) { - featureInfoList.add(FeatureInfoProto.Builder() - .name(featureInfoProto.name) - .version(featureInfoProto.version) - .build()) - } - builder.featureInfo = featureInfoList - builder.systemSharedLibraryNames(deviceInfoCollect.systemSharedLibraryNames) - .locales(deviceInfoCollect.locales) - .unknownFlag(false) - return SyncRequest.Builder().deviceCapabilitiesPayload(builder.build()).build() - } - - private fun createDeviceAccountsPayloadRequest(context: Context, androidId: String): SyncRequest? { - var deviceAccountsPayloadRequest: SyncRequest? = null - try { - val accountManager = context.getSystemService(Context.ACCOUNT_SERVICE) as AccountManager - val accounts = accountManager.accounts - val builder = DeviceAccountsPayload.Builder() - val accountAssValues = builder.accountAss.toMutableList() - for (account in accounts) { - accountAssValues.add(AccountAssValue.Builder().value_(accountSha256(androidId, account)).build()) - } - builder.accountAss = accountAssValues - deviceAccountsPayloadRequest = SyncRequest.Builder().deviceAccountsPayload(builder.build()).build() - } catch (e: Exception) { - Log.w(TAG, "createDeviceAccountsPayloadRequest error", e) - } - return deviceAccountsPayloadRequest - } - - @SuppressLint("HardwareIds") - private fun createCarrierPropertiesPayloadRequest(context: Context): SyncRequest? { - var carrierPropertiesPayloadRequest: SyncRequest? = null - try { - val telephonyManager = context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager - var simCardId = 0 - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - simCardId = telephonyManager.simCarrierId - } - var carrierIdFromSimMccMnc = 0 - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - carrierIdFromSimMccMnc = telephonyManager.carrierIdFromSimMccMnc - } - val telephonyInfo = TelephonyInfo.Builder().subscriberId(((telephonyManager.subscriberId.toLong() / 100000L).toString() + "00000").toLong()) - .operatorName(telephonyManager.simOperatorName).groupIdLevel(telephonyManager.groupIdLevel1).simCardId(simCardId) - .carrierIdFromSimMccMnc(carrierIdFromSimMccMnc).build() - val telephonyStateWrapper = TelephonyStateWrapper.Builder().telephonyInfo(telephonyInfo).build() - val carrierPropertiesPayload = - CarrierPropertiesPayload.Builder().telephonyStateValue(telephonyStateWrapper).simOperator(telephonyManager.simOperator).build() - carrierPropertiesPayloadRequest = SyncRequest.Builder().carrierPropertiesPayload(carrierPropertiesPayload).build() - } catch (securityException: SecurityException) { - Log.w(TAG, "SecurityException when reading IMSI.", securityException) - } catch (stateException: IllegalStateException) { - Log.w(TAG, "IllegalStateException when reading IMSI. This is a known SDK 31 Samsung bug.", stateException) - } - return carrierPropertiesPayloadRequest - } - - private fun accountSha256(androidId: String, account: Account): String? { - return try { - val androidIdAcc = (androidId + "-" + account.name).toByteArray() - val messageDigest0 = MessageDigest.getInstance("SHA256") - messageDigest0.update(androidIdAcc, 0, androidIdAcc.size) - Base64.encodeToString(messageDigest0.digest(), 11) - } catch (ignored: Exception) { - null - } - } - - private fun generateRandomIMEI(): String { - val random = Random() - val imeiBuilder = StringBuilder() - for (i in 0..13) { - val digit = random.nextInt(10) - imeiBuilder.append(digit) - } - val imei = imeiBuilder.toString() - val checkDigit = calculateCheckDigit(imei) - imeiBuilder.append(checkDigit) - return imeiBuilder.toString() - } - - private fun calculateCheckDigit(imei: String): Int { - var sum = 0 - for (i in imei.indices) { - var digit = Character.getNumericValue(imei[i]) - if (i % 2 == 1) { - digit *= 2 - } - if (digit > 9) { - digit -= 9 - } - sum += digit - } - return (10 - (sum % 10)) % 10 - } - - private fun calculateSHA(data: ByteArray, algorithm: String?): String? { - val messageDigest0: MessageDigest - try { - messageDigest0 = algorithm?.let { MessageDigest.getInstance(it) }!! - } catch (noSuchAlgorithmException0: NoSuchAlgorithmException) { - Log.w(TAG, "[DC] No support for %s?", noSuchAlgorithmException0) - return null - } - messageDigest0.update(data, 0, data.size) - return Base64.encodeToString(messageDigest0.digest(), 11) - } - - private fun fetchGLInfo(): ArrayList { - if(glInfoList.isNotEmpty()) return glInfoList - try { - val eGL100 = EGLContext.getEGL() as? EGL10 - val result = ArrayList() - val egl10Instance = eGL100?.let { EGL10Wrapper(it) } - val eglDisplay = eGL100!!.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY) - eGL100.eglInitialize(eglDisplay, IntArray(2)) - val ints = IntArray(1) - val configCount = if (eGL100.eglGetConfigs(eglDisplay, null, 0, ints)) ints[0] else 0 - val arrEglConfig = arrayOfNulls(configCount) - val eglConfigs = if (eGL100.eglGetConfigs(eglDisplay, arrEglConfig, configCount, IntArray(1))) arrEglConfig else null - val arrV1 = intArrayOf(0x3057, 1, 0x3056, 1, 0x3038) - for (v1 in 0 until configCount) { - if (egl10Instance?.eglGetConfigAttrib(eglDisplay, eglConfigs?.get(v1), 0x3027) != 0x3050 - && (egl10Instance?.eglGetConfigAttrib(eglDisplay, eglConfigs?.get(v1), 0x3033)?.and(1)) != 0) { - val v2 = egl10Instance?.eglGetConfigAttrib(eglDisplay, eglConfigs?.get(v1), 0x3040) - if ((v2?.and(1)) != 0) { - egl10Instance?.let { wrapper -> buildGLStrings(wrapper, eglDisplay, eglConfigs?.get(v1), arrV1, null)?.let { result.add(it) } } - } - if ((v2?.and(4)) != 0) { - egl10Instance?.let { wrapper -> buildGLStrings(wrapper, eglDisplay, eglConfigs?.get(v1), arrV1, intArrayOf(0x3098, 2, 0x3038))?.let { result.add(it) } } - } - } - } - egl10Instance?.instance?.eglTerminate(eglDisplay) - return result.also { glInfoList.addAll(it) } - }catch (e:Exception){ - Log.d(TAG, "fetchGLInfo: error", e) - } - return ArrayList() - } - - private fun buildGLStrings(egl10Tools: EGL10Wrapper, eglDisplay: EGLDisplay, eglConfig: EGLConfig?, arrV: IntArray, arrV1: IntArray?): FetchedGlStrings? { - val eglContext = egl10Tools.instance.eglCreateContext(eglDisplay, eglConfig, EGL10.EGL_NO_CONTEXT, arrV1) - if (eglContext != EGL10.EGL_NO_CONTEXT) { - val eglSurface = egl10Tools.instance.eglCreatePbufferSurface(eglDisplay, eglConfig, arrV) - if (eglSurface == EGL10.EGL_NO_SURFACE) { - egl10Tools.eglDestroyContext(eglDisplay, eglContext) - return null - } - egl10Tools.eglMakeCurrent(eglDisplay, eglSurface, eglSurface, eglContext) - val result = FetchedGlStrings(0, null, null, null, null) - val glExtensions = GLES10.glGetString(GLES10.GL_EXTENSIONS) - if (!TextUtils.isEmpty(glExtensions)) { - result.glExtensions = glExtensions.split(" ".toRegex()).dropLastWhile { it.isEmpty() } - } - result.glRenderer = GLES10.glGetString(GLES10.GL_RENDERER) - result.glVendor = GLES10.glGetString(GLES10.GL_VENDOR) - result.glVersion = GLES10.glGetString(GLES10.GL_VERSION) - if (result.glExtensions != null) { - egl10Tools.eglMakeCurrent(eglDisplay, EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_CONTEXT) - egl10Tools.instance.eglDestroySurface(eglDisplay, eglSurface) - egl10Tools.eglDestroyContext(eglDisplay, eglContext) - return result - } - throw IllegalStateException("Missing required properties ") - } - return null - } - - private fun getSystemProperty(key: String?, defaultValue: String?): String? { - var value = defaultValue - try { - @SuppressLint("PrivateApi") val systemPropertiesClass = Class.forName("android.os.SystemProperties") - val getMethod = systemPropertiesClass.getMethod("get", String::class.java, String::class.java) - value = getMethod.invoke(null, key, defaultValue) as String - } catch (e: Exception) { - Log.w(TAG, "Unable to retrieve system property", e) - } - return value - } - - private fun calculatePoint(point: Point, v: Int): Int { - val f = point.x.toFloat() - val v1 = ((point.y.toFloat()) * (160.0f / (v.toFloat()))).toInt() - if (v1 < 470) { - return 17 - } - val v2 = (f * (160.0f / (v.toFloat()))).toInt() - if (v1 >= 960 && v2 >= 720) { - return if (v1 * 3 / 5 < v2 - 1) 20 else 4 - } - val v3 = if (v1 < 640 || v2 < 480) 2 else 3 - return if (v1 * 3 / 5 < v2 - 1) v3 or 16 else v3 - } - - internal class EGL10Wrapper(val instance: EGL10) { - fun eglGetConfigAttrib(eglDisplay: EGLDisplay?, eglConfig: EGLConfig?, v: Int): Int { - val value = IntArray(1) - instance.eglGetConfigAttrib(eglDisplay, eglConfig, v, value) - return value[0] - } - - fun eglDestroyContext(eglDisplay: EGLDisplay?, eglContext: EGLContext?) { - instance.eglDestroyContext(eglDisplay, eglContext) - } - - fun eglMakeCurrent(eglDisplay: EGLDisplay?, draw: EGLSurface?, read: EGLSurface?, eglContext: EGLContext?) { - instance.eglMakeCurrent(eglDisplay, draw, read, eglContext) - } - } - - internal class FetchedGlStrings(var contextClientVersion: Int, - var glExtensions: List?, - var glRenderer: String?, - var glVendor: String?, - var glVersion: String?) -} \ No newline at end of file diff --git a/vending-app/src/main/proto/SyncRequest.proto b/vending-app/src/main/proto/SyncRequest.proto deleted file mode 100644 index 546b390380..0000000000 --- a/vending-app/src/main/proto/SyncRequest.proto +++ /dev/null @@ -1,188 +0,0 @@ -option java_package = "com.google.android.finsky"; -option java_multiple_files = true; - -message SyncReqWrapper { - repeated SyncRequest request = 1; -} - -message SyncRequest { - oneof payload { - AccountAssociationPayload accountAssociationPayload = 7; - DeviceAccountsPayload deviceAccountsPayload = 8; - CarrierPropertiesPayload carrierPropertiesPayload = 9; - DeviceCapabilitiesPayload deviceCapabilitiesPayload = 10; - DeviceInputPropertiesPayload deviceInputPropertiesPayload = 11; - DeviceModelPayload deviceModelPayload = 12; - EnterprisePropertiesPayload enterprisePropertiesPayload = 13; - HardwareIdentifierPayload hardwareIdentifierPayload = 14; - HardwarePropertiesPayload hardwarePropertiesPayload = 15; - LocalePropertiesPayload localePropertiesPayload = 16; - NotificationRoutingInfoPayload notificationRoutingInfoPayload = 17; - PlayPartnerPropertiesPayload playPartnerPropertiesPayload = 18; - PlayPropertiesPayload playPropertiesPayload = 19; - ScreenPropertiesPayload screenPropertiesPayload = 20; - SystemPropertiesPayload systemPropertiesPayload = 21; - GpuPayload gpuPayload = 24; - } -} - -message AccountAssociationPayload { - optional AccountAssValue accountAss = 1; -} - -message AccountAssValue { - optional string value = 1; -} - -message DeviceAccountsPayload { - repeated AccountAssValue accountAss = 1; -} - -message CarrierPropertiesPayload { - optional string simOperator = 1; - optional TelephonyStateWrapper telephonyStateValue = 2; -} - -message TelephonyStateWrapper { - optional TelephonyInfo telephonyInfo = 1; -} - -message TelephonyInfo { - optional int64 subscriberId = 1; - optional string operatorName = 2; - optional string groupIdLevel = 3; - optional int32 simCardId = 6; - optional int32 carrierIdFromSimMccMnc = 7; -} - -message DeviceCapabilitiesPayload { - repeated FeatureInfoProto featureInfo = 1; - repeated string systemSharedLibraryNames = 2; - repeated string locales = 3; - repeated string glExtensions = 4; - optional bool unknownFlag = 5; -} - -message DeviceInputPropertiesPayload { - optional int32 reqKeyboardType = 1; - optional bool reqInputFeatures = 2; - optional int32 reqNavigation = 3; -} - -message DeviceModelPayload { - optional string manufacturer = 1; - optional string model = 2; - optional string device = 3; - optional string product = 4; - optional string brand = 5; -} - -message EnterprisePropertiesPayload { - optional ProfileInfo profileOwner = 1; - repeated ProfileInfo default = 2; -} - -message ProfileInfo { - optional string pkgName = 1; - optional string pkgSHA1 = 2; - optional string pkgSHA256 = 3; - optional MangedScope policyType = 4; -} - -enum MangedScope { - UNKNOWN_MANAGED_SCOPE = 0; - MANAGED_DEVICES = 1; - MANAGED_PROFILES = 2; - MANAGED_AVENGER = 3; - LEGACY_DEVICE_ADMINS = 4; -} - -message HardwareIdentifierPayload { - optional fixed64 imeId = 1; -} - -message HardwarePropertiesPayload { - optional bool isLowRamDevice = 1; - optional int64 totalMem = 2; - optional int32 availableProcessors = 3; - repeated string supportedAbi = 4; -} - -message LocalePropertiesPayload { - optional string locale = 1; -} - -message NotificationRoutingInfoPayload { - optional string info = 1; -} - -message PlayPartnerPropertiesPayload { - optional string marketId = 1; - optional string partnerIdMs = 2; - optional string partnerIdAd = 3; -} - -message PlayPropertiesPayload { - optional int32 playVersion = 2; -} - -message ScreenPropertiesPayload { - optional int32 reqTouchScreen = 1; - optional int32 displayX = 2; - optional int32 displayY = 3; - optional int32 deviceStablePoint = 4; - optional int32 deviceStable = 5; -} - -message SystemPropertiesPayload { - optional string fingerprint = 1; - optional int64 sdkInt = 2; - optional string previewSdkFingerprint = 3; - optional string buildCodeName = 4; - optional string oemKey = 5; - optional int32 reqGlEsVersion = 6; -} - -message GpuPayload { - optional GpuInfoWrapper gpuInfo = 1; -} - -message GpuInfoWrapper { - optional string glRenderer = 1; - optional string glVendor = 2; - optional string glVersion = 3; -} - -message DeviceInfoCollect { - optional int32 reqTouchScreen = 1; - optional int32 reqKeyboardType = 2; - optional int32 reqNavigation = 3; - optional int32 deviceStablePoint = 4; - optional bool reqInputFeaturesV1 = 5; - optional bool reqInputFeaturesV2 = 6; - optional int32 deviceStable = 7; - optional int32 reqGlEsVersion = 8; - repeated string systemSharedLibraryNames = 9; - repeated string featureNames = 10; - repeated string supportedAbi = 11; - optional int32 displayX = 12; - optional int32 displayY = 13; - repeated string locales = 14; - repeated string glExtensions = 15; - optional int32 smallestScreenWidthDp = 18; - optional bool isLowRamDevice = 19; - optional int64 totalMem = 20; - optional int32 availableProcessors = 21; - repeated FeatureInfoProto featureInfoList = 26; - optional int32 screenLayout = 27; - optional string oemKey = 29; - optional string buildCodeName = 30; - optional string previewSdkFingerprint = 31; -} - -message FeatureInfoProto { - optional string name = 1; - optional int32 version = 2; -} - -message SyncResponse {} From 500d052e6ef2276a53500727a8e3d5b19508946c Mon Sep 17 00:00:00 2001 From: davinci9196 Date: Tue, 12 Nov 2024 21:04:56 +0800 Subject: [PATCH 081/132] Asset Modules: cleaned permission --- vending-app/src/main/AndroidManifest.xml | 3 --- 1 file changed, 3 deletions(-) diff --git a/vending-app/src/main/AndroidManifest.xml b/vending-app/src/main/AndroidManifest.xml index 754c7ddc1b..eb12ae7378 100644 --- a/vending-app/src/main/AndroidManifest.xml +++ b/vending-app/src/main/AndroidManifest.xml @@ -19,9 +19,6 @@ - - Date: Wed, 13 Nov 2024 14:43:12 +0800 Subject: [PATCH 082/132] Asset Modules: Added cancelDownloads method and cleaned up after resource installation --- .../assetmoduleservice/AssetModuleService.kt | 34 +++++++++++++------ .../com/google/android/finsky/extensions.kt | 10 +++--- 2 files changed, 28 insertions(+), 16 deletions(-) diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/AssetModuleService.kt b/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/AssetModuleService.kt index 64b396db3b..ddc072d444 100644 --- a/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/AssetModuleService.kt +++ b/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/AssetModuleService.kt @@ -16,6 +16,7 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleService import com.google.android.finsky.API_NOT_AVAILABLE +import com.google.android.finsky.CANCELED import com.google.android.finsky.DownloadManager import com.google.android.finsky.KEY_BYTE_LENGTH import com.google.android.finsky.KEY_CHUNK_FILE_DESCRIPTOR @@ -25,7 +26,6 @@ import com.google.android.finsky.KEY_ERROR_CODE import com.google.android.finsky.KEY_INDEX import com.google.android.finsky.KEY_INSTALLED_ASSET_MODULE import com.google.android.finsky.KEY_MODULE_NAME -import com.google.android.finsky.KEY_PACK_NAMES import com.google.android.finsky.KEY_PLAY_CORE_VERSION_CODE import com.google.android.finsky.KEY_RESOURCE_BLOCK_NAME import com.google.android.finsky.KEY_RESOURCE_PACKAGE_NAME @@ -83,6 +83,10 @@ class AssetModuleServiceImpl( val requestedAssetModuleNames = list.map { it.getString(KEY_MODULE_NAME) }.filter { !it.isNullOrEmpty() } val playCoreVersionCode = bundle.getInt(KEY_PLAY_CORE_VERSION_CODE) downloadData = httpClient.initAssertModuleData(context, packageName, accountManager, requestedAssetModuleNames, playCoreVersionCode) + if (downloadData == null) { + callback?.onError(Bundle().apply { putInt(KEY_ERROR_CODE, API_NOT_AVAILABLE) }) + return + } } list.forEach { val moduleName = it.getString(KEY_MODULE_NAME) @@ -163,7 +167,18 @@ class AssetModuleServiceImpl( Log.d(TAG, "notify: moduleName: $moduleName packNames: ${downloadData?.moduleNames}") downloadData?.updateDownloadStatus(moduleName, STATUS_COMPLETED) sendBroadcastForExistingFile(context, downloadData!!, moduleName, null, null) - callback?.onNotifyModuleCompleted(bundle, bundle) + + val sessionId = downloadData!!.sessionIds[moduleName] + val downLoadFile = "${context.filesDir.absolutePath}/assetpacks/$sessionId/$moduleName" + + val directory = File(downLoadFile) + if (directory.exists()) { + directory.deleteRecursively() + Log.d(TAG, "Directory $downLoadFile deleted successfully.") + } else { + Log.d(TAG, "Directory $downLoadFile does not exist.") + } + callback?.onNotifyModuleCompleted(bundle, bundle2) } override fun notifySessionFailed(packageName: String?, bundle: Bundle?, bundle2: Bundle?, callback: IAssetModuleServiceCallback?) { @@ -205,18 +220,11 @@ class AssetModuleServiceImpl( val requestedAssetModuleNames = list.map { it.getString(KEY_MODULE_NAME) }.filter { !it.isNullOrEmpty() } val playCoreVersionCode = bundle.getInt(KEY_PLAY_CORE_VERSION_CODE) downloadData = httpClient.initAssertModuleData(context, packageName, accountManager, requestedAssetModuleNames, playCoreVersionCode) - } - if (downloadData?.errorCode == API_NOT_AVAILABLE) { - if (moduleErrorRequested.contains(packageName)) { + if (downloadData == null) { callback?.onError(Bundle().apply { putInt(KEY_ERROR_CODE, API_NOT_AVAILABLE) }) return } - moduleErrorRequested.add(packageName) - val result = Bundle().apply { putStringArrayList(KEY_PACK_NAMES, arrayListOf()) } - callback?.onRequestDownloadInfo(result, result) - return } - moduleErrorRequested.remove(packageName) val bundleData = buildDownloadBundle(downloadData!!,list) Log.d(TAG, "requestDownloadInfo: $bundleData") callback?.onRequestDownloadInfo(bundleData, bundleData) @@ -228,12 +236,16 @@ class AssetModuleServiceImpl( override fun cancelDownloads(packageName: String?, list: MutableList?, bundle: Bundle?, callback: IAssetModuleServiceCallback?) { Log.d(TAG, "Method (cancelDownloads) called but not implemented by packageName -> $packageName") + list?.forEach { + val moduleName = it.getString(KEY_MODULE_NAME) + downloadData?.updateDownloadStatus(moduleName!!, CANCELED) + sendBroadcastForExistingFile(context, downloadData!!, moduleName!!, null, null) + } callback?.onCancelDownloads(Bundle()) } companion object { @Volatile private var downloadData: DownloadData? = null - private val moduleErrorRequested = arrayListOf() } } \ No newline at end of file diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/extensions.kt b/vending-app/src/main/kotlin/com/google/android/finsky/extensions.kt index 8710e1173c..e9ec4c95d9 100644 --- a/vending-app/src/main/kotlin/com/google/android/finsky/extensions.kt +++ b/vending-app/src/main/kotlin/com/google/android/finsky/extensions.kt @@ -96,11 +96,11 @@ fun HttpClient.initAssertModuleData( accountManager: AccountManager, requestedAssetModuleNames: List, playCoreVersionCode: Int, -): DownloadData { +): DownloadData? { val accounts = accountManager.getAccountsByType(AuthConstants.DEFAULT_ACCOUNT_TYPE) var oauthToken: String? = null if (accounts.isEmpty()) { - return DownloadData(errorCode = API_NOT_AVAILABLE) + return null } else { for (account: Account in accounts) { oauthToken = runBlocking { @@ -113,7 +113,7 @@ fun HttpClient.initAssertModuleData( } if (oauthToken == null) { - return DownloadData(errorCode = API_NOT_AVAILABLE) + return null } val requestPayload = AssetModuleDeliveryRequest.Builder() @@ -142,9 +142,9 @@ fun HttpClient.initAssertModuleData( return initModuleDownloadInfo(context, packageName, moduleDeliveryInfo) } -fun initModuleDownloadInfo(context: Context, packageName: String, deliveryInfo: ModuleDeliveryInfo?): DownloadData { +private fun initModuleDownloadInfo(context: Context, packageName: String, deliveryInfo: ModuleDeliveryInfo?): DownloadData? { if (deliveryInfo == null || deliveryInfo.status != null) { - return DownloadData(errorCode = API_NOT_AVAILABLE) + return null } val packNames: ArraySet = arraySetOf() var moduleDownloadByteLength = 0L From e68f432ec457b3bdbcad9469c95105d5107f2337 Mon Sep 17 00:00:00 2001 From: davinci9196 Date: Wed, 13 Nov 2024 14:50:30 +0800 Subject: [PATCH 083/132] Asset Modules: Formatting Code Formatting Code --- .../protocol/IAssetModuleServiceCallback.aidl | 8 ++-- .../google/android/finsky/DownloadManager.kt | 13 +++--- .../assetmoduleservice/AssetModuleService.kt | 39 ++++++++++------- .../com/google/android/finsky/extensions.kt | 43 +++++++++++++------ 4 files changed, 62 insertions(+), 41 deletions(-) diff --git a/vending-app/src/main/aidl/com/google/android/play/core/assetpacks/protocol/IAssetModuleServiceCallback.aidl b/vending-app/src/main/aidl/com/google/android/play/core/assetpacks/protocol/IAssetModuleServiceCallback.aidl index f695429424..50167317af 100644 --- a/vending-app/src/main/aidl/com/google/android/play/core/assetpacks/protocol/IAssetModuleServiceCallback.aidl +++ b/vending-app/src/main/aidl/com/google/android/play/core/assetpacks/protocol/IAssetModuleServiceCallback.aidl @@ -10,13 +10,13 @@ interface IAssetModuleServiceCallback { oneway void onCancelDownload(int status, in Bundle bundle) = 2; oneway void onGetSession(int status, in Bundle bundle) = 3; oneway void onGetSessionStates(in List list) = 4; - oneway void onNotifyChunkTransferred(in Bundle bundle,in Bundle bundle2) = 5; + oneway void onNotifyChunkTransferred(in Bundle bundle, in Bundle bundle2) = 5; oneway void onError(in Bundle bundle) = 6; - oneway void onNotifyModuleCompleted(in Bundle bundle,in Bundle bundle2) = 7; - oneway void onNotifySessionFailed(in Bundle bundle,in Bundle bundle2) = 9; + oneway void onNotifyModuleCompleted(in Bundle bundle, in Bundle bundle2) = 7; + oneway void onNotifySessionFailed(in Bundle bundle, in Bundle bundle2) = 9; oneway void onKeepAlive(in Bundle bundle, in Bundle bundle2) = 10; oneway void onGetChunkFileDescriptor(in Bundle bundle, in Bundle bundle2) = 11; oneway void onRequestDownloadInfo(in Bundle bundle, in Bundle bundle2) = 12; - oneway void onRemoveModule(in Bundle bundle,in Bundle bundle2) = 13; + oneway void onRemoveModule(in Bundle bundle, in Bundle bundle2) = 13; oneway void onCancelDownloads(in Bundle bundle) = 14; } \ No newline at end of file diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/DownloadManager.kt b/vending-app/src/main/kotlin/com/google/android/finsky/DownloadManager.kt index 5459bc9a7f..4b68e58f72 100644 --- a/vending-app/src/main/kotlin/com/google/android/finsky/DownloadManager.kt +++ b/vending-app/src/main/kotlin/com/google/android/finsky/DownloadManager.kt @@ -19,7 +19,6 @@ import android.graphics.Canvas import android.graphics.drawable.BitmapDrawable import android.net.Uri import android.os.Build -import android.os.Bundle import android.util.Log import android.widget.RemoteViews import androidx.core.app.NotificationCompat @@ -27,7 +26,6 @@ import androidx.core.app.NotificationManagerCompat import androidx.core.content.ContextCompat import com.android.vending.R import com.google.android.finsky.assetmoduleservice.DownloadData -import com.google.android.play.core.assetpacks.protocol.IAssetModuleServiceCallback import java.io.File import java.io.FileOutputStream import java.io.IOException @@ -147,12 +145,12 @@ class DownloadManager(private val context: Context) { } @Synchronized - fun shouldStop(shouldStop:Boolean){ + fun shouldStop(shouldStop: Boolean) { shouldStops = shouldStop } @Synchronized - fun prepareDownload(downloadData: DownloadData, moduleName: String, callback: IAssetModuleServiceCallback?) { + fun prepareDownload(downloadData: DownloadData, moduleName: String) { Log.d(TAG, "prepareDownload: ${downloadData.packageName}") initNotification(moduleName, downloadData.packageName) val future = executor.submit { @@ -169,7 +167,7 @@ class DownloadManager(private val context: Context) { } val filesDir = "${context.filesDir}/assetpacks/$index/$resourcePackageName/$chunkName/" val destination = File(filesDir, resourceBlockName) - startDownload(moduleName, resourceLink, destination, downloadData,callback) + startDownload(moduleName, resourceLink, destination, downloadData) sendBroadcastForExistingFile(context, downloadData, moduleName, dataBundle, destination) } updateProgress(moduleName, 100) @@ -180,7 +178,7 @@ class DownloadManager(private val context: Context) { } @Synchronized - private fun cancelDownload(moduleName: String) { + fun cancelDownload(moduleName: String) { Log.d(TAG, "Download for module $moduleName has been canceled.") downloadingRecord[moduleName]?.cancel(true) shouldStops = true @@ -188,7 +186,7 @@ class DownloadManager(private val context: Context) { NotificationManagerCompat.from(context).cancel(NOTIFICATION_ID) } - private fun startDownload(moduleName: String, downloadLink: String, destinationFile: File, downloadData: DownloadData, callback: IAssetModuleServiceCallback?) { + private fun startDownload(moduleName: String, downloadLink: String, destinationFile: File, downloadData: DownloadData) { val packData = downloadData.getModuleData(moduleName) val uri = Uri.parse(downloadLink).toString() val connection = URL(uri).openConnection() as HttpURLConnection @@ -231,7 +229,6 @@ class DownloadManager(private val context: Context) { Log.e(TAG, "prepareDownload: startDownload error ", e) downloadData.updateDownloadStatus(moduleName, STATUS_FAILED) cancelDownload(moduleName) - callback?.onError(Bundle().apply { putInt(KEY_ERROR_CODE, ACCESS_DENIED) }) } finally { connection.disconnect() } diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/AssetModuleService.kt b/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/AssetModuleService.kt index ddc072d444..da0df1a7e9 100644 --- a/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/AssetModuleService.kt +++ b/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/AssetModuleService.kt @@ -16,7 +16,6 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleService import com.google.android.finsky.API_NOT_AVAILABLE -import com.google.android.finsky.CANCELED import com.google.android.finsky.DownloadManager import com.google.android.finsky.KEY_BYTE_LENGTH import com.google.android.finsky.KEY_CHUNK_FILE_DESCRIPTOR @@ -31,9 +30,11 @@ import com.google.android.finsky.KEY_RESOURCE_BLOCK_NAME import com.google.android.finsky.KEY_RESOURCE_PACKAGE_NAME import com.google.android.finsky.KEY_SESSION_ID import com.google.android.finsky.KEY_SLICE_ID +import com.google.android.finsky.NETWORK_ERROR import com.google.android.finsky.NO_ERROR import com.google.android.finsky.STATUS_COMPLETED import com.google.android.finsky.STATUS_DOWNLOADING +import com.google.android.finsky.STATUS_FAILED import com.google.android.finsky.STATUS_INITIAL_STATE import com.google.android.finsky.TAG_REQUEST import com.google.android.finsky.buildDownloadBundle @@ -91,28 +92,31 @@ class AssetModuleServiceImpl( list.forEach { val moduleName = it.getString(KEY_MODULE_NAME) val packData = downloadData?.getModuleData(moduleName!!) - if (packData?.status != STATUS_DOWNLOADING && packData?.status != STATUS_COMPLETED){ + if (packData?.status != STATUS_DOWNLOADING && packData?.status != STATUS_COMPLETED) { downloadData?.updateDownloadStatus(moduleName!!, STATUS_INITIAL_STATE) } + if (packData?.status == STATUS_FAILED) { + callback?.onError(Bundle().apply { putInt(KEY_ERROR_CODE, NETWORK_ERROR) }) + } } - val bundleData = buildDownloadBundle(downloadData!!,list) + val bundleData = buildDownloadBundle(downloadData!!, list) Log.d(TAG, "startDownload: $bundleData") callback?.onStartDownload(-1, bundleData) list.forEach { val moduleName = it.getString(KEY_MODULE_NAME) val packData = downloadData?.getModuleData(moduleName!!) - if (packData?.status == STATUS_INITIAL_STATE){ + if (packData?.status == STATUS_INITIAL_STATE) { DownloadManager.get(context).shouldStop(false) - DownloadManager.get(context).prepareDownload(downloadData!!, moduleName!!,callback) + DownloadManager.get(context).prepareDownload(downloadData!!, moduleName!!) } } } override fun getSessionStates(packageName: String?, bundle: Bundle?, callback: IAssetModuleServiceCallback?) { Log.d(TAG, "Method (getSessionStates) called by packageName -> $packageName") - val installedAssetModuleNames = bundle?.getParcelableArrayList(KEY_INSTALLED_ASSET_MODULE) - ?.flatMap { it.keySet().mapNotNull { subKey -> it.get(subKey) as? String } } - ?.toMutableList() ?: mutableListOf() + val installedAssetModuleNames = + bundle?.getParcelableArrayList(KEY_INSTALLED_ASSET_MODULE)?.flatMap { it.keySet().mapNotNull { subKey -> it.get(subKey) as? String } } + ?.toMutableList() ?: mutableListOf() val listBundleData: MutableList = mutableListOf() @@ -183,7 +187,7 @@ class AssetModuleServiceImpl( override fun notifySessionFailed(packageName: String?, bundle: Bundle?, bundle2: Bundle?, callback: IAssetModuleServiceCallback?) { Log.d(TAG, "Method (notifySessionFailed) called but not implemented by packageName -> $packageName") - callback?.onNotifySessionFailed(Bundle(),Bundle()) + callback?.onNotifySessionFailed(Bundle(), Bundle()) } override fun keepAlive(packageName: String?, bundle: Bundle?, callback: IAssetModuleServiceCallback?) { @@ -225,23 +229,26 @@ class AssetModuleServiceImpl( return } } - val bundleData = buildDownloadBundle(downloadData!!,list) + list.forEach { + val moduleName = it.getString(KEY_MODULE_NAME) + val packData = downloadData?.getModuleData(moduleName!!) + if (packData?.status == STATUS_FAILED) { + callback?.onError(Bundle().apply { putInt(KEY_ERROR_CODE, NETWORK_ERROR) }) + } + } + val bundleData = buildDownloadBundle(downloadData!!, list) Log.d(TAG, "requestDownloadInfo: $bundleData") callback?.onRequestDownloadInfo(bundleData, bundleData) } override fun removeModule(packageName: String?, bundle: Bundle?, bundle2: Bundle?, callback: IAssetModuleServiceCallback?) { Log.d(TAG, "Method (removeModule) called but not implemented by packageName -> $packageName") + callback?.onError(Bundle().apply { putInt(KEY_ERROR_CODE, API_NOT_AVAILABLE) }) } override fun cancelDownloads(packageName: String?, list: MutableList?, bundle: Bundle?, callback: IAssetModuleServiceCallback?) { Log.d(TAG, "Method (cancelDownloads) called but not implemented by packageName -> $packageName") - list?.forEach { - val moduleName = it.getString(KEY_MODULE_NAME) - downloadData?.updateDownloadStatus(moduleName!!, CANCELED) - sendBroadcastForExistingFile(context, downloadData!!, moduleName!!, null, null) - } - callback?.onCancelDownloads(Bundle()) + callback?.onError(Bundle().apply { putInt(KEY_ERROR_CODE, API_NOT_AVAILABLE) }) } companion object { diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/extensions.kt b/vending-app/src/main/kotlin/com/google/android/finsky/extensions.kt index e9ec4c95d9..16b8924373 100644 --- a/vending-app/src/main/kotlin/com/google/android/finsky/extensions.kt +++ b/vending-app/src/main/kotlin/com/google/android/finsky/extensions.kt @@ -20,12 +20,12 @@ import com.android.vending.licensing.getAuthToken import com.android.vending.licensing.getLicenseRequestHeaders import com.google.android.finsky.assetmoduleservice.DownloadData import com.google.android.finsky.assetmoduleservice.ModuleData +import kotlinx.coroutines.runBlocking import org.microg.gms.auth.AuthConstants import org.microg.vending.billing.GServices import org.microg.vending.billing.core.HttpClient import java.io.File import java.util.Collections -import kotlinx.coroutines.runBlocking const val STATUS_NOT_INSTALLED = 8 const val CANCELED = 6 @@ -34,7 +34,7 @@ const val STATUS_COMPLETED = 4 const val STATUS_DOWNLOADING = 2 const val STATUS_INITIAL_STATE = 1 -const val ACCESS_DENIED = -7 +const val NETWORK_ERROR = -6 const val API_NOT_AVAILABLE = -5 const val NO_ERROR = 0 @@ -116,13 +116,9 @@ fun HttpClient.initAssertModuleData( return null } - val requestPayload = AssetModuleDeliveryRequest.Builder() - .callerInfo(CallerInfo(getAppVersionCode(context, packageName)?.toInt())) - .packageName(packageName) - .playCoreVersion(playCoreVersionCode) - .pageSource(listOf(PageSource.UNKNOWN_SEARCH_TRAFFIC_SOURCE, PageSource.BOOKS_HOME_PAGE)) - .callerState(listOf(CallerState.CALLER_APP_REQUEST, CallerState.CALLER_APP_DEBUGGABLE)) - .moduleInfo(ArrayList().apply { + val requestPayload = AssetModuleDeliveryRequest.Builder().callerInfo(CallerInfo(getAppVersionCode(context, packageName)?.toInt())).packageName(packageName) + .playCoreVersion(playCoreVersionCode).pageSource(listOf(PageSource.UNKNOWN_SEARCH_TRAFFIC_SOURCE, PageSource.BOOKS_HOME_PAGE)) + .callerState(listOf(CallerState.CALLER_APP_REQUEST, CallerState.CALLER_APP_DEBUGGABLE)).moduleInfo(ArrayList().apply { requestedAssetModuleNames.forEach { add(AssetModuleInfo.Builder().name(it).build()) } }).build() @@ -192,11 +188,31 @@ private fun initModuleDownloadInfo(context: Context, packageName: String, delive } packDownloadByteLength += resDownloadByteLength } - val moduleData = ModuleData(appVersionCode = appVersionCode, moduleVersion = 0, sessionId = STATUS_NOT_INSTALLED, errorCode = NO_ERROR, status = STATUS_NOT_INSTALLED, bytesDownloaded = 0, totalBytesToDownload = packDownloadByteLength, packBundleList = dataBundle, listOfSubcontractNames = listOfSubcontractNames) + val moduleData = ModuleData( + appVersionCode = appVersionCode, + moduleVersion = 0, + sessionId = STATUS_NOT_INSTALLED, + errorCode = NO_ERROR, + status = STATUS_NOT_INSTALLED, + bytesDownloaded = 0, + totalBytesToDownload = packDownloadByteLength, + packBundleList = dataBundle, + listOfSubcontractNames = listOfSubcontractNames + ) moduleDownloadByteLength += packDownloadByteLength moduleDataList[resourcePackageName] = moduleData } - return DownloadData(packageName = packageName, errorCode = NO_ERROR, sessionIds = sessionIds, bytesDownloaded = 0, status = STATUS_NOT_INSTALLED, moduleNames = packNames, appVersionCode = appVersionCode, totalBytesToDownload = moduleDownloadByteLength, moduleDataList) + return DownloadData( + packageName = packageName, + errorCode = NO_ERROR, + sessionIds = sessionIds, + bytesDownloaded = 0, + status = STATUS_NOT_INSTALLED, + moduleNames = packNames, + appVersionCode = appVersionCode, + totalBytesToDownload = moduleDownloadByteLength, + moduleDataList + ) } fun buildDownloadBundle(downloadData: DownloadData, list: List? = null): Bundle { @@ -233,8 +249,9 @@ fun sendBroadcastForExistingFile(context: Context, downloadData: DownloadData, m val downloadBundle = Bundle() downloadBundle.putInt(KEY_APP_VERSION_CODE, downloadData.appVersionCode.toInt()) downloadBundle.putInt(KEY_ERROR_CODE, NO_ERROR) - downloadBundle.putInt(KEY_SESSION_ID, downloadData.sessionIds[moduleName] - ?: downloadData.status) + downloadBundle.putInt( + KEY_SESSION_ID, downloadData.sessionIds[moduleName] ?: downloadData.status + ) downloadBundle.putInt(KEY_STATUS, packData.status) downloadBundle.putStringArrayList(KEY_PACK_NAMES, arrayListOf(moduleName)) downloadBundle.putLong(KEY_BYTES_DOWNLOADED, packData.bytesDownloaded) From 75049bc08035c23a83ce2ba134540e150cf0f4b0 Mon Sep 17 00:00:00 2001 From: davinci9196 Date: Thu, 14 Nov 2024 11:37:55 +0800 Subject: [PATCH 084/132] Asset Modules: Change notification method --- .../google/android/finsky/DownloadManager.kt | 61 +++++++------------ .../assetmoduleservice/AssetModuleService.kt | 2 +- .../layout/layout_download_notification.xml | 51 ---------------- 3 files changed, 24 insertions(+), 90 deletions(-) delete mode 100644 vending-app/src/main/res/layout/layout_download_notification.xml diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/DownloadManager.kt b/vending-app/src/main/kotlin/com/google/android/finsky/DownloadManager.kt index 4b68e58f72..c45865be52 100644 --- a/vending-app/src/main/kotlin/com/google/android/finsky/DownloadManager.kt +++ b/vending-app/src/main/kotlin/com/google/android/finsky/DownloadManager.kt @@ -20,7 +20,6 @@ import android.graphics.drawable.BitmapDrawable import android.net.Uri import android.os.Build import android.util.Log -import android.widget.RemoteViews import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import androidx.core.content.ContextCompat @@ -50,7 +49,6 @@ private const val TAG = "DownloadManager" class DownloadManager(private val context: Context) { private val notifyBuilderMap = ConcurrentHashMap() - private val notificationLayoutMap = ConcurrentHashMap() private val downloadingRecord = ConcurrentHashMap>() @Volatile @@ -73,12 +71,7 @@ class DownloadManager(private val context: Context) { private fun createNotificationChannel() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val name = "Download Progress" - val descriptionText = "Shows download progress" - val importance = NotificationManager.IMPORTANCE_LOW - val channel = NotificationChannel(CHANNEL_ID, name, importance).apply { - description = descriptionText - } + val channel = NotificationChannel(CHANNEL_ID, "Download Progress", NotificationManager.IMPORTANCE_LOW) val notificationManager: NotificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager notificationManager.createNotificationChannel(channel) } @@ -101,47 +94,39 @@ class DownloadManager(private val context: Context) { val applicationInfo = packageManager.getApplicationInfo(packageName, 0) val appName = packageManager.getApplicationLabel(applicationInfo).toString() val appIcon = packageManager.getApplicationIcon(applicationInfo) - val bitmap = if (appIcon is BitmapDrawable) { + val largeIconBitmap = if (appIcon is BitmapDrawable) { appIcon.bitmap } else { - val bitmapTemp = Bitmap.createBitmap( - appIcon.intrinsicWidth, appIcon.intrinsicHeight, Bitmap.Config.ARGB_8888 - ) - val canvas = Canvas(bitmapTemp) - appIcon.setBounds(0, 0, canvas.width, canvas.height) - appIcon.draw(canvas) - bitmapTemp + Bitmap.createBitmap(appIcon.intrinsicWidth, appIcon.intrinsicHeight, Bitmap.Config.ARGB_8888).apply { + val canvas = Canvas(this) + appIcon.setBounds(0, 0, canvas.width, canvas.height) + appIcon.draw(canvas) + } } - val notificationLayout = RemoteViews(context.packageName, R.layout.layout_download_notification) - notificationLayout.setTextViewText( - R.id.notification_title, context.getString(R.string.download_notification_attachment_file, appName) - ) - notificationLayout.setTextViewText( - R.id.notification_text, context.getString(R.string.download_notification_tips) - ) - notificationLayout.setProgressBar(R.id.progress_bar, 100, 0, false) - notificationLayout.setImageViewBitmap(R.id.app_icon, bitmap) - notificationLayout.setOnClickPendingIntent(R.id.cancel_button, cancelPendingIntent) - - val notifyBuilder = - NotificationCompat.Builder(context, CHANNEL_ID).setSmallIcon(R.drawable.ic_app_foreground).setStyle(NotificationCompat.DecoratedCustomViewStyle()) - .setCustomContentView(notificationLayout).setPriority(NotificationCompat.PRIORITY_LOW).setOngoing(true).setOnlyAlertOnce(true) - .setColor(ContextCompat.getColor(context, R.color.notification_color)).setColorized(true) + val notifyBuilder = NotificationCompat.Builder(context, CHANNEL_ID) + .setSmallIcon(R.drawable.ic_app_foreground) + .setContentTitle(context.getString(R.string.download_notification_attachment_file, appName)) + .setContentText(context.getString(R.string.download_notification_tips)) + .setLargeIcon(largeIconBitmap) + .setProgress(100, 0, false) + .setPriority(NotificationCompat.PRIORITY_HIGH) + .setOngoing(true) + .setOnlyAlertOnce(true) + .setColor(ContextCompat.getColor(context, R.color.notification_color)) + .setColorized(true) + .addAction(R.drawable.ic_notification, context.getString(R.string.download_notification_cancel), cancelPendingIntent) + notifyBuilderMap[moduleName] = notifyBuilder - notificationLayoutMap[moduleName] = notificationLayout - NotificationManagerCompat.from(context).notify(NOTIFICATION_ID, notifyBuilder.setCustomContentView(notificationLayout).build()) + NotificationManagerCompat.from(context).notify(NOTIFICATION_ID, notifyBuilder.build()) } - private fun updateProgress(moduleName: String, progress: Int) { - val notificationLayout = notificationLayoutMap[moduleName] ?: return val notifyBuilder = notifyBuilderMap[moduleName] ?: return - notificationLayout.setProgressBar(R.id.progress_bar, 100, progress, false) - notifyBuilder.setCustomContentView(notificationLayout) - NotificationManagerCompat.from(context).notify(NOTIFICATION_ID, notifyBuilder.setCustomContentView(notificationLayout).build()) + notifyBuilder.setProgress(100, progress, false) + NotificationManagerCompat.from(context).notify(NOTIFICATION_ID, notifyBuilder.build()) } @Synchronized diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/AssetModuleService.kt b/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/AssetModuleService.kt index da0df1a7e9..97f5d1134e 100644 --- a/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/AssetModuleService.kt +++ b/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/AssetModuleService.kt @@ -187,7 +187,7 @@ class AssetModuleServiceImpl( override fun notifySessionFailed(packageName: String?, bundle: Bundle?, bundle2: Bundle?, callback: IAssetModuleServiceCallback?) { Log.d(TAG, "Method (notifySessionFailed) called but not implemented by packageName -> $packageName") - callback?.onNotifySessionFailed(Bundle(), Bundle()) + callback?.onError(Bundle().apply { putInt(KEY_ERROR_CODE, API_NOT_AVAILABLE) }) } override fun keepAlive(packageName: String?, bundle: Bundle?, callback: IAssetModuleServiceCallback?) { diff --git a/vending-app/src/main/res/layout/layout_download_notification.xml b/vending-app/src/main/res/layout/layout_download_notification.xml deleted file mode 100644 index 58edb0b4bb..0000000000 --- a/vending-app/src/main/res/layout/layout_download_notification.xml +++ /dev/null @@ -1,51 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file From b3a44e4fc6649bc93567c424d7689f1e1ae227ec Mon Sep 17 00:00:00 2001 From: davinci9196 Date: Fri, 15 Nov 2024 19:43:43 +0800 Subject: [PATCH 085/132] Asset Modules: Repair installation failure caused by abnormal request --- .../com/google/android/finsky/DownloadManager.kt | 6 +++--- .../finsky/assetmoduleservice/AssetModuleService.kt | 13 +++++++++++++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/DownloadManager.kt b/vending-app/src/main/kotlin/com/google/android/finsky/DownloadManager.kt index c45865be52..04d4a9c9bb 100644 --- a/vending-app/src/main/kotlin/com/google/android/finsky/DownloadManager.kt +++ b/vending-app/src/main/kotlin/com/google/android/finsky/DownloadManager.kt @@ -24,6 +24,8 @@ import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import androidx.core.content.ContextCompat import com.android.vending.R +import com.google.android.finsky.assetmoduleservice.AssetModuleServiceImpl +import com.google.android.finsky.assetmoduleservice.AssetModuleServiceImpl.Companion import com.google.android.finsky.assetmoduleservice.DownloadData import java.io.File import java.io.FileOutputStream @@ -129,12 +131,10 @@ class DownloadManager(private val context: Context) { NotificationManagerCompat.from(context).notify(NOTIFICATION_ID, notifyBuilder.build()) } - @Synchronized fun shouldStop(shouldStop: Boolean) { shouldStops = shouldStop } - @Synchronized fun prepareDownload(downloadData: DownloadData, moduleName: String) { Log.d(TAG, "prepareDownload: ${downloadData.packageName}") initNotification(moduleName, downloadData.packageName) @@ -162,7 +162,6 @@ class DownloadManager(private val context: Context) { downloadingRecord[moduleName] = future } - @Synchronized fun cancelDownload(moduleName: String) { Log.d(TAG, "Download for module $moduleName has been canceled.") downloadingRecord[moduleName]?.cancel(true) @@ -214,6 +213,7 @@ class DownloadManager(private val context: Context) { Log.e(TAG, "prepareDownload: startDownload error ", e) downloadData.updateDownloadStatus(moduleName, STATUS_FAILED) cancelDownload(moduleName) + downloadData.getModuleData(moduleName).bytesDownloaded = 0 } finally { connection.disconnect() } diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/AssetModuleService.kt b/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/AssetModuleService.kt index 97f5d1134e..7a7c1e1534 100644 --- a/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/AssetModuleService.kt +++ b/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/AssetModuleService.kt @@ -98,6 +98,19 @@ class AssetModuleServiceImpl( if (packData?.status == STATUS_FAILED) { callback?.onError(Bundle().apply { putInt(KEY_ERROR_CODE, NETWORK_ERROR) }) } + downloadData?.getModuleData(moduleName!!)?.packBundleList?.forEach { dataBundle -> + val destination = dataBundle.run { + val resourcePackageName = getString(KEY_RESOURCE_PACKAGE_NAME) + val chunkName = getString(KEY_CHUNK_NAME) + val resourceBlockName = getString(KEY_RESOURCE_BLOCK_NAME) + if (resourcePackageName == null || chunkName == null || resourceBlockName == null) return@forEach + File("${context.filesDir}/assetpacks/${getInt(KEY_INDEX)}/$resourcePackageName/$chunkName/", resourceBlockName) + } + + if (destination.exists() && destination.length() == dataBundle.getLong(KEY_BYTE_LENGTH)) { + sendBroadcastForExistingFile(context, downloadData!!, moduleName, dataBundle, destination) + } + } } val bundleData = buildDownloadBundle(downloadData!!, list) Log.d(TAG, "startDownload: $bundleData") From 2882d023cc0ec92e4c422846936040db6bba2c55 Mon Sep 17 00:00:00 2001 From: Marvin W Date: Wed, 20 Nov 2024 11:35:27 -0600 Subject: [PATCH 086/132] Asset Modules: Add configuration option to enable/disable --- .../org/microg/gms/settings/SettingsContract.kt | 2 ++ .../org/microg/gms/settings/SettingsProvider.kt | 2 ++ .../src/huawei/AndroidManifest.xml | 3 +++ .../kotlin/org/microg/gms/ui/VendingFragment.kt | 15 +++++++++++++++ .../org/microg/gms/vending/VendingPreferences.kt | 16 ++++++++++++++++ .../src/main/res/values/strings.xml | 4 ++++ .../src/main/res/xml/preferences_vending.xml | 11 +++++++++++ .../com/android/vending/VendingPreferences.kt | 9 +++++++++ .../assetmoduleservice/AssetModuleService.kt | 11 ++++++----- 9 files changed, 68 insertions(+), 5 deletions(-) diff --git a/play-services-base/core/src/main/kotlin/org/microg/gms/settings/SettingsContract.kt b/play-services-base/core/src/main/kotlin/org/microg/gms/settings/SettingsContract.kt index 5a0ee849ba..4b5575bb3b 100644 --- a/play-services-base/core/src/main/kotlin/org/microg/gms/settings/SettingsContract.kt +++ b/play-services-base/core/src/main/kotlin/org/microg/gms/settings/SettingsContract.kt @@ -198,11 +198,13 @@ object SettingsContract { const val LICENSING = "vending_licensing" const val LICENSING_PURCHASE_FREE_APPS = "vending_licensing_purchase_free_apps" const val BILLING = "vending_billing" + const val ASSET_DELIVERY = "vending_asset_delivery" val PROJECTION = arrayOf( LICENSING, LICENSING_PURCHASE_FREE_APPS, BILLING, + ASSET_DELIVERY, ) } diff --git a/play-services-base/core/src/main/kotlin/org/microg/gms/settings/SettingsProvider.kt b/play-services-base/core/src/main/kotlin/org/microg/gms/settings/SettingsProvider.kt index 124932d270..796f6feb85 100644 --- a/play-services-base/core/src/main/kotlin/org/microg/gms/settings/SettingsProvider.kt +++ b/play-services-base/core/src/main/kotlin/org/microg/gms/settings/SettingsProvider.kt @@ -355,6 +355,7 @@ class SettingsProvider : ContentProvider() { Vending.LICENSING -> getSettingsBoolean(key, false) Vending.LICENSING_PURCHASE_FREE_APPS -> getSettingsBoolean(key, false) Vending.BILLING -> getSettingsBoolean(key, false) + Vending.ASSET_DELIVERY -> getSettingsBoolean(key, false) else -> throw IllegalArgumentException("Unknown key: $key") } } @@ -367,6 +368,7 @@ class SettingsProvider : ContentProvider() { Vending.LICENSING -> editor.putBoolean(key, value as Boolean) Vending.LICENSING_PURCHASE_FREE_APPS -> editor.putBoolean(key, value as Boolean) Vending.BILLING -> editor.putBoolean(key, value as Boolean) + Vending.ASSET_DELIVERY -> editor.putBoolean(key, value as Boolean) else -> throw IllegalArgumentException("Unknown key: $key") } } diff --git a/play-services-core/src/huawei/AndroidManifest.xml b/play-services-core/src/huawei/AndroidManifest.xml index 9f4224aa37..b4c56a066f 100644 --- a/play-services-core/src/huawei/AndroidManifest.xml +++ b/play-services-core/src/huawei/AndroidManifest.xml @@ -37,5 +37,8 @@ + \ No newline at end of file diff --git a/play-services-core/src/main/kotlin/org/microg/gms/ui/VendingFragment.kt b/play-services-core/src/main/kotlin/org/microg/gms/ui/VendingFragment.kt index cac4c0f1f7..d20702e79f 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/ui/VendingFragment.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/ui/VendingFragment.kt @@ -18,6 +18,7 @@ class VendingFragment : PreferenceFragmentCompat() { private lateinit var licensingEnabled: TwoStatePreference private lateinit var licensingPurchaseFreeAppsEnabled: TwoStatePreference private lateinit var iapEnable: TwoStatePreference + private lateinit var assetDeliveryEnabled: TwoStatePreference override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { addPreferencesFromResource(R.xml.preferences_vending) @@ -60,6 +61,18 @@ class VendingFragment : PreferenceFragmentCompat() { } true } + + assetDeliveryEnabled = preferenceScreen.findPreference(PREF_ASSET_DELIVERY_ENABLED) ?: assetDeliveryEnabled + assetDeliveryEnabled.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> + val appContext = requireContext().applicationContext + lifecycleScope.launchWhenResumed { + if (newValue is Boolean) { + VendingPreferences.setAssetDeliveryEnabled(appContext, newValue) + } + updateContent() + } + true + } } override fun onResume() { @@ -73,6 +86,7 @@ class VendingFragment : PreferenceFragmentCompat() { licensingEnabled.isChecked = VendingPreferences.isLicensingEnabled(appContext) licensingPurchaseFreeAppsEnabled.isChecked = VendingPreferences.isLicensingPurchaseFreeAppsEnabled(appContext) iapEnable.isChecked = VendingPreferences.isBillingEnabled(appContext) + assetDeliveryEnabled.isChecked = VendingPreferences.isAssetDeliveryEnabled(appContext) } } @@ -80,5 +94,6 @@ class VendingFragment : PreferenceFragmentCompat() { const val PREF_LICENSING_ENABLED = "vending_licensing" const val PREF_LICENSING_PURCHASE_FREE_APPS_ENABLED = "vending_licensing_purchase_free_apps" const val PREF_IAP_ENABLED = "vending_iap" + const val PREF_ASSET_DELIVERY_ENABLED = "vending_asset_delivery" } } \ No newline at end of file diff --git a/play-services-core/src/main/kotlin/org/microg/gms/vending/VendingPreferences.kt b/play-services-core/src/main/kotlin/org/microg/gms/vending/VendingPreferences.kt index ec39c1586a..ec9e1a42dc 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/vending/VendingPreferences.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/vending/VendingPreferences.kt @@ -1,5 +1,6 @@ /* * SPDX-FileCopyrightText: 2023, e Foundation + * SPDX-FileCopyrightText: 2024 microG Project Team * SPDX-License-Identifier: Apache-2.0 */ @@ -53,4 +54,19 @@ object VendingPreferences { put(SettingsContract.Vending.BILLING, enabled) } } + + @JvmStatic + fun isAssetDeliveryEnabled(context: Context): Boolean { + val projection = arrayOf(SettingsContract.Vending.ASSET_DELIVERY) + return SettingsContract.getSettings(context, SettingsContract.Vending.getContentUri(context), projection) { c -> + c.getInt(0) != 0 + } + } + + @JvmStatic + fun setAssetDeliveryEnabled(context: Context, enabled: Boolean) { + SettingsContract.setSettings(context, SettingsContract.Vending.getContentUri(context)) { + put(SettingsContract.Vending.ASSET_DELIVERY, enabled) + } + } } \ No newline at end of file diff --git a/play-services-core/src/main/res/values/strings.xml b/play-services-core/src/main/res/values/strings.xml index 062aa8d1e7..6fd512c5eb 100644 --- a/play-services-core/src/main/res/values/strings.xml +++ b/play-services-core/src/main/res/values/strings.xml @@ -294,6 +294,10 @@ Please set up a password, PIN, or pattern lock screen." This feature is experimental and may lead to loss of money. You have been warned. Some apps may require you to also enable license verification to verify your purchases. + Google Play Asset Delivery + Enable on-demand asset delivery + Download additional assets when requested by apps that use Play Asset Delivery + Cancel Continue Signing you in diff --git a/play-services-core/src/main/res/xml/preferences_vending.xml b/play-services-core/src/main/res/xml/preferences_vending.xml index 3cc42b4fa9..509e994b83 100644 --- a/play-services-core/src/main/res/xml/preferences_vending.xml +++ b/play-services-core/src/main/res/xml/preferences_vending.xml @@ -51,4 +51,15 @@ android:summary="@string/pref_vending_billing_note_licensing" app:iconSpaceReserved="false" /> + + + + diff --git a/vending-app/src/main/java/com/android/vending/VendingPreferences.kt b/vending-app/src/main/java/com/android/vending/VendingPreferences.kt index 4786bbb370..ddd17e07b6 100644 --- a/vending-app/src/main/java/com/android/vending/VendingPreferences.kt +++ b/vending-app/src/main/java/com/android/vending/VendingPreferences.kt @@ -1,5 +1,6 @@ /* * SPDX-FileCopyrightText: 2023, e Foundation + * SPDX-FileCopyrightText: 2024 microG Project Team * SPDX-License-Identifier: Apache-2.0 */ @@ -32,4 +33,12 @@ object VendingPreferences { c.getInt(0) != 0 } } + + @JvmStatic + fun isAssetDeliveryEnabled(context: Context): Boolean { + val projection = arrayOf(SettingsContract.Vending.ASSET_DELIVERY) + return SettingsContract.getSettings(context, SettingsContract.Vending.getContentUri(context), projection) { c -> + c.getInt(0) != 0 + } + } } \ No newline at end of file diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/AssetModuleService.kt b/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/AssetModuleService.kt index 7a7c1e1534..8332d861a3 100644 --- a/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/AssetModuleService.kt +++ b/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/AssetModuleService.kt @@ -15,6 +15,7 @@ import android.util.Log import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleService +import com.android.vending.VendingPreferences import com.google.android.finsky.API_NOT_AVAILABLE import com.google.android.finsky.DownloadManager import com.google.android.finsky.KEY_BYTE_LENGTH @@ -76,7 +77,7 @@ class AssetModuleServiceImpl( override fun startDownload(packageName: String?, list: MutableList?, bundle: Bundle?, callback: IAssetModuleServiceCallback?) { Log.d(TAG, "Method (startDownload) called by packageName -> $packageName") - if (packageName == null || list == null || bundle == null) { + if (packageName == null || list == null || bundle == null || !VendingPreferences.isAssetDeliveryEnabled(context)) { callback?.onError(Bundle().apply { putInt(KEY_ERROR_CODE, API_NOT_AVAILABLE) }) return } @@ -161,7 +162,7 @@ class AssetModuleServiceImpl( override fun notifyChunkTransferred(packageName: String?, bundle: Bundle?, bundle2: Bundle?, callback: IAssetModuleServiceCallback?) { Log.d(TAG, "Method (notifyChunkTransferred) called by packageName -> $packageName") val moduleName = bundle?.getString(KEY_MODULE_NAME) - if (moduleName.isNullOrEmpty()) { + if (moduleName.isNullOrEmpty() || !VendingPreferences.isAssetDeliveryEnabled(context)) { callback?.onError(Bundle().apply { putInt(KEY_ERROR_CODE, API_NOT_AVAILABLE) }) return } @@ -177,7 +178,7 @@ class AssetModuleServiceImpl( override fun notifyModuleCompleted(packageName: String?, bundle: Bundle?, bundle2: Bundle?, callback: IAssetModuleServiceCallback?) { Log.d(TAG, "Method (notifyModuleCompleted) called but not implemented by packageName -> $packageName") val moduleName = bundle?.getString(KEY_MODULE_NAME) - if (moduleName.isNullOrEmpty()) { + if (moduleName.isNullOrEmpty() || !VendingPreferences.isAssetDeliveryEnabled(context)) { callback?.onError(Bundle().apply { putInt(KEY_ERROR_CODE, API_NOT_AVAILABLE) }) return } @@ -210,7 +211,7 @@ class AssetModuleServiceImpl( override fun getChunkFileDescriptor(packageName: String, bundle: Bundle, bundle2: Bundle, callback: IAssetModuleServiceCallback?) { Log.d(TAG, "Method (getChunkFileDescriptor) called by packageName -> $packageName") val moduleName = bundle.getString(KEY_MODULE_NAME) - if (moduleName.isNullOrEmpty()) { + if (moduleName.isNullOrEmpty() || !VendingPreferences.isAssetDeliveryEnabled(context)) { callback?.onError(Bundle().apply { putInt(KEY_ERROR_CODE, API_NOT_AVAILABLE) }) return } @@ -229,7 +230,7 @@ class AssetModuleServiceImpl( override fun requestDownloadInfo(packageName: String?, list: MutableList?, bundle: Bundle?, callback: IAssetModuleServiceCallback?) { Log.d(TAG, "Method (requestDownloadInfo) called by packageName -> $packageName") - if (packageName == null || list == null || bundle == null) { + if (packageName == null || list == null || bundle == null || !VendingPreferences.isAssetDeliveryEnabled(context)) { callback?.onError(Bundle().apply { putInt(KEY_ERROR_CODE, API_NOT_AVAILABLE) }) return } From dfc41d7ba23a262c784f3dba9af149e6949b396f Mon Sep 17 00:00:00 2001 From: Marvin W Date: Wed, 20 Nov 2024 11:40:31 -0600 Subject: [PATCH 087/132] Asset Modules: Remove custom notification styling code --- .../google/android/finsky/DownloadManager.kt | 2 -- .../main/res/drawable/download_progress_bar.xml | 17 ----------------- vending-app/src/main/res/values/colors.xml | 4 ---- 3 files changed, 23 deletions(-) delete mode 100644 vending-app/src/main/res/drawable/download_progress_bar.xml diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/DownloadManager.kt b/vending-app/src/main/kotlin/com/google/android/finsky/DownloadManager.kt index 04d4a9c9bb..4001de3771 100644 --- a/vending-app/src/main/kotlin/com/google/android/finsky/DownloadManager.kt +++ b/vending-app/src/main/kotlin/com/google/android/finsky/DownloadManager.kt @@ -115,8 +115,6 @@ class DownloadManager(private val context: Context) { .setPriority(NotificationCompat.PRIORITY_HIGH) .setOngoing(true) .setOnlyAlertOnce(true) - .setColor(ContextCompat.getColor(context, R.color.notification_color)) - .setColorized(true) .addAction(R.drawable.ic_notification, context.getString(R.string.download_notification_cancel), cancelPendingIntent) notifyBuilderMap[moduleName] = notifyBuilder diff --git a/vending-app/src/main/res/drawable/download_progress_bar.xml b/vending-app/src/main/res/drawable/download_progress_bar.xml deleted file mode 100644 index e121118550..0000000000 --- a/vending-app/src/main/res/drawable/download_progress_bar.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/vending-app/src/main/res/values/colors.xml b/vending-app/src/main/res/values/colors.xml index dbe602e2c6..4ad8d9df03 100644 --- a/vending-app/src/main/res/values/colors.xml +++ b/vending-app/src/main/res/values/colors.xml @@ -2,8 +2,4 @@ #01875f @android:color/white - #FFFFFF - #000000 - #555555 - #20FF2A \ No newline at end of file From 21b7f8140a043a1d3cc04aeaba0e51b40bb93d3c Mon Sep 17 00:00:00 2001 From: Marvin W Date: Wed, 20 Nov 2024 11:53:01 -0600 Subject: [PATCH 088/132] AssetPacks: Add stub client library implementation This is mostly to understand the protocol details, we should eventually factor this out into a standalone library. --- .../protocol/IAssetModuleServiceCallback.aidl | 4 +- .../play/core/assetpacks/AssetLocation.java | 36 ++ .../core/assetpacks/AssetPackException.java | 40 ++ .../core/assetpacks/AssetPackLocation.java | 46 ++ .../core/assetpacks/AssetPackManager.java | 194 ++++++++ .../assetpacks/AssetPackManagerFactory.java | 27 ++ .../core/assetpacks/AssetPackManagerImpl.java | 104 ++++ .../assetpacks/AssetPackServiceClient.java | 366 ++++++++++++++ .../play/core/assetpacks/AssetPackState.java | 65 +++ .../core/assetpacks/AssetPackStateImpl.java | 130 +++++ .../AssetPackStateUpdateListener.java | 14 + .../play/core/assetpacks/AssetPackStates.java | 23 + .../core/assetpacks/AssetPackStatesImpl.java | 65 +++ .../assetpacks/model/AssetPackErrorCode.java | 85 ++++ .../assetpacks/model/AssetPackStatus.java | 71 +++ .../model/AssetPackStorageMethod.java | 34 ++ .../model/AssetPackUpdateAvailability.java | 22 + .../protocol/BroadcastConstants.java | 16 + .../core/assetpacks/protocol/BundleKeys.java | 448 ++++++++++++++++++ .../protocol/CompressionFormat.java | 24 + .../core/assetpacks/protocol/PatchFormat.java | 31 ++ .../core/listener/StateUpdateListener.java | 16 + 22 files changed, 1859 insertions(+), 2 deletions(-) create mode 100644 vending-app/src/main/java/com/google/android/play/core/assetpacks/AssetLocation.java create mode 100644 vending-app/src/main/java/com/google/android/play/core/assetpacks/AssetPackException.java create mode 100644 vending-app/src/main/java/com/google/android/play/core/assetpacks/AssetPackLocation.java create mode 100644 vending-app/src/main/java/com/google/android/play/core/assetpacks/AssetPackManager.java create mode 100644 vending-app/src/main/java/com/google/android/play/core/assetpacks/AssetPackManagerFactory.java create mode 100644 vending-app/src/main/java/com/google/android/play/core/assetpacks/AssetPackManagerImpl.java create mode 100644 vending-app/src/main/java/com/google/android/play/core/assetpacks/AssetPackServiceClient.java create mode 100644 vending-app/src/main/java/com/google/android/play/core/assetpacks/AssetPackState.java create mode 100644 vending-app/src/main/java/com/google/android/play/core/assetpacks/AssetPackStateImpl.java create mode 100644 vending-app/src/main/java/com/google/android/play/core/assetpacks/AssetPackStateUpdateListener.java create mode 100644 vending-app/src/main/java/com/google/android/play/core/assetpacks/AssetPackStates.java create mode 100644 vending-app/src/main/java/com/google/android/play/core/assetpacks/AssetPackStatesImpl.java create mode 100644 vending-app/src/main/java/com/google/android/play/core/assetpacks/model/AssetPackErrorCode.java create mode 100644 vending-app/src/main/java/com/google/android/play/core/assetpacks/model/AssetPackStatus.java create mode 100644 vending-app/src/main/java/com/google/android/play/core/assetpacks/model/AssetPackStorageMethod.java create mode 100644 vending-app/src/main/java/com/google/android/play/core/assetpacks/model/AssetPackUpdateAvailability.java create mode 100644 vending-app/src/main/java/com/google/android/play/core/assetpacks/protocol/BroadcastConstants.java create mode 100644 vending-app/src/main/java/com/google/android/play/core/assetpacks/protocol/BundleKeys.java create mode 100644 vending-app/src/main/java/com/google/android/play/core/assetpacks/protocol/CompressionFormat.java create mode 100644 vending-app/src/main/java/com/google/android/play/core/assetpacks/protocol/PatchFormat.java create mode 100644 vending-app/src/main/java/com/google/android/play/core/listener/StateUpdateListener.java diff --git a/vending-app/src/main/aidl/com/google/android/play/core/assetpacks/protocol/IAssetModuleServiceCallback.aidl b/vending-app/src/main/aidl/com/google/android/play/core/assetpacks/protocol/IAssetModuleServiceCallback.aidl index 50167317af..25b47d92ef 100644 --- a/vending-app/src/main/aidl/com/google/android/play/core/assetpacks/protocol/IAssetModuleServiceCallback.aidl +++ b/vending-app/src/main/aidl/com/google/android/play/core/assetpacks/protocol/IAssetModuleServiceCallback.aidl @@ -6,14 +6,14 @@ package com.google.android.play.core.assetpacks.protocol; interface IAssetModuleServiceCallback { - oneway void onStartDownload(int status, in Bundle bundle) = 1; + oneway void onStartDownload(int sessionId, in Bundle bundle) = 1; oneway void onCancelDownload(int status, in Bundle bundle) = 2; oneway void onGetSession(int status, in Bundle bundle) = 3; oneway void onGetSessionStates(in List list) = 4; oneway void onNotifyChunkTransferred(in Bundle bundle, in Bundle bundle2) = 5; oneway void onError(in Bundle bundle) = 6; oneway void onNotifyModuleCompleted(in Bundle bundle, in Bundle bundle2) = 7; - oneway void onNotifySessionFailed(in Bundle bundle, in Bundle bundle2) = 9; + oneway void onNotifySessionFailed(in Bundle bundle) = 9; oneway void onKeepAlive(in Bundle bundle, in Bundle bundle2) = 10; oneway void onGetChunkFileDescriptor(in Bundle bundle, in Bundle bundle2) = 11; oneway void onRequestDownloadInfo(in Bundle bundle, in Bundle bundle2) = 12; diff --git a/vending-app/src/main/java/com/google/android/play/core/assetpacks/AssetLocation.java b/vending-app/src/main/java/com/google/android/play/core/assetpacks/AssetLocation.java new file mode 100644 index 0000000000..4ec647459b --- /dev/null +++ b/vending-app/src/main/java/com/google/android/play/core/assetpacks/AssetLocation.java @@ -0,0 +1,36 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.play.core.assetpacks; + +import com.google.android.play.core.assetpacks.model.AssetPackStorageMethod; + +/** + * Location of a single asset, belonging to an asset pack. + *

+ * If the AssetPackStorageMethod for the pack is {@link AssetPackStorageMethod#APK_ASSETS}, this will be the path to the + * APK containing the asset, the offset of the asset inside the APK and the size of the asset. The asset file will be + * uncompressed, unless `bundletool` has been explicitly configured to compress the asset pack. + *

+ * If the AssetPackStorageMethod for the pack is {@link AssetPackStorageMethod#STORAGE_FILES}, this will be the path to + * the specific asset, the offset will be 0 and the size will be the size of the asset file. The asset file will be + * uncompressed. + */ +public abstract class AssetLocation { + /** + * Returns the file offset where the asset starts, in bytes. + */ + public abstract long offset(); + + /** + * Returns the path to the file containing the asset. + */ + public abstract String path(); + + /** + * Returns the size of the asset, in bytes. + */ + public abstract long size(); +} diff --git a/vending-app/src/main/java/com/google/android/play/core/assetpacks/AssetPackException.java b/vending-app/src/main/java/com/google/android/play/core/assetpacks/AssetPackException.java new file mode 100644 index 0000000000..090c592fcb --- /dev/null +++ b/vending-app/src/main/java/com/google/android/play/core/assetpacks/AssetPackException.java @@ -0,0 +1,40 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.play.core.assetpacks; + +import com.google.android.gms.common.api.ApiException; +import com.google.android.gms.common.api.Status; +import com.google.android.play.core.assetpacks.model.AssetPackErrorCode; +import org.microg.gms.common.Hide; + +/** + * An exception indicating something went wrong with the Asset Delivery API. + *

+ * See {@link #getErrorCode()} for the specific problem. + */ +public class AssetPackException extends ApiException { + @Hide + public AssetPackException(@AssetPackErrorCode int errorCode) { + super(new Status(errorCode, "Asset Pack Download Error(" + errorCode + ")")); + } + + /** + * Returns an error code value from {@link AssetPackErrorCode}. + */ + @AssetPackErrorCode + public int getErrorCode() { + return super.getStatusCode(); + } + + /** + * Returns the error code resulting from the operation. The value is one of the constants in {@link AssetPackErrorCode}. + * getStatusCode() is unsupported by AssetPackException, please use getErrorCode() instead. + */ + @Override + public int getStatusCode() { + return super.getStatusCode(); + } +} diff --git a/vending-app/src/main/java/com/google/android/play/core/assetpacks/AssetPackLocation.java b/vending-app/src/main/java/com/google/android/play/core/assetpacks/AssetPackLocation.java new file mode 100644 index 0000000000..abf6e6e71d --- /dev/null +++ b/vending-app/src/main/java/com/google/android/play/core/assetpacks/AssetPackLocation.java @@ -0,0 +1,46 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.play.core.assetpacks; + +import androidx.annotation.Nullable; +import com.google.android.play.core.assetpacks.model.AssetPackStorageMethod; + +/** + * Location of an asset pack on the device. + */ +public abstract class AssetPackLocation { + /** + * Returns the file path to the folder containing the pack's assets, if the storage method is + * {@link AssetPackStorageMethod#STORAGE_FILES}. + *

+ * The files found at this path should not be modified. + *

+ * If the storage method is {@link AssetPackStorageMethod#APK_ASSETS}, this method will return {@code null}. To access assets + * from packs installed as APKs, use Asset Manager. + */ + @Nullable + public abstract String assetsPath(); + + /** + * Returns whether the pack is installed as an APK or extracted into a folder on the filesystem. + * + * @return a value from {@link AssetPackStorageMethod} + */ + @AssetPackStorageMethod + public abstract int packStorageMethod(); + + /** + * Returns the file path to the folder containing the extracted asset pack, if the storage method is + * {@link AssetPackStorageMethod#STORAGE_FILES}. + *

+ * The files found at this path should not be modified. + *

+ * If the storage method is {@link AssetPackStorageMethod#APK_ASSETS}, this method will return {@code null}. To access assets + * from packs installed as APKs, use Asset Manager. + */ + @Nullable + public abstract String path(); +} diff --git a/vending-app/src/main/java/com/google/android/play/core/assetpacks/AssetPackManager.java b/vending-app/src/main/java/com/google/android/play/core/assetpacks/AssetPackManager.java new file mode 100644 index 0000000000..c4c23ae1c3 --- /dev/null +++ b/vending-app/src/main/java/com/google/android/play/core/assetpacks/AssetPackManager.java @@ -0,0 +1,194 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.play.core.assetpacks; + +import android.app.Activity; + +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.IntentSenderRequest; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.google.android.gms.tasks.Task; +import com.google.android.play.core.assetpacks.model.AssetPackStatus; + +import java.util.List; +import java.util.Map; + +/** + * Manages downloads of asset packs. + */ +public interface AssetPackManager { + + /** + * Requests to cancel the download of the specified asset packs. + *

+ * Note: Only active downloads can be canceled. + * + * @return The new state for all specified packs. + */ + AssetPackStates cancel(@NonNull List packNames); + + /** + * Unregisters all listeners previously added using {@link #registerListener}. + */ + void clearListeners(); + + /** + * Requests to download the specified asset packs. + *

+ * This method will fail if the app is not in the foreground. + * + * @return the state of all specified pack names + */ + Task fetch(List packNames); + + /** + * [advanced API] Returns the location of an asset in a pack, or {@code null} if the asset is not present in the given pack. + *

+ * You don't need to use this API for common use-cases: you can use the standard File API for accessing assets from + * asset packs that were extracted into the filesystem; and you can use Android's AssetManager API to access assets + * from packs that were installed as APKs. + *

+ * This API is useful for game engines that don't use Asset Manager and for developers that want a unified method to + * access assets, independently from the delivery mode. + */ + @Nullable + AssetLocation getAssetLocation(@NonNull String packName, @NonNull String assetPath); + + /** + * Returns the location of the specified asset pack on the device or {@code null} if this pack is not downloaded. + *

+ * The files found at this path should not be modified. + */ + @Nullable + AssetPackLocation getPackLocation(@NonNull String packName); + + /** + * Returns the location of all installed asset packs as a mapping from the asset pack name to an {@link AssetPackLocation}. + *

+ * The files found at these paths should not be modified. + */ + Map getPackLocations(); + + /** + * Requests download state or details for the specified asset packs. + *

+ * Do not use this method to determine whether an asset pack is downloaded. Instead use {@link #getPackLocation}. + */ + Task getPackStates(List packNames); + + /** + * Registers a listener that will be notified of changes to the state of pack downloads for this app. Listeners should be + * subsequently unregistered using {@link #unregisterListener}. + */ + void registerListener(@NonNull AssetPackStateUpdateListener listener); + + /** + * Deletes the specified asset pack from the internal storage of the app. + *

+ * Use this method to delete asset packs instead of deleting files manually. This ensures that the Asset Pack will not be + * re-downloaded during an app update. + *

+ * If the asset pack is currently being downloaded or installed, this method does not cancel the process. For this case, + * use {@link #cancel} instead. + * + * @return A task that will be successful only if files were successfully deleted. + */ + + Task removePack(@NonNull String packName); + + /** + * Shows a confirmation dialog to resume all pack downloads that are currently in the + * {@link AssetPackStatus#WAITING_FOR_WIFI} state. If the user accepts the dialog, packs are downloaded over cellular data. + *

+ * The status of an asset pack is set to {@link AssetPackStatus#WAITING_FOR_WIFI} if the user is currently not on a Wi-Fi + * connection and the asset pack is large or the user has set their download preference in the Play Store to only + * download apps over Wi-Fi. By showing this dialog, your app can ask the user if they accept downloading the asset + * pack over cellular data instead of waiting for Wi-Fi. + *

+ * The confirmation activity returns one of the following values: + *

    + *
  • {@link Activity#RESULT_OK Activity#RESULT_OK} if the user accepted. + *
  • {@link Activity#RESULT_CANCELED Activity#RESULT_CANCELED} if the user denied or the dialog has been closed in any other way (e.g. + * backpress). + *
+ * + * @param activityResultLauncher an activityResultLauncher to launch the confirmation dialog. + * @return whether the confirmation dialog has been started. + * @deprecated This API has been deprecated in favor of {@link #showConfirmationDialog(ActivityResultLauncher)}. + */ + @Deprecated + boolean showCellularDataConfirmation(@NonNull ActivityResultLauncher activityResultLauncher); + + /** + * Shows a confirmation dialog to resume all pack downloads that are currently in the + * {@link AssetPackStatus#WAITING_FOR_WIFI} state. If the user accepts the dialog, packs are downloaded over cellular data. + *

+ * The status of an asset pack is set to {@link AssetPackStatus#WAITING_FOR_WIFI} if the user is currently not on a Wi-Fi + * connection and the asset pack is large or the user has set their download preference in the Play Store to only + * download apps over Wi-Fi. By showing this dialog, your app can ask the user if they accept downloading the asset + * pack over cellular data instead of waiting for Wi-Fi. + * + * @param activity the activity on top of which the confirmation dialog is displayed. Use your current + * activity for this. + * @return A {@link Task} that completes once the dialog has been accepted, denied or closed. A successful task + * result contains one of the following values: + *

    + *
  • {@link Activity#RESULT_OK Activity#RESULT_OK} if the user accepted. + *
  • {@link Activity#RESULT_CANCELED Activity#RESULT_CANCELED} if the user denied or the dialog has been closed in any other way (e.g. + * backpress). + *
+ * @deprecated This API has been deprecated in favor of {@link #showConfirmationDialog(Activity)}. + */ + @Deprecated + Task showCellularDataConfirmation(@NonNull Activity activity); + + /** + * Shows a dialog that asks the user for consent to download packs that are currently in either the + * {@link AssetPackStatus#REQUIRES_USER_CONFIRMATION} state or the {@link AssetPackStatus#WAITING_FOR_WIFI} state. + *

+ * If the app has not been installed by Play, an update may be triggered to ensure that a valid version is installed. This + * will cause the app to restart and all asset requests to be cancelled. These assets should be requested again after the + * app restarts. + *

+ * The confirmation activity returns one of the following values: + *

    + *
  • {@link Activity#RESULT_OK Activity#RESULT_OK} if the user accepted. + *
  • {@link Activity#RESULT_CANCELED Activity#RESULT_CANCELED} if the user denied or the dialog has been closed in any other way (e.g. + * backpress). + *
+ * + * @param activityResultLauncher an activityResultLauncher to launch the confirmation dialog. + * @return whether the confirmation dialog has been started. + */ + boolean showConfirmationDialog(@NonNull ActivityResultLauncher activityResultLauncher); + + /** + * Shows a dialog that asks the user for consent to download packs that are currently in either the + * {@link AssetPackStatus#REQUIRES_USER_CONFIRMATION} state or the {@link AssetPackStatus#WAITING_FOR_WIFI} state. + *

+ * If the app has not been installed by Play, an update may be triggered to ensure that a valid version is installed. This + * will cause the app to restart and all asset requests to be cancelled. These assets should be requested again after the + * app restarts. + * + * @param activity the activity on top of which the confirmation dialog is displayed. Use your current + * activity for this. + * @return A {@link Task} that completes once the dialog has been accepted, denied or closed. A successful task + * result contains one of the following values: + *

    + *
  • {@link Activity#RESULT_OK Activity#RESULT_OK} if the user accepted. + *
  • {@link Activity#RESULT_CANCELED Activity#RESULT_CANCELED} if the user denied or the dialog has been closed in any other way (e.g. + * backpress). + *
+ */ + Task showConfirmationDialog(@NonNull Activity activity); + + /** + * Unregisters a listener previously added using {@link #registerListener}. + */ + void unregisterListener(@NonNull AssetPackStateUpdateListener listener); +} diff --git a/vending-app/src/main/java/com/google/android/play/core/assetpacks/AssetPackManagerFactory.java b/vending-app/src/main/java/com/google/android/play/core/assetpacks/AssetPackManagerFactory.java new file mode 100644 index 0000000000..272d90efa5 --- /dev/null +++ b/vending-app/src/main/java/com/google/android/play/core/assetpacks/AssetPackManagerFactory.java @@ -0,0 +1,27 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.play.core.assetpacks; + +import android.content.Context; +import androidx.annotation.NonNull; + +/** + * Creates instances of {@link AssetPackManager}. + */ +public final class AssetPackManagerFactory { + private AssetPackManagerFactory() { + } + + /** + * Creates an instance of {@link AssetPackManager}. + * + * @param applicationContext a fully initialized application context + */ + @NonNull + public static AssetPackManager getInstance(Context applicationContext) { + return new AssetPackManagerImpl(); + } +} diff --git a/vending-app/src/main/java/com/google/android/play/core/assetpacks/AssetPackManagerImpl.java b/vending-app/src/main/java/com/google/android/play/core/assetpacks/AssetPackManagerImpl.java new file mode 100644 index 0000000000..4ee68d8c9e --- /dev/null +++ b/vending-app/src/main/java/com/google/android/play/core/assetpacks/AssetPackManagerImpl.java @@ -0,0 +1,104 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.play.core.assetpacks; + +import android.app.Activity; +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.IntentSenderRequest; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.android.gms.tasks.Task; +import com.google.android.play.core.assetpacks.model.AssetPackStatus; +import org.microg.gms.common.Hide; + +import java.util.List; +import java.util.Map; + +@Hide +public class AssetPackManagerImpl implements AssetPackManager { + @Override + public AssetPackStates cancel(@NonNull List packNames) { + throw new UnsupportedOperationException(); + } + + @Override + public void clearListeners() { + } + + @Override + public Task fetch(List packNames) { + throw new UnsupportedOperationException(); + } + + @Nullable + @Override + public AssetLocation getAssetLocation(@NonNull String packName, @NonNull String assetPath) { + throw new UnsupportedOperationException(); + } + + @Nullable + @Override + public AssetPackLocation getPackLocation(@NonNull String packName) { + throw new UnsupportedOperationException(); + } + + @Override + public Map getPackLocations() { + throw new UnsupportedOperationException(); + } + + @Override + public Task getPackStates(List packNames) { + throw new UnsupportedOperationException(); + } + + @Override + public void registerListener(@NonNull AssetPackStateUpdateListener listener) { + throw new UnsupportedOperationException(); + } + + @Override + public Task removePack(@NonNull String packName) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean showCellularDataConfirmation(@NonNull ActivityResultLauncher activityResultLauncher) { + throw new UnsupportedOperationException(); + } + + @Override + public Task showCellularDataConfirmation(@NonNull Activity activity) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean showConfirmationDialog(@NonNull ActivityResultLauncher activityResultLauncher) { + throw new UnsupportedOperationException(); + } + + @Override + public Task showConfirmationDialog(@NonNull Activity activity) { + throw new UnsupportedOperationException(); + } + + @Override + public void unregisterListener(@NonNull AssetPackStateUpdateListener listener) { + + } + + public @AssetPackStatus int getLocalStatus(String packName, int remoteStatus) { + throw new UnsupportedOperationException(); + } + + public int getTransferProgressPercentage(String packName) { + throw new UnsupportedOperationException(); + } + + public String getInstalledVersionTag(String packName) { + throw new UnsupportedOperationException(); + } +} diff --git a/vending-app/src/main/java/com/google/android/play/core/assetpacks/AssetPackServiceClient.java b/vending-app/src/main/java/com/google/android/play/core/assetpacks/AssetPackServiceClient.java new file mode 100644 index 0000000000..321e86ea8a --- /dev/null +++ b/vending-app/src/main/java/com/google/android/play/core/assetpacks/AssetPackServiceClient.java @@ -0,0 +1,366 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.play.core.assetpacks; + +import android.content.Context; +import android.os.Bundle; +import android.os.ParcelFileDescriptor; +import android.os.RemoteException; +import android.util.Log; +import androidx.annotation.NonNull; +import com.google.android.gms.tasks.Task; +import com.google.android.gms.tasks.TaskCompletionSource; +import com.google.android.play.core.assetpacks.model.AssetPackErrorCode; +import com.google.android.play.core.assetpacks.model.AssetPackStatus; +import com.google.android.play.core.assetpacks.protocol.*; +import org.microg.gms.common.Hide; + +import java.util.*; + +@Hide +public class AssetPackServiceClient { + private static final String TAG = "AssetPackServiceClient"; + private List> pendingCalls = new ArrayList<>(); + private Context context; + private AssetPackManagerImpl assetPackManager; + + private interface PendingCall { + void execute(IAssetModuleService service, TaskCompletionSource completionSource) throws Exception; + } + + private Task execute(PendingCall pendingCall) { + TaskCompletionSource completionSource = new TaskCompletionSource<>(); + pendingCalls.add(completionSource); + try { + pendingCall.execute(null, completionSource); + } catch (Exception e) { + completionSource.trySetException(e); + } + Task task = completionSource.getTask(); + task.addOnCompleteListener(ignored -> pendingCalls.remove(completionSource)); + return task; + } + + private Bundle getOptionsBundle() { + Bundle options = new Bundle(); + // TODO + BundleKeys.put(options, BundleKeys.PLAY_CORE_VERSION_CODE, 20202); + BundleKeys.put(options, BundleKeys.SUPPORTED_COMPRESSION_FORMATS, new ArrayList<>(Arrays.asList(CompressionFormat.UNSPECIFIED, CompressionFormat.BROTLI))); + BundleKeys.put(options, BundleKeys.SUPPORTED_PATCH_FORMATS, new ArrayList<>(Arrays.asList(PatchFormat.PATCH_GDIFF, PatchFormat.GZIPPED_GDIFF))); + return options; + } + + private ArrayList getModuleNameBundles(List packNames) { + ArrayList moduleNameBundles = new ArrayList<>(); + for (String packName : packNames) { + Bundle arg = new Bundle(); + BundleKeys.put(arg, BundleKeys.MODULE_NAME, packName); + moduleNameBundles.add(arg); + } + return moduleNameBundles; + } + + private Bundle getInstalledAssetModulesBundle(Map installedAssetModules) { + Bundle installedAssetModulesBundle = getOptionsBundle(); + ArrayList installedAssetModuleBundles = new ArrayList<>(); + for (String moduleName : installedAssetModules.keySet()) { + Bundle installedAssetModuleBundle = new Bundle(); + BundleKeys.put(installedAssetModuleBundle, BundleKeys.INSTALLED_ASSET_MODULE_NAME, moduleName); + BundleKeys.put(installedAssetModuleBundle, BundleKeys.INSTALLED_ASSET_MODULE_VERSION, installedAssetModules.get(moduleName)); + installedAssetModuleBundles.add(installedAssetModuleBundle); + } + BundleKeys.put(installedAssetModulesBundle, BundleKeys.INSTALLED_ASSET_MODULE, installedAssetModuleBundles); + return installedAssetModulesBundle; + } + + private Bundle getSessionIdentifierBundle(int sessionId) { + Bundle sessionIdentifierBundle = new Bundle(); + BundleKeys.put(sessionIdentifierBundle, BundleKeys.SESSION_ID, sessionId); + return sessionIdentifierBundle; + } + + private Bundle getModuleIdentifierBundle(int sessionId, String moduleName) { + Bundle moduleIdentifierBundle = getSessionIdentifierBundle(sessionId); + BundleKeys.put(moduleIdentifierBundle, BundleKeys.MODULE_NAME, moduleName); + return moduleIdentifierBundle; + } + + private Bundle getChunkIdentifierBundle(int sessionId, String moduleName, String sliceId, int chunkNumber) { + Bundle chunkIdentifierBundle = getModuleIdentifierBundle(sessionId, moduleName); + BundleKeys.put(chunkIdentifierBundle, BundleKeys.SLICE_ID, sliceId); + BundleKeys.put(chunkIdentifierBundle, BundleKeys.CHUNK_NUMBER, chunkNumber); + return chunkIdentifierBundle; + } + + public Task getChunkFileDescriptor(int sessionId, String moduleName, String sliceId, int chunkNumber) { + return execute((service, completionSource) -> { + service.getChunkFileDescriptor(context.getPackageName(), getChunkIdentifierBundle(sessionId, moduleName, sliceId, chunkNumber), getOptionsBundle(), new BaseCallback(completionSource) { + @Override + public void onGetChunkFileDescriptor(ParcelFileDescriptor chunkFileDescriptor) { + completionSource.trySetResult(chunkFileDescriptor); + } + }); + }); + } + + public Task getPackStates(List packNames, Map installedAssetModules) { + return execute((service, completionSource) -> { + service.requestDownloadInfo(context.getPackageName(), getModuleNameBundles(packNames), getInstalledAssetModulesBundle(installedAssetModules), new BaseCallback(completionSource) { + @Override + public void onRequestDownloadInfo(Bundle bundle, Bundle bundle2) { + completionSource.trySetResult(AssetPackStatesImpl.fromBundle(bundle, assetPackManager)); + } + }); + }); + } + + public Task startDownload(List packNames, Map installedAssetModules) { + Task task = execute((service, completionSource) -> { + service.startDownload(context.getPackageName(), getModuleNameBundles(packNames), getInstalledAssetModulesBundle(installedAssetModules), new BaseCallback(completionSource) { + @Override + public void onStartDownload(int status, Bundle bundle) { + completionSource.trySetResult(AssetPackStatesImpl.fromBundle(bundle, assetPackManager, true)); + } + }); + }); + task.addOnSuccessListener(ignored -> keepAlive()); + return task; + } + + public Task> syncPacks(Map installedAssetModules) { + return execute((service, completionSource) -> { + service.getSessionStates(context.getPackageName(), getInstalledAssetModulesBundle(installedAssetModules), new BaseCallback(completionSource) { + @Override + public void onGetSessionStates(List list) { + ArrayList packNames = new ArrayList<>(); + for (Bundle bundle : list) { + Collection packStates = AssetPackStatesImpl.fromBundle(bundle, assetPackManager, true).packStates().values(); + if (!packStates.isEmpty()) { + AssetPackState state = packStates.iterator().next(); + switch (state.status()) { + case AssetPackStatus.PENDING: + case AssetPackStatus.DOWNLOADING: + case AssetPackStatus.TRANSFERRING: + case AssetPackStatus.WAITING_FOR_WIFI: + case AssetPackStatus.REQUIRES_USER_CONFIRMATION: + packNames.add(state.name()); + } + } + } + completionSource.trySetResult(packNames); + } + }); + }); + } + + public void cancelDownloads(List packNames) { + execute((service, completionSource) -> { + service.cancelDownloads(context.getPackageName(), getModuleNameBundles(packNames), getOptionsBundle(), new BaseCallback(completionSource) { + @Override + public void onCancelDownloads() { + completionSource.trySetResult(null); + } + }); + }); + } + + public void keepAlive() { + // TODO + } + + public void notifyChunkTransferred(int sessionId, String moduleName, String sliceId, int chunkNumber) { + execute((service, completionSource) -> { + service.notifyChunkTransferred(context.getPackageName(), getChunkIdentifierBundle(sessionId, moduleName, sliceId, chunkNumber), getOptionsBundle(), new BaseCallback(completionSource) { + @Override + public void onNotifyChunkTransferred(int sessionId, String moduleName, String sliceId, int chunkNumber) { + completionSource.trySetResult(null); + } + }); + }); + } + + public void notifyModuleCompleted(int sessionId, String moduleName) { + notifyModuleCompleted(sessionId, moduleName, 10); + } + + public void notifyModuleCompleted(int sessionId, String moduleName, int maxRetries) { + execute((service, completionSource) -> { + service.notifyModuleCompleted(context.getPackageName(), getModuleIdentifierBundle(sessionId, moduleName), getOptionsBundle(), new BaseCallback(completionSource) { + @Override + public void onError(int errorCode) { + if (maxRetries > 0) { + notifyModuleCompleted(sessionId, moduleName, maxRetries - 1); + } + } + }); + }); + } + + public void notifySessionFailed(int sessionId) { + execute((service, completionSource) -> { + service.notifySessionFailed(context.getPackageName(), getSessionIdentifierBundle(sessionId), getOptionsBundle(), new BaseCallback(completionSource) { + @Override + public void onNotifySessionFailed(int sessionId) { + completionSource.trySetResult(null); + } + }); + }); + } + + public void removePack(String packName) { + execute((service, completionSource) -> { + service.removeModule(context.getPackageName(), getModuleIdentifierBundle(0, packName), getOptionsBundle(), new BaseCallback(completionSource) { + @Override + public void onRemoveModule() { + completionSource.trySetResult(null); + } + }); + }); + } + + private static class BaseCallback extends IAssetModuleServiceCallback.Stub { + @NonNull + private final TaskCompletionSource completionSource; + + public BaseCallback(@NonNull TaskCompletionSource completionSource) { + this.completionSource = completionSource; + } + + @Override + public void onStartDownload(int sessionId, Bundle bundle) { + Log.i(TAG, "onStartDownload(" + sessionId + ")"); + onStartDownload(sessionId); + } + + public void onStartDownload(int sessionId) { + completionSource.trySetException(new Exception("Unexpected callback: onStartDownload")); + } + + @Override + public void onCancelDownload(int status, Bundle bundle) { + Log.i(TAG, "onCancelDownload(" + status + ")"); + onCancelDownload(status); + } + + public void onCancelDownload(int status) { + completionSource.trySetException(new Exception("Unexpected callback: onCancelDownload")); + } + + @Override + public void onGetSession(int status, Bundle bundle) { + Log.i(TAG, "onGetSession(" + status + ")"); + onGetSession(status); + } + + public void onGetSession(int status) { + completionSource.trySetException(new Exception("Unexpected callback: onGetSession")); + } + + @Override + public void onGetSessionStates(List list) { + completionSource.trySetException(new Exception("Unexpected callback: onGetSessionStates")); + } + + @Override + public void onNotifyChunkTransferred(Bundle bundle, Bundle bundle2) { + int sessionId = BundleKeys.get(bundle, BundleKeys.SESSION_ID, 0); + String moduleName = BundleKeys.get(bundle, BundleKeys.MODULE_NAME); + String sliceId = BundleKeys.get(bundle, BundleKeys.SLICE_ID); + int chunkNumber = BundleKeys.get(bundle, BundleKeys.CHUNK_NUMBER, 0); + Log.i(TAG, "onNotifyChunkTransferred(" + sessionId + ", " + moduleName + ", " + sliceId + ", " + chunkNumber + ")"); + onNotifyChunkTransferred(sessionId, moduleName, sliceId, chunkNumber); + } + + public void onNotifyChunkTransferred(int sessionId, String moduleName, String sliceId, int chunkNumber) { + completionSource.trySetException(new Exception("Unexpected callback: onNotifyChunkTransferred")); + } + + @Override + public void onError(Bundle bundle) { + int errorCode = BundleKeys.get(bundle, BundleKeys.ERROR_CODE, AssetPackErrorCode.INTERNAL_ERROR); + onError(errorCode); + } + + public void onError(int errorCode) { + completionSource.trySetException(new AssetPackException(errorCode)); + } + + @Override + public void onNotifyModuleCompleted(Bundle bundle, Bundle bundle2) { + int sessionId = BundleKeys.get(bundle, BundleKeys.SESSION_ID, 0); + String moduleName = BundleKeys.get(bundle, BundleKeys.MODULE_NAME); + Log.i(TAG, "onNotifyModuleCompleted(" + sessionId + ", " + moduleName + ")"); + onNotifyModuleCompleted(sessionId, moduleName); + } + + public void onNotifyModuleCompleted(int sessionId, String moduleName) { + completionSource.trySetException(new Exception("Unexpected callback: onNotifyModuleCompleted")); + } + + @Override + public void onNotifySessionFailed(Bundle bundle) { + int sessionId = BundleKeys.get(bundle, BundleKeys.SESSION_ID, 0); + Log.i(TAG, "onNotifySessionFailed(" + sessionId + ")"); + onNotifySessionFailed(sessionId); + } + + public void onNotifySessionFailed(int sessionId) { + completionSource.trySetException(new Exception("Unexpected callback: onNotifySessionFailed")); + } + + @Override + public void onKeepAlive(Bundle bundle, Bundle bundle2) { + boolean keepAlive = BundleKeys.get(bundle, BundleKeys.KEEP_ALIVE, false); + Log.i(TAG, "onKeepAlive(" + keepAlive + ")"); + onKeepAlive(keepAlive); + } + + public void onKeepAlive(boolean keepAlive) { + completionSource.trySetException(new Exception("Unexpected callback: onKeepAlive")); + } + + @Override + public void onGetChunkFileDescriptor(Bundle bundle, Bundle bundle2) { + ParcelFileDescriptor chunkFileDescriptor = BundleKeys.get(bundle, BundleKeys.CHUNK_FILE_DESCRIPTOR); + Log.i(TAG, "onGetChunkFileDescriptor(...)"); + onGetChunkFileDescriptor(chunkFileDescriptor); + } + + public void onGetChunkFileDescriptor(ParcelFileDescriptor chunkFileDescriptor) { + completionSource.trySetException(new Exception("Unexpected callback: onGetChunkFileDescriptor")); + } + + @Override + public void onRequestDownloadInfo(Bundle bundle, Bundle bundle2) { + Log.i(TAG, "onRequestDownloadInfo()"); + onRequestDownloadInfo(); + } + + public void onRequestDownloadInfo() { + completionSource.trySetException(new Exception("Unexpected callback: onRequestDownloadInfo")); + } + + @Override + public void onRemoveModule(Bundle bundle, Bundle bundle2) { + Log.i(TAG, "onRemoveModule()"); + onRemoveModule(); + } + + public void onRemoveModule() { + completionSource.trySetException(new Exception("Unexpected callback: onRemoveModule")); + } + + @Override + public void onCancelDownloads(Bundle bundle) { + Log.i(TAG, "onCancelDownload()"); + onCancelDownloads(); + } + + public void onCancelDownloads() { + completionSource.trySetException(new Exception("Unexpected callback: onCancelDownloads")); + } + } +} diff --git a/vending-app/src/main/java/com/google/android/play/core/assetpacks/AssetPackState.java b/vending-app/src/main/java/com/google/android/play/core/assetpacks/AssetPackState.java new file mode 100644 index 0000000000..c7028e9666 --- /dev/null +++ b/vending-app/src/main/java/com/google/android/play/core/assetpacks/AssetPackState.java @@ -0,0 +1,65 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.play.core.assetpacks; + +import com.google.android.play.core.assetpacks.model.AssetPackErrorCode; +import com.google.android.play.core.assetpacks.model.AssetPackStatus; +import com.google.android.play.core.assetpacks.model.AssetPackUpdateAvailability; + +/** + * The state of an individual asset pack. + */ +public abstract class AssetPackState { + public abstract String availableVersionTag(); + + /** + * Returns the total number of bytes already downloaded for the pack. + */ + public abstract long bytesDownloaded(); + + /** + * Returns the error code for the pack, if Play has failed to download the pack. Returns + * {@link AssetPackErrorCode#NO_ERROR} if the download was successful or is in progress or has not been attempted. + * + * @return A value from {@link AssetPackErrorCode}. + */ + @AssetPackErrorCode + public abstract int errorCode(); + + public abstract String installedVersionTag(); + + /** + * Returns the name of the pack. + */ + public abstract String name(); + + /** + * Returns the download status of the pack. + *

+ * If the pack has never been requested before its status is {@link AssetPackStatus#UNKNOWN}. + * + * @return a value from {@link AssetPackStatus} + */ + @AssetPackStatus + public abstract int status(); + + /** + * Returns the total size of the pack in bytes. + */ + public abstract long totalBytesToDownload(); + + /** + * Returns the percentage of the asset pack already transferred to the app. + *

+ * This value is only defined when the status is {@link AssetPackStatus#TRANSFERRING}. + * + * @return a value between 0 and 100 inclusive. + */ + public abstract int transferProgressPercentage(); + + @AssetPackUpdateAvailability + public abstract int updateAvailability(); +} diff --git a/vending-app/src/main/java/com/google/android/play/core/assetpacks/AssetPackStateImpl.java b/vending-app/src/main/java/com/google/android/play/core/assetpacks/AssetPackStateImpl.java new file mode 100644 index 0000000000..18e1c81ee5 --- /dev/null +++ b/vending-app/src/main/java/com/google/android/play/core/assetpacks/AssetPackStateImpl.java @@ -0,0 +1,130 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.play.core.assetpacks; + +import android.os.Bundle; +import androidx.annotation.NonNull; +import com.google.android.play.core.assetpacks.model.AssetPackErrorCode; +import com.google.android.play.core.assetpacks.model.AssetPackStatus; +import com.google.android.play.core.assetpacks.model.AssetPackUpdateAvailability; +import com.google.android.play.core.assetpacks.protocol.BundleKeys; +import org.microg.gms.common.Hide; +import org.microg.gms.utils.ToStringHelper; + +@Hide +public class AssetPackStateImpl extends AssetPackState { + private final String name; + private final @AssetPackStatus int status; + private final @AssetPackErrorCode int errorCode; + private final long bytesDownloaded; + private final long totalBytesToDownload; + private final int transferProgressPercentage; + @AssetPackUpdateAvailability + private final int updateAvailability; + private final String availableVersionTag; + private final String installedVersionTag; + + public AssetPackStateImpl(String name, @AssetPackStatus int status, @AssetPackErrorCode int errorCode, long bytesDownloaded, long totalBytesToDownload, int transferProgressPercentage, @AssetPackUpdateAvailability int updateAvailability, String availableVersionTag, String installedVersionTag) { + this.name = name; + this.status = status; + this.errorCode = errorCode; + this.bytesDownloaded = bytesDownloaded; + this.totalBytesToDownload = totalBytesToDownload; + this.transferProgressPercentage = transferProgressPercentage; + this.updateAvailability = updateAvailability; + this.availableVersionTag = availableVersionTag; + this.installedVersionTag = installedVersionTag; + } + + @NonNull + public static AssetPackState fromBundle(Bundle bundle, @NonNull String name, AssetPackManagerImpl assetPackManager) { + return fromBundle(bundle, name, assetPackManager, false); + } + + @NonNull + public static AssetPackState fromBundle(Bundle bundle, @NonNull String name, AssetPackManagerImpl assetPackManager, boolean ignoreLocalStatus) { + @AssetPackStatus int remoteStatus = BundleKeys.get(bundle, BundleKeys.STATUS, name, 0); + @AssetPackStatus int status = ignoreLocalStatus ? remoteStatus : assetPackManager.getLocalStatus(name, remoteStatus); + @AssetPackErrorCode int errorCode = BundleKeys.get(bundle, BundleKeys.ERROR_CODE, name, 0); + long bytesDownloaded = BundleKeys.get(bundle, BundleKeys.BYTES_DOWNLOADED, name, 0L); + long totalBytesToDownload = BundleKeys.get(bundle, BundleKeys.TOTAL_BYTES_TO_DOWNLOAD, name, 0L); + int transferProgressPercentage = assetPackManager.getTransferProgressPercentage(name); + long packVersion = BundleKeys.get(bundle, BundleKeys.PACK_VERSION, name, 0L); + long packBaseVersion = BundleKeys.get(bundle, BundleKeys.PACK_BASE_VERSION, name, 0L); + int appVersionCode = BundleKeys.get(bundle, BundleKeys.APP_VERSION_CODE, 0); + String availableVersionTag = BundleKeys.get(bundle, BundleKeys.PACK_VERSION_TAG, name, Integer.toString(appVersionCode)); + String installedVersionTag = assetPackManager.getInstalledVersionTag(name); + int updateAvailability = AssetPackUpdateAvailability.UPDATE_NOT_AVAILABLE; + if (status == AssetPackStatus.COMPLETED && packBaseVersion != 0 && packBaseVersion != packVersion) { + updateAvailability = AssetPackUpdateAvailability.UPDATE_AVAILABLE; + } + return new AssetPackStateImpl(name, status, errorCode, bytesDownloaded, totalBytesToDownload, transferProgressPercentage, updateAvailability, availableVersionTag, installedVersionTag); + } + + @Override + public String availableVersionTag() { + return availableVersionTag; + } + + @Override + public long bytesDownloaded() { + return bytesDownloaded; + } + + @Override + @AssetPackErrorCode + public int errorCode() { + return errorCode; + } + + @Override + public String installedVersionTag() { + return installedVersionTag; + } + + @Override + public String name() { + return name; + } + + @Override + @AssetPackStatus + public int status() { + return status; + } + + @Override + public long totalBytesToDownload() { + return totalBytesToDownload; + } + + @Override + public int transferProgressPercentage() { + return transferProgressPercentage; + } + + @Override + @AssetPackUpdateAvailability + public int updateAvailability() { + return updateAvailability; + } + + @NonNull + @Override + public String toString() { + return ToStringHelper.name("AssetPackState") + .field("name", name) + .field("status", status) + .field("errorCode", errorCode) + .field("bytesDownloaded", bytesDownloaded) + .field("totalBytesToDownload", totalBytesToDownload) + .field("transferProgressPercentage", transferProgressPercentage) + .field("updateAvailability", updateAvailability) + .field("availableVersionTag", availableVersionTag) + .field("installedVersionTag", installedVersionTag) + .end(); + } +} diff --git a/vending-app/src/main/java/com/google/android/play/core/assetpacks/AssetPackStateUpdateListener.java b/vending-app/src/main/java/com/google/android/play/core/assetpacks/AssetPackStateUpdateListener.java new file mode 100644 index 0000000000..1193f59aed --- /dev/null +++ b/vending-app/src/main/java/com/google/android/play/core/assetpacks/AssetPackStateUpdateListener.java @@ -0,0 +1,14 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.play.core.assetpacks; + +import com.google.android.play.core.listener.StateUpdateListener; + +/** + * Listener that may be registered for updates on the state of the download of asset packs. + */ +public interface AssetPackStateUpdateListener extends StateUpdateListener { +} diff --git a/vending-app/src/main/java/com/google/android/play/core/assetpacks/AssetPackStates.java b/vending-app/src/main/java/com/google/android/play/core/assetpacks/AssetPackStates.java new file mode 100644 index 0000000000..2fc5332c17 --- /dev/null +++ b/vending-app/src/main/java/com/google/android/play/core/assetpacks/AssetPackStates.java @@ -0,0 +1,23 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.play.core.assetpacks; + +import java.util.Map; + +/** + * Contains the state for all requested packs. + */ +public abstract class AssetPackStates { + /** + * Returns a map from a pack's name to its state. + */ + public abstract Map packStates(); + + /** + * Returns total size of all requested packs in bytes. + */ + public abstract long totalBytes(); +} diff --git a/vending-app/src/main/java/com/google/android/play/core/assetpacks/AssetPackStatesImpl.java b/vending-app/src/main/java/com/google/android/play/core/assetpacks/AssetPackStatesImpl.java new file mode 100644 index 0000000000..445a1cc993 --- /dev/null +++ b/vending-app/src/main/java/com/google/android/play/core/assetpacks/AssetPackStatesImpl.java @@ -0,0 +1,65 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.play.core.assetpacks; + +import android.os.Bundle; +import androidx.annotation.NonNull; +import com.google.android.play.core.assetpacks.protocol.BundleKeys; +import org.microg.gms.common.Hide; +import org.microg.gms.utils.ToStringHelper; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; + +@Hide +public class AssetPackStatesImpl extends AssetPackStates { + private final long totalBytes; + @NonNull + private final Map packStates; + + public AssetPackStatesImpl(long totalBytes, @NonNull Map packStates) { + this.totalBytes = totalBytes; + this.packStates = packStates; + } + + public static AssetPackStates fromBundle(@NonNull Bundle bundle, @NonNull AssetPackManagerImpl assetPackManager) { + return fromBundle(bundle, assetPackManager, false); + } + + @NonNull + public static AssetPackStates fromBundle(@NonNull Bundle bundle, @NonNull AssetPackManagerImpl assetPackManager, boolean ignoreLocalStatus) { + long totalBytes = BundleKeys.get(bundle, BundleKeys.TOTAL_BYTES_TO_DOWNLOAD, 0L); + ArrayList packNames = BundleKeys.get(bundle, BundleKeys.PACK_NAMES); + Map packStates = new HashMap<>(); + if (packNames != null) { + for (String packName : packNames) { + packStates.put(packName, AssetPackStateImpl.fromBundle(bundle, packName, assetPackManager, ignoreLocalStatus)); + } + } + return new AssetPackStatesImpl(totalBytes, packStates); + } + + @Override + @NonNull + public Map packStates() { + return packStates; + } + + @Override + public long totalBytes() { + return totalBytes; + } + + @NonNull + @Override + public String toString() { + return ToStringHelper.name("AssetPackStates") + .field("totalBytes", totalBytes) + .field("packStates", packStates) + .end(); + } +} diff --git a/vending-app/src/main/java/com/google/android/play/core/assetpacks/model/AssetPackErrorCode.java b/vending-app/src/main/java/com/google/android/play/core/assetpacks/model/AssetPackErrorCode.java new file mode 100644 index 0000000000..65f8b294e9 --- /dev/null +++ b/vending-app/src/main/java/com/google/android/play/core/assetpacks/model/AssetPackErrorCode.java @@ -0,0 +1,85 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.play.core.assetpacks.model; + +import android.app.Activity; +import androidx.annotation.IntDef; +import com.google.android.play.core.assetpacks.AssetPackManager; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Error codes for the download of an asset pack. + */ +@Target({ElementType.TYPE_USE}) +@Retention(RetentionPolicy.CLASS) +@IntDef({AssetPackErrorCode.NO_ERROR, AssetPackErrorCode.APP_UNAVAILABLE, AssetPackErrorCode.PACK_UNAVAILABLE, AssetPackErrorCode.INVALID_REQUEST, AssetPackErrorCode.DOWNLOAD_NOT_FOUND, AssetPackErrorCode.API_NOT_AVAILABLE, AssetPackErrorCode.NETWORK_ERROR, AssetPackErrorCode.ACCESS_DENIED, AssetPackErrorCode.INSUFFICIENT_STORAGE, AssetPackErrorCode.APP_NOT_OWNED, AssetPackErrorCode.PLAY_STORE_NOT_FOUND, AssetPackErrorCode.NETWORK_UNRESTRICTED, AssetPackErrorCode.CONFIRMATION_NOT_REQUIRED, AssetPackErrorCode.UNRECOGNIZED_INSTALLATION, AssetPackErrorCode.INTERNAL_ERROR}) +public @interface AssetPackErrorCode { + int NO_ERROR = 0; + /** + * The requesting app is unavailable. + */ + int APP_UNAVAILABLE = -1; + /** + * The requested asset pack isn't available. + *

+ * This can happen if the asset pack wasn't included in the Android App Bundle that was published to the Play Store. + */ + int PACK_UNAVAILABLE = -2; + /** + * The request is invalid. + */ + int INVALID_REQUEST = -3; + /** + * The requested download isn't found. + */ + int DOWNLOAD_NOT_FOUND = -4; + /** + * The Asset Delivery API isn't available. + */ + int API_NOT_AVAILABLE = -5; + /** + * Network error. Unable to obtain the asset pack details. + */ + int NETWORK_ERROR = -6; + /** + * Download not permitted under the current device circumstances (e.g. in background). + */ + int ACCESS_DENIED = -7; + /** + * Asset pack download failed due to insufficient storage. + */ + int INSUFFICIENT_STORAGE = -10; + /** + * The Play Store app is either not installed or not the official version. + */ + int PLAY_STORE_NOT_FOUND = -11; + /** + * Returned if {@link AssetPackManager#showCellularDataConfirmation(Activity)} is called but no asset packs are + * waiting for Wi-Fi. + */ + int NETWORK_UNRESTRICTED = -12; + /** + * The app isn't owned by any user on this device. An app is "owned" if it has been installed via the Play Store. + */ + int APP_NOT_OWNED = -13; + /** + * Returned if {@link AssetPackManager#showConfirmationDialog(Activity)} is called but no asset packs require user + * confirmation. + */ + int CONFIRMATION_NOT_REQUIRED = -14; + /** + * The installed app version is not recognized by Play. This can happen if the app was not installed by Play. + */ + int UNRECOGNIZED_INSTALLATION = -15; + /** + * Unknown error downloading an asset pack. + */ + int INTERNAL_ERROR = -100; +} diff --git a/vending-app/src/main/java/com/google/android/play/core/assetpacks/model/AssetPackStatus.java b/vending-app/src/main/java/com/google/android/play/core/assetpacks/model/AssetPackStatus.java new file mode 100644 index 0000000000..d6f7d2c174 --- /dev/null +++ b/vending-app/src/main/java/com/google/android/play/core/assetpacks/model/AssetPackStatus.java @@ -0,0 +1,71 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.play.core.assetpacks.model; + +import android.app.Activity; +import androidx.annotation.IntDef; +import com.google.android.play.core.assetpacks.AssetPackManager; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Status of the download of an asset pack. + */ +@Target({ElementType.TYPE_USE}) +@Retention(RetentionPolicy.CLASS) +@IntDef({AssetPackStatus.UNKNOWN, AssetPackStatus.PENDING, AssetPackStatus.DOWNLOADING, AssetPackStatus.TRANSFERRING, AssetPackStatus.COMPLETED, AssetPackStatus.FAILED, AssetPackStatus.CANCELED, AssetPackStatus.WAITING_FOR_WIFI, AssetPackStatus.NOT_INSTALLED, AssetPackStatus.REQUIRES_USER_CONFIRMATION}) +public @interface AssetPackStatus { + /** + * The asset pack state is unknown. + */ + int UNKNOWN = 0; + /** + * The asset pack download is pending and will be processed soon. + */ + int PENDING = 1; + /** + * The asset pack download is in progress. + */ + int DOWNLOADING = 2; + /** + * The asset pack is being decompressed and copied (or patched) to the app's internal storage. + */ + int TRANSFERRING = 3; + /** + * The asset pack download and transfer is complete; the assets are available to the app. + */ + int COMPLETED = 4; + /** + * The asset pack download or transfer has failed. + */ + int FAILED = 5; + /** + * The asset pack download has been canceled by the user through the Play Store or the download notification. + */ + int CANCELED = 6; + /** + * The asset pack download is waiting for Wi-Fi to become available before proceeding. + *

+ * The app can ask the user to download a session that is waiting for Wi-Fi over cellular data by using + * {@link AssetPackManager#showCellularDataConfirmation(Activity)}. + */ + int WAITING_FOR_WIFI = 7; + /** + * The asset pack is not installed. + */ + int NOT_INSTALLED = 8; + /** + * The asset pack requires user consent to be downloaded. + *

+ * This can happen if the current app version was not installed by Play. + *

+ * If the asset pack is also waiting for Wi-Fi, this state takes precedence. + */ + int REQUIRES_USER_CONFIRMATION = 9; +} diff --git a/vending-app/src/main/java/com/google/android/play/core/assetpacks/model/AssetPackStorageMethod.java b/vending-app/src/main/java/com/google/android/play/core/assetpacks/model/AssetPackStorageMethod.java new file mode 100644 index 0000000000..a3f94502db --- /dev/null +++ b/vending-app/src/main/java/com/google/android/play/core/assetpacks/model/AssetPackStorageMethod.java @@ -0,0 +1,34 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.play.core.assetpacks.model; + +import androidx.annotation.IntDef; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Method used to store an asset pack. + */ +@Target({ElementType.TYPE_USE}) +@Retention(RetentionPolicy.CLASS) +@IntDef({AssetPackStorageMethod.STORAGE_FILES, AssetPackStorageMethod.APK_ASSETS}) +public @interface AssetPackStorageMethod { + /** + * The asset pack is extracted into a folder containing individual asset files. + *

+ * Assets contained by this asset pack can be accessed via standard File APIs. + */ + int STORAGE_FILES = 0; + /** + * The asset pack is installed as APKs containing asset files. + *

+ * Assets contained by this asset pack can be accessed via Asset Manager. + */ + int APK_ASSETS = 1; +} diff --git a/vending-app/src/main/java/com/google/android/play/core/assetpacks/model/AssetPackUpdateAvailability.java b/vending-app/src/main/java/com/google/android/play/core/assetpacks/model/AssetPackUpdateAvailability.java new file mode 100644 index 0000000000..faff47315f --- /dev/null +++ b/vending-app/src/main/java/com/google/android/play/core/assetpacks/model/AssetPackUpdateAvailability.java @@ -0,0 +1,22 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.play.core.assetpacks.model; + +import androidx.annotation.IntDef; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.TYPE_USE}) +@Retention(RetentionPolicy.CLASS) +@IntDef({AssetPackUpdateAvailability.UNKNOWN, AssetPackUpdateAvailability.UPDATE_NOT_AVAILABLE, AssetPackUpdateAvailability.UPDATE_AVAILABLE}) +public @interface AssetPackUpdateAvailability { + int UNKNOWN = 0; + int UPDATE_NOT_AVAILABLE = 1; + int UPDATE_AVAILABLE = 2; +} diff --git a/vending-app/src/main/java/com/google/android/play/core/assetpacks/protocol/BroadcastConstants.java b/vending-app/src/main/java/com/google/android/play/core/assetpacks/protocol/BroadcastConstants.java new file mode 100644 index 0000000000..399d566c73 --- /dev/null +++ b/vending-app/src/main/java/com/google/android/play/core/assetpacks/protocol/BroadcastConstants.java @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.play.core.assetpacks.protocol; + +import org.microg.gms.common.Hide; + +@Hide +public class BroadcastConstants { + public static String ACTION_SESSION_UPDATE = "com.google.android.play.core.assetpacks.receiver.ACTION_SESSION_UPDATE"; + public static String EXTRA_SESSION_STATE = "com.google.android.play.core.assetpacks.receiver.EXTRA_SESSION_STATE"; + public static String EXTRA_FLAGS = "com.google.android.play.core.FLAGS"; + public static String KEY_USING_EXTRACTOR_STREAM = "usingExtractorStream"; +} diff --git a/vending-app/src/main/java/com/google/android/play/core/assetpacks/protocol/BundleKeys.java b/vending-app/src/main/java/com/google/android/play/core/assetpacks/protocol/BundleKeys.java new file mode 100644 index 0000000000..8293f80358 --- /dev/null +++ b/vending-app/src/main/java/com/google/android/play/core/assetpacks/protocol/BundleKeys.java @@ -0,0 +1,448 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.play.core.assetpacks.protocol; + +import android.content.Intent; +import android.os.Bundle; +import android.os.ParcelFileDescriptor; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.os.BundleCompat; +import org.microg.gms.common.Hide; + +import java.util.ArrayList; + +@Hide +public final class BundleKeys { + public static RootKey APP_VERSION_CODE = new RootKey.Int("app_version_code"); + public static RootKey CHUNK_NUMBER = new RootKey.Int("chunk_number"); + public static RootKey CHUNK_FILE_DESCRIPTOR = new RootKey.Parcelable<>("chunk_file_descriptor", ParcelFileDescriptor.class); + public static RootKey KEEP_ALIVE = new RootKey.Bool("keep_alive"); + public static RootKey MODULE_NAME = new RootKey.String("module_name"); + public static RootKey SLICE_ID = new RootKey.String("slice_id"); + public static RootKey> PACK_NAMES = new RootKey.StringArrayList("pack_names"); + + // OptionsBundle + public static RootKey PLAY_CORE_VERSION_CODE = new RootKey.Int("playcore_version_code"); + public static RootKey> SUPPORTED_COMPRESSION_FORMATS = new RootKey.IntArrayList("supported_compression_formats"); + public static RootKey> SUPPORTED_PATCH_FORMATS = new RootKey.IntArrayList("supported_patch_formats"); + + // InstalledAssetModulesBundle + public static RootKey> INSTALLED_ASSET_MODULE = new RootKey.ParcelableArrayList<>("installed_asset_module", Bundle.class); + public static RootKey INSTALLED_ASSET_MODULE_NAME = new RootKey.String("installed_asset_module_name"); + public static RootKey INSTALLED_ASSET_MODULE_VERSION = new RootKey.Long("installed_asset_module_version"); + + public static RootAndPackKey SESSION_ID = new RootAndPackKey.Int("session_id"); + public static RootAndPackKey STATUS = new RootAndPackKey.Int("status"); + public static RootAndPackKey ERROR_CODE = new RootAndPackKey.Int("error_code"); + public static RootAndPackKey BYTES_DOWNLOADED = new RootAndPackKey.Long("bytes_downloaded"); + public static RootAndPackKey TOTAL_BYTES_TO_DOWNLOAD = new RootAndPackKey.Long("total_bytes_to_download"); + + public static PackKey PACK_VERSION = new PackKey.Long("pack_version"); + public static PackKey PACK_BASE_VERSION = new PackKey.Long("pack_base_version"); + public static PackKey PACK_VERSION_TAG = new PackKey.String("pack_version_tag"); + public static PackKey> SLICE_IDS = new PackKey.StringArrayList("slice_ids"); + + public static SliceKey> CHUNK_INTENTS = new SliceKey.ParcelableArrayList<>("chunk_intents", Intent.class); + public static SliceKey COMPRESSION_FORMAT = new SliceKey.Int("compression_format"); + public static SliceKey PATCH_FORMAT = new SliceKey.Int("patch_format"); + public static SliceKey UNCOMPRESSED_HASH_SHA256 = new SliceKey.String("uncompressed_hash_sha256"); + public static SliceKey UNCOMPRESSED_SIZE = new SliceKey.Long("uncompressed_size"); + + private BundleKeys() { + } + + @Nullable + public static T get(Bundle bundle, @NonNull RootKey key) { + return key.get(bundle, key.baseKey()); + } + + public static T get(Bundle bundle, @NonNull RootKey key, T def) { + return key.get(bundle, key.baseKey(), def); + } + + public static void put(Bundle bundle, @NonNull RootKey key, T value) { + key.put(bundle, key.baseKey(), value); + } + + @Nullable + public static T get(Bundle bundle, @NonNull PackKey key, String packName) { + return key.get(bundle, packKey(packName, key.baseKey())); + } + + public static T get(Bundle bundle, @NonNull PackKey key, String packName, T def) { + return key.get(bundle, packKey(packName, key.baseKey()), def); + } + + public static void put(Bundle bundle, @NonNull PackKey key, String packName, T value) { + key.put(bundle, packKey(packName, key.baseKey()), value); + } + + @Nullable + public static T get(Bundle bundle, @NonNull SliceKey key, String packName, String sliceId) { + return key.get(bundle, sliceKey(packName, sliceId, key.baseKey())); + } + + public static T get(Bundle bundle, @NonNull SliceKey key, String packName, String sliceId, T def) { + return key.get(bundle, sliceKey(packName, sliceId, key.baseKey()), def); + } + + public static void put(Bundle bundle, @NonNull SliceKey key, String packName, String sliceId, T value) { + key.put(bundle, sliceKey(packName, sliceId, key.baseKey()), value); + } + + @NonNull + private static String packKey(String packName, String baseKey) { + return baseKey + ":" + packName; + } + + @NonNull + private static String sliceKey(String packName, String sliceId, String baseKey) { + return baseKey + ":" + packName + ":" + sliceId; + } + + public interface TypedBundleKey { + @NonNull + java.lang.String baseKey(); + + @Nullable + T get(@NonNull Bundle bundle, @NonNull java.lang.String key); + + T get(@NonNull Bundle bundle, @NonNull java.lang.String key, T def); + + void put(@NonNull Bundle bundle, @NonNull java.lang.String key, T value); + + abstract class Base implements TypedBundleKey { + @NonNull + public final java.lang.String baseKey; + + public Base(@NonNull java.lang.String baseKey) { + this.baseKey = baseKey; + } + + @NonNull + @Override + public java.lang.String baseKey() { + return baseKey; + } + } + + class Int extends Base { + + public Int(@NonNull java.lang.String key) { + super(key); + } + + @Override + public Integer get(@NonNull Bundle bundle, @NonNull java.lang.String key) { + return bundle.getInt(key); + } + + @Override + public Integer get(@NonNull Bundle bundle, @NonNull java.lang.String key, Integer def) { + return bundle.getInt(key, def); + } + + @Override + public void put(@NonNull Bundle bundle, @NonNull java.lang.String key, Integer value) { + bundle.putInt(key, value); + } + } + + class Long extends Base { + + public Long(@NonNull java.lang.String key) { + super(key); + } + + @Override + public java.lang.Long get(@NonNull Bundle bundle, @NonNull java.lang.String key) { + return bundle.getLong(key); + } + + @Override + public java.lang.Long get(@NonNull Bundle bundle, @NonNull java.lang.String key, java.lang.Long def) { + return bundle.getLong(key, def); + } + + @Override + public void put(@NonNull Bundle bundle, @NonNull java.lang.String key, java.lang.Long value) { + bundle.putLong(key, value); + } + } + + class Bool extends Base { + + public Bool(@NonNull java.lang.String key) { + super(key); + } + + @Override + public Boolean get(@NonNull Bundle bundle, @NonNull java.lang.String key) { + return bundle.getBoolean(key); + } + + @Override + public Boolean get(@NonNull Bundle bundle, @NonNull java.lang.String key, Boolean def) { + return bundle.getBoolean(key, def); + } + + @Override + public void put(@NonNull Bundle bundle, @NonNull java.lang.String key, Boolean value) { + bundle.putBoolean(key, value); + } + } + + class String extends Base { + + public String(@NonNull java.lang.String key) { + super(key); + } + + @Override + public java.lang.String get(@NonNull Bundle bundle, @NonNull java.lang.String key) { + return bundle.getString(key); + } + + @Override + public java.lang.String get(@NonNull Bundle bundle, @NonNull java.lang.String key, java.lang.String def) { + return bundle.getString(key, def); + } + + @Override + public void put(@NonNull Bundle bundle, @NonNull java.lang.String key, java.lang.String value) { + bundle.putString(key, value); + } + } + + class Parcelable extends Base { + @NonNull + private final Class tClass; + + public Parcelable(@NonNull java.lang.String key, @NonNull Class tClass) { + super(key); + this.tClass = tClass; + } + + @Override + public T get(@NonNull Bundle bundle, @NonNull java.lang.String key) { + return BundleCompat.getParcelable(bundle, key, tClass); + } + + @Override + public T get(@NonNull Bundle bundle, @NonNull java.lang.String key, T def) { + if (bundle.containsKey(key)) { + return BundleCompat.getParcelable(bundle, key, tClass); + } else { + return def; + } + } + + @Override + public void put(@NonNull Bundle bundle, @NonNull java.lang.String key, T value) { + bundle.putParcelable(key, value); + } + } + + class StringArrayList extends Base> { + public StringArrayList(@NonNull java.lang.String key) { + super(key); + } + + @Override + public ArrayList get(@NonNull Bundle bundle, @NonNull java.lang.String key) { + return bundle.getStringArrayList(key); + } + + @Override + public ArrayList get(@NonNull Bundle bundle, @NonNull java.lang.String key, ArrayList def) { + if (bundle.containsKey(key)) { + return bundle.getStringArrayList(key); + } else { + return def; + } + } + + @Override + public void put(@NonNull Bundle bundle, @NonNull java.lang.String key, ArrayList value) { + bundle.putStringArrayList(key, value); + } + } + + class IntArrayList extends Base> { + public IntArrayList(@NonNull java.lang.String key) { + super(key); + } + + @Override + public ArrayList get(@NonNull Bundle bundle, @NonNull java.lang.String key) { + return bundle.getIntegerArrayList(key); + } + + @Override + public ArrayList get(@NonNull Bundle bundle, @NonNull java.lang.String key, ArrayList def) { + if (bundle.containsKey(key)) { + return bundle.getIntegerArrayList(key); + } else { + return def; + } + } + + @Override + public void put(@NonNull Bundle bundle, @NonNull java.lang.String key, ArrayList value) { + bundle.putIntegerArrayList(key, value); + } + } + + class ParcelableArrayList extends Base> { + @NonNull + private final Class tClass; + + public ParcelableArrayList(@NonNull java.lang.String key, @NonNull Class tClass) { + super(key); + this.tClass = tClass; + } + + @Override + public ArrayList get(@NonNull Bundle bundle, @NonNull java.lang.String key) { + return BundleCompat.getParcelableArrayList(bundle, key, tClass); + } + + @Override + public ArrayList get(@NonNull Bundle bundle, @NonNull java.lang.String key, ArrayList def) { + if (bundle.containsKey(key)) { + return BundleCompat.getParcelableArrayList(bundle, key, tClass); + } else { + return def; + } + } + + @Override + public void put(@NonNull Bundle bundle, @NonNull java.lang.String key, ArrayList value) { + bundle.putParcelableArrayList(key, value); + } + } + } + + + public interface PackKey extends TypedBundleKey { + class Int extends TypedBundleKey.Int implements PackKey { + public Int(@NonNull java.lang.String key) { + super(key); + } + } + + class Long extends TypedBundleKey.Long implements PackKey { + public Long(@NonNull java.lang.String key) { + super(key); + } + } + + class String extends TypedBundleKey.String implements PackKey { + public String(@NonNull java.lang.String key) { + super(key); + } + } + + class StringArrayList extends TypedBundleKey.StringArrayList implements PackKey> { + public StringArrayList(@NonNull java.lang.String key) { + super(key); + } + } + } + + public interface SliceKey extends TypedBundleKey { + class Int extends TypedBundleKey.Int implements SliceKey { + public Int(@NonNull java.lang.String key) { + super(key); + } + } + + class Long extends TypedBundleKey.Long implements SliceKey { + public Long(@NonNull java.lang.String key) { + super(key); + } + } + + class String extends TypedBundleKey.String implements SliceKey { + public String(@NonNull java.lang.String key) { + super(key); + } + } + + class ParcelableArrayList extends TypedBundleKey.ParcelableArrayList implements SliceKey> { + public ParcelableArrayList(@NonNull java.lang.String key, @NonNull Class tClass) { + super(key, tClass); + } + } + } + + public interface RootKey extends TypedBundleKey { + class Int extends TypedBundleKey.Int implements RootKey { + public Int(@NonNull java.lang.String key) { + super(key); + } + } + + class Long extends TypedBundleKey.Long implements RootKey { + public Long(@NonNull java.lang.String key) { + super(key); + } + } + + class Bool extends TypedBundleKey.Bool implements RootKey { + public Bool(@NonNull java.lang.String key) { + super(key); + } + } + + class String extends TypedBundleKey.String implements RootKey { + public String(@NonNull java.lang.String key) { + super(key); + } + } + + class Parcelable extends TypedBundleKey.Parcelable implements RootKey { + public Parcelable(@NonNull java.lang.String key, @NonNull Class tClass) { + super(key, tClass); + } + } + + class StringArrayList extends TypedBundleKey.StringArrayList implements RootKey> { + public StringArrayList(@NonNull java.lang.String key) { + super(key); + } + } + + class IntArrayList extends TypedBundleKey.IntArrayList implements RootKey> { + public IntArrayList(@NonNull java.lang.String key) { + super(key); + } + } + + class ParcelableArrayList extends TypedBundleKey.ParcelableArrayList implements RootKey> { + public ParcelableArrayList(@NonNull java.lang.String key, @NonNull Class tClass) { + super(key, tClass); + } + } + } + + public interface RootAndPackKey extends RootKey, PackKey { + + class Int extends TypedBundleKey.Int implements RootAndPackKey { + public Int(@NonNull java.lang.String key) { + super(key); + } + } + + class Long extends TypedBundleKey.Long implements RootAndPackKey { + public Long(@NonNull java.lang.String key) { + super(key); + } + } + } + +} diff --git a/vending-app/src/main/java/com/google/android/play/core/assetpacks/protocol/CompressionFormat.java b/vending-app/src/main/java/com/google/android/play/core/assetpacks/protocol/CompressionFormat.java new file mode 100644 index 0000000000..5b5ab46625 --- /dev/null +++ b/vending-app/src/main/java/com/google/android/play/core/assetpacks/protocol/CompressionFormat.java @@ -0,0 +1,24 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.play.core.assetpacks.protocol; + +import androidx.annotation.IntDef; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.TYPE_USE}) +@Retention(RetentionPolicy.CLASS) +@IntDef({CompressionFormat.UNSPECIFIED, CompressionFormat.BROTLI, CompressionFormat.GZIP, CompressionFormat.CHUNKED_GZIP, CompressionFormat.CHUNKED_BROTLI}) +public @interface CompressionFormat { + int UNSPECIFIED = 0; + int BROTLI = 1; + int GZIP = 2; + int CHUNKED_GZIP = 3; + int CHUNKED_BROTLI = 4; +} diff --git a/vending-app/src/main/java/com/google/android/play/core/assetpacks/protocol/PatchFormat.java b/vending-app/src/main/java/com/google/android/play/core/assetpacks/protocol/PatchFormat.java new file mode 100644 index 0000000000..627c7c725a --- /dev/null +++ b/vending-app/src/main/java/com/google/android/play/core/assetpacks/protocol/PatchFormat.java @@ -0,0 +1,31 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.play.core.assetpacks.protocol; + + +import androidx.annotation.IntDef; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.TYPE_USE}) +@Retention(RetentionPolicy.CLASS) +@IntDef({PatchFormat.UNKNOWN_PATCHING_FORMAT, PatchFormat.PATCH_GDIFF, PatchFormat.GZIPPED_GDIFF, PatchFormat.GZIPPED_BSDIFF, PatchFormat.GZIPPED_FILEBYFILE, PatchFormat.BROTLI_FILEBYFILE, PatchFormat.BROTLI_BSDIFF, PatchFormat.BROTLI_FILEBYFILE_RECURSIVE, PatchFormat.BROTLI_FILEBYFILE_ANDROID_AWARE, PatchFormat.BROTLI_FILEBYFILE_RECURSIVE_ANDROID_AWARE, PatchFormat.BROTLI_FILEBYFILE_ANDROID_AWARE_NO_RECOMPRESSION}) +public @interface PatchFormat { + int UNKNOWN_PATCHING_FORMAT = 0; + int PATCH_GDIFF = 1; + int GZIPPED_GDIFF = 2; + int GZIPPED_BSDIFF = 3; + int GZIPPED_FILEBYFILE = 4; + int BROTLI_FILEBYFILE = 5; + int BROTLI_BSDIFF = 6; + int BROTLI_FILEBYFILE_RECURSIVE = 7; + int BROTLI_FILEBYFILE_ANDROID_AWARE = 8; + int BROTLI_FILEBYFILE_RECURSIVE_ANDROID_AWARE = 9; + int BROTLI_FILEBYFILE_ANDROID_AWARE_NO_RECOMPRESSION = 10; +} diff --git a/vending-app/src/main/java/com/google/android/play/core/listener/StateUpdateListener.java b/vending-app/src/main/java/com/google/android/play/core/listener/StateUpdateListener.java new file mode 100644 index 0000000000..80433f2dc3 --- /dev/null +++ b/vending-app/src/main/java/com/google/android/play/core/listener/StateUpdateListener.java @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.play.core.listener; + +/** + * Base interface for state update listeners. + */ +public interface StateUpdateListener { + /** + * Callback triggered whenever the state has changed. + */ + void onStateUpdate(StateT state); +} From 3342a326d9476106c790b9389510541cbbd0a67a Mon Sep 17 00:00:00 2001 From: Marvin W Date: Wed, 20 Nov 2024 14:39:25 -0600 Subject: [PATCH 089/132] Asset Modules: Fix names in protobuf --- .../com/google/android/finsky/extensions.kt | 86 +++++++++--------- vending-app/src/main/proto/AssetModule.proto | 89 +++++++------------ 2 files changed, 75 insertions(+), 100 deletions(-) diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/extensions.kt b/vending-app/src/main/kotlin/com/google/android/finsky/extensions.kt index 16b8924373..9135e6cd94 100644 --- a/vending-app/src/main/kotlin/com/google/android/finsky/extensions.kt +++ b/vending-app/src/main/kotlin/com/google/android/finsky/extensions.kt @@ -117,8 +117,8 @@ fun HttpClient.initAssertModuleData( } val requestPayload = AssetModuleDeliveryRequest.Builder().callerInfo(CallerInfo(getAppVersionCode(context, packageName)?.toInt())).packageName(packageName) - .playCoreVersion(playCoreVersionCode).pageSource(listOf(PageSource.UNKNOWN_SEARCH_TRAFFIC_SOURCE, PageSource.BOOKS_HOME_PAGE)) - .callerState(listOf(CallerState.CALLER_APP_REQUEST, CallerState.CALLER_APP_DEBUGGABLE)).moduleInfo(ArrayList().apply { + .playCoreVersion(playCoreVersionCode).supportedCompressionFormats(listOf(0, 3)) + .supportedPatchFormats(listOf(1, 2)).modules(ArrayList().apply { requestedAssetModuleNames.forEach { add(AssetModuleInfo.Builder().name(it).build()) } }).build() @@ -142,65 +142,65 @@ private fun initModuleDownloadInfo(context: Context, packageName: String, delive if (deliveryInfo == null || deliveryInfo.status != null) { return null } - val packNames: ArraySet = arraySetOf() - var moduleDownloadByteLength = 0L - var appVersionCode = 0L + val moduleNames: ArraySet = arraySetOf() + var totalBytesToDownload = 0L + var packVersionCode = 0L val sessionIds = arrayMapOf() - val moduleDataList = arrayMapOf() - for (deliveryIndex in deliveryInfo.res.indices) { - val resource: ModuleResource = deliveryInfo.res[deliveryIndex] - appVersionCode = resource.versionCode ?: 0 - val resourceList: List = resource.packResource - val resourcePackageName: String = resource.packName ?: continue - var packDownloadByteLength = 0L - packNames.add(resourcePackageName) - sessionIds[resourcePackageName] = deliveryIndex + 10 + val moduleDataMap = arrayMapOf() + for (moduleIndex in deliveryInfo.modules.indices) { + val moduleInfo: ModuleInfo = deliveryInfo.modules[moduleIndex] + packVersionCode = moduleInfo.packVersion ?: 0 + val slices: List = moduleInfo.slices + val moduleName: String = moduleInfo.moduleName ?: continue + var moduleBytesToDownload = 0L + moduleNames.add(moduleName) + sessionIds[moduleName] = moduleIndex + 10 var totalSumOfSubcontractedModules = 0 - val listOfSubcontractNames: ArrayList = ArrayList() + val sliceIds: ArrayList = ArrayList() val dataBundle: ArrayList = arrayListOf() - for (resIndex in resourceList.indices) { - val packResource: PackResource = resourceList[resIndex] - if (packResource.downloadInfo == null || packResource.chunkInfo == null) { + for (sliceIndex in slices.indices) { + val sliceInfo: SliceInfo = slices[sliceIndex] + if (sliceInfo.metadata == null || sliceInfo.fullDownloadInfo == null) { continue } - val downloadList = packResource.downloadInfo.download - val numberOfSubcontractors = downloadList.size - val uncompressedSize = packResource.downloadInfo.uncompressedSize - val uncompressedHashSha256 = packResource.downloadInfo.uncompressedHashCode - val chunkName = packResource.chunkInfo.chunkName?.also { listOfSubcontractNames.add(it) } - var resDownloadByteLength = 0L - for (downIndex in downloadList.indices) { - val dResource: Download = downloadList[downIndex] - resDownloadByteLength += dResource.byteLength!! + val chunks = sliceInfo.fullDownloadInfo.chunks + val chunkNumber = chunks.size + val uncompressedSize = sliceInfo.fullDownloadInfo.uncompressedSize + val uncompressedHashSha256 = sliceInfo.fullDownloadInfo.uncompressedHashSha256 + val chunkName = sliceInfo.metadata.sliceId?.also { sliceIds.add(it) } + var sliceBytesToDownload = 0L + for (chunkIndex in chunks.indices) { + val dResource: ChunkInfo = chunks[chunkIndex] + sliceBytesToDownload += dResource.bytesToDownload!! totalSumOfSubcontractedModules += 1 val bundle = Bundle() bundle.putString(KEY_CACHE_DIR, context.cacheDir.toString()) - bundle.putInt(KEY_INDEX, deliveryIndex + 10) - bundle.putString(KEY_RESOURCE_PACKAGE_NAME, resourcePackageName) + bundle.putInt(KEY_INDEX, moduleIndex + 10) + bundle.putString(KEY_RESOURCE_PACKAGE_NAME, moduleName) bundle.putString(KEY_CHUNK_NAME, chunkName) - bundle.putString(KEY_RESOURCE_LINK, dResource.resourceLink) - bundle.putLong(KEY_BYTE_LENGTH, dResource.byteLength) - bundle.putString(KEY_RESOURCE_BLOCK_NAME, downIndex.toString()) + bundle.putString(KEY_RESOURCE_LINK, dResource.sourceUri) + bundle.putLong(KEY_BYTE_LENGTH, dResource.bytesToDownload) + bundle.putString(KEY_RESOURCE_BLOCK_NAME, chunkIndex.toString()) bundle.putLong(KEY_UNCOMPRESSED_SIZE, uncompressedSize ?: 0) bundle.putString(KEY_UNCOMPRESSED_HASH_SHA256, uncompressedHashSha256) - bundle.putInt(KEY_NUMBER_OF_SUBCONTRACTORS, numberOfSubcontractors) + bundle.putInt(KEY_NUMBER_OF_SUBCONTRACTORS, chunkNumber) dataBundle.add(bundle) } - packDownloadByteLength += resDownloadByteLength + moduleBytesToDownload += sliceBytesToDownload } val moduleData = ModuleData( - appVersionCode = appVersionCode, + appVersionCode = packVersionCode, moduleVersion = 0, sessionId = STATUS_NOT_INSTALLED, errorCode = NO_ERROR, status = STATUS_NOT_INSTALLED, bytesDownloaded = 0, - totalBytesToDownload = packDownloadByteLength, + totalBytesToDownload = moduleBytesToDownload, packBundleList = dataBundle, - listOfSubcontractNames = listOfSubcontractNames + listOfSubcontractNames = sliceIds ) - moduleDownloadByteLength += packDownloadByteLength - moduleDataList[resourcePackageName] = moduleData + totalBytesToDownload += moduleBytesToDownload + moduleDataMap[moduleName] = moduleData } return DownloadData( packageName = packageName, @@ -208,10 +208,10 @@ private fun initModuleDownloadInfo(context: Context, packageName: String, delive sessionIds = sessionIds, bytesDownloaded = 0, status = STATUS_NOT_INSTALLED, - moduleNames = packNames, - appVersionCode = appVersionCode, - totalBytesToDownload = moduleDownloadByteLength, - moduleDataList + moduleNames = moduleNames, + appVersionCode = packVersionCode, + totalBytesToDownload = totalBytesToDownload, + moduleDataMap ) } diff --git a/vending-app/src/main/proto/AssetModule.proto b/vending-app/src/main/proto/AssetModule.proto index 31f2db45dd..e154155623 100644 --- a/vending-app/src/main/proto/AssetModule.proto +++ b/vending-app/src/main/proto/AssetModule.proto @@ -7,15 +7,16 @@ message AssetModuleDeliveryRequest { optional string packageName = 1; optional CallerInfo callerInfo = 2; optional uint32 playCoreVersion = 3; - repeated PageSource pageSource = 4; - repeated CallerState callerState = 5; - repeated AssetModuleInfo moduleInfo = 6; + repeated uint32 supportedCompressionFormats = 4; + repeated uint32 supportedPatchFormats = 5; + repeated AssetModuleInfo modules = 6; + optional bool isInstantApp = 7; } message CallerInfo { oneof Version { - int32 versionCode = 1; - string versionName = 3; + int32 appVersionCode = 1; + string internalSharingId = 3; } } @@ -24,38 +25,6 @@ message AssetModuleInfo { optional int64 type = 2; } -enum PageSource { - UNKNOWN_SEARCH_TRAFFIC_SOURCE = 0; - GAMES_HOME_PAGE = 1; - APPS_HOME_PAGE = 2; - BOOKS_HOME_PAGE = 3; - FAMILY_HOME_PAGE = 4; - KIDS_HOME_PAGE = 5; - PLAY_PASS_HOME_PAGE = 6; - NOW_HOME_PAGE = 7; - DEALS_HOME_PAGE = 8; - SEARCH_HOME_PAGE = 9; - DETAILS_PAGE = 10; - APPS_SEARCH_PAGE = 11; - EBOOKS_SEARCH_PAGE = 12; - DEEP_LINK_SEARCH = 13; - TABBED_BROWSE_FRAGMENT_HOME_PAGE = 14; - DETAILS_PAGE_TOOLBAR = 15; - UNKNOWN_SEARCH_TRAFFIC_SOURCE_CAR = 16; - LOYALTY_HOME_PAGE = 17; -} - -enum CallerState { - UNKNOWN_SECURITY_CHECK_TYPE = 0; - CALLER_APP_REQUEST = 1; - CALLER_APP_DEBUGGABLE = 2; - CALLER_APP_INTENT_HANDLING_PACKAGE_MISSING = 3; - CALLER_APP_INSTALLER_PACKAGE_NAME_UNKNOWN = 4; - CALLER_APP_INSTALLER_PACKAGE_NAME_3P = 5; - CALLER_APP_INSTALLED_CERTIFICATE_MISSING = 6; - CALLER_APP_CERTIFICATE_UNMATCHED = 7; -} - message AssetModuleDeliveryResponse { optional ModuleDeliveryWrapper wrapper = 1; } @@ -65,36 +34,42 @@ message ModuleDeliveryWrapper { } message ModuleDeliveryInfo { - repeated ModuleResource res = 3; + repeated ModuleInfo modules = 3; optional int32 status = 4; } -message ModuleResource { - optional string packName = 1; - optional int64 versionCode = 2; - repeated PackResource packResource = 3; - optional int64 unknownInt4 = 4; - optional string unknownString5 = 5; +message ModuleInfo { + optional string moduleName = 1; + optional int64 packVersion = 2; + repeated SliceInfo slices = 3; + optional int64 packBaseVersion = 4; + optional string packVersionTag = 5; } -message PackResource { - optional ChunkInfo chunkInfo = 1; - optional DownloadInfo downloadInfo = 2; +message SliceInfo { + optional SliceMetadata metadata = 1; + optional FullDownloadInfo fullDownloadInfo = 2; + optional PatchInfo patchInfo = 3; } -message ChunkInfo { - optional string chunkName = 1; +message SliceMetadata { + optional string sliceId = 1; } -message DownloadInfo { +message FullDownloadInfo { optional int64 uncompressedSize = 1; - optional string uncompressedHashCode = 2; - optional PageSource state = 3; - repeated Download download = 4; + optional string uncompressedHashSha256 = 2; + optional uint32 compressionFormat = 3; + repeated ChunkInfo chunks = 4; +} + +message PatchInfo { + optional uint32 patchFormat = 1; + repeated ChunkInfo chunks = 2; } -message Download { - optional int64 byteLength = 1; - optional string uncompressed = 2; - optional string resourceLink = 3; +message ChunkInfo { + optional int64 bytesToDownload = 1; + optional string unknown = 2; + optional string sourceUri = 3; } From b655ddf8537ad8d37aff826dd5cc68785b6d2fec Mon Sep 17 00:00:00 2001 From: Marvin W Date: Wed, 20 Nov 2024 20:10:19 -0600 Subject: [PATCH 090/132] Asset Modules: Use protocol constants from client code in service --- .../google/android/finsky/DownloadManager.kt | 7 +- .../assetmoduleservice/AssetModuleService.kt | 105 ++++++-------- .../com/google/android/finsky/extensions.kt | 134 ++++++++---------- 3 files changed, 102 insertions(+), 144 deletions(-) diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/DownloadManager.kt b/vending-app/src/main/kotlin/com/google/android/finsky/DownloadManager.kt index 4001de3771..93aa2b57e9 100644 --- a/vending-app/src/main/kotlin/com/google/android/finsky/DownloadManager.kt +++ b/vending-app/src/main/kotlin/com/google/android/finsky/DownloadManager.kt @@ -27,6 +27,7 @@ import com.android.vending.R import com.google.android.finsky.assetmoduleservice.AssetModuleServiceImpl import com.google.android.finsky.assetmoduleservice.AssetModuleServiceImpl.Companion import com.google.android.finsky.assetmoduleservice.DownloadData +import com.google.android.play.core.assetpacks.model.AssetPackStatus import java.io.File import java.io.FileOutputStream import java.io.IOException @@ -138,7 +139,7 @@ class DownloadManager(private val context: Context) { initNotification(moduleName, downloadData.packageName) val future = executor.submit { val packData = downloadData.getModuleData(moduleName) - downloadData.updateDownloadStatus(moduleName, STATUS_DOWNLOADING) + downloadData.updateDownloadStatus(moduleName, AssetPackStatus.DOWNLOADING) for (dataBundle in packData.packBundleList) { val resourcePackageName: String? = dataBundle.getString(KEY_RESOURCE_PACKAGE_NAME) val chunkName: String? = dataBundle.getString(KEY_CHUNK_NAME) @@ -192,7 +193,7 @@ class DownloadManager(private val context: Context) { while (input.read(buffer).also { bytesRead = it } != -1) { if (shouldStops) { Log.d(TAG, "Download interrupted for module: $moduleName") - downloadData.updateDownloadStatus(moduleName, CANCELED) + downloadData.updateDownloadStatus(moduleName, AssetPackStatus.CANCELED) return } output.write(buffer, 0, bytesRead) @@ -209,7 +210,7 @@ class DownloadManager(private val context: Context) { } } catch (e: Exception) { Log.e(TAG, "prepareDownload: startDownload error ", e) - downloadData.updateDownloadStatus(moduleName, STATUS_FAILED) + downloadData.updateDownloadStatus(moduleName, AssetPackStatus.FAILED) cancelDownload(moduleName) downloadData.getModuleData(moduleName).bytesDownloaded = 0 } finally { diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/AssetModuleService.kt b/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/AssetModuleService.kt index 8332d861a3..6c6979cbd9 100644 --- a/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/AssetModuleService.kt +++ b/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/AssetModuleService.kt @@ -16,31 +16,10 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleService import com.android.vending.VendingPreferences -import com.google.android.finsky.API_NOT_AVAILABLE -import com.google.android.finsky.DownloadManager -import com.google.android.finsky.KEY_BYTE_LENGTH -import com.google.android.finsky.KEY_CHUNK_FILE_DESCRIPTOR -import com.google.android.finsky.KEY_CHUNK_NAME -import com.google.android.finsky.KEY_CHUNK_NUMBER -import com.google.android.finsky.KEY_ERROR_CODE -import com.google.android.finsky.KEY_INDEX -import com.google.android.finsky.KEY_INSTALLED_ASSET_MODULE -import com.google.android.finsky.KEY_MODULE_NAME -import com.google.android.finsky.KEY_PLAY_CORE_VERSION_CODE -import com.google.android.finsky.KEY_RESOURCE_BLOCK_NAME -import com.google.android.finsky.KEY_RESOURCE_PACKAGE_NAME -import com.google.android.finsky.KEY_SESSION_ID -import com.google.android.finsky.KEY_SLICE_ID -import com.google.android.finsky.NETWORK_ERROR -import com.google.android.finsky.NO_ERROR -import com.google.android.finsky.STATUS_COMPLETED -import com.google.android.finsky.STATUS_DOWNLOADING -import com.google.android.finsky.STATUS_FAILED -import com.google.android.finsky.STATUS_INITIAL_STATE -import com.google.android.finsky.TAG_REQUEST -import com.google.android.finsky.buildDownloadBundle -import com.google.android.finsky.initAssertModuleData -import com.google.android.finsky.sendBroadcastForExistingFile +import com.google.android.finsky.* +import com.google.android.play.core.assetpacks.model.AssetPackErrorCode +import com.google.android.play.core.assetpacks.model.AssetPackStatus +import com.google.android.play.core.assetpacks.protocol.BundleKeys import com.google.android.play.core.assetpacks.protocol.IAssetModuleService import com.google.android.play.core.assetpacks.protocol.IAssetModuleServiceCallback import org.microg.gms.profile.ProfileManager @@ -78,26 +57,26 @@ class AssetModuleServiceImpl( override fun startDownload(packageName: String?, list: MutableList?, bundle: Bundle?, callback: IAssetModuleServiceCallback?) { Log.d(TAG, "Method (startDownload) called by packageName -> $packageName") if (packageName == null || list == null || bundle == null || !VendingPreferences.isAssetDeliveryEnabled(context)) { - callback?.onError(Bundle().apply { putInt(KEY_ERROR_CODE, API_NOT_AVAILABLE) }) + callback?.onError(Bundle().apply { put(BundleKeys.ERROR_CODE, AssetPackErrorCode.API_NOT_AVAILABLE) }) return } if (downloadData == null || downloadData?.packageName != packageName) { - val requestedAssetModuleNames = list.map { it.getString(KEY_MODULE_NAME) }.filter { !it.isNullOrEmpty() } - val playCoreVersionCode = bundle.getInt(KEY_PLAY_CORE_VERSION_CODE) - downloadData = httpClient.initAssertModuleData(context, packageName, accountManager, requestedAssetModuleNames, playCoreVersionCode) + val requestedAssetModuleNames = list.map { it.get(BundleKeys.MODULE_NAME) }.filter { !it.isNullOrEmpty() } + val playCoreVersionCode = bundle.get(BundleKeys.PLAY_CORE_VERSION_CODE) + downloadData = httpClient.initAssertModuleData(context, packageName, accountManager, requestedAssetModuleNames, playCoreVersionCode ?: 0) if (downloadData == null) { - callback?.onError(Bundle().apply { putInt(KEY_ERROR_CODE, API_NOT_AVAILABLE) }) + callback?.onError(Bundle().apply { put(BundleKeys.ERROR_CODE, AssetPackErrorCode.API_NOT_AVAILABLE) }) return } } list.forEach { - val moduleName = it.getString(KEY_MODULE_NAME) + val moduleName = it.get(BundleKeys.MODULE_NAME) val packData = downloadData?.getModuleData(moduleName!!) - if (packData?.status != STATUS_DOWNLOADING && packData?.status != STATUS_COMPLETED) { - downloadData?.updateDownloadStatus(moduleName!!, STATUS_INITIAL_STATE) + if (packData?.status != AssetPackStatus.DOWNLOADING && packData?.status != AssetPackStatus.COMPLETED) { + downloadData?.updateDownloadStatus(moduleName!!, AssetPackStatus.PENDING) } - if (packData?.status == STATUS_FAILED) { - callback?.onError(Bundle().apply { putInt(KEY_ERROR_CODE, NETWORK_ERROR) }) + if (packData?.status == AssetPackStatus.FAILED) { + callback?.onError(Bundle().apply { put(BundleKeys.ERROR_CODE, AssetPackErrorCode.NETWORK_ERROR) }) } downloadData?.getModuleData(moduleName!!)?.packBundleList?.forEach { dataBundle -> val destination = dataBundle.run { @@ -117,9 +96,9 @@ class AssetModuleServiceImpl( Log.d(TAG, "startDownload: $bundleData") callback?.onStartDownload(-1, bundleData) list.forEach { - val moduleName = it.getString(KEY_MODULE_NAME) + val moduleName = it.get(BundleKeys.MODULE_NAME) val packData = downloadData?.getModuleData(moduleName!!) - if (packData?.status == STATUS_INITIAL_STATE) { + if (packData?.status == AssetPackStatus.PENDING) { DownloadManager.get(context).shouldStop(false) DownloadManager.get(context).prepareDownload(downloadData!!, moduleName!!) } @@ -129,7 +108,7 @@ class AssetModuleServiceImpl( override fun getSessionStates(packageName: String?, bundle: Bundle?, callback: IAssetModuleServiceCallback?) { Log.d(TAG, "Method (getSessionStates) called by packageName -> $packageName") val installedAssetModuleNames = - bundle?.getParcelableArrayList(KEY_INSTALLED_ASSET_MODULE)?.flatMap { it.keySet().mapNotNull { subKey -> it.get(subKey) as? String } } + bundle?.get(BundleKeys.INSTALLED_ASSET_MODULE)?.flatMap { it.keySet().mapNotNull { subKey -> it.get(subKey) as? String } } ?.toMutableList() ?: mutableListOf() val listBundleData: MutableList = mutableListOf() @@ -161,29 +140,29 @@ class AssetModuleServiceImpl( override fun notifyChunkTransferred(packageName: String?, bundle: Bundle?, bundle2: Bundle?, callback: IAssetModuleServiceCallback?) { Log.d(TAG, "Method (notifyChunkTransferred) called by packageName -> $packageName") - val moduleName = bundle?.getString(KEY_MODULE_NAME) + val moduleName = bundle?.get(BundleKeys.MODULE_NAME) if (moduleName.isNullOrEmpty() || !VendingPreferences.isAssetDeliveryEnabled(context)) { - callback?.onError(Bundle().apply { putInt(KEY_ERROR_CODE, API_NOT_AVAILABLE) }) + callback?.onError(Bundle().apply { put(BundleKeys.ERROR_CODE, AssetPackErrorCode.API_NOT_AVAILABLE) }) return } - val sessionId = bundle.getInt(KEY_SESSION_ID) - val sliceId = bundle.getString(KEY_SLICE_ID) - val chunkNumber = bundle.getInt(KEY_CHUNK_NUMBER) + val sessionId = bundle.get(BundleKeys.SESSION_ID) + val sliceId = bundle.get(BundleKeys.SLICE_ID) + val chunkNumber = bundle.get(BundleKeys.CHUNK_NUMBER) val downLoadFile = "${context.filesDir.absolutePath}/assetpacks/$sessionId/$moduleName/$sliceId/$chunkNumber" fileDescriptorMap[downLoadFile]?.close() fileDescriptorMap.remove(downLoadFile) - callback?.onNotifyChunkTransferred(bundle, Bundle().apply { putInt(KEY_ERROR_CODE, NO_ERROR) }) + callback?.onNotifyChunkTransferred(bundle, Bundle().apply { put(BundleKeys.ERROR_CODE, AssetPackErrorCode.NO_ERROR) }) } override fun notifyModuleCompleted(packageName: String?, bundle: Bundle?, bundle2: Bundle?, callback: IAssetModuleServiceCallback?) { Log.d(TAG, "Method (notifyModuleCompleted) called but not implemented by packageName -> $packageName") - val moduleName = bundle?.getString(KEY_MODULE_NAME) + val moduleName = bundle?.get(BundleKeys.MODULE_NAME) if (moduleName.isNullOrEmpty() || !VendingPreferences.isAssetDeliveryEnabled(context)) { - callback?.onError(Bundle().apply { putInt(KEY_ERROR_CODE, API_NOT_AVAILABLE) }) + callback?.onError(Bundle().apply { put(BundleKeys.ERROR_CODE, AssetPackErrorCode.API_NOT_AVAILABLE) }) return } Log.d(TAG, "notify: moduleName: $moduleName packNames: ${downloadData?.moduleNames}") - downloadData?.updateDownloadStatus(moduleName, STATUS_COMPLETED) + downloadData?.updateDownloadStatus(moduleName, AssetPackStatus.COMPLETED) sendBroadcastForExistingFile(context, downloadData!!, moduleName, null, null) val sessionId = downloadData!!.sessionIds[moduleName] @@ -201,7 +180,7 @@ class AssetModuleServiceImpl( override fun notifySessionFailed(packageName: String?, bundle: Bundle?, bundle2: Bundle?, callback: IAssetModuleServiceCallback?) { Log.d(TAG, "Method (notifySessionFailed) called but not implemented by packageName -> $packageName") - callback?.onError(Bundle().apply { putInt(KEY_ERROR_CODE, API_NOT_AVAILABLE) }) + callback?.onError(Bundle().apply { put(BundleKeys.ERROR_CODE, AssetPackErrorCode.API_NOT_AVAILABLE) }) } override fun keepAlive(packageName: String?, bundle: Bundle?, callback: IAssetModuleServiceCallback?) { @@ -210,44 +189,44 @@ class AssetModuleServiceImpl( override fun getChunkFileDescriptor(packageName: String, bundle: Bundle, bundle2: Bundle, callback: IAssetModuleServiceCallback?) { Log.d(TAG, "Method (getChunkFileDescriptor) called by packageName -> $packageName") - val moduleName = bundle.getString(KEY_MODULE_NAME) + val moduleName = bundle.get(BundleKeys.MODULE_NAME) if (moduleName.isNullOrEmpty() || !VendingPreferences.isAssetDeliveryEnabled(context)) { - callback?.onError(Bundle().apply { putInt(KEY_ERROR_CODE, API_NOT_AVAILABLE) }) + callback?.onError(Bundle().apply { put(BundleKeys.ERROR_CODE, AssetPackErrorCode.API_NOT_AVAILABLE) }) return } val parcelFileDescriptor = runCatching { - val sessionId = bundle.getInt(KEY_SESSION_ID) - val sliceId = bundle.getString(KEY_SLICE_ID) - val chunkNumber = bundle.getInt(KEY_CHUNK_NUMBER) + val sessionId = bundle.get(BundleKeys.SESSION_ID) + val sliceId = bundle.get(BundleKeys.SLICE_ID) + val chunkNumber = bundle.get(BundleKeys.CHUNK_NUMBER) val downLoadFile = "${context.filesDir.absolutePath}/assetpacks/$sessionId/$moduleName/$sliceId/$chunkNumber" val filePath = Uri.parse(downLoadFile).path?.let { File(it) } ParcelFileDescriptor.open(filePath, ParcelFileDescriptor.MODE_READ_ONLY).also { fileDescriptorMap[downLoadFile] = it } }.getOrNull() - callback?.onGetChunkFileDescriptor(Bundle().apply { putParcelable(KEY_CHUNK_FILE_DESCRIPTOR, parcelFileDescriptor) }, Bundle()) + callback?.onGetChunkFileDescriptor(Bundle().apply { put(BundleKeys.CHUNK_FILE_DESCRIPTOR, parcelFileDescriptor) }, Bundle()) } override fun requestDownloadInfo(packageName: String?, list: MutableList?, bundle: Bundle?, callback: IAssetModuleServiceCallback?) { Log.d(TAG, "Method (requestDownloadInfo) called by packageName -> $packageName") if (packageName == null || list == null || bundle == null || !VendingPreferences.isAssetDeliveryEnabled(context)) { - callback?.onError(Bundle().apply { putInt(KEY_ERROR_CODE, API_NOT_AVAILABLE) }) + callback?.onError(Bundle().apply { put(BundleKeys.ERROR_CODE, AssetPackErrorCode.API_NOT_AVAILABLE) }) return } if (downloadData == null || downloadData?.packageName != packageName) { - val requestedAssetModuleNames = list.map { it.getString(KEY_MODULE_NAME) }.filter { !it.isNullOrEmpty() } - val playCoreVersionCode = bundle.getInt(KEY_PLAY_CORE_VERSION_CODE) + val requestedAssetModuleNames = list.map { it.get(BundleKeys.MODULE_NAME) }.filter { !it.isNullOrEmpty() } + val playCoreVersionCode = bundle.get(BundleKeys.PLAY_CORE_VERSION_CODE) ?: 0 downloadData = httpClient.initAssertModuleData(context, packageName, accountManager, requestedAssetModuleNames, playCoreVersionCode) if (downloadData == null) { - callback?.onError(Bundle().apply { putInt(KEY_ERROR_CODE, API_NOT_AVAILABLE) }) + callback?.onError(Bundle().apply { put(BundleKeys.ERROR_CODE, AssetPackErrorCode.API_NOT_AVAILABLE) }) return } } list.forEach { - val moduleName = it.getString(KEY_MODULE_NAME) + val moduleName = it.get(BundleKeys.MODULE_NAME) val packData = downloadData?.getModuleData(moduleName!!) - if (packData?.status == STATUS_FAILED) { - callback?.onError(Bundle().apply { putInt(KEY_ERROR_CODE, NETWORK_ERROR) }) + if (packData?.status == AssetPackStatus.FAILED) { + callback?.onError(Bundle().apply { put(BundleKeys.ERROR_CODE, AssetPackErrorCode.NETWORK_ERROR) }) } } val bundleData = buildDownloadBundle(downloadData!!, list) @@ -257,12 +236,12 @@ class AssetModuleServiceImpl( override fun removeModule(packageName: String?, bundle: Bundle?, bundle2: Bundle?, callback: IAssetModuleServiceCallback?) { Log.d(TAG, "Method (removeModule) called but not implemented by packageName -> $packageName") - callback?.onError(Bundle().apply { putInt(KEY_ERROR_CODE, API_NOT_AVAILABLE) }) + callback?.onError(Bundle().apply { put(BundleKeys.ERROR_CODE, AssetPackErrorCode.API_NOT_AVAILABLE) }) } override fun cancelDownloads(packageName: String?, list: MutableList?, bundle: Bundle?, callback: IAssetModuleServiceCallback?) { Log.d(TAG, "Method (cancelDownloads) called but not implemented by packageName -> $packageName") - callback?.onError(Bundle().apply { putInt(KEY_ERROR_CODE, API_NOT_AVAILABLE) }) + callback?.onError(Bundle().apply { put(BundleKeys.ERROR_CODE, AssetPackErrorCode.API_NOT_AVAILABLE) }) } companion object { diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/extensions.kt b/vending-app/src/main/kotlin/com/google/android/finsky/extensions.kt index 9135e6cd94..36f5c643f6 100644 --- a/vending-app/src/main/kotlin/com/google/android/finsky/extensions.kt +++ b/vending-app/src/main/kotlin/com/google/android/finsky/extensions.kt @@ -20,6 +20,10 @@ import com.android.vending.licensing.getAuthToken import com.android.vending.licensing.getLicenseRequestHeaders import com.google.android.finsky.assetmoduleservice.DownloadData import com.google.android.finsky.assetmoduleservice.ModuleData +import com.google.android.play.core.assetpacks.model.AssetPackErrorCode +import com.google.android.play.core.assetpacks.model.AssetPackStatus +import com.google.android.play.core.assetpacks.protocol.BroadcastConstants +import com.google.android.play.core.assetpacks.protocol.BundleKeys import kotlinx.coroutines.runBlocking import org.microg.gms.auth.AuthConstants import org.microg.vending.billing.GServices @@ -27,37 +31,13 @@ import org.microg.vending.billing.core.HttpClient import java.io.File import java.util.Collections -const val STATUS_NOT_INSTALLED = 8 -const val CANCELED = 6 -const val STATUS_FAILED = 5 -const val STATUS_COMPLETED = 4 -const val STATUS_DOWNLOADING = 2 -const val STATUS_INITIAL_STATE = 1 - -const val NETWORK_ERROR = -6 -const val API_NOT_AVAILABLE = -5 -const val NO_ERROR = 0 - const val KEY_ERROR_CODE = "error_code" const val KEY_MODULE_NAME = "module_name" const val KEY_RESOURCE_PACKAGE_NAME = "resourcePackageName" -const val KEY_SESSION_ID = "session_id" -const val KEY_STATUS = "status" -const val KEY_PACK_VERSION = "pack_version" -const val KEY_PACK_BASE_VERSION = "pack_base_version" -const val KEY_BYTES_DOWNLOADED = "bytes_downloaded" -const val KEY_TOTAL_BYTES_TO_DOWNLOAD = "total_bytes_to_download" -const val KEY_PACK_NAMES = "pack_names" -const val KEY_APP_VERSION_CODE = "app_version_code" -const val KEY_INSTALLED_ASSET_MODULE = "installed_asset_module" - const val KEY_CACHE_DIR = "CacheDir" const val KEY_INDEX = "index" const val KEY_CHUNK_NAME = "chunkName" -const val KEY_CHUNK_INTENT = "chunk_intents" -const val KEY_CHUNK_NUMBER = "chunk_number" -const val KEY_CHUNK_FILE_DESCRIPTOR = "chunk_file_descriptor" const val KEY_RESOURCE_LINK = "resourceLink" const val KEY_BYTE_LENGTH = "byteLength" const val KEY_RESOURCE_BLOCK_NAME = "resourceBlockName" @@ -66,36 +46,36 @@ const val KEY_UNCOMPRESSED_HASH_SHA256 = "uncompressed_hash_sha256" const val KEY_NUMBER_OF_SUBCONTRACTORS = "numberOfSubcontractors" const val KEY_USING_EXTRACTOR_STREAM = "usingExtractorStream" -const val KEY_COMPRESSION_FORMAT = "compression_format" -const val KEY_SLICE_IDS = "slice_ids" -const val KEY_SLICE_ID = "slice_id" -const val KEY_PLAY_CORE_VERSION_CODE = "playcore_version_code" const val ACTION_VIEW = "android.intent.action.VIEW" const val TAG_REQUEST = "asset_module" -private const val ACTION_SESSION_UPDATE = "com.google.android.play.core.assetpacks.receiver.ACTION_SESSION_UPDATE" -private const val EXTRA_SESSION_STATE = "com.google.android.play.core.assetpacks.receiver.EXTRA_SESSION_STATE" private const val FLAGS = "com.google.android.play.core.FLAGS" private const val ASSET_MODULE_DELIVERY_URL = "https://play-fe.googleapis.com/fdfe/assetModuleDelivery" private const val TAG = "AssetModuleRequest" -fun combineModule(key: String, vararg moduleNames: String?): String { - return moduleNames.joinToString(separator = ":", prefix = "$key:") -} - fun getAppVersionCode(context: Context, packageName: String): String? { return runCatching { context.packageManager.getPackageInfo(packageName, 0).versionCode.toString() }.getOrNull() } +fun Bundle?.get(key: BundleKeys.RootKey): T? = if (this == null) null else BundleKeys.get(this, key) +fun Bundle?.get(key: BundleKeys.RootKey, def: T): T = if (this == null) def else BundleKeys.get(this, key, def) +fun Bundle.put(key: BundleKeys.RootKey, v: T) = BundleKeys.put(this, key, v) +fun Bundle.put(key: BundleKeys.PackKey, packName: String, v: T) = BundleKeys.put(this, key, packName, v) +fun Bundle.put(key: BundleKeys.SliceKey, packName: String, sliceId: String, v: T) = BundleKeys.put(this, key, packName, sliceId, v) +fun bundleOf(pair: Pair, T>): Bundle = Bundle().apply { put(pair.first, pair.second) } + + fun HttpClient.initAssertModuleData( context: Context, packageName: String, accountManager: AccountManager, requestedAssetModuleNames: List, playCoreVersionCode: Int, + supportedCompressionFormats: List = listOf(0, 1), + supportedPatchFormats: List = listOf(1, 2), ): DownloadData? { val accounts = accountManager.getAccountsByType(AuthConstants.DEFAULT_ACCOUNT_TYPE) var oauthToken: String? = null @@ -117,8 +97,8 @@ fun HttpClient.initAssertModuleData( } val requestPayload = AssetModuleDeliveryRequest.Builder().callerInfo(CallerInfo(getAppVersionCode(context, packageName)?.toInt())).packageName(packageName) - .playCoreVersion(playCoreVersionCode).supportedCompressionFormats(listOf(0, 3)) - .supportedPatchFormats(listOf(1, 2)).modules(ArrayList().apply { + .playCoreVersion(playCoreVersionCode).supportedCompressionFormats(supportedCompressionFormats) + .supportedPatchFormats(supportedPatchFormats).modules(ArrayList().apply { requestedAssetModuleNames.forEach { add(AssetModuleInfo.Builder().name(it).build()) } }).build() @@ -191,9 +171,9 @@ private fun initModuleDownloadInfo(context: Context, packageName: String, delive val moduleData = ModuleData( appVersionCode = packVersionCode, moduleVersion = 0, - sessionId = STATUS_NOT_INSTALLED, - errorCode = NO_ERROR, - status = STATUS_NOT_INSTALLED, + sessionId = AssetPackStatus.NOT_INSTALLED, + errorCode = AssetPackErrorCode.NO_ERROR, + status = AssetPackStatus.NOT_INSTALLED, bytesDownloaded = 0, totalBytesToDownload = moduleBytesToDownload, packBundleList = dataBundle, @@ -204,10 +184,10 @@ private fun initModuleDownloadInfo(context: Context, packageName: String, delive } return DownloadData( packageName = packageName, - errorCode = NO_ERROR, + errorCode = AssetPackErrorCode.NO_ERROR, sessionIds = sessionIds, bytesDownloaded = 0, - status = STATUS_NOT_INSTALLED, + status = AssetPackStatus.NOT_INSTALLED, moduleNames = moduleNames, appVersionCode = packVersionCode, totalBytesToDownload = totalBytesToDownload, @@ -224,22 +204,22 @@ fun buildDownloadBundle(downloadData: DownloadData, list: List? = null) list?.forEach { val moduleName = it?.getString(KEY_MODULE_NAME) val packData = downloadData.getModuleData(moduleName!!) - bundleData.putInt(KEY_STATUS, packData.status) - downloadData.sessionIds[moduleName]?.let { sessionId -> bundleData.putInt(KEY_SESSION_ID, sessionId) } - bundleData.putInt(combineModule(KEY_SESSION_ID, moduleName), packData.sessionId) - bundleData.putInt(combineModule(KEY_STATUS, moduleName), packData.status) - bundleData.putInt(combineModule(KEY_ERROR_CODE, moduleName), packData.errorCode) - bundleData.putInt(combineModule(KEY_PACK_VERSION, moduleName), packData.appVersionCode.toInt()) - bundleData.putLong(combineModule(KEY_PACK_BASE_VERSION, moduleName), packData.moduleVersion) - bundleData.putLong(combineModule(KEY_BYTES_DOWNLOADED, moduleName), packData.bytesDownloaded) - bundleData.putLong(combineModule(KEY_TOTAL_BYTES_TO_DOWNLOAD, moduleName), packData.totalBytesToDownload) + bundleData.put(BundleKeys.STATUS, packData.status) + downloadData.sessionIds[moduleName]?.let { sessionId -> bundleData.put(BundleKeys.SESSION_ID, sessionId) } + bundleData.put(BundleKeys.SESSION_ID, moduleName, packData.sessionId) + bundleData.put(BundleKeys.STATUS, moduleName, packData.status) + bundleData.put(BundleKeys.ERROR_CODE, moduleName, packData.errorCode) + bundleData.put(BundleKeys.PACK_VERSION, moduleName, packData.appVersionCode) + bundleData.put(BundleKeys.PACK_BASE_VERSION, moduleName, packData.moduleVersion) + bundleData.put(BundleKeys.BYTES_DOWNLOADED, moduleName, packData.bytesDownloaded) + bundleData.put(BundleKeys.TOTAL_BYTES_TO_DOWNLOAD, moduleName, packData.totalBytesToDownload) arrayList.add(moduleName) totalBytesToDownload += packData.totalBytesToDownload bytesDownloaded += packData.bytesDownloaded } - bundleData.putStringArrayList(KEY_PACK_NAMES, arrayList) - bundleData.putLong(KEY_TOTAL_BYTES_TO_DOWNLOAD, totalBytesToDownload) - bundleData.putLong(KEY_BYTES_DOWNLOADED, bytesDownloaded) + bundleData.put(BundleKeys.PACK_NAMES, arrayList) + bundleData.put(BundleKeys.TOTAL_BYTES_TO_DOWNLOAD, totalBytesToDownload) + bundleData.put(BundleKeys.BYTES_DOWNLOADED, bytesDownloaded) return bundleData } @@ -247,21 +227,19 @@ fun sendBroadcastForExistingFile(context: Context, downloadData: DownloadData, m val packData = downloadData.getModuleData(moduleName) try { val downloadBundle = Bundle() - downloadBundle.putInt(KEY_APP_VERSION_CODE, downloadData.appVersionCode.toInt()) - downloadBundle.putInt(KEY_ERROR_CODE, NO_ERROR) - downloadBundle.putInt( - KEY_SESSION_ID, downloadData.sessionIds[moduleName] ?: downloadData.status - ) - downloadBundle.putInt(KEY_STATUS, packData.status) - downloadBundle.putStringArrayList(KEY_PACK_NAMES, arrayListOf(moduleName)) - downloadBundle.putLong(KEY_BYTES_DOWNLOADED, packData.bytesDownloaded) - downloadBundle.putLong(KEY_TOTAL_BYTES_TO_DOWNLOAD, packData.totalBytesToDownload) - downloadBundle.putLong(combineModule(KEY_TOTAL_BYTES_TO_DOWNLOAD, moduleName), packData.totalBytesToDownload) - downloadBundle.putLong(combineModule(KEY_PACK_VERSION, moduleName), packData.appVersionCode) - downloadBundle.putInt(combineModule(KEY_STATUS, moduleName), packData.status) - downloadBundle.putInt(combineModule(KEY_ERROR_CODE, moduleName), NO_ERROR) - downloadBundle.putLong(combineModule(KEY_BYTES_DOWNLOADED, moduleName), packData.bytesDownloaded) - downloadBundle.putLong(combineModule(KEY_PACK_BASE_VERSION, moduleName), packData.moduleVersion) + downloadBundle.put(BundleKeys.APP_VERSION_CODE, downloadData.appVersionCode.toInt()) + downloadBundle.put(BundleKeys.ERROR_CODE, AssetPackErrorCode.NO_ERROR) + downloadBundle.put(BundleKeys.SESSION_ID, downloadData.sessionIds[moduleName] ?: downloadData.status) + downloadBundle.put(BundleKeys.STATUS, packData.status) + downloadBundle.put(BundleKeys.PACK_NAMES, arrayListOf(moduleName)) + downloadBundle.put(BundleKeys.BYTES_DOWNLOADED, packData.bytesDownloaded) + downloadBundle.put(BundleKeys.TOTAL_BYTES_TO_DOWNLOAD, packData.totalBytesToDownload) + downloadBundle.put(BundleKeys.TOTAL_BYTES_TO_DOWNLOAD, moduleName, packData.totalBytesToDownload) + downloadBundle.put(BundleKeys.PACK_VERSION, moduleName, packData.appVersionCode) + downloadBundle.put(BundleKeys.STATUS, moduleName, packData.status) + downloadBundle.put(BundleKeys.ERROR_CODE, moduleName, AssetPackErrorCode.NO_ERROR) + downloadBundle.put(BundleKeys.BYTES_DOWNLOADED, moduleName, packData.bytesDownloaded) + downloadBundle.put(BundleKeys.PACK_BASE_VERSION, moduleName, packData.moduleVersion) val resultList = arraySetOf() packData.packBundleList.forEach { val result = Bundle() @@ -273,7 +251,7 @@ fun sendBroadcastForExistingFile(context: Context, downloadData: DownloadData, m resultList.add(result) } resultList.forEach { - val chunkName = it.getString(KEY_CHUNK_NAME) + val sliceId = it.getString(KEY_CHUNK_NAME) ?: "" val uncompressedSize = it.getLong(KEY_UNCOMPRESSED_SIZE) val uncompressedHashSha256 = it.getString(KEY_UNCOMPRESSED_HASH_SHA256) val numberOfSubcontractors = it.getInt(KEY_NUMBER_OF_SUBCONTRACTORS) @@ -289,30 +267,30 @@ fun sendBroadcastForExistingFile(context: Context, downloadData: DownloadData, m intent.setDataAndType(uri, context.contentResolver.getType(uri)) intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) val resourceBlockIndex = bundle?.getString(KEY_RESOURCE_BLOCK_NAME)?.toInt() - if (uFile?.exists() == true && bundle?.getString(KEY_CHUNK_NAME) == chunkName && resourceBlockIndex != null) { + if (uFile?.exists() == true && bundle?.getString(KEY_CHUNK_NAME) == sliceId && resourceBlockIndex != null) { if (chunkIntents[resourceBlockIndex] == null) { chunkIntents[resourceBlockIndex] = intent } } } - downloadBundle.putParcelableArrayList(combineModule(KEY_CHUNK_INTENT, moduleName, chunkName), chunkIntents) - downloadBundle.putLong(combineModule(KEY_UNCOMPRESSED_SIZE, moduleName, chunkName), uncompressedSize) - downloadBundle.putInt(combineModule(KEY_COMPRESSION_FORMAT, moduleName, chunkName), 1) - downloadBundle.putString(combineModule(KEY_UNCOMPRESSED_HASH_SHA256, moduleName, chunkName), uncompressedHashSha256) + downloadBundle.put(BundleKeys.CHUNK_INTENTS, moduleName, sliceId, chunkIntents) + downloadBundle.put(BundleKeys.UNCOMPRESSED_SIZE, moduleName, sliceId, uncompressedSize) + downloadBundle.put(BundleKeys.COMPRESSION_FORMAT, moduleName, sliceId, 1) // TODO + downloadBundle.put(BundleKeys.UNCOMPRESSED_HASH_SHA256, moduleName, sliceId, uncompressedHashSha256) } - downloadBundle.putStringArrayList(combineModule(KEY_SLICE_IDS, moduleName), packData.listOfSubcontractNames) + downloadBundle.put(BundleKeys.SLICE_IDS, moduleName, packData.listOfSubcontractNames) sendBroadCast(context, downloadData, downloadBundle) return downloadBundle } catch (e: Exception) { Log.w(TAG, "sendBroadcastForExistingFile error:" + e.message) - return Bundle(Bundle().apply { putInt(KEY_ERROR_CODE, API_NOT_AVAILABLE) }) + return Bundle(Bundle().apply { putInt(KEY_ERROR_CODE, AssetPackErrorCode.API_NOT_AVAILABLE) }) } } private fun sendBroadCast(context: Context, downloadData: DownloadData, result: Bundle) { val intent = Intent() - intent.setAction(ACTION_SESSION_UPDATE) - intent.putExtra(EXTRA_SESSION_STATE, result) + intent.setAction(BroadcastConstants.ACTION_SESSION_UPDATE) + intent.putExtra(BroadcastConstants.EXTRA_SESSION_STATE, result) intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY) intent.putExtra(FLAGS, Bundle().apply { putBoolean(KEY_USING_EXTRACTOR_STREAM, true) }) intent.setPackage(downloadData.packageName) From f78ffed510081b92908512b48b55697c0f70c3fc Mon Sep 17 00:00:00 2001 From: Marvin W Date: Wed, 20 Nov 2024 22:49:15 -0600 Subject: [PATCH 091/132] Asset Modules: Improve field naming --- .../google/android/finsky/DownloadManager.kt | 25 ++--- .../assetmoduleservice/AssetModuleService.kt | 32 +++--- .../finsky/assetmoduleservice/DownloadData.kt | 29 +++-- .../com/google/android/finsky/extensions.kt | 104 ++++++++---------- 4 files changed, 92 insertions(+), 98 deletions(-) diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/DownloadManager.kt b/vending-app/src/main/kotlin/com/google/android/finsky/DownloadManager.kt index 93aa2b57e9..43b4ff146d 100644 --- a/vending-app/src/main/kotlin/com/google/android/finsky/DownloadManager.kt +++ b/vending-app/src/main/kotlin/com/google/android/finsky/DownloadManager.kt @@ -22,10 +22,7 @@ import android.os.Build import android.util.Log import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat -import androidx.core.content.ContextCompat import com.android.vending.R -import com.google.android.finsky.assetmoduleservice.AssetModuleServiceImpl -import com.google.android.finsky.assetmoduleservice.AssetModuleServiceImpl.Companion import com.google.android.finsky.assetmoduleservice.DownloadData import com.google.android.play.core.assetpacks.model.AssetPackStatus import java.io.File @@ -140,19 +137,19 @@ class DownloadManager(private val context: Context) { val future = executor.submit { val packData = downloadData.getModuleData(moduleName) downloadData.updateDownloadStatus(moduleName, AssetPackStatus.DOWNLOADING) - for (dataBundle in packData.packBundleList) { - val resourcePackageName: String? = dataBundle.getString(KEY_RESOURCE_PACKAGE_NAME) - val chunkName: String? = dataBundle.getString(KEY_CHUNK_NAME) - val resourceLink: String? = dataBundle.getString(KEY_RESOURCE_LINK) - val index: Int = dataBundle.getInt(KEY_INDEX) - val resourceBlockName: String? = dataBundle.getString(KEY_RESOURCE_BLOCK_NAME) - if (resourcePackageName == null || chunkName == null || resourceLink == null || resourceBlockName == null) { + for (chunkData in packData.chunks) { + val moduleName: String? = chunkData.moduleName + val sliceId: String? = chunkData.sliceId + val chunkSourceUri: String? = chunkData.chunkSourceUri + val sessionId: Int = chunkData.sessionId + val chunkIndex: Int = chunkData.chunkIndex + if (moduleName == null || sliceId == null || chunkSourceUri == null || chunkIndex == null) { continue } - val filesDir = "${context.filesDir}/assetpacks/$index/$resourcePackageName/$chunkName/" - val destination = File(filesDir, resourceBlockName) - startDownload(moduleName, resourceLink, destination, downloadData) - sendBroadcastForExistingFile(context, downloadData, moduleName, dataBundle, destination) + val filesDir = "${context.filesDir}/assetpacks/$sessionId/$moduleName/$sliceId/" + val destination = File(filesDir, chunkIndex.toString()) + startDownload(moduleName, chunkSourceUri, destination, downloadData) + sendBroadcastForExistingFile(context, downloadData, moduleName, chunkData, destination) } updateProgress(moduleName, 100) notifyBuilderMap[moduleName]?.setOngoing(false) diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/AssetModuleService.kt b/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/AssetModuleService.kt index 6c6979cbd9..3ea2a7b07d 100644 --- a/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/AssetModuleService.kt +++ b/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/AssetModuleService.kt @@ -22,6 +22,7 @@ import com.google.android.play.core.assetpacks.model.AssetPackStatus import com.google.android.play.core.assetpacks.protocol.BundleKeys import com.google.android.play.core.assetpacks.protocol.IAssetModuleService import com.google.android.play.core.assetpacks.protocol.IAssetModuleServiceCallback +import org.microg.gms.common.PackageUtils import org.microg.gms.profile.ProfileManager import org.microg.vending.billing.core.HttpClient import java.io.File @@ -78,17 +79,14 @@ class AssetModuleServiceImpl( if (packData?.status == AssetPackStatus.FAILED) { callback?.onError(Bundle().apply { put(BundleKeys.ERROR_CODE, AssetPackErrorCode.NETWORK_ERROR) }) } - downloadData?.getModuleData(moduleName!!)?.packBundleList?.forEach { dataBundle -> - val destination = dataBundle.run { - val resourcePackageName = getString(KEY_RESOURCE_PACKAGE_NAME) - val chunkName = getString(KEY_CHUNK_NAME) - val resourceBlockName = getString(KEY_RESOURCE_BLOCK_NAME) - if (resourcePackageName == null || chunkName == null || resourceBlockName == null) return@forEach - File("${context.filesDir}/assetpacks/${getInt(KEY_INDEX)}/$resourcePackageName/$chunkName/", resourceBlockName) + downloadData?.getModuleData(moduleName!!)?.chunks?.forEach { chunkData -> + val destination = chunkData.run { + if (this.moduleName == null || sliceId == null || chunkIndex == null) return@forEach + File("${context.filesDir}/assetpacks/$sessionId/${this.moduleName}/$sliceId/", chunkIndex.toString()) } - if (destination.exists() && destination.length() == dataBundle.getLong(KEY_BYTE_LENGTH)) { - sendBroadcastForExistingFile(context, downloadData!!, moduleName, dataBundle, destination) + if (destination.exists() && destination.length() == chunkData.chunkBytesToDownload) { + sendBroadcastForExistingFile(context, downloadData!!, moduleName, chunkData, destination) } } } @@ -106,6 +104,7 @@ class AssetModuleServiceImpl( } override fun getSessionStates(packageName: String?, bundle: Bundle?, callback: IAssetModuleServiceCallback?) { + PackageUtils.getAndCheckCallingPackage(context, packageName) Log.d(TAG, "Method (getSessionStates) called by packageName -> $packageName") val installedAssetModuleNames = bundle?.get(BundleKeys.INSTALLED_ASSET_MODULE)?.flatMap { it.keySet().mapNotNull { subKey -> it.get(subKey) as? String } } @@ -119,17 +118,14 @@ class AssetModuleServiceImpl( listBundleData.add(sendBroadcastForExistingFile(context, downloadData!!, moduleName, null, null)) - downloadData?.getModuleData(moduleName)?.packBundleList?.forEach { dataBundle -> - val destination = dataBundle.run { - val resourcePackageName = getString(KEY_RESOURCE_PACKAGE_NAME) - val chunkName = getString(KEY_CHUNK_NAME) - val resourceBlockName = getString(KEY_RESOURCE_BLOCK_NAME) - if (resourcePackageName == null || chunkName == null || resourceBlockName == null) return@forEach - File("${context.filesDir}/assetpacks/${getInt(KEY_INDEX)}/$resourcePackageName/$chunkName/", resourceBlockName) + downloadData?.getModuleData(moduleName)?.chunks?.forEach { chunkData -> + val destination = chunkData.run { + if (this.moduleName == null || sliceId == null || chunkIndex == null) return@forEach + File("${context.filesDir}/assetpacks/$sessionId/${this.moduleName}/$sliceId/", chunkIndex.toString()) } - if (destination.exists() && destination.length() == dataBundle.getLong(KEY_BYTE_LENGTH)) { - listBundleData.add(sendBroadcastForExistingFile(context, downloadData!!, moduleName, dataBundle, destination)) + if (destination.exists() && destination.length() == chunkData.chunkBytesToDownload) { + listBundleData.add(sendBroadcastForExistingFile(context, downloadData!!, moduleName, chunkData, destination)) } } } diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/DownloadData.kt b/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/DownloadData.kt index 68423bb9f3..2a00c27270 100644 --- a/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/DownloadData.kt +++ b/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/DownloadData.kt @@ -5,7 +5,6 @@ package com.google.android.finsky.assetmoduleservice -import android.os.Bundle import java.io.Serializable data class DownloadData( @@ -17,11 +16,11 @@ data class DownloadData( var moduleNames: Set = emptySet(), var appVersionCode: Long = 0, var totalBytesToDownload: Long = 0, - var moduleDataList: Map = emptyMap() + var moduleDataMap: Map = emptyMap() ) : Serializable { - fun getModuleData(packName: String): ModuleData { - return moduleDataList[packName] ?: throw IllegalArgumentException("ModuleData for packName '$packName' not found.") + fun getModuleData(moduleName: String): ModuleData { + return moduleDataMap[moduleName] ?: throw IllegalArgumentException("ModuleData for moduleName '$moduleName' not found.") } fun incrementModuleBytesDownloaded(packName: String, bytes: Long) { @@ -32,7 +31,6 @@ data class DownloadData( fun updateDownloadStatus(packName: String, statusCode: Int) { getModuleData(packName).apply { status = statusCode - sessionId = statusCode } } @@ -42,17 +40,28 @@ data class DownloadData( } data class ModuleData( - var appVersionCode: Long = 0, + var packVersionCode: Long = 0, var moduleVersion: Long = 0, - var sessionId: Int = 0, var errorCode: Int = 0, var status: Int = 0, var bytesDownloaded: Long = 0, var totalBytesToDownload: Long = 0, - var packBundleList: List = emptyList(), - var listOfSubcontractNames: ArrayList? = null + var chunks: List = emptyList(), + var sliceIds: ArrayList? = null ) : Serializable { fun incrementBytesDownloaded(bytes: Long) { bytesDownloaded += bytes } -} \ No newline at end of file +} + +data class ChunkData( + val sessionId: Int, + val moduleName: String, + val sliceId: String?, + val chunkSourceUri: String?, + val chunkBytesToDownload: Long, + val chunkIndex: Int, + val sliceUncompressedSize: Long, + val sliceUncompressedHashSha256: String?, + val numberOfChunksInSlice: Int +) \ No newline at end of file diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/extensions.kt b/vending-app/src/main/kotlin/com/google/android/finsky/extensions.kt index 36f5c643f6..6ab47bb7e5 100644 --- a/vending-app/src/main/kotlin/com/google/android/finsky/extensions.kt +++ b/vending-app/src/main/kotlin/com/google/android/finsky/extensions.kt @@ -20,6 +20,7 @@ import com.android.vending.licensing.getAuthToken import com.android.vending.licensing.getLicenseRequestHeaders import com.google.android.finsky.assetmoduleservice.DownloadData import com.google.android.finsky.assetmoduleservice.ModuleData +import com.google.android.finsky.assetmoduleservice.ChunkData import com.google.android.play.core.assetpacks.model.AssetPackErrorCode import com.google.android.play.core.assetpacks.model.AssetPackStatus import com.google.android.play.core.assetpacks.protocol.BroadcastConstants @@ -33,17 +34,6 @@ import java.util.Collections const val KEY_ERROR_CODE = "error_code" const val KEY_MODULE_NAME = "module_name" -const val KEY_RESOURCE_PACKAGE_NAME = "resourcePackageName" - -const val KEY_CACHE_DIR = "CacheDir" -const val KEY_INDEX = "index" -const val KEY_CHUNK_NAME = "chunkName" -const val KEY_RESOURCE_LINK = "resourceLink" -const val KEY_BYTE_LENGTH = "byteLength" -const val KEY_RESOURCE_BLOCK_NAME = "resourceBlockName" -const val KEY_UNCOMPRESSED_SIZE = "uncompressed_size" -const val KEY_UNCOMPRESSED_HASH_SHA256 = "uncompressed_hash_sha256" -const val KEY_NUMBER_OF_SUBCONTRACTORS = "numberOfSubcontractors" const val KEY_USING_EXTRACTOR_STREAM = "usingExtractorStream" @@ -118,6 +108,18 @@ fun HttpClient.initAssertModuleData( return initModuleDownloadInfo(context, packageName, moduleDeliveryInfo) } +private val lock = Any() +private fun Context.generateSessionId(): Int { + synchronized(lock) { + val sharedPreferences = getSharedPreferences("AssetModuleSessionIdGenerator", 0) + val latest = sharedPreferences.getInt("Latest", 0) + 1 + val edit = sharedPreferences.edit() + edit.putInt("Latest", latest) + edit.commit() + return latest + } +} + private fun initModuleDownloadInfo(context: Context, packageName: String, deliveryInfo: ModuleDeliveryInfo?): DownloadData? { if (deliveryInfo == null || deliveryInfo.status != null) { return null @@ -137,47 +139,45 @@ private fun initModuleDownloadInfo(context: Context, packageName: String, delive sessionIds[moduleName] = moduleIndex + 10 var totalSumOfSubcontractedModules = 0 val sliceIds: ArrayList = ArrayList() - val dataBundle: ArrayList = arrayListOf() + val chunkDatas: ArrayList = arrayListOf() for (sliceIndex in slices.indices) { val sliceInfo: SliceInfo = slices[sliceIndex] if (sliceInfo.metadata == null || sliceInfo.fullDownloadInfo == null) { continue } val chunks = sliceInfo.fullDownloadInfo.chunks - val chunkNumber = chunks.size + val numberOfChunks = chunks.size val uncompressedSize = sliceInfo.fullDownloadInfo.uncompressedSize val uncompressedHashSha256 = sliceInfo.fullDownloadInfo.uncompressedHashSha256 - val chunkName = sliceInfo.metadata.sliceId?.also { sliceIds.add(it) } + val sliceId = sliceInfo.metadata.sliceId?.also { sliceIds.add(it) } var sliceBytesToDownload = 0L for (chunkIndex in chunks.indices) { val dResource: ChunkInfo = chunks[chunkIndex] sliceBytesToDownload += dResource.bytesToDownload!! totalSumOfSubcontractedModules += 1 - val bundle = Bundle() - bundle.putString(KEY_CACHE_DIR, context.cacheDir.toString()) - bundle.putInt(KEY_INDEX, moduleIndex + 10) - bundle.putString(KEY_RESOURCE_PACKAGE_NAME, moduleName) - bundle.putString(KEY_CHUNK_NAME, chunkName) - bundle.putString(KEY_RESOURCE_LINK, dResource.sourceUri) - bundle.putLong(KEY_BYTE_LENGTH, dResource.bytesToDownload) - bundle.putString(KEY_RESOURCE_BLOCK_NAME, chunkIndex.toString()) - bundle.putLong(KEY_UNCOMPRESSED_SIZE, uncompressedSize ?: 0) - bundle.putString(KEY_UNCOMPRESSED_HASH_SHA256, uncompressedHashSha256) - bundle.putInt(KEY_NUMBER_OF_SUBCONTRACTORS, chunkNumber) - dataBundle.add(bundle) + chunkDatas.add(ChunkData( + sessionId = sessionIds[moduleName]!!, + moduleName = moduleName, + sliceId = sliceId, + chunkSourceUri = dResource.sourceUri, + chunkBytesToDownload = dResource.bytesToDownload, + chunkIndex = chunkIndex, + sliceUncompressedSize = uncompressedSize ?: 0, + sliceUncompressedHashSha256 = uncompressedHashSha256, + numberOfChunksInSlice = numberOfChunks + )) } moduleBytesToDownload += sliceBytesToDownload } val moduleData = ModuleData( - appVersionCode = packVersionCode, + packVersionCode = packVersionCode, moduleVersion = 0, - sessionId = AssetPackStatus.NOT_INSTALLED, errorCode = AssetPackErrorCode.NO_ERROR, status = AssetPackStatus.NOT_INSTALLED, bytesDownloaded = 0, totalBytesToDownload = moduleBytesToDownload, - packBundleList = dataBundle, - listOfSubcontractNames = sliceIds + chunks = chunkDatas, + sliceIds = sliceIds ) totalBytesToDownload += moduleBytesToDownload moduleDataMap[moduleName] = moduleData @@ -202,14 +202,16 @@ fun buildDownloadBundle(downloadData: DownloadData, list: List? = null) var bytesDownloaded = 0L list?.forEach { - val moduleName = it?.getString(KEY_MODULE_NAME) + val moduleName = it?.get(BundleKeys.MODULE_NAME) val packData = downloadData.getModuleData(moduleName!!) bundleData.put(BundleKeys.STATUS, packData.status) - downloadData.sessionIds[moduleName]?.let { sessionId -> bundleData.put(BundleKeys.SESSION_ID, sessionId) } - bundleData.put(BundleKeys.SESSION_ID, moduleName, packData.sessionId) + downloadData.sessionIds[moduleName]?.let { sessionId -> + bundleData.put(BundleKeys.SESSION_ID, sessionId) + bundleData.put(BundleKeys.SESSION_ID, moduleName, packData.status) + } bundleData.put(BundleKeys.STATUS, moduleName, packData.status) bundleData.put(BundleKeys.ERROR_CODE, moduleName, packData.errorCode) - bundleData.put(BundleKeys.PACK_VERSION, moduleName, packData.appVersionCode) + bundleData.put(BundleKeys.PACK_VERSION, moduleName, packData.packVersionCode) bundleData.put(BundleKeys.PACK_BASE_VERSION, moduleName, packData.moduleVersion) bundleData.put(BundleKeys.BYTES_DOWNLOADED, moduleName, packData.bytesDownloaded) bundleData.put(BundleKeys.TOTAL_BYTES_TO_DOWNLOAD, moduleName, packData.totalBytesToDownload) @@ -223,7 +225,7 @@ fun buildDownloadBundle(downloadData: DownloadData, list: List? = null) return bundleData } -fun sendBroadcastForExistingFile(context: Context, downloadData: DownloadData, moduleName: String, bundle: Bundle?, destination: File?): Bundle { +fun sendBroadcastForExistingFile(context: Context, downloadData: DownloadData, moduleName: String, chunkData: ChunkData?, destination: File?): Bundle { val packData = downloadData.getModuleData(moduleName) try { val downloadBundle = Bundle() @@ -235,39 +237,29 @@ fun sendBroadcastForExistingFile(context: Context, downloadData: DownloadData, m downloadBundle.put(BundleKeys.BYTES_DOWNLOADED, packData.bytesDownloaded) downloadBundle.put(BundleKeys.TOTAL_BYTES_TO_DOWNLOAD, packData.totalBytesToDownload) downloadBundle.put(BundleKeys.TOTAL_BYTES_TO_DOWNLOAD, moduleName, packData.totalBytesToDownload) - downloadBundle.put(BundleKeys.PACK_VERSION, moduleName, packData.appVersionCode) + downloadBundle.put(BundleKeys.PACK_VERSION, moduleName, packData.packVersionCode) downloadBundle.put(BundleKeys.STATUS, moduleName, packData.status) downloadBundle.put(BundleKeys.ERROR_CODE, moduleName, AssetPackErrorCode.NO_ERROR) downloadBundle.put(BundleKeys.BYTES_DOWNLOADED, moduleName, packData.bytesDownloaded) downloadBundle.put(BundleKeys.PACK_BASE_VERSION, moduleName, packData.moduleVersion) - val resultList = arraySetOf() - packData.packBundleList.forEach { - val result = Bundle() - result.putString(KEY_CHUNK_NAME, it.getString(KEY_CHUNK_NAME)) - result.putLong(KEY_UNCOMPRESSED_SIZE, it.getLong(KEY_UNCOMPRESSED_SIZE)) - result.putString(KEY_UNCOMPRESSED_HASH_SHA256, it.getString(KEY_UNCOMPRESSED_HASH_SHA256)) - result.putInt(KEY_NUMBER_OF_SUBCONTRACTORS, it.getInt(KEY_NUMBER_OF_SUBCONTRACTORS)) - result.putLong(KEY_BYTE_LENGTH, it.getLong(KEY_BYTE_LENGTH)) - resultList.add(result) - } - resultList.forEach { - val sliceId = it.getString(KEY_CHUNK_NAME) ?: "" - val uncompressedSize = it.getLong(KEY_UNCOMPRESSED_SIZE) - val uncompressedHashSha256 = it.getString(KEY_UNCOMPRESSED_HASH_SHA256) - val numberOfSubcontractors = it.getInt(KEY_NUMBER_OF_SUBCONTRACTORS) + packData.chunks.map { it.copy() }.forEach { + val sliceId = it.sliceId ?: "" + val uncompressedSize = it.sliceUncompressedSize + val uncompressedHashSha256 = it.sliceUncompressedHashSha256 + val numberOfChunksInSlice = it.numberOfChunksInSlice val chunkIntents: ArrayList if (destination == null) { - chunkIntents = ArrayList(Collections.nCopies(numberOfSubcontractors, null)) + chunkIntents = ArrayList(Collections.nCopies(numberOfChunksInSlice, null)) } else { val uFile = Uri.parse(destination.absolutePath).path?.let { path -> File(path) } - chunkIntents = ArrayList(Collections.nCopies(numberOfSubcontractors, null)) + chunkIntents = ArrayList(Collections.nCopies(numberOfChunksInSlice, null)) val uri = Uri.fromFile(uFile) context.grantUriPermission(moduleName, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION) val intent = Intent(ACTION_VIEW) intent.setDataAndType(uri, context.contentResolver.getType(uri)) intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) - val resourceBlockIndex = bundle?.getString(KEY_RESOURCE_BLOCK_NAME)?.toInt() - if (uFile?.exists() == true && bundle?.getString(KEY_CHUNK_NAME) == sliceId && resourceBlockIndex != null) { + val resourceBlockIndex = chunkData?.chunkIndex?.toInt() + if (uFile?.exists() == true && chunkData?.sliceId == sliceId && resourceBlockIndex != null) { if (chunkIntents[resourceBlockIndex] == null) { chunkIntents[resourceBlockIndex] = intent } @@ -278,7 +270,7 @@ fun sendBroadcastForExistingFile(context: Context, downloadData: DownloadData, m downloadBundle.put(BundleKeys.COMPRESSION_FORMAT, moduleName, sliceId, 1) // TODO downloadBundle.put(BundleKeys.UNCOMPRESSED_HASH_SHA256, moduleName, sliceId, uncompressedHashSha256) } - downloadBundle.put(BundleKeys.SLICE_IDS, moduleName, packData.listOfSubcontractNames) + downloadBundle.put(BundleKeys.SLICE_IDS, moduleName, ArrayList(packData.chunks.mapNotNull { it.sliceId })) sendBroadCast(context, downloadData, downloadBundle) return downloadBundle } catch (e: Exception) { From 90c5fd8091137fc16fd90dd146a64e607e2fd281 Mon Sep 17 00:00:00 2001 From: Marvin W Date: Wed, 20 Nov 2024 22:50:32 -0600 Subject: [PATCH 092/132] Asset Modules: Allow multiple apps to download concurrently --- .../assetmoduleservice/AssetModuleService.kt | 82 ++++++++++--------- .../com/google/android/finsky/extensions.kt | 6 +- 2 files changed, 47 insertions(+), 41 deletions(-) diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/AssetModuleService.kt b/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/AssetModuleService.kt index 3ea2a7b07d..d493842c8f 100644 --- a/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/AssetModuleService.kt +++ b/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/AssetModuleService.kt @@ -30,7 +30,6 @@ import java.io.File private const val TAG = "AssetModuleService" class AssetModuleService : LifecycleService() { - private lateinit var httpClient: HttpClient private lateinit var accountManager: AccountManager @@ -40,65 +39,71 @@ class AssetModuleService : LifecycleService() { ProfileManager.ensureInitialized(this) accountManager = AccountManager.get(this) httpClient = HttpClient(this) - return AssetModuleServiceImpl(this, lifecycle, httpClient, accountManager).asBinder() + return AssetModuleServiceImpl(this, lifecycle, httpClient, accountManager, packageDownloadData).asBinder() } - override fun onUnbind(intent: Intent?): Boolean { - Log.d(TAG, "onUnbind: ") + override fun onDestroy() { + Log.d(TAG, "onDestroy: ") httpClient.requestQueue.cancelAll(TAG_REQUEST) - return super.onUnbind(intent) + super.onDestroy() + } + + companion object { + private val packageDownloadData = mutableMapOf() } } class AssetModuleServiceImpl( - val context: Context, override val lifecycle: Lifecycle, private val httpClient: HttpClient, private val accountManager: AccountManager + val context: Context, override val lifecycle: Lifecycle, private val httpClient: HttpClient, private val accountManager: AccountManager, + val packageDownloadData: MutableMap ) : IAssetModuleService.Stub(), LifecycleOwner { private val fileDescriptorMap = mutableMapOf() override fun startDownload(packageName: String?, list: MutableList?, bundle: Bundle?, callback: IAssetModuleServiceCallback?) { + PackageUtils.getAndCheckCallingPackage(context, packageName) Log.d(TAG, "Method (startDownload) called by packageName -> $packageName") if (packageName == null || list == null || bundle == null || !VendingPreferences.isAssetDeliveryEnabled(context)) { callback?.onError(Bundle().apply { put(BundleKeys.ERROR_CODE, AssetPackErrorCode.API_NOT_AVAILABLE) }) return } - if (downloadData == null || downloadData?.packageName != packageName) { + if (packageDownloadData[packageName] == null || packageDownloadData[packageName]?.packageName != packageName) { val requestedAssetModuleNames = list.map { it.get(BundleKeys.MODULE_NAME) }.filter { !it.isNullOrEmpty() } val playCoreVersionCode = bundle.get(BundleKeys.PLAY_CORE_VERSION_CODE) - downloadData = httpClient.initAssertModuleData(context, packageName, accountManager, requestedAssetModuleNames, playCoreVersionCode ?: 0) - if (downloadData == null) { + packageDownloadData[packageName] = httpClient.initAssertModuleData(context, packageName, accountManager, requestedAssetModuleNames, playCoreVersionCode ?: 0) + if (packageDownloadData[packageName] == null) { callback?.onError(Bundle().apply { put(BundleKeys.ERROR_CODE, AssetPackErrorCode.API_NOT_AVAILABLE) }) return } } list.forEach { val moduleName = it.get(BundleKeys.MODULE_NAME) - val packData = downloadData?.getModuleData(moduleName!!) - if (packData?.status != AssetPackStatus.DOWNLOADING && packData?.status != AssetPackStatus.COMPLETED) { - downloadData?.updateDownloadStatus(moduleName!!, AssetPackStatus.PENDING) + val moduleData = packageDownloadData[packageName]?.getModuleData(moduleName!!) + if (moduleData?.status != AssetPackStatus.DOWNLOADING && moduleData?.status != AssetPackStatus.COMPLETED) { + packageDownloadData[packageName]?.updateDownloadStatus(moduleName!!, AssetPackStatus.PENDING) } - if (packData?.status == AssetPackStatus.FAILED) { + if (moduleData?.status == AssetPackStatus.FAILED) { callback?.onError(Bundle().apply { put(BundleKeys.ERROR_CODE, AssetPackErrorCode.NETWORK_ERROR) }) } - downloadData?.getModuleData(moduleName!!)?.chunks?.forEach { chunkData -> + packageDownloadData[packageName]?.getModuleData(moduleName!!)?.chunks?.forEach { chunkData -> val destination = chunkData.run { if (this.moduleName == null || sliceId == null || chunkIndex == null) return@forEach File("${context.filesDir}/assetpacks/$sessionId/${this.moduleName}/$sliceId/", chunkIndex.toString()) } if (destination.exists() && destination.length() == chunkData.chunkBytesToDownload) { - sendBroadcastForExistingFile(context, downloadData!!, moduleName, chunkData, destination) + sendBroadcastForExistingFile(context, packageDownloadData[packageName]!!, moduleName, chunkData, destination) } } } - val bundleData = buildDownloadBundle(downloadData!!, list) + val bundleData = buildDownloadBundle(packageDownloadData[packageName]!!, list) Log.d(TAG, "startDownload: $bundleData") callback?.onStartDownload(-1, bundleData) list.forEach { val moduleName = it.get(BundleKeys.MODULE_NAME) - val packData = downloadData?.getModuleData(moduleName!!) + val packData = packageDownloadData[packageName]?.getModuleData(moduleName!!) if (packData?.status == AssetPackStatus.PENDING) { DownloadManager.get(context).shouldStop(false) - DownloadManager.get(context).prepareDownload(downloadData!!, moduleName!!) + DownloadManager.get(context).prepareDownload(packageDownloadData[packageName]!!, moduleName!!) } } } @@ -112,20 +117,20 @@ class AssetModuleServiceImpl( val listBundleData: MutableList = mutableListOf() - packageName.takeIf { it == downloadData?.packageName }?.let { - downloadData?.moduleNames?.forEach { moduleName -> + packageName.takeIf { it == packageDownloadData[packageName]?.packageName }?.let { + packageDownloadData[packageName]?.moduleNames?.forEach { moduleName -> if (moduleName in installedAssetModuleNames) return@forEach - listBundleData.add(sendBroadcastForExistingFile(context, downloadData!!, moduleName, null, null)) + listBundleData.add(sendBroadcastForExistingFile(context, packageDownloadData[packageName]!!, moduleName, null, null)) - downloadData?.getModuleData(moduleName)?.chunks?.forEach { chunkData -> + packageDownloadData[packageName]?.getModuleData(moduleName)?.chunks?.forEach { chunkData -> val destination = chunkData.run { if (this.moduleName == null || sliceId == null || chunkIndex == null) return@forEach File("${context.filesDir}/assetpacks/$sessionId/${this.moduleName}/$sliceId/", chunkIndex.toString()) } if (destination.exists() && destination.length() == chunkData.chunkBytesToDownload) { - listBundleData.add(sendBroadcastForExistingFile(context, downloadData!!, moduleName, chunkData, destination)) + listBundleData.add(sendBroadcastForExistingFile(context, packageDownloadData[packageName]!!, moduleName, chunkData, destination)) } } } @@ -135,6 +140,7 @@ class AssetModuleServiceImpl( } override fun notifyChunkTransferred(packageName: String?, bundle: Bundle?, bundle2: Bundle?, callback: IAssetModuleServiceCallback?) { + PackageUtils.getAndCheckCallingPackage(context, packageName) Log.d(TAG, "Method (notifyChunkTransferred) called by packageName -> $packageName") val moduleName = bundle?.get(BundleKeys.MODULE_NAME) if (moduleName.isNullOrEmpty() || !VendingPreferences.isAssetDeliveryEnabled(context)) { @@ -151,17 +157,18 @@ class AssetModuleServiceImpl( } override fun notifyModuleCompleted(packageName: String?, bundle: Bundle?, bundle2: Bundle?, callback: IAssetModuleServiceCallback?) { + PackageUtils.getAndCheckCallingPackage(context, packageName) Log.d(TAG, "Method (notifyModuleCompleted) called but not implemented by packageName -> $packageName") val moduleName = bundle?.get(BundleKeys.MODULE_NAME) if (moduleName.isNullOrEmpty() || !VendingPreferences.isAssetDeliveryEnabled(context)) { callback?.onError(Bundle().apply { put(BundleKeys.ERROR_CODE, AssetPackErrorCode.API_NOT_AVAILABLE) }) return } - Log.d(TAG, "notify: moduleName: $moduleName packNames: ${downloadData?.moduleNames}") - downloadData?.updateDownloadStatus(moduleName, AssetPackStatus.COMPLETED) - sendBroadcastForExistingFile(context, downloadData!!, moduleName, null, null) + Log.d(TAG, "notify: moduleName: $moduleName packNames: ${packageDownloadData[packageName]?.moduleNames}") + packageDownloadData[packageName]?.updateDownloadStatus(moduleName, AssetPackStatus.COMPLETED) + sendBroadcastForExistingFile(context, packageDownloadData[packageName]!!, moduleName, null, null) - val sessionId = downloadData!!.sessionIds[moduleName] + val sessionId = packageDownloadData[packageName]!!.sessionIds[moduleName] val downLoadFile = "${context.filesDir.absolutePath}/assetpacks/$sessionId/$moduleName" val directory = File(downLoadFile) @@ -175,15 +182,18 @@ class AssetModuleServiceImpl( } override fun notifySessionFailed(packageName: String?, bundle: Bundle?, bundle2: Bundle?, callback: IAssetModuleServiceCallback?) { + PackageUtils.getAndCheckCallingPackage(context, packageName) Log.d(TAG, "Method (notifySessionFailed) called but not implemented by packageName -> $packageName") callback?.onError(Bundle().apply { put(BundleKeys.ERROR_CODE, AssetPackErrorCode.API_NOT_AVAILABLE) }) } override fun keepAlive(packageName: String?, bundle: Bundle?, callback: IAssetModuleServiceCallback?) { + PackageUtils.getAndCheckCallingPackage(context, packageName) Log.d(TAG, "Method (keepAlive) called but not implemented by packageName -> $packageName") } override fun getChunkFileDescriptor(packageName: String, bundle: Bundle, bundle2: Bundle, callback: IAssetModuleServiceCallback?) { + PackageUtils.getAndCheckCallingPackage(context, packageName) Log.d(TAG, "Method (getChunkFileDescriptor) called by packageName -> $packageName") val moduleName = bundle.get(BundleKeys.MODULE_NAME) if (moduleName.isNullOrEmpty() || !VendingPreferences.isAssetDeliveryEnabled(context)) { @@ -204,44 +214,42 @@ class AssetModuleServiceImpl( } override fun requestDownloadInfo(packageName: String?, list: MutableList?, bundle: Bundle?, callback: IAssetModuleServiceCallback?) { + PackageUtils.getAndCheckCallingPackage(context, packageName) Log.d(TAG, "Method (requestDownloadInfo) called by packageName -> $packageName") if (packageName == null || list == null || bundle == null || !VendingPreferences.isAssetDeliveryEnabled(context)) { callback?.onError(Bundle().apply { put(BundleKeys.ERROR_CODE, AssetPackErrorCode.API_NOT_AVAILABLE) }) return } - if (downloadData == null || downloadData?.packageName != packageName) { + if (packageDownloadData[packageName] == null || packageDownloadData[packageName]?.packageName != packageName) { val requestedAssetModuleNames = list.map { it.get(BundleKeys.MODULE_NAME) }.filter { !it.isNullOrEmpty() } val playCoreVersionCode = bundle.get(BundleKeys.PLAY_CORE_VERSION_CODE) ?: 0 - downloadData = httpClient.initAssertModuleData(context, packageName, accountManager, requestedAssetModuleNames, playCoreVersionCode) - if (downloadData == null) { + packageDownloadData[packageName] = httpClient.initAssertModuleData(context, packageName, accountManager, requestedAssetModuleNames, playCoreVersionCode) + if (packageDownloadData[packageName] == null) { callback?.onError(Bundle().apply { put(BundleKeys.ERROR_CODE, AssetPackErrorCode.API_NOT_AVAILABLE) }) return } } list.forEach { val moduleName = it.get(BundleKeys.MODULE_NAME) - val packData = downloadData?.getModuleData(moduleName!!) + val packData = packageDownloadData[packageName]?.getModuleData(moduleName!!) if (packData?.status == AssetPackStatus.FAILED) { callback?.onError(Bundle().apply { put(BundleKeys.ERROR_CODE, AssetPackErrorCode.NETWORK_ERROR) }) } } - val bundleData = buildDownloadBundle(downloadData!!, list) + val bundleData = buildDownloadBundle(packageDownloadData[packageName]!!, list) Log.d(TAG, "requestDownloadInfo: $bundleData") callback?.onRequestDownloadInfo(bundleData, bundleData) } override fun removeModule(packageName: String?, bundle: Bundle?, bundle2: Bundle?, callback: IAssetModuleServiceCallback?) { + PackageUtils.getAndCheckCallingPackage(context, packageName) Log.d(TAG, "Method (removeModule) called but not implemented by packageName -> $packageName") callback?.onError(Bundle().apply { put(BundleKeys.ERROR_CODE, AssetPackErrorCode.API_NOT_AVAILABLE) }) } override fun cancelDownloads(packageName: String?, list: MutableList?, bundle: Bundle?, callback: IAssetModuleServiceCallback?) { + PackageUtils.getAndCheckCallingPackage(context, packageName) Log.d(TAG, "Method (cancelDownloads) called but not implemented by packageName -> $packageName") callback?.onError(Bundle().apply { put(BundleKeys.ERROR_CODE, AssetPackErrorCode.API_NOT_AVAILABLE) }) } - - companion object { - @Volatile - private var downloadData: DownloadData? = null - } } \ No newline at end of file diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/extensions.kt b/vending-app/src/main/kotlin/com/google/android/finsky/extensions.kt index 6ab47bb7e5..eb2c028711 100644 --- a/vending-app/src/main/kotlin/com/google/android/finsky/extensions.kt +++ b/vending-app/src/main/kotlin/com/google/android/finsky/extensions.kt @@ -32,12 +32,10 @@ import org.microg.vending.billing.core.HttpClient import java.io.File import java.util.Collections -const val KEY_ERROR_CODE = "error_code" const val KEY_MODULE_NAME = "module_name" const val KEY_USING_EXTRACTOR_STREAM = "usingExtractorStream" -const val ACTION_VIEW = "android.intent.action.VIEW" const val TAG_REQUEST = "asset_module" private const val FLAGS = "com.google.android.play.core.FLAGS" @@ -255,7 +253,7 @@ fun sendBroadcastForExistingFile(context: Context, downloadData: DownloadData, m chunkIntents = ArrayList(Collections.nCopies(numberOfChunksInSlice, null)) val uri = Uri.fromFile(uFile) context.grantUriPermission(moduleName, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION) - val intent = Intent(ACTION_VIEW) + val intent = Intent(Intent.ACTION_VIEW) intent.setDataAndType(uri, context.contentResolver.getType(uri)) intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) val resourceBlockIndex = chunkData?.chunkIndex?.toInt() @@ -275,7 +273,7 @@ fun sendBroadcastForExistingFile(context: Context, downloadData: DownloadData, m return downloadBundle } catch (e: Exception) { Log.w(TAG, "sendBroadcastForExistingFile error:" + e.message) - return Bundle(Bundle().apply { putInt(KEY_ERROR_CODE, AssetPackErrorCode.API_NOT_AVAILABLE) }) + return Bundle(Bundle().apply { put(BundleKeys.ERROR_CODE, AssetPackErrorCode.API_NOT_AVAILABLE) }) } } From b1f33911229613e8af378003312fd6ab2e58d748 Mon Sep 17 00:00:00 2001 From: davinci9196 Date: Fri, 22 Nov 2024 11:48:24 +0800 Subject: [PATCH 093/132] Asset Modules: Modify the interface resource list to version V3 Implement deduplication processing for broadcast data to fix resource download and installation anomalies. --- .../src/main/kotlin/com/google/android/finsky/extensions.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/extensions.kt b/vending-app/src/main/kotlin/com/google/android/finsky/extensions.kt index eb2c028711..9c9d63020d 100644 --- a/vending-app/src/main/kotlin/com/google/android/finsky/extensions.kt +++ b/vending-app/src/main/kotlin/com/google/android/finsky/extensions.kt @@ -62,7 +62,7 @@ fun HttpClient.initAssertModuleData( accountManager: AccountManager, requestedAssetModuleNames: List, playCoreVersionCode: Int, - supportedCompressionFormats: List = listOf(0, 1), + supportedCompressionFormats: List = listOf(0, 3), supportedPatchFormats: List = listOf(1, 2), ): DownloadData? { val accounts = accountManager.getAccountsByType(AuthConstants.DEFAULT_ACCOUNT_TYPE) @@ -268,7 +268,7 @@ fun sendBroadcastForExistingFile(context: Context, downloadData: DownloadData, m downloadBundle.put(BundleKeys.COMPRESSION_FORMAT, moduleName, sliceId, 1) // TODO downloadBundle.put(BundleKeys.UNCOMPRESSED_HASH_SHA256, moduleName, sliceId, uncompressedHashSha256) } - downloadBundle.put(BundleKeys.SLICE_IDS, moduleName, ArrayList(packData.chunks.mapNotNull { it.sliceId })) + downloadBundle.put(BundleKeys.SLICE_IDS, moduleName, ArrayList(packData.chunks.mapNotNull { it.sliceId }.distinct())) sendBroadCast(context, downloadData, downloadBundle) return downloadBundle } catch (e: Exception) { From 5a99724b8098ecfee6f3c3e07b91b55cedb448f8 Mon Sep 17 00:00:00 2001 From: davinci9196 Date: Fri, 22 Nov 2024 11:51:26 +0800 Subject: [PATCH 094/132] Asset Modules: Resolving conflicts --- play-services-core/src/huawei/AndroidManifest.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/play-services-core/src/huawei/AndroidManifest.xml b/play-services-core/src/huawei/AndroidManifest.xml index b4c56a066f..a6ca2b9e94 100644 --- a/play-services-core/src/huawei/AndroidManifest.xml +++ b/play-services-core/src/huawei/AndroidManifest.xml @@ -32,10 +32,10 @@ android:name="org.microg.gms.settings.vending_billing" android:value="true" /> Date: Wed, 4 Dec 2024 10:36:27 +0800 Subject: [PATCH 095/132] Asset Modules: Fixed the issue that when the application requests the sub-package content multiple times, the same sessionId causes repeated calls. --- .../google/android/finsky/DownloadManager.kt | 4 +-- .../assetmoduleservice/AssetModuleService.kt | 29 +++++++++------- .../finsky/assetmoduleservice/DownloadData.kt | 4 --- .../com/google/android/finsky/extensions.kt | 34 ++++++++++++++----- 4 files changed, 44 insertions(+), 27 deletions(-) diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/DownloadManager.kt b/vending-app/src/main/kotlin/com/google/android/finsky/DownloadManager.kt index 43b4ff146d..1b8af213df 100644 --- a/vending-app/src/main/kotlin/com/google/android/finsky/DownloadManager.kt +++ b/vending-app/src/main/kotlin/com/google/android/finsky/DownloadManager.kt @@ -138,12 +138,12 @@ class DownloadManager(private val context: Context) { val packData = downloadData.getModuleData(moduleName) downloadData.updateDownloadStatus(moduleName, AssetPackStatus.DOWNLOADING) for (chunkData in packData.chunks) { - val moduleName: String? = chunkData.moduleName + val moduleName: String = chunkData.moduleName val sliceId: String? = chunkData.sliceId val chunkSourceUri: String? = chunkData.chunkSourceUri val sessionId: Int = chunkData.sessionId val chunkIndex: Int = chunkData.chunkIndex - if (moduleName == null || sliceId == null || chunkSourceUri == null || chunkIndex == null) { + if (sliceId == null || chunkSourceUri == null) { continue } val filesDir = "${context.filesDir}/assetpacks/$sessionId/$moduleName/$sliceId/" diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/AssetModuleService.kt b/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/AssetModuleService.kt index d493842c8f..c650fb66ed 100644 --- a/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/AssetModuleService.kt +++ b/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/AssetModuleService.kt @@ -55,7 +55,7 @@ class AssetModuleService : LifecycleService() { class AssetModuleServiceImpl( val context: Context, override val lifecycle: Lifecycle, private val httpClient: HttpClient, private val accountManager: AccountManager, - val packageDownloadData: MutableMap + private val packageDownloadData: MutableMap ) : IAssetModuleService.Stub(), LifecycleOwner { private val fileDescriptorMap = mutableMapOf() @@ -66,8 +66,11 @@ class AssetModuleServiceImpl( callback?.onError(Bundle().apply { put(BundleKeys.ERROR_CODE, AssetPackErrorCode.API_NOT_AVAILABLE) }) return } - if (packageDownloadData[packageName] == null || packageDownloadData[packageName]?.packageName != packageName) { - val requestedAssetModuleNames = list.map { it.get(BundleKeys.MODULE_NAME) }.filter { !it.isNullOrEmpty() } + val requestedAssetModuleNames = list.map { it.getString(KEY_MODULE_NAME) }.filter { !it.isNullOrEmpty() } + if (packageDownloadData[packageName] == null || + packageDownloadData[packageName]?.packageName != packageName || + packageDownloadData[packageName]?.moduleNames?.intersect(requestedAssetModuleNames.toSet())?.isEmpty() == true + ) { val playCoreVersionCode = bundle.get(BundleKeys.PLAY_CORE_VERSION_CODE) packageDownloadData[packageName] = httpClient.initAssertModuleData(context, packageName, accountManager, requestedAssetModuleNames, playCoreVersionCode ?: 0) if (packageDownloadData[packageName] == null) { @@ -75,18 +78,18 @@ class AssetModuleServiceImpl( return } } - list.forEach { - val moduleName = it.get(BundleKeys.MODULE_NAME) + list.forEach { data -> + val moduleName = data.get(BundleKeys.MODULE_NAME) val moduleData = packageDownloadData[packageName]?.getModuleData(moduleName!!) if (moduleData?.status != AssetPackStatus.DOWNLOADING && moduleData?.status != AssetPackStatus.COMPLETED) { packageDownloadData[packageName]?.updateDownloadStatus(moduleName!!, AssetPackStatus.PENDING) + sendBroadcastForExistingFile(context, packageDownloadData[packageName]!!, moduleName!!, null, null) } if (moduleData?.status == AssetPackStatus.FAILED) { callback?.onError(Bundle().apply { put(BundleKeys.ERROR_CODE, AssetPackErrorCode.NETWORK_ERROR) }) } - packageDownloadData[packageName]?.getModuleData(moduleName!!)?.chunks?.forEach { chunkData -> + packageDownloadData[packageName]?.getModuleData(moduleName!!)?.chunks?.filter { it.sliceId != null }?.forEach { chunkData -> val destination = chunkData.run { - if (this.moduleName == null || sliceId == null || chunkIndex == null) return@forEach File("${context.filesDir}/assetpacks/$sessionId/${this.moduleName}/$sliceId/", chunkIndex.toString()) } @@ -123,14 +126,13 @@ class AssetModuleServiceImpl( listBundleData.add(sendBroadcastForExistingFile(context, packageDownloadData[packageName]!!, moduleName, null, null)) - packageDownloadData[packageName]?.getModuleData(moduleName)?.chunks?.forEach { chunkData -> + packageDownloadData[packageName]?.getModuleData(moduleName)?.chunks?.filter { it.sliceId != null }?.forEach { chunkData -> val destination = chunkData.run { - if (this.moduleName == null || sliceId == null || chunkIndex == null) return@forEach File("${context.filesDir}/assetpacks/$sessionId/${this.moduleName}/$sliceId/", chunkIndex.toString()) } if (destination.exists() && destination.length() == chunkData.chunkBytesToDownload) { - listBundleData.add(sendBroadcastForExistingFile(context, packageDownloadData[packageName]!!, moduleName, chunkData, destination)) + sendBroadcastForExistingFile(context, packageDownloadData[packageName]!!, moduleName, chunkData, destination) } } } @@ -220,8 +222,11 @@ class AssetModuleServiceImpl( callback?.onError(Bundle().apply { put(BundleKeys.ERROR_CODE, AssetPackErrorCode.API_NOT_AVAILABLE) }) return } - if (packageDownloadData[packageName] == null || packageDownloadData[packageName]?.packageName != packageName) { - val requestedAssetModuleNames = list.map { it.get(BundleKeys.MODULE_NAME) }.filter { !it.isNullOrEmpty() } + val requestedAssetModuleNames = list.map { it.getString(KEY_MODULE_NAME) }.filter { !it.isNullOrEmpty() } + if (packageDownloadData[packageName] == null || + packageDownloadData[packageName]?.packageName != packageName || + packageDownloadData[packageName]?.moduleNames?.intersect(requestedAssetModuleNames.toSet())?.isEmpty() == true + ) { val playCoreVersionCode = bundle.get(BundleKeys.PLAY_CORE_VERSION_CODE) ?: 0 packageDownloadData[packageName] = httpClient.initAssertModuleData(context, packageName, accountManager, requestedAssetModuleNames, playCoreVersionCode) if (packageDownloadData[packageName] == null) { diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/DownloadData.kt b/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/DownloadData.kt index 2a00c27270..053da8fa65 100644 --- a/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/DownloadData.kt +++ b/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/DownloadData.kt @@ -33,10 +33,6 @@ data class DownloadData( status = statusCode } } - - fun updateModuleDownloadStatus(statusCode: Int) { - this.status = statusCode - } } data class ModuleData( diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/extensions.kt b/vending-app/src/main/kotlin/com/google/android/finsky/extensions.kt index 9c9d63020d..a26220585f 100644 --- a/vending-app/src/main/kotlin/com/google/android/finsky/extensions.kt +++ b/vending-app/src/main/kotlin/com/google/android/finsky/extensions.kt @@ -34,12 +34,8 @@ import java.util.Collections const val KEY_MODULE_NAME = "module_name" -const val KEY_USING_EXTRACTOR_STREAM = "usingExtractorStream" - const val TAG_REQUEST = "asset_module" -private const val FLAGS = "com.google.android.play.core.FLAGS" - private const val ASSET_MODULE_DELIVERY_URL = "https://play-fe.googleapis.com/fdfe/assetModuleDelivery" private const val TAG = "AssetModuleRequest" @@ -103,9 +99,11 @@ fun HttpClient.initAssertModuleData( }.getOrNull() } Log.d(TAG, "initAssertModuleData: moduleDeliveryInfo-> $moduleDeliveryInfo") - return initModuleDownloadInfo(context, packageName, moduleDeliveryInfo) + return initModuleDownloadInfo(packageName, moduleDeliveryInfo) } +private val sessionIdMap: MutableMap = mutableMapOf() + private val lock = Any() private fun Context.generateSessionId(): Int { synchronized(lock) { @@ -118,7 +116,20 @@ private fun Context.generateSessionId(): Int { } } -private fun initModuleDownloadInfo(context: Context, packageName: String, deliveryInfo: ModuleDeliveryInfo?): DownloadData? { +private fun getSessionIdForPackage(packageName: String): Int { + synchronized(lock) { + return sessionIdMap.getOrPut(packageName) { 10 } + } +} + +private fun updateSessionIdForPackage(packageName: String, increment: Int) { + synchronized(lock) { + val currentSessionId = sessionIdMap[packageName] ?: 10 + sessionIdMap[packageName] = currentSessionId + increment + } +} + +private fun initModuleDownloadInfo(packageName: String, deliveryInfo: ModuleDeliveryInfo?): DownloadData? { if (deliveryInfo == null || deliveryInfo.status != null) { return null } @@ -127,6 +138,7 @@ private fun initModuleDownloadInfo(context: Context, packageName: String, delive var packVersionCode = 0L val sessionIds = arrayMapOf() val moduleDataMap = arrayMapOf() + val baseSessionId = getSessionIdForPackage(packageName) for (moduleIndex in deliveryInfo.modules.indices) { val moduleInfo: ModuleInfo = deliveryInfo.modules[moduleIndex] packVersionCode = moduleInfo.packVersion ?: 0 @@ -134,7 +146,7 @@ private fun initModuleDownloadInfo(context: Context, packageName: String, delive val moduleName: String = moduleInfo.moduleName ?: continue var moduleBytesToDownload = 0L moduleNames.add(moduleName) - sessionIds[moduleName] = moduleIndex + 10 + sessionIds[moduleName] = baseSessionId + moduleIndex var totalSumOfSubcontractedModules = 0 val sliceIds: ArrayList = ArrayList() val chunkDatas: ArrayList = arrayListOf() @@ -180,6 +192,7 @@ private fun initModuleDownloadInfo(context: Context, packageName: String, delive totalBytesToDownload += moduleBytesToDownload moduleDataMap[moduleName] = moduleData } + updateSessionIdForPackage(packageName, deliveryInfo.modules.size) return DownloadData( packageName = packageName, errorCode = AssetPackErrorCode.NO_ERROR, @@ -207,6 +220,7 @@ fun buildDownloadBundle(downloadData: DownloadData, list: List? = null) bundleData.put(BundleKeys.SESSION_ID, sessionId) bundleData.put(BundleKeys.SESSION_ID, moduleName, packData.status) } + bundleData.put(BundleKeys.PACK_VERSION_TAG, moduleName, null) bundleData.put(BundleKeys.STATUS, moduleName, packData.status) bundleData.put(BundleKeys.ERROR_CODE, moduleName, packData.errorCode) bundleData.put(BundleKeys.PACK_VERSION, moduleName, packData.packVersionCode) @@ -217,6 +231,7 @@ fun buildDownloadBundle(downloadData: DownloadData, list: List? = null) totalBytesToDownload += packData.totalBytesToDownload bytesDownloaded += packData.bytesDownloaded } + bundleData.put(BundleKeys.ERROR_CODE, downloadData.errorCode) bundleData.put(BundleKeys.PACK_NAMES, arrayList) bundleData.put(BundleKeys.TOTAL_BYTES_TO_DOWNLOAD, totalBytesToDownload) bundleData.put(BundleKeys.BYTES_DOWNLOADED, bytesDownloaded) @@ -240,6 +255,7 @@ fun sendBroadcastForExistingFile(context: Context, downloadData: DownloadData, m downloadBundle.put(BundleKeys.ERROR_CODE, moduleName, AssetPackErrorCode.NO_ERROR) downloadBundle.put(BundleKeys.BYTES_DOWNLOADED, moduleName, packData.bytesDownloaded) downloadBundle.put(BundleKeys.PACK_BASE_VERSION, moduleName, packData.moduleVersion) + downloadBundle.put(BundleKeys.PACK_VERSION_TAG, moduleName, null) packData.chunks.map { it.copy() }.forEach { val sliceId = it.sliceId ?: "" val uncompressedSize = it.sliceUncompressedSize @@ -256,7 +272,7 @@ fun sendBroadcastForExistingFile(context: Context, downloadData: DownloadData, m val intent = Intent(Intent.ACTION_VIEW) intent.setDataAndType(uri, context.contentResolver.getType(uri)) intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) - val resourceBlockIndex = chunkData?.chunkIndex?.toInt() + val resourceBlockIndex = chunkData?.chunkIndex if (uFile?.exists() == true && chunkData?.sliceId == sliceId && resourceBlockIndex != null) { if (chunkIntents[resourceBlockIndex] == null) { chunkIntents[resourceBlockIndex] = intent @@ -282,7 +298,7 @@ private fun sendBroadCast(context: Context, downloadData: DownloadData, result: intent.setAction(BroadcastConstants.ACTION_SESSION_UPDATE) intent.putExtra(BroadcastConstants.EXTRA_SESSION_STATE, result) intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY) - intent.putExtra(FLAGS, Bundle().apply { putBoolean(KEY_USING_EXTRACTOR_STREAM, true) }) + intent.putExtra(BroadcastConstants.EXTRA_FLAGS, Bundle().apply { putBoolean(BroadcastConstants.KEY_USING_EXTRACTOR_STREAM, true) }) intent.setPackage(downloadData.packageName) context.sendBroadcast(intent) } From f1fd932f47d733d4c5b2043c25abf6fdfff26fc1 Mon Sep 17 00:00:00 2001 From: Marvin W Date: Sat, 23 Nov 2024 18:11:23 -0600 Subject: [PATCH 096/132] Asset Modules: Improve API parameter / response handling --- .../google/android/finsky/DownloadManager.kt | 2 +- .../assetmoduleservice/AssetModuleService.kt | 279 +++++++++++++----- .../finsky/assetmoduleservice/DownloadData.kt | 14 +- .../com/google/android/finsky/extensions.kt | 29 +- vending-app/src/main/proto/AssetModule.proto | 2 +- vending-app/src/main/res/values/strings.xml | 1 - 6 files changed, 238 insertions(+), 89 deletions(-) diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/DownloadManager.kt b/vending-app/src/main/kotlin/com/google/android/finsky/DownloadManager.kt index 1b8af213df..342a12057e 100644 --- a/vending-app/src/main/kotlin/com/google/android/finsky/DownloadManager.kt +++ b/vending-app/src/main/kotlin/com/google/android/finsky/DownloadManager.kt @@ -113,7 +113,7 @@ class DownloadManager(private val context: Context) { .setPriority(NotificationCompat.PRIORITY_HIGH) .setOngoing(true) .setOnlyAlertOnce(true) - .addAction(R.drawable.ic_notification, context.getString(R.string.download_notification_cancel), cancelPendingIntent) + .addAction(R.drawable.ic_notification, context.getString(android.R.string.cancel), cancelPendingIntent) notifyBuilderMap[moduleName] = notifyBuilder diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/AssetModuleService.kt b/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/AssetModuleService.kt index c650fb66ed..8197d526e3 100644 --- a/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/AssetModuleService.kt +++ b/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/AssetModuleService.kt @@ -59,70 +59,101 @@ class AssetModuleServiceImpl( ) : IAssetModuleService.Stub(), LifecycleOwner { private val fileDescriptorMap = mutableMapOf() + private fun List.getModuleNames(): List = mapNotNull { it.get(BundleKeys.MODULE_NAME) } + private fun Bundle?.getInstalledAssetModules(): Map = get(BundleKeys.INSTALLED_ASSET_MODULE).orEmpty() + .map { it.get(BundleKeys.INSTALLED_ASSET_MODULE_NAME) to it.get(BundleKeys.INSTALLED_ASSET_MODULE_VERSION) } + .filter { it.first != null && it.second != null } + .associate { it.first!! to it.second!! } + + private fun Bundle?.getOptions() = Options( + this.get(BundleKeys.PLAY_CORE_VERSION_CODE, 0), + this.get(BundleKeys.SUPPORTED_COMPRESSION_FORMATS).orEmpty(), + this.get(BundleKeys.SUPPORTED_PATCH_FORMATS).orEmpty(), + ) + + private fun sendError(callback: IAssetModuleServiceCallback?, method: String, errorCode: @AssetPackErrorCode Int) { + Log.w(TAG, "Sending error from $method: $errorCode") + callback?.onError(bundleOf(BundleKeys.ERROR_CODE to errorCode)) + } + override fun startDownload(packageName: String?, list: MutableList?, bundle: Bundle?, callback: IAssetModuleServiceCallback?) { PackageUtils.getAndCheckCallingPackage(context, packageName) - Log.d(TAG, "Method (startDownload) called by packageName -> $packageName") - if (packageName == null || list == null || bundle == null || !VendingPreferences.isAssetDeliveryEnabled(context)) { - callback?.onError(Bundle().apply { put(BundleKeys.ERROR_CODE, AssetPackErrorCode.API_NOT_AVAILABLE) }) - return + + val moduleNames = list?.getModuleNames() + val options = bundle.getOptions() + val installedAssetModules = bundle.getInstalledAssetModules() + + Log.d(TAG, "startDownload[$packageName](moduleNames: $moduleNames, installedAssetModules: $installedAssetModules)") + + if (!VendingPreferences.isAssetDeliveryEnabled(context)) { + return sendError(callback, "startDownload", AssetPackErrorCode.API_NOT_AVAILABLE) + } + + if (packageName == null || moduleNames.isNullOrEmpty()) { + return sendError(callback, "startDownload", AssetPackErrorCode.INVALID_REQUEST) } - val requestedAssetModuleNames = list.map { it.getString(KEY_MODULE_NAME) }.filter { !it.isNullOrEmpty() } + if (packageDownloadData[packageName] == null || packageDownloadData[packageName]?.packageName != packageName || - packageDownloadData[packageName]?.moduleNames?.intersect(requestedAssetModuleNames.toSet())?.isEmpty() == true - ) { - val playCoreVersionCode = bundle.get(BundleKeys.PLAY_CORE_VERSION_CODE) - packageDownloadData[packageName] = httpClient.initAssertModuleData(context, packageName, accountManager, requestedAssetModuleNames, playCoreVersionCode ?: 0) + packageDownloadData[packageName]?.moduleNames?.intersect(moduleNames.toSet())?.isEmpty() == true) { + packageDownloadData[packageName] = httpClient.initAssetModuleData(context, packageName, accountManager, moduleNames, options) if (packageDownloadData[packageName] == null) { - callback?.onError(Bundle().apply { put(BundleKeys.ERROR_CODE, AssetPackErrorCode.API_NOT_AVAILABLE) }) - return + return sendError(callback, "startDownload", AssetPackErrorCode.API_NOT_AVAILABLE) } } - list.forEach { data -> - val moduleName = data.get(BundleKeys.MODULE_NAME) - val moduleData = packageDownloadData[packageName]?.getModuleData(moduleName!!) + moduleNames.forEach { + val moduleData = packageDownloadData[packageName]?.getModuleData(it) if (moduleData?.status != AssetPackStatus.DOWNLOADING && moduleData?.status != AssetPackStatus.COMPLETED) { - packageDownloadData[packageName]?.updateDownloadStatus(moduleName!!, AssetPackStatus.PENDING) - sendBroadcastForExistingFile(context, packageDownloadData[packageName]!!, moduleName!!, null, null) + packageDownloadData[packageName]?.updateDownloadStatus(it, AssetPackStatus.PENDING) + sendBroadcastForExistingFile(context, packageDownloadData[packageName]!!, it, null, null) } if (moduleData?.status == AssetPackStatus.FAILED) { + // FIXME: If we start download later, we shouldn't send a failure callback now callback?.onError(Bundle().apply { put(BundleKeys.ERROR_CODE, AssetPackErrorCode.NETWORK_ERROR) }) } - packageDownloadData[packageName]?.getModuleData(moduleName!!)?.chunks?.filter { it.sliceId != null }?.forEach { chunkData -> + packageDownloadData[packageName]?.getModuleData(it)?.chunks?.filter { it.sliceId != null }?.forEach { chunkData -> val destination = chunkData.run { File("${context.filesDir}/assetpacks/$sessionId/${this.moduleName}/$sliceId/", chunkIndex.toString()) } if (destination.exists() && destination.length() == chunkData.chunkBytesToDownload) { - sendBroadcastForExistingFile(context, packageDownloadData[packageName]!!, moduleName, chunkData, destination) + sendBroadcastForExistingFile(context, packageDownloadData[packageName]!!, it, chunkData, destination) } } } val bundleData = buildDownloadBundle(packageDownloadData[packageName]!!, list) Log.d(TAG, "startDownload: $bundleData") callback?.onStartDownload(-1, bundleData) - list.forEach { - val moduleName = it.get(BundleKeys.MODULE_NAME) - val packData = packageDownloadData[packageName]?.getModuleData(moduleName!!) + moduleNames.forEach { + val packData = packageDownloadData[packageName]?.getModuleData(it) if (packData?.status == AssetPackStatus.PENDING) { DownloadManager.get(context).shouldStop(false) - DownloadManager.get(context).prepareDownload(packageDownloadData[packageName]!!, moduleName!!) + DownloadManager.get(context).prepareDownload(packageDownloadData[packageName]!!, it) } } } override fun getSessionStates(packageName: String?, bundle: Bundle?, callback: IAssetModuleServiceCallback?) { PackageUtils.getAndCheckCallingPackage(context, packageName) - Log.d(TAG, "Method (getSessionStates) called by packageName -> $packageName") - val installedAssetModuleNames = - bundle?.get(BundleKeys.INSTALLED_ASSET_MODULE)?.flatMap { it.keySet().mapNotNull { subKey -> it.get(subKey) as? String } } - ?.toMutableList() ?: mutableListOf() + + val options = bundle.getOptions() + val installedAssetModules = bundle.getInstalledAssetModules() + + Log.d(TAG, "getSessionStates[$packageName](installedAssetModules: $installedAssetModules)") + + if (!VendingPreferences.isAssetDeliveryEnabled(context)) { + return sendError(callback, "getSessionStates", AssetPackErrorCode.API_NOT_AVAILABLE) + } + + if (packageName == null || bundle == null) { + return sendError(callback, "getSessionStates", AssetPackErrorCode.INVALID_REQUEST) + } val listBundleData: MutableList = mutableListOf() packageName.takeIf { it == packageDownloadData[packageName]?.packageName }?.let { packageDownloadData[packageName]?.moduleNames?.forEach { moduleName -> - if (moduleName in installedAssetModuleNames) return@forEach + if (moduleName in installedAssetModules) return@forEach listBundleData.add(sendBroadcastForExistingFile(context, packageDownloadData[packageName]!!, moduleName, null, null)) @@ -143,34 +174,60 @@ class AssetModuleServiceImpl( override fun notifyChunkTransferred(packageName: String?, bundle: Bundle?, bundle2: Bundle?, callback: IAssetModuleServiceCallback?) { PackageUtils.getAndCheckCallingPackage(context, packageName) - Log.d(TAG, "Method (notifyChunkTransferred) called by packageName -> $packageName") - val moduleName = bundle?.get(BundleKeys.MODULE_NAME) - if (moduleName.isNullOrEmpty() || !VendingPreferences.isAssetDeliveryEnabled(context)) { - callback?.onError(Bundle().apply { put(BundleKeys.ERROR_CODE, AssetPackErrorCode.API_NOT_AVAILABLE) }) - return - } - val sessionId = bundle.get(BundleKeys.SESSION_ID) + + val moduleName = bundle.get(BundleKeys.MODULE_NAME) val sliceId = bundle.get(BundleKeys.SLICE_ID) - val chunkNumber = bundle.get(BundleKeys.CHUNK_NUMBER) + val chunkNumber = bundle.get(BundleKeys.CHUNK_NUMBER, 0) + val sessionId = bundle.get(BundleKeys.SESSION_ID, packageDownloadData[packageName]?.sessionIds?.get(moduleName) ?: 0) + val options = bundle2.getOptions() + + Log.d(TAG, "notifyChunkTransferred[$packageName](sessionId: $sessionId, moduleName: $moduleName, sliceId: $sliceId, chunkNumber: $chunkNumber)") + + if (!VendingPreferences.isAssetDeliveryEnabled(context)) { + return sendError(callback, "notifyChunkTransferred", AssetPackErrorCode.API_NOT_AVAILABLE) + } + + if (packageName == null || bundle == null || moduleName.isNullOrEmpty() || sliceId.isNullOrEmpty()) { + return sendError(callback, "notifyChunkTransferred", AssetPackErrorCode.INVALID_REQUEST) + } + + if (packageDownloadData[packageName]?.sessionIds?.values?.contains(sessionId) != true) { + Log.w(TAG, "No active session with id $sessionId in $packageName") + return sendError(callback, "notifyChunkTransferred", AssetPackErrorCode.ACCESS_DENIED) + } + val downLoadFile = "${context.filesDir.absolutePath}/assetpacks/$sessionId/$moduleName/$sliceId/$chunkNumber" fileDescriptorMap[downLoadFile]?.close() fileDescriptorMap.remove(downLoadFile) + // TODO: Remove chunk after successful transfer of chunk or only with module? callback?.onNotifyChunkTransferred(bundle, Bundle().apply { put(BundleKeys.ERROR_CODE, AssetPackErrorCode.NO_ERROR) }) } override fun notifyModuleCompleted(packageName: String?, bundle: Bundle?, bundle2: Bundle?, callback: IAssetModuleServiceCallback?) { PackageUtils.getAndCheckCallingPackage(context, packageName) - Log.d(TAG, "Method (notifyModuleCompleted) called but not implemented by packageName -> $packageName") - val moduleName = bundle?.get(BundleKeys.MODULE_NAME) - if (moduleName.isNullOrEmpty() || !VendingPreferences.isAssetDeliveryEnabled(context)) { - callback?.onError(Bundle().apply { put(BundleKeys.ERROR_CODE, AssetPackErrorCode.API_NOT_AVAILABLE) }) - return + + val moduleName = bundle.get(BundleKeys.MODULE_NAME) + val sessionId = bundle.get(BundleKeys.SESSION_ID, packageDownloadData[packageName]?.sessionIds?.get(moduleName) ?: 0) + val options = bundle2.getOptions() + + Log.d(TAG, "notifyModuleCompleted[$packageName](sessionId: $sessionId, moduleName: $moduleName)") + + if (!VendingPreferences.isAssetDeliveryEnabled(context)) { + return sendError(callback, "notifyModuleCompleted", AssetPackErrorCode.API_NOT_AVAILABLE) + } + + if (packageName == null || bundle == null || moduleName.isNullOrEmpty()) { + return sendError(callback, "notifyModuleCompleted", AssetPackErrorCode.INVALID_REQUEST) + } + + if (packageDownloadData[packageName]?.sessionIds?.values?.contains(sessionId) != true) { + Log.w(TAG, "No active session with id $sessionId in $packageName") + return sendError(callback, "notifyModuleCompleted", AssetPackErrorCode.ACCESS_DENIED) } - Log.d(TAG, "notify: moduleName: $moduleName packNames: ${packageDownloadData[packageName]?.moduleNames}") + packageDownloadData[packageName]?.updateDownloadStatus(moduleName, AssetPackStatus.COMPLETED) sendBroadcastForExistingFile(context, packageDownloadData[packageName]!!, moduleName, null, null) - val sessionId = packageDownloadData[packageName]!!.sessionIds[moduleName] val downLoadFile = "${context.filesDir.absolutePath}/assetpacks/$sessionId/$moduleName" val directory = File(downLoadFile) @@ -185,27 +242,71 @@ class AssetModuleServiceImpl( override fun notifySessionFailed(packageName: String?, bundle: Bundle?, bundle2: Bundle?, callback: IAssetModuleServiceCallback?) { PackageUtils.getAndCheckCallingPackage(context, packageName) - Log.d(TAG, "Method (notifySessionFailed) called but not implemented by packageName -> $packageName") - callback?.onError(Bundle().apply { put(BundleKeys.ERROR_CODE, AssetPackErrorCode.API_NOT_AVAILABLE) }) + + val sessionId = bundle.get(BundleKeys.SESSION_ID, 0) + val options = bundle2.getOptions() + + Log.d(TAG, "notifySessionFailed[$packageName](sessionId: $sessionId)") + + if (!VendingPreferences.isAssetDeliveryEnabled(context)) { + return sendError(callback, "notifySessionFailed", AssetPackErrorCode.API_NOT_AVAILABLE) + } + + if (packageName == null || bundle == null) { + return sendError(callback, "notifySessionFailed", AssetPackErrorCode.INVALID_REQUEST) + } + + if (packageDownloadData[packageName]?.sessionIds?.values?.contains(sessionId) != true) { + Log.w(TAG, "No active session with id $sessionId in $packageName") + return sendError(callback, "notifySessionFailed", AssetPackErrorCode.ACCESS_DENIED) + } + + // TODO: Implement + return sendError(callback, "notifySessionFailed", AssetPackErrorCode.API_NOT_AVAILABLE) } override fun keepAlive(packageName: String?, bundle: Bundle?, callback: IAssetModuleServiceCallback?) { PackageUtils.getAndCheckCallingPackage(context, packageName) - Log.d(TAG, "Method (keepAlive) called but not implemented by packageName -> $packageName") + + val options = bundle.getOptions() + Log.d(TAG, "keepAlive[$packageName]()") + + if (!VendingPreferences.isAssetDeliveryEnabled(context)) { + return sendError(callback, "keepAlive", AssetPackErrorCode.API_NOT_AVAILABLE) + } + + if (packageName == null) { + return sendError(callback, "keepAlive", AssetPackErrorCode.INVALID_REQUEST) + } + + // TODO: Implement } - override fun getChunkFileDescriptor(packageName: String, bundle: Bundle, bundle2: Bundle, callback: IAssetModuleServiceCallback?) { + override fun getChunkFileDescriptor(packageName: String?, bundle: Bundle?, bundle2: Bundle?, callback: IAssetModuleServiceCallback?) { PackageUtils.getAndCheckCallingPackage(context, packageName) - Log.d(TAG, "Method (getChunkFileDescriptor) called by packageName -> $packageName") + val moduleName = bundle.get(BundleKeys.MODULE_NAME) - if (moduleName.isNullOrEmpty() || !VendingPreferences.isAssetDeliveryEnabled(context)) { - callback?.onError(Bundle().apply { put(BundleKeys.ERROR_CODE, AssetPackErrorCode.API_NOT_AVAILABLE) }) - return + val sessionId = bundle.get(BundleKeys.SESSION_ID, packageDownloadData[packageName]?.sessionIds?.get(moduleName) ?: 0) + val sliceId = bundle.get(BundleKeys.SLICE_ID) + val chunkNumber = bundle.get(BundleKeys.CHUNK_NUMBER, 0) + val options = bundle2.getOptions() + + Log.d(TAG, "getChunkFileDescriptor[$packageName](sessionId: $sessionId, moduleName: $moduleName, sliceId: $sliceId, chunkNumber: $chunkNumber)") + + if (!VendingPreferences.isAssetDeliveryEnabled(context)) { + return sendError(callback, "getChunkFileDescriptor", AssetPackErrorCode.API_NOT_AVAILABLE) + } + + if (packageName == null || bundle == null || moduleName.isNullOrEmpty() || sliceId.isNullOrEmpty()) { + return sendError(callback, "getChunkFileDescriptor", AssetPackErrorCode.INVALID_REQUEST) } + + if (packageDownloadData[packageName]?.sessionIds?.values?.contains(sessionId) != true) { + Log.w(TAG, "No active session with id $sessionId in $packageName") + return sendError(callback, "getChunkFileDescriptor", AssetPackErrorCode.ACCESS_DENIED) + } + val parcelFileDescriptor = runCatching { - val sessionId = bundle.get(BundleKeys.SESSION_ID) - val sliceId = bundle.get(BundleKeys.SLICE_ID) - val chunkNumber = bundle.get(BundleKeys.CHUNK_NUMBER) val downLoadFile = "${context.filesDir.absolutePath}/assetpacks/$sessionId/$moduleName/$sliceId/$chunkNumber" val filePath = Uri.parse(downLoadFile).path?.let { File(it) } ParcelFileDescriptor.open(filePath, ParcelFileDescriptor.MODE_READ_ONLY).also { @@ -217,26 +318,32 @@ class AssetModuleServiceImpl( override fun requestDownloadInfo(packageName: String?, list: MutableList?, bundle: Bundle?, callback: IAssetModuleServiceCallback?) { PackageUtils.getAndCheckCallingPackage(context, packageName) - Log.d(TAG, "Method (requestDownloadInfo) called by packageName -> $packageName") - if (packageName == null || list == null || bundle == null || !VendingPreferences.isAssetDeliveryEnabled(context)) { - callback?.onError(Bundle().apply { put(BundleKeys.ERROR_CODE, AssetPackErrorCode.API_NOT_AVAILABLE) }) - return + + val moduleNames = list?.getModuleNames() + val options = bundle.getOptions() + val installedAssetModules = bundle.getInstalledAssetModules() + + Log.d(TAG, "requestDownloadInfo[$packageName](moduleNames: $moduleNames, installedAssetModules: $installedAssetModules)") + + if (!VendingPreferences.isAssetDeliveryEnabled(context)) { + return sendError(callback, "requestDownloadInfo", AssetPackErrorCode.API_NOT_AVAILABLE) + } + + if (packageName == null || moduleNames.isNullOrEmpty()) { + return sendError(callback, "requestDownloadInfo", AssetPackErrorCode.INVALID_REQUEST) } - val requestedAssetModuleNames = list.map { it.getString(KEY_MODULE_NAME) }.filter { !it.isNullOrEmpty() } + if (packageDownloadData[packageName] == null || packageDownloadData[packageName]?.packageName != packageName || - packageDownloadData[packageName]?.moduleNames?.intersect(requestedAssetModuleNames.toSet())?.isEmpty() == true - ) { - val playCoreVersionCode = bundle.get(BundleKeys.PLAY_CORE_VERSION_CODE) ?: 0 - packageDownloadData[packageName] = httpClient.initAssertModuleData(context, packageName, accountManager, requestedAssetModuleNames, playCoreVersionCode) + packageDownloadData[packageName]?.moduleNames?.intersect(moduleNames.toSet())?.isEmpty() == true) { + packageDownloadData[packageName] = httpClient.initAssetModuleData(context, packageName, accountManager, moduleNames, options) if (packageDownloadData[packageName] == null) { callback?.onError(Bundle().apply { put(BundleKeys.ERROR_CODE, AssetPackErrorCode.API_NOT_AVAILABLE) }) return } } - list.forEach { - val moduleName = it.get(BundleKeys.MODULE_NAME) - val packData = packageDownloadData[packageName]?.getModuleData(moduleName!!) + moduleNames.forEach { + val packData = packageDownloadData[packageName]?.getModuleData(it) if (packData?.status == AssetPackStatus.FAILED) { callback?.onError(Bundle().apply { put(BundleKeys.ERROR_CODE, AssetPackErrorCode.NETWORK_ERROR) }) } @@ -248,13 +355,47 @@ class AssetModuleServiceImpl( override fun removeModule(packageName: String?, bundle: Bundle?, bundle2: Bundle?, callback: IAssetModuleServiceCallback?) { PackageUtils.getAndCheckCallingPackage(context, packageName) - Log.d(TAG, "Method (removeModule) called but not implemented by packageName -> $packageName") - callback?.onError(Bundle().apply { put(BundleKeys.ERROR_CODE, AssetPackErrorCode.API_NOT_AVAILABLE) }) + + val moduleName = bundle?.get(BundleKeys.MODULE_NAME) + val sessionId = bundle.get(BundleKeys.SESSION_ID, packageDownloadData[packageName]?.sessionIds?.get(moduleName) ?: 0) + val options = bundle2.getOptions() + + Log.d(TAG, "removeModule[$packageName](sessionId: $sessionId, moduleName: $moduleName)") + + if (!VendingPreferences.isAssetDeliveryEnabled(context)) { + return sendError(callback, "removeModule", AssetPackErrorCode.API_NOT_AVAILABLE) + } + + if (packageName == null || bundle == null || moduleName.isNullOrEmpty()) { + return sendError(callback, "removeModule", AssetPackErrorCode.INVALID_REQUEST) + } + + if (packageDownloadData[packageName]?.sessionIds?.values?.contains(sessionId) != true) { + Log.w(TAG, "No active session with id $sessionId in $packageName") + return sendError(callback, "removeModule", AssetPackErrorCode.ACCESS_DENIED) + } + + // TODO: Implement + return sendError(callback, "removeModule", AssetPackErrorCode.API_NOT_AVAILABLE) } override fun cancelDownloads(packageName: String?, list: MutableList?, bundle: Bundle?, callback: IAssetModuleServiceCallback?) { PackageUtils.getAndCheckCallingPackage(context, packageName) - Log.d(TAG, "Method (cancelDownloads) called but not implemented by packageName -> $packageName") - callback?.onError(Bundle().apply { put(BundleKeys.ERROR_CODE, AssetPackErrorCode.API_NOT_AVAILABLE) }) + + val moduleNames = list?.getModuleNames() + val options = bundle.getOptions() + + Log.d(TAG, "cancelDownloads[$packageName](moduleNames: $moduleNames)") + + if (!VendingPreferences.isAssetDeliveryEnabled(context)) { + return sendError(callback, "cancelDownloads", AssetPackErrorCode.API_NOT_AVAILABLE) + } + + if (packageName == null || moduleNames.isNullOrEmpty()) { + return sendError(callback, "cancelDownloads", AssetPackErrorCode.INVALID_REQUEST) + } + + // TODO: Implement + return sendError(callback, "cancelDownloads", AssetPackErrorCode.API_NOT_AVAILABLE) } } \ No newline at end of file diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/DownloadData.kt b/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/DownloadData.kt index 053da8fa65..4f03df6e16 100644 --- a/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/DownloadData.kt +++ b/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/DownloadData.kt @@ -11,11 +11,9 @@ data class DownloadData( var packageName: String = "", var errorCode: Int = 0, var sessionIds: Map = emptyMap(), - var bytesDownloaded: Long = 0, var status: Int = 0, var moduleNames: Set = emptySet(), var appVersionCode: Long = 0, - var totalBytesToDownload: Long = 0, var moduleDataMap: Map = emptyMap() ) : Serializable { @@ -25,7 +23,6 @@ data class DownloadData( fun incrementModuleBytesDownloaded(packName: String, bytes: Long) { getModuleData(packName).incrementBytesDownloaded(bytes) - bytesDownloaded += bytes } fun updateDownloadStatus(packName: String, statusCode: Int) { @@ -35,6 +32,15 @@ data class DownloadData( } } +fun DownloadData?.merge(data: DownloadData?): DownloadData? { + if (this == null) return data + if (data == null) return this + moduleNames += data.moduleNames + sessionIds += data.sessionIds.filter { it.key !in sessionIds.keys } + moduleDataMap += data.moduleDataMap.filter { it.key !in moduleDataMap.keys } + return this +} + data class ModuleData( var packVersionCode: Long = 0, var moduleVersion: Long = 0, @@ -51,7 +57,7 @@ data class ModuleData( } data class ChunkData( - val sessionId: Int, + var sessionId: Int, val moduleName: String, val sliceId: String?, val chunkSourceUri: String?, diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/extensions.kt b/vending-app/src/main/kotlin/com/google/android/finsky/extensions.kt index a26220585f..8570256ffe 100644 --- a/vending-app/src/main/kotlin/com/google/android/finsky/extensions.kt +++ b/vending-app/src/main/kotlin/com/google/android/finsky/extensions.kt @@ -15,6 +15,7 @@ import android.util.Log import androidx.collection.ArraySet import androidx.collection.arrayMapOf import androidx.collection.arraySetOf +import androidx.core.content.pm.PackageInfoCompat import com.android.vending.licensing.AUTH_TOKEN_SCOPE import com.android.vending.licensing.getAuthToken import com.android.vending.licensing.getLicenseRequestHeaders @@ -40,8 +41,8 @@ private const val ASSET_MODULE_DELIVERY_URL = "https://play-fe.googleapis.com/fd private const val TAG = "AssetModuleRequest" -fun getAppVersionCode(context: Context, packageName: String): String? { - return runCatching { context.packageManager.getPackageInfo(packageName, 0).versionCode.toString() }.getOrNull() +fun getAppVersionCode(context: Context, packageName: String): Long? { + return runCatching { PackageInfoCompat.getLongVersionCode(context.packageManager.getPackageInfo(packageName, 0)) }.getOrNull() } fun Bundle?.get(key: BundleKeys.RootKey): T? = if (this == null) null else BundleKeys.get(this, key) @@ -51,15 +52,17 @@ fun Bundle.put(key: BundleKeys.PackKey, packName: String, v: T) = BundleK fun Bundle.put(key: BundleKeys.SliceKey, packName: String, sliceId: String, v: T) = BundleKeys.put(this, key, packName, sliceId, v) fun bundleOf(pair: Pair, T>): Bundle = Bundle().apply { put(pair.first, pair.second) } +data class Options(val playCoreVersionCode: Int, val supportedCompressionFormats: List, val supportedPatchFormats: List) -fun HttpClient.initAssertModuleData( +fun HttpClient.initAssetModuleData( context: Context, packageName: String, accountManager: AccountManager, requestedAssetModuleNames: List, - playCoreVersionCode: Int, - supportedCompressionFormats: List = listOf(0, 3), - supportedPatchFormats: List = listOf(1, 2), + options: Options, + playCoreVersionCode: Int = options.playCoreVersionCode, + supportedCompressionFormats: List = options.supportedCompressionFormats.takeIf { it.isNotEmpty() } ?: listOf(0, 3), + supportedPatchFormats: List = options.supportedPatchFormats.takeIf { it.isNotEmpty() } ?: listOf(1, 2), ): DownloadData? { val accounts = accountManager.getAccountsByType(AuthConstants.DEFAULT_ACCOUNT_TYPE) var oauthToken: String? = null @@ -80,7 +83,8 @@ fun HttpClient.initAssertModuleData( return null } - val requestPayload = AssetModuleDeliveryRequest.Builder().callerInfo(CallerInfo(getAppVersionCode(context, packageName)?.toInt())).packageName(packageName) + val appVersionCode = getAppVersionCode(context, packageName) + val requestPayload = AssetModuleDeliveryRequest.Builder().callerInfo(CallerInfo(appVersionCode)).packageName(packageName) .playCoreVersion(playCoreVersionCode).supportedCompressionFormats(supportedCompressionFormats) .supportedPatchFormats(supportedPatchFormats).modules(ArrayList().apply { requestedAssetModuleNames.forEach { add(AssetModuleInfo.Builder().name(it).build()) } @@ -88,6 +92,7 @@ fun HttpClient.initAssertModuleData( val androidId = GServices.getString(context.contentResolver, "android_id", "0")?.toLong() ?: 1 + // FIXME: Don't runBlocking, use async val moduleDeliveryInfo = runBlocking { runCatching { post( @@ -98,8 +103,8 @@ fun HttpClient.initAssertModuleData( ).wrapper?.deliveryInfo }.getOrNull() } - Log.d(TAG, "initAssertModuleData: moduleDeliveryInfo-> $moduleDeliveryInfo") - return initModuleDownloadInfo(packageName, moduleDeliveryInfo) + Log.d(TAG, "initAssetModuleData: moduleDeliveryInfo-> $moduleDeliveryInfo") + return initModuleDownloadInfo(packageName, appVersionCode, moduleDeliveryInfo) } private val sessionIdMap: MutableMap = mutableMapOf() @@ -129,7 +134,7 @@ private fun updateSessionIdForPackage(packageName: String, increment: Int) { } } -private fun initModuleDownloadInfo(packageName: String, deliveryInfo: ModuleDeliveryInfo?): DownloadData? { +private fun initModuleDownloadInfo(packageName: String, appVersionCode: Long?, deliveryInfo: ModuleDeliveryInfo?): DownloadData? { if (deliveryInfo == null || deliveryInfo.status != null) { return null } @@ -197,11 +202,9 @@ private fun initModuleDownloadInfo(packageName: String, deliveryInfo: ModuleDeli packageName = packageName, errorCode = AssetPackErrorCode.NO_ERROR, sessionIds = sessionIds, - bytesDownloaded = 0, status = AssetPackStatus.NOT_INSTALLED, moduleNames = moduleNames, - appVersionCode = packVersionCode, - totalBytesToDownload = totalBytesToDownload, + appVersionCode = appVersionCode ?: packVersionCode, moduleDataMap ) } diff --git a/vending-app/src/main/proto/AssetModule.proto b/vending-app/src/main/proto/AssetModule.proto index e154155623..0f51b0815c 100644 --- a/vending-app/src/main/proto/AssetModule.proto +++ b/vending-app/src/main/proto/AssetModule.proto @@ -15,7 +15,7 @@ message AssetModuleDeliveryRequest { message CallerInfo { oneof Version { - int32 appVersionCode = 1; + int64 appVersionCode = 1; string internalSharingId = 3; } } diff --git a/vending-app/src/main/res/values/strings.xml b/vending-app/src/main/res/values/strings.xml index 6b9a242f61..f2683abdfc 100644 --- a/vending-app/src/main/res/values/strings.xml +++ b/vending-app/src/main/res/values/strings.xml @@ -29,7 +29,6 @@ Learn more Verify - Cancel Additional files for %s Downloading From 2f592dded87c8bd76b2d7274a9e5ca33f47480a8 Mon Sep 17 00:00:00 2001 From: Marvin W Date: Sun, 24 Nov 2024 18:20:14 -0600 Subject: [PATCH 097/132] Asset Modules: Split logic from interface --- vending-app/build.gradle | 3 + .../protocol/IAssetModuleService.aidl | 20 +- .../google/android/finsky/DownloadManager.kt | 17 +- .../AbstractAssetModuleServiceImpl.kt | 221 ++++++++++++ .../assetmoduleservice/AssetModuleService.kt | 327 ++++-------------- .../finsky/assetmoduleservice/DownloadData.kt | 9 +- .../com/google/android/finsky/extensions.kt | 57 +-- .../src/main/res/drawable/ic_cancel.xml | 17 + 8 files changed, 371 insertions(+), 300 deletions(-) create mode 100644 vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/AbstractAssetModuleServiceImpl.kt create mode 100644 vending-app/src/main/res/drawable/ic_cancel.xml diff --git a/vending-app/build.gradle b/vending-app/build.gradle index c8281eef76..7cc40124fe 100644 --- a/vending-app/build.gradle +++ b/vending-app/build.gradle @@ -113,6 +113,9 @@ dependencies { //droidguard implementation project(':play-services-droidguard') + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutineVersion" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutineVersion" + //androidx implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion" implementation "androidx.core:core-ktx:$coreVersion" diff --git a/vending-app/src/main/aidl/com/google/android/play/core/assetpacks/protocol/IAssetModuleService.aidl b/vending-app/src/main/aidl/com/google/android/play/core/assetpacks/protocol/IAssetModuleService.aidl index e70ef8e65e..d77f14d833 100644 --- a/vending-app/src/main/aidl/com/google/android/play/core/assetpacks/protocol/IAssetModuleService.aidl +++ b/vending-app/src/main/aidl/com/google/android/play/core/assetpacks/protocol/IAssetModuleService.aidl @@ -8,14 +8,14 @@ package com.google.android.play.core.assetpacks.protocol; import com.google.android.play.core.assetpacks.protocol.IAssetModuleServiceCallback; interface IAssetModuleService { - void startDownload(String packageName, in List list, in Bundle bundle, in IAssetModuleServiceCallback callback) = 1; - void getSessionStates(String packageName, in Bundle bundle, in IAssetModuleServiceCallback callback) = 4; - void notifyChunkTransferred(String packageName, in Bundle bundle, in Bundle bundle2, in IAssetModuleServiceCallback callback) = 5; - void notifyModuleCompleted(String packageName, in Bundle bundle, in Bundle bundle2, in IAssetModuleServiceCallback callback) = 6; - void notifySessionFailed(String packageName, in Bundle bundle, in Bundle bundle2, in IAssetModuleServiceCallback callback) = 8; - void keepAlive(String packageName, in Bundle bundle, in IAssetModuleServiceCallback callback) = 9; - void getChunkFileDescriptor(String packageName, in Bundle bundle, in Bundle bundle2, in IAssetModuleServiceCallback callback) = 10; - void requestDownloadInfo(String packageName, in List list, in Bundle bundle, in IAssetModuleServiceCallback callback) = 11; - void removeModule(String packageName, in Bundle bundle, in Bundle bundle2, in IAssetModuleServiceCallback callback) = 12; - void cancelDownloads(String packageName, in List list, in Bundle bundle, in IAssetModuleServiceCallback callback) = 13; + oneway void startDownload(String packageName, in List list, in Bundle bundle, in IAssetModuleServiceCallback callback) = 1; + oneway void getSessionStates(String packageName, in Bundle bundle, in IAssetModuleServiceCallback callback) = 4; + oneway void notifyChunkTransferred(String packageName, in Bundle bundle, in Bundle bundle2, in IAssetModuleServiceCallback callback) = 5; + oneway void notifyModuleCompleted(String packageName, in Bundle bundle, in Bundle bundle2, in IAssetModuleServiceCallback callback) = 6; + oneway void notifySessionFailed(String packageName, in Bundle bundle, in Bundle bundle2, in IAssetModuleServiceCallback callback) = 8; + oneway void keepAlive(String packageName, in Bundle bundle, in IAssetModuleServiceCallback callback) = 9; + oneway void getChunkFileDescriptor(String packageName, in Bundle bundle, in Bundle bundle2, in IAssetModuleServiceCallback callback) = 10; + oneway void requestDownloadInfo(String packageName, in List list, in Bundle bundle, in IAssetModuleServiceCallback callback) = 11; + oneway void removeModule(String packageName, in Bundle bundle, in Bundle bundle2, in IAssetModuleServiceCallback callback) = 12; + oneway void cancelDownloads(String packageName, in List list, in Bundle bundle, in IAssetModuleServiceCallback callback) = 13; } \ No newline at end of file diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/DownloadManager.kt b/vending-app/src/main/kotlin/com/google/android/finsky/DownloadManager.kt index 342a12057e..74709ed204 100644 --- a/vending-app/src/main/kotlin/com/google/android/finsky/DownloadManager.kt +++ b/vending-app/src/main/kotlin/com/google/android/finsky/DownloadManager.kt @@ -21,9 +21,11 @@ import android.net.Uri import android.os.Build import android.util.Log import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationCompat.Action import androidx.core.app.NotificationManagerCompat import com.android.vending.R import com.google.android.finsky.assetmoduleservice.DownloadData +import com.google.android.finsky.assetmoduleservice.getChunkFile import com.google.android.play.core.assetpacks.model.AssetPackStatus import java.io.File import java.io.FileOutputStream @@ -105,7 +107,7 @@ class DownloadManager(private val context: Context) { } val notifyBuilder = NotificationCompat.Builder(context, CHANNEL_ID) - .setSmallIcon(R.drawable.ic_app_foreground) + .setSmallIcon(R.drawable.ic_notification) .setContentTitle(context.getString(R.string.download_notification_attachment_file, appName)) .setContentText(context.getString(R.string.download_notification_tips)) .setLargeIcon(largeIconBitmap) @@ -113,7 +115,7 @@ class DownloadManager(private val context: Context) { .setPriority(NotificationCompat.PRIORITY_HIGH) .setOngoing(true) .setOnlyAlertOnce(true) - .addAction(R.drawable.ic_notification, context.getString(android.R.string.cancel), cancelPendingIntent) + .addAction(Action.Builder(R.drawable.ic_cancel, context.getString(android.R.string.cancel), cancelPendingIntent).setSemanticAction(Action.SEMANTIC_ACTION_DELETE).build()) notifyBuilderMap[moduleName] = notifyBuilder @@ -139,15 +141,8 @@ class DownloadManager(private val context: Context) { downloadData.updateDownloadStatus(moduleName, AssetPackStatus.DOWNLOADING) for (chunkData in packData.chunks) { val moduleName: String = chunkData.moduleName - val sliceId: String? = chunkData.sliceId - val chunkSourceUri: String? = chunkData.chunkSourceUri - val sessionId: Int = chunkData.sessionId - val chunkIndex: Int = chunkData.chunkIndex - if (sliceId == null || chunkSourceUri == null) { - continue - } - val filesDir = "${context.filesDir}/assetpacks/$sessionId/$moduleName/$sliceId/" - val destination = File(filesDir, chunkIndex.toString()) + val chunkSourceUri: String = chunkData.chunkSourceUri ?: continue + val destination = chunkData.getChunkFile(context) startDownload(moduleName, chunkSourceUri, destination, downloadData) sendBroadcastForExistingFile(context, downloadData, moduleName, chunkData, destination) } diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/AbstractAssetModuleServiceImpl.kt b/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/AbstractAssetModuleServiceImpl.kt new file mode 100644 index 0000000000..bafb20a8b7 --- /dev/null +++ b/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/AbstractAssetModuleServiceImpl.kt @@ -0,0 +1,221 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.finsky.assetmoduleservice + +import android.content.Context +import android.os.Bundle +import android.os.Parcel +import android.util.Log +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.lifecycleScope +import com.android.vending.VendingPreferences +import com.google.android.finsky.AssetModuleOptions +import com.google.android.finsky.bundleOf +import com.google.android.finsky.get +import com.google.android.play.core.assetpacks.model.AssetPackErrorCode +import com.google.android.play.core.assetpacks.protocol.BundleKeys +import com.google.android.play.core.assetpacks.protocol.IAssetModuleService +import com.google.android.play.core.assetpacks.protocol.IAssetModuleServiceCallback +import org.microg.gms.common.PackageUtils +import org.microg.gms.utils.warnOnTransactionIssues + +class AssetPackException(val code: @AssetPackErrorCode Int, message: String? = null) : Exception(message ?: "AssetPackException(code=$code)") + +data class StartDownloadParameters(val moduleNames: List, val installedAssetModules: Map, val options: AssetModuleOptions) +data class GetSessionStatesParameters(val installedAssetModules: Map, val options: AssetModuleOptions) +data class NotifyModuleCompletedParameters(val moduleName: String, val sessionId: Int, val options: AssetModuleOptions) +data class NotifySessionFailedParameters(val sessionId: Int, val options: AssetModuleOptions) +data class RequestDownloadInfoParameters(val moduleNames: List, val installedAssetModules: Map, val options: AssetModuleOptions) +data class RemoveModuleParameters(val moduleName: String, val sessionId: Int, val options: AssetModuleOptions) +data class CancelDownloadsParameters(val moduleNames: List, val options: AssetModuleOptions) +data class KeepAliveParameters(val options: AssetModuleOptions) + +data class NotifyChunkTransferredParameters( + val moduleName: String, + val sliceId: String, + val chunkNumber: Int, + val sessionId: Int, + val options: AssetModuleOptions +) + +data class GetChunkFileDescriptorParameters( + val moduleName: String, + val sliceId: String, + val chunkNumber: Int, + val sessionId: Int, + val options: AssetModuleOptions +) + +abstract class AbstractAssetModuleServiceImpl(val context: Context, override val lifecycle: Lifecycle) : IAssetModuleService.Stub(), LifecycleOwner { + private fun List.getModuleNames(): List = mapNotNull { it.get(BundleKeys.MODULE_NAME).takeIf { !it.isNullOrBlank() } } + private fun Bundle?.getInstalledAssetModules(): Map = get(BundleKeys.INSTALLED_ASSET_MODULE).orEmpty() + .map { it.get(BundleKeys.INSTALLED_ASSET_MODULE_NAME) to it.get(BundleKeys.INSTALLED_ASSET_MODULE_VERSION) } + .filter { it.first != null && it.second != null } + .associate { it.first!! to it.second!! } + + private fun Bundle?.getOptions() = AssetModuleOptions( + this.get(BundleKeys.PLAY_CORE_VERSION_CODE, 0), + this.get(BundleKeys.SUPPORTED_COMPRESSION_FORMATS).orEmpty(), + this.get(BundleKeys.SUPPORTED_PATCH_FORMATS).orEmpty(), + ) + + private fun sendError(callback: IAssetModuleServiceCallback?, method: String, errorCode: @AssetPackErrorCode Int) { + Log.w(TAG, "Sending error from $method: $errorCode") + callback?.onError(bundleOf(BundleKeys.ERROR_CODE to errorCode)) + } + + private fun run( + uncheckedPackageName: String?, + method: String, + callback: IAssetModuleServiceCallback?, + parseParameters: (packageName: String) -> T, + logic: suspend (params: T, packageName: String, callback: IAssetModuleServiceCallback?) -> Unit + ) { + val packageName = try { + PackageUtils.getAndCheckCallingPackage(context, uncheckedPackageName)!! + } catch (e: Exception) { + Log.w(TAG, e) + return sendError(callback, method, AssetPackErrorCode.ACCESS_DENIED) + } + + if (!VendingPreferences.isAssetDeliveryEnabled(context)) { + return sendError(callback, method, AssetPackErrorCode.API_NOT_AVAILABLE) + } + + val input = try { + parseParameters(packageName) + } catch (e: AssetPackException) { + return sendError(callback, method, e.code) + } catch (e: Exception) { + Log.w(TAG, e) + return sendError(callback, method, AssetPackErrorCode.INVALID_REQUEST) + } + + Log.d(TAG, "$method[$packageName]${input.toString().substring(input.javaClass.simpleName.length)}") + + lifecycleScope.launchWhenStarted { + try { + logic.invoke(input, packageName, callback) + } catch (e: AssetPackException) { + sendError(callback, method, e.code) + } catch (e: UnsupportedOperationException) { + Log.w(TAG, "Unsupported: $method") + sendError(callback, method, AssetPackErrorCode.API_NOT_AVAILABLE) + } catch (e: Exception) { + Log.w(TAG, e) + sendError(callback, method, AssetPackErrorCode.INTERNAL_ERROR) + } + } + } + + protected abstract fun getDefaultSessionId(packageName: String, moduleName: String): Int + + override fun startDownload(uncheckedPackageName: String?, list: MutableList?, bundle: Bundle?, callback: IAssetModuleServiceCallback?) { + run(uncheckedPackageName, "startDownload", callback, { _ -> + StartDownloadParameters(list!!.getModuleNames().also { require(it.isNotEmpty()) }, bundle.getInstalledAssetModules(), bundle.getOptions()) + }, this::startDownload) + } + + abstract suspend fun startDownload(params: StartDownloadParameters, packageName: String, callback: IAssetModuleServiceCallback?) + + override fun getSessionStates(uncheckedPackageName: String?, bundle: Bundle?, callback: IAssetModuleServiceCallback?) { + run(uncheckedPackageName, "getSessionStates", callback, { _ -> + GetSessionStatesParameters(bundle.getInstalledAssetModules(), bundle.getOptions()) + }, this::getSessionStates) + } + + abstract suspend fun getSessionStates(params: GetSessionStatesParameters, packageName: String, callback: IAssetModuleServiceCallback?) + + override fun notifyChunkTransferred(uncheckedPackageName: String?, bundle: Bundle?, bundle2: Bundle?, callback: IAssetModuleServiceCallback?) { + run(uncheckedPackageName, "notifyChunkTransferred", callback, { packageName -> + val moduleName = bundle.get(BundleKeys.MODULE_NAME)!!.also { require(it.isNotEmpty()) } + NotifyChunkTransferredParameters( + moduleName, + bundle.get(BundleKeys.SLICE_ID)!!.also { require(it.isNotEmpty()) }, + bundle.get(BundleKeys.CHUNK_NUMBER, 0), + bundle.get(BundleKeys.SESSION_ID, getDefaultSessionId(packageName, moduleName)), + bundle2.getOptions() + ) + }, this::notifyChunkTransferred) + } + + abstract suspend fun notifyChunkTransferred(params: NotifyChunkTransferredParameters, packageName: String, callback: IAssetModuleServiceCallback?) + + override fun notifyModuleCompleted(uncheckedPackageName: String?, bundle: Bundle?, bundle2: Bundle?, callback: IAssetModuleServiceCallback?) { + run(uncheckedPackageName, "notifyModuleCompleted", callback, { packageName -> + val moduleName = bundle.get(BundleKeys.MODULE_NAME)!!.also { require(it.isNotEmpty()) } + NotifyModuleCompletedParameters( + moduleName, + bundle.get(BundleKeys.SESSION_ID, getDefaultSessionId(packageName, moduleName)), + bundle2.getOptions() + ) + }, this::notifyModuleCompleted) + } + + abstract suspend fun notifyModuleCompleted(params: NotifyModuleCompletedParameters, packageName: String, callback: IAssetModuleServiceCallback?) + + override fun notifySessionFailed(uncheckedPackageName: String?, bundle: Bundle?, bundle2: Bundle?, callback: IAssetModuleServiceCallback?) { + run(uncheckedPackageName, "notifySessionFailed", callback, { _ -> + NotifySessionFailedParameters(bundle.get(BundleKeys.SESSION_ID, 0), bundle2.getOptions()) + }, this::notifySessionFailed) + } + + abstract suspend fun notifySessionFailed(params: NotifySessionFailedParameters, packageName: String, callback: IAssetModuleServiceCallback?) + + override fun getChunkFileDescriptor(uncheckedPackageName: String?, bundle: Bundle?, bundle2: Bundle?, callback: IAssetModuleServiceCallback?) { + run(uncheckedPackageName, "getChunkFileDescriptor", callback, { packageName -> + val moduleName = bundle.get(BundleKeys.MODULE_NAME)!!.also { require(it.isNotEmpty()) } + GetChunkFileDescriptorParameters( + moduleName, + bundle.get(BundleKeys.SLICE_ID)!!, + bundle.get(BundleKeys.CHUNK_NUMBER, 0), + bundle.get(BundleKeys.SESSION_ID, getDefaultSessionId(packageName, moduleName)), + bundle2.getOptions() + ) + }, this::getChunkFileDescriptor) + } + + abstract suspend fun getChunkFileDescriptor(params: GetChunkFileDescriptorParameters, packageName: String, callback: IAssetModuleServiceCallback?) + + override fun requestDownloadInfo(uncheckedPackageName: String?, list: MutableList?, bundle: Bundle?, callback: IAssetModuleServiceCallback?) { + run(uncheckedPackageName, "requestDownloadInfo", callback, { _ -> + RequestDownloadInfoParameters(list!!.getModuleNames().also { require(it.isNotEmpty()) }, bundle.getInstalledAssetModules(), bundle.getOptions()) + }, this::requestDownloadInfo) + } + + abstract suspend fun requestDownloadInfo(params: RequestDownloadInfoParameters, packageName: String, callback: IAssetModuleServiceCallback?) + + override fun removeModule(uncheckedPackageName: String?, bundle: Bundle?, bundle2: Bundle?, callback: IAssetModuleServiceCallback?) { + run(uncheckedPackageName, "removeModule", callback, { packageName -> + val moduleName = bundle?.get(BundleKeys.MODULE_NAME)!! + RemoveModuleParameters( + moduleName, + bundle.get(BundleKeys.SESSION_ID, getDefaultSessionId(packageName, moduleName)), + bundle2.getOptions() + ) + }, this::removeModule) + } + + abstract suspend fun removeModule(params: RemoveModuleParameters, packageName: String, callback: IAssetModuleServiceCallback?) + + override fun cancelDownloads(uncheckedPackageName: String?, list: MutableList?, bundle: Bundle?, callback: IAssetModuleServiceCallback?) { + run(uncheckedPackageName, "cancelDownloads", callback, { _ -> + CancelDownloadsParameters(list!!.getModuleNames().also { require(it.isNotEmpty()) }, bundle.getOptions()) + }, this::cancelDownloads) + } + + abstract suspend fun cancelDownloads(params: CancelDownloadsParameters, packageName: String, callback: IAssetModuleServiceCallback?) + + override fun keepAlive(packageName: String?, bundle: Bundle?, callback: IAssetModuleServiceCallback?) { + run(packageName, "keepAlive", callback, { KeepAliveParameters(bundle.getOptions()) }, this::keepAlive) + } + + abstract suspend fun keepAlive(params: KeepAliveParameters, packageName: String, callback: IAssetModuleServiceCallback?) + + override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean = + warnOnTransactionIssues(code, reply, flags, TAG) { super.onTransact(code, data, reply, flags) } +} \ No newline at end of file diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/AssetModuleService.kt b/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/AssetModuleService.kt index 8197d526e3..02e90a84f6 100644 --- a/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/AssetModuleService.kt +++ b/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/AssetModuleService.kt @@ -13,21 +13,17 @@ import android.os.IBinder import android.os.ParcelFileDescriptor import android.util.Log import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleService -import com.android.vending.VendingPreferences import com.google.android.finsky.* import com.google.android.play.core.assetpacks.model.AssetPackErrorCode import com.google.android.play.core.assetpacks.model.AssetPackStatus import com.google.android.play.core.assetpacks.protocol.BundleKeys -import com.google.android.play.core.assetpacks.protocol.IAssetModuleService import com.google.android.play.core.assetpacks.protocol.IAssetModuleServiceCallback -import org.microg.gms.common.PackageUtils import org.microg.gms.profile.ProfileManager import org.microg.vending.billing.core.HttpClient import java.io.File -private const val TAG = "AssetModuleService" +const val TAG = "AssetModuleService" class AssetModuleService : LifecycleService() { private lateinit var httpClient: HttpClient @@ -54,54 +50,33 @@ class AssetModuleService : LifecycleService() { } class AssetModuleServiceImpl( - val context: Context, override val lifecycle: Lifecycle, private val httpClient: HttpClient, private val accountManager: AccountManager, + context: Context, lifecycle: Lifecycle, + private val httpClient: HttpClient, + private val accountManager: AccountManager, private val packageDownloadData: MutableMap -) : IAssetModuleService.Stub(), LifecycleOwner { - private val fileDescriptorMap = mutableMapOf() +) : AbstractAssetModuleServiceImpl(context, lifecycle) { + private val fileDescriptorMap = mutableMapOf() - private fun List.getModuleNames(): List = mapNotNull { it.get(BundleKeys.MODULE_NAME) } - private fun Bundle?.getInstalledAssetModules(): Map = get(BundleKeys.INSTALLED_ASSET_MODULE).orEmpty() - .map { it.get(BundleKeys.INSTALLED_ASSET_MODULE_NAME) to it.get(BundleKeys.INSTALLED_ASSET_MODULE_VERSION) } - .filter { it.first != null && it.second != null } - .associate { it.first!! to it.second!! } - - private fun Bundle?.getOptions() = Options( - this.get(BundleKeys.PLAY_CORE_VERSION_CODE, 0), - this.get(BundleKeys.SUPPORTED_COMPRESSION_FORMATS).orEmpty(), - this.get(BundleKeys.SUPPORTED_PATCH_FORMATS).orEmpty(), - ) - - private fun sendError(callback: IAssetModuleServiceCallback?, method: String, errorCode: @AssetPackErrorCode Int) { - Log.w(TAG, "Sending error from $method: $errorCode") - callback?.onError(bundleOf(BundleKeys.ERROR_CODE to errorCode)) - } - - override fun startDownload(packageName: String?, list: MutableList?, bundle: Bundle?, callback: IAssetModuleServiceCallback?) { - PackageUtils.getAndCheckCallingPackage(context, packageName) - - val moduleNames = list?.getModuleNames() - val options = bundle.getOptions() - val installedAssetModules = bundle.getInstalledAssetModules() - - Log.d(TAG, "startDownload[$packageName](moduleNames: $moduleNames, installedAssetModules: $installedAssetModules)") - - if (!VendingPreferences.isAssetDeliveryEnabled(context)) { - return sendError(callback, "startDownload", AssetPackErrorCode.API_NOT_AVAILABLE) + private fun checkSessionValid(packageName: String, sessionId: Int) { + if (packageDownloadData[packageName]?.sessionIds?.values?.contains(sessionId) != true) { + Log.w(TAG, "No active session with id $sessionId in $packageName") + throw AssetPackException(AssetPackErrorCode.ACCESS_DENIED) } + } - if (packageName == null || moduleNames.isNullOrEmpty()) { - return sendError(callback, "startDownload", AssetPackErrorCode.INVALID_REQUEST) - } + override fun getDefaultSessionId(packageName: String, moduleName: String): Int = + packageDownloadData[packageName]?.sessionIds?.get(moduleName) ?: 0 + override suspend fun startDownload(params: StartDownloadParameters, packageName: String, callback: IAssetModuleServiceCallback?) { if (packageDownloadData[packageName] == null || packageDownloadData[packageName]?.packageName != packageName || - packageDownloadData[packageName]?.moduleNames?.intersect(moduleNames.toSet())?.isEmpty() == true) { - packageDownloadData[packageName] = httpClient.initAssetModuleData(context, packageName, accountManager, moduleNames, options) + packageDownloadData[packageName]?.moduleNames?.intersect(params.moduleNames.toSet())?.isEmpty() == true) { + packageDownloadData[packageName] = httpClient.initAssetModuleData(context, packageName, accountManager, params.moduleNames, params.options) if (packageDownloadData[packageName] == null) { - return sendError(callback, "startDownload", AssetPackErrorCode.API_NOT_AVAILABLE) + throw AssetPackException(AssetPackErrorCode.API_NOT_AVAILABLE) } } - moduleNames.forEach { + params.moduleNames.forEach { val moduleData = packageDownloadData[packageName]?.getModuleData(it) if (moduleData?.status != AssetPackStatus.DOWNLOADING && moduleData?.status != AssetPackStatus.COMPLETED) { packageDownloadData[packageName]?.updateDownloadStatus(it, AssetPackStatus.PENDING) @@ -111,20 +86,17 @@ class AssetModuleServiceImpl( // FIXME: If we start download later, we shouldn't send a failure callback now callback?.onError(Bundle().apply { put(BundleKeys.ERROR_CODE, AssetPackErrorCode.NETWORK_ERROR) }) } - packageDownloadData[packageName]?.getModuleData(it)?.chunks?.filter { it.sliceId != null }?.forEach { chunkData -> - val destination = chunkData.run { - File("${context.filesDir}/assetpacks/$sessionId/${this.moduleName}/$sliceId/", chunkIndex.toString()) - } - + packageDownloadData[packageName]?.getModuleData(it)?.chunks?.forEach { chunkData -> + val destination = chunkData.getChunkFile(context) if (destination.exists() && destination.length() == chunkData.chunkBytesToDownload) { sendBroadcastForExistingFile(context, packageDownloadData[packageName]!!, it, chunkData, destination) } } } - val bundleData = buildDownloadBundle(packageDownloadData[packageName]!!, list) + val bundleData = buildDownloadBundle(packageDownloadData[packageName]!!, params.moduleNames) Log.d(TAG, "startDownload: $bundleData") callback?.onStartDownload(-1, bundleData) - moduleNames.forEach { + params.moduleNames.forEach { val packData = packageDownloadData[packageName]?.getModuleData(it) if (packData?.status == AssetPackStatus.PENDING) { DownloadManager.get(context).shouldStop(false) @@ -133,35 +105,16 @@ class AssetModuleServiceImpl( } } - override fun getSessionStates(packageName: String?, bundle: Bundle?, callback: IAssetModuleServiceCallback?) { - PackageUtils.getAndCheckCallingPackage(context, packageName) - - val options = bundle.getOptions() - val installedAssetModules = bundle.getInstalledAssetModules() - - Log.d(TAG, "getSessionStates[$packageName](installedAssetModules: $installedAssetModules)") - - if (!VendingPreferences.isAssetDeliveryEnabled(context)) { - return sendError(callback, "getSessionStates", AssetPackErrorCode.API_NOT_AVAILABLE) - } - - if (packageName == null || bundle == null) { - return sendError(callback, "getSessionStates", AssetPackErrorCode.INVALID_REQUEST) - } - + override suspend fun getSessionStates(params: GetSessionStatesParameters, packageName: String, callback: IAssetModuleServiceCallback?) { val listBundleData: MutableList = mutableListOf() - packageName.takeIf { it == packageDownloadData[packageName]?.packageName }?.let { packageDownloadData[packageName]?.moduleNames?.forEach { moduleName -> - if (moduleName in installedAssetModules) return@forEach + if (moduleName in params.installedAssetModules) return@forEach listBundleData.add(sendBroadcastForExistingFile(context, packageDownloadData[packageName]!!, moduleName, null, null)) - packageDownloadData[packageName]?.getModuleData(moduleName)?.chunks?.filter { it.sliceId != null }?.forEach { chunkData -> - val destination = chunkData.run { - File("${context.filesDir}/assetpacks/$sessionId/${this.moduleName}/$sliceId/", chunkIndex.toString()) - } - + packageDownloadData[packageName]?.getModuleData(moduleName)?.chunks?.forEach { chunkData -> + val destination = chunkData.getChunkFile(context) if (destination.exists() && destination.length() == chunkData.chunkBytesToDownload) { sendBroadcastForExistingFile(context, packageDownloadData[packageName]!!, moduleName, chunkData, destination) } @@ -172,230 +125,98 @@ class AssetModuleServiceImpl( callback?.onGetSessionStates(listBundleData) } - override fun notifyChunkTransferred(packageName: String?, bundle: Bundle?, bundle2: Bundle?, callback: IAssetModuleServiceCallback?) { - PackageUtils.getAndCheckCallingPackage(context, packageName) - - val moduleName = bundle.get(BundleKeys.MODULE_NAME) - val sliceId = bundle.get(BundleKeys.SLICE_ID) - val chunkNumber = bundle.get(BundleKeys.CHUNK_NUMBER, 0) - val sessionId = bundle.get(BundleKeys.SESSION_ID, packageDownloadData[packageName]?.sessionIds?.get(moduleName) ?: 0) - val options = bundle2.getOptions() - - Log.d(TAG, "notifyChunkTransferred[$packageName](sessionId: $sessionId, moduleName: $moduleName, sliceId: $sliceId, chunkNumber: $chunkNumber)") + override suspend fun notifyChunkTransferred(params: NotifyChunkTransferredParameters, packageName: String, callback: IAssetModuleServiceCallback?) { + checkSessionValid(packageName, params.sessionId) - if (!VendingPreferences.isAssetDeliveryEnabled(context)) { - return sendError(callback, "notifyChunkTransferred", AssetPackErrorCode.API_NOT_AVAILABLE) - } - - if (packageName == null || bundle == null || moduleName.isNullOrEmpty() || sliceId.isNullOrEmpty()) { - return sendError(callback, "notifyChunkTransferred", AssetPackErrorCode.INVALID_REQUEST) - } - - if (packageDownloadData[packageName]?.sessionIds?.values?.contains(sessionId) != true) { - Log.w(TAG, "No active session with id $sessionId in $packageName") - return sendError(callback, "notifyChunkTransferred", AssetPackErrorCode.ACCESS_DENIED) - } - - val downLoadFile = "${context.filesDir.absolutePath}/assetpacks/$sessionId/$moduleName/$sliceId/$chunkNumber" + val downLoadFile = context.getChunkFile(params.sessionId, params.moduleName, params.sliceId, params.chunkNumber) fileDescriptorMap[downLoadFile]?.close() fileDescriptorMap.remove(downLoadFile) // TODO: Remove chunk after successful transfer of chunk or only with module? - callback?.onNotifyChunkTransferred(bundle, Bundle().apply { put(BundleKeys.ERROR_CODE, AssetPackErrorCode.NO_ERROR) }) + callback?.onNotifyChunkTransferred( + bundleOf(BundleKeys.MODULE_NAME to params.moduleName) + + (BundleKeys.SLICE_ID to params.sliceId) + + (BundleKeys.CHUNK_NUMBER to params.chunkNumber) + + (BundleKeys.SESSION_ID to params.sessionId), + bundleOf(BundleKeys.ERROR_CODE to AssetPackErrorCode.NO_ERROR) + ) } - override fun notifyModuleCompleted(packageName: String?, bundle: Bundle?, bundle2: Bundle?, callback: IAssetModuleServiceCallback?) { - PackageUtils.getAndCheckCallingPackage(context, packageName) - - val moduleName = bundle.get(BundleKeys.MODULE_NAME) - val sessionId = bundle.get(BundleKeys.SESSION_ID, packageDownloadData[packageName]?.sessionIds?.get(moduleName) ?: 0) - val options = bundle2.getOptions() - - Log.d(TAG, "notifyModuleCompleted[$packageName](sessionId: $sessionId, moduleName: $moduleName)") - - if (!VendingPreferences.isAssetDeliveryEnabled(context)) { - return sendError(callback, "notifyModuleCompleted", AssetPackErrorCode.API_NOT_AVAILABLE) - } - - if (packageName == null || bundle == null || moduleName.isNullOrEmpty()) { - return sendError(callback, "notifyModuleCompleted", AssetPackErrorCode.INVALID_REQUEST) - } - - if (packageDownloadData[packageName]?.sessionIds?.values?.contains(sessionId) != true) { - Log.w(TAG, "No active session with id $sessionId in $packageName") - return sendError(callback, "notifyModuleCompleted", AssetPackErrorCode.ACCESS_DENIED) - } - - packageDownloadData[packageName]?.updateDownloadStatus(moduleName, AssetPackStatus.COMPLETED) - sendBroadcastForExistingFile(context, packageDownloadData[packageName]!!, moduleName, null, null) + override suspend fun notifyModuleCompleted(params: NotifyModuleCompletedParameters, packageName: String, callback: IAssetModuleServiceCallback?) { + checkSessionValid(packageName, params.sessionId) - val downLoadFile = "${context.filesDir.absolutePath}/assetpacks/$sessionId/$moduleName" + packageDownloadData[packageName]?.updateDownloadStatus(params.moduleName, AssetPackStatus.COMPLETED) + sendBroadcastForExistingFile(context, packageDownloadData[packageName]!!, params.moduleName, null, null) - val directory = File(downLoadFile) + val directory = context.getModuleDir(params.sessionId, params.moduleName) if (directory.exists()) { directory.deleteRecursively() - Log.d(TAG, "Directory $downLoadFile deleted successfully.") + Log.d(TAG, "Directory $directory deleted successfully.") } else { - Log.d(TAG, "Directory $downLoadFile does not exist.") + Log.d(TAG, "Directory $directory does not exist.") } - callback?.onNotifyModuleCompleted(bundle, bundle2) + callback?.onNotifyModuleCompleted( + bundleOf(BundleKeys.MODULE_NAME to params.moduleName) + + (BundleKeys.SESSION_ID to params.sessionId), + bundleOf(BundleKeys.ERROR_CODE to AssetPackErrorCode.NO_ERROR) + ) } - override fun notifySessionFailed(packageName: String?, bundle: Bundle?, bundle2: Bundle?, callback: IAssetModuleServiceCallback?) { - PackageUtils.getAndCheckCallingPackage(context, packageName) - - val sessionId = bundle.get(BundleKeys.SESSION_ID, 0) - val options = bundle2.getOptions() - - Log.d(TAG, "notifySessionFailed[$packageName](sessionId: $sessionId)") - - if (!VendingPreferences.isAssetDeliveryEnabled(context)) { - return sendError(callback, "notifySessionFailed", AssetPackErrorCode.API_NOT_AVAILABLE) - } - - if (packageName == null || bundle == null) { - return sendError(callback, "notifySessionFailed", AssetPackErrorCode.INVALID_REQUEST) - } - - if (packageDownloadData[packageName]?.sessionIds?.values?.contains(sessionId) != true) { - Log.w(TAG, "No active session with id $sessionId in $packageName") - return sendError(callback, "notifySessionFailed", AssetPackErrorCode.ACCESS_DENIED) - } + override suspend fun notifySessionFailed(params: NotifySessionFailedParameters, packageName: String, callback: IAssetModuleServiceCallback?) { + checkSessionValid(packageName, params.sessionId) // TODO: Implement - return sendError(callback, "notifySessionFailed", AssetPackErrorCode.API_NOT_AVAILABLE) + throw UnsupportedOperationException() } - override fun keepAlive(packageName: String?, bundle: Bundle?, callback: IAssetModuleServiceCallback?) { - PackageUtils.getAndCheckCallingPackage(context, packageName) - - val options = bundle.getOptions() - Log.d(TAG, "keepAlive[$packageName]()") - - if (!VendingPreferences.isAssetDeliveryEnabled(context)) { - return sendError(callback, "keepAlive", AssetPackErrorCode.API_NOT_AVAILABLE) - } - - if (packageName == null) { - return sendError(callback, "keepAlive", AssetPackErrorCode.INVALID_REQUEST) - } - + override suspend fun keepAlive(params: KeepAliveParameters, packageName: String, callback: IAssetModuleServiceCallback?) { // TODO: Implement + // Not throwing an exception is the better fallback implementation } - override fun getChunkFileDescriptor(packageName: String?, bundle: Bundle?, bundle2: Bundle?, callback: IAssetModuleServiceCallback?) { - PackageUtils.getAndCheckCallingPackage(context, packageName) - - val moduleName = bundle.get(BundleKeys.MODULE_NAME) - val sessionId = bundle.get(BundleKeys.SESSION_ID, packageDownloadData[packageName]?.sessionIds?.get(moduleName) ?: 0) - val sliceId = bundle.get(BundleKeys.SLICE_ID) - val chunkNumber = bundle.get(BundleKeys.CHUNK_NUMBER, 0) - val options = bundle2.getOptions() - - Log.d(TAG, "getChunkFileDescriptor[$packageName](sessionId: $sessionId, moduleName: $moduleName, sliceId: $sliceId, chunkNumber: $chunkNumber)") - - if (!VendingPreferences.isAssetDeliveryEnabled(context)) { - return sendError(callback, "getChunkFileDescriptor", AssetPackErrorCode.API_NOT_AVAILABLE) - } - - if (packageName == null || bundle == null || moduleName.isNullOrEmpty() || sliceId.isNullOrEmpty()) { - return sendError(callback, "getChunkFileDescriptor", AssetPackErrorCode.INVALID_REQUEST) - } - - if (packageDownloadData[packageName]?.sessionIds?.values?.contains(sessionId) != true) { - Log.w(TAG, "No active session with id $sessionId in $packageName") - return sendError(callback, "getChunkFileDescriptor", AssetPackErrorCode.ACCESS_DENIED) - } + override suspend fun getChunkFileDescriptor(params: GetChunkFileDescriptorParameters, packageName: String, callback: IAssetModuleServiceCallback?) { + checkSessionValid(packageName, params.sessionId) val parcelFileDescriptor = runCatching { - val downLoadFile = "${context.filesDir.absolutePath}/assetpacks/$sessionId/$moduleName/$sliceId/$chunkNumber" - val filePath = Uri.parse(downLoadFile).path?.let { File(it) } - ParcelFileDescriptor.open(filePath, ParcelFileDescriptor.MODE_READ_ONLY).also { + val downLoadFile = context.getChunkFile(params.sessionId, params.moduleName, params.sliceId, params.chunkNumber) + ParcelFileDescriptor.open(downLoadFile, ParcelFileDescriptor.MODE_READ_ONLY).also { fileDescriptorMap[downLoadFile] = it } }.getOrNull() - callback?.onGetChunkFileDescriptor(Bundle().apply { put(BundleKeys.CHUNK_FILE_DESCRIPTOR, parcelFileDescriptor) }, Bundle()) - } - - override fun requestDownloadInfo(packageName: String?, list: MutableList?, bundle: Bundle?, callback: IAssetModuleServiceCallback?) { - PackageUtils.getAndCheckCallingPackage(context, packageName) - - val moduleNames = list?.getModuleNames() - val options = bundle.getOptions() - val installedAssetModules = bundle.getInstalledAssetModules() - Log.d(TAG, "requestDownloadInfo[$packageName](moduleNames: $moduleNames, installedAssetModules: $installedAssetModules)") - - if (!VendingPreferences.isAssetDeliveryEnabled(context)) { - return sendError(callback, "requestDownloadInfo", AssetPackErrorCode.API_NOT_AVAILABLE) - } - - if (packageName == null || moduleNames.isNullOrEmpty()) { - return sendError(callback, "requestDownloadInfo", AssetPackErrorCode.INVALID_REQUEST) - } + callback?.onGetChunkFileDescriptor( + bundleOf(BundleKeys.CHUNK_FILE_DESCRIPTOR to parcelFileDescriptor), + bundleOf(BundleKeys.ERROR_CODE to AssetPackErrorCode.NO_ERROR) + ) + } + override suspend fun requestDownloadInfo(params: RequestDownloadInfoParameters, packageName: String, callback: IAssetModuleServiceCallback?) { if (packageDownloadData[packageName] == null || packageDownloadData[packageName]?.packageName != packageName || - packageDownloadData[packageName]?.moduleNames?.intersect(moduleNames.toSet())?.isEmpty() == true) { - packageDownloadData[packageName] = httpClient.initAssetModuleData(context, packageName, accountManager, moduleNames, options) + packageDownloadData[packageName]?.moduleNames?.intersect(params.moduleNames.toSet())?.isEmpty() == true) { + packageDownloadData[packageName] = httpClient.initAssetModuleData(context, packageName, accountManager, params.moduleNames, params.options) if (packageDownloadData[packageName] == null) { - callback?.onError(Bundle().apply { put(BundleKeys.ERROR_CODE, AssetPackErrorCode.API_NOT_AVAILABLE) }) - return + throw AssetPackException(AssetPackErrorCode.API_NOT_AVAILABLE) } } - moduleNames.forEach { + params.moduleNames.forEach { val packData = packageDownloadData[packageName]?.getModuleData(it) if (packData?.status == AssetPackStatus.FAILED) { + // FIXME: If we start download later, we shouldn't send a failure callback now callback?.onError(Bundle().apply { put(BundleKeys.ERROR_CODE, AssetPackErrorCode.NETWORK_ERROR) }) } } - val bundleData = buildDownloadBundle(packageDownloadData[packageName]!!, list) - Log.d(TAG, "requestDownloadInfo: $bundleData") + val bundleData = buildDownloadBundle(packageDownloadData[packageName]!!, params.moduleNames) callback?.onRequestDownloadInfo(bundleData, bundleData) } - override fun removeModule(packageName: String?, bundle: Bundle?, bundle2: Bundle?, callback: IAssetModuleServiceCallback?) { - PackageUtils.getAndCheckCallingPackage(context, packageName) - - val moduleName = bundle?.get(BundleKeys.MODULE_NAME) - val sessionId = bundle.get(BundleKeys.SESSION_ID, packageDownloadData[packageName]?.sessionIds?.get(moduleName) ?: 0) - val options = bundle2.getOptions() - - Log.d(TAG, "removeModule[$packageName](sessionId: $sessionId, moduleName: $moduleName)") - - if (!VendingPreferences.isAssetDeliveryEnabled(context)) { - return sendError(callback, "removeModule", AssetPackErrorCode.API_NOT_AVAILABLE) - } - - if (packageName == null || bundle == null || moduleName.isNullOrEmpty()) { - return sendError(callback, "removeModule", AssetPackErrorCode.INVALID_REQUEST) - } - - if (packageDownloadData[packageName]?.sessionIds?.values?.contains(sessionId) != true) { - Log.w(TAG, "No active session with id $sessionId in $packageName") - return sendError(callback, "removeModule", AssetPackErrorCode.ACCESS_DENIED) - } - + override suspend fun removeModule(params: RemoveModuleParameters, packageName: String, callback: IAssetModuleServiceCallback?) { + checkSessionValid(packageName, params.sessionId) // TODO: Implement - return sendError(callback, "removeModule", AssetPackErrorCode.API_NOT_AVAILABLE) + throw UnsupportedOperationException() } - override fun cancelDownloads(packageName: String?, list: MutableList?, bundle: Bundle?, callback: IAssetModuleServiceCallback?) { - PackageUtils.getAndCheckCallingPackage(context, packageName) - - val moduleNames = list?.getModuleNames() - val options = bundle.getOptions() - - Log.d(TAG, "cancelDownloads[$packageName](moduleNames: $moduleNames)") - - if (!VendingPreferences.isAssetDeliveryEnabled(context)) { - return sendError(callback, "cancelDownloads", AssetPackErrorCode.API_NOT_AVAILABLE) - } - - if (packageName == null || moduleNames.isNullOrEmpty()) { - return sendError(callback, "cancelDownloads", AssetPackErrorCode.INVALID_REQUEST) - } - + override suspend fun cancelDownloads(params: CancelDownloadsParameters, packageName: String, callback: IAssetModuleServiceCallback?) { // TODO: Implement - return sendError(callback, "cancelDownloads", AssetPackErrorCode.API_NOT_AVAILABLE) + throw UnsupportedOperationException() } } \ No newline at end of file diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/DownloadData.kt b/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/DownloadData.kt index 4f03df6e16..137930242a 100644 --- a/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/DownloadData.kt +++ b/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/DownloadData.kt @@ -5,6 +5,9 @@ package com.google.android.finsky.assetmoduleservice +import android.content.Context +import com.google.android.finsky.getChunkFile +import java.io.File import java.io.Serializable data class DownloadData( @@ -59,11 +62,13 @@ data class ModuleData( data class ChunkData( var sessionId: Int, val moduleName: String, - val sliceId: String?, + val sliceId: String, val chunkSourceUri: String?, val chunkBytesToDownload: Long, val chunkIndex: Int, val sliceUncompressedSize: Long, val sliceUncompressedHashSha256: String?, val numberOfChunksInSlice: Int -) \ No newline at end of file +) + +fun ChunkData.getChunkFile(context: Context) = context.getChunkFile(sessionId, moduleName, sliceId, chunkIndex) \ No newline at end of file diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/extensions.kt b/vending-app/src/main/kotlin/com/google/android/finsky/extensions.kt index 8570256ffe..ddb3e20493 100644 --- a/vending-app/src/main/kotlin/com/google/android/finsky/extensions.kt +++ b/vending-app/src/main/kotlin/com/google/android/finsky/extensions.kt @@ -26,6 +26,8 @@ import com.google.android.play.core.assetpacks.model.AssetPackErrorCode import com.google.android.play.core.assetpacks.model.AssetPackStatus import com.google.android.play.core.assetpacks.protocol.BroadcastConstants import com.google.android.play.core.assetpacks.protocol.BundleKeys +import com.google.android.play.core.assetpacks.protocol.CompressionFormat +import com.google.android.play.core.assetpacks.protocol.PatchFormat import kotlinx.coroutines.runBlocking import org.microg.gms.auth.AuthConstants import org.microg.vending.billing.GServices @@ -51,18 +53,31 @@ fun Bundle.put(key: BundleKeys.RootKey, v: T) = BundleKeys.put(this, key, fun Bundle.put(key: BundleKeys.PackKey, packName: String, v: T) = BundleKeys.put(this, key, packName, v) fun Bundle.put(key: BundleKeys.SliceKey, packName: String, sliceId: String, v: T) = BundleKeys.put(this, key, packName, sliceId, v) fun bundleOf(pair: Pair, T>): Bundle = Bundle().apply { put(pair.first, pair.second) } +operator fun Bundle.plus(other: Bundle): Bundle = Bundle(this).apply { putAll(other) } +operator fun Bundle.plus(pair: Pair, T>): Bundle = this + bundleOf(pair) -data class Options(val playCoreVersionCode: Int, val supportedCompressionFormats: List, val supportedPatchFormats: List) +val Context.assetPacksDir: File + get() = File(filesDir, "assetpacks") +fun Context.getSessionDir(sessionId: Int) = + File(assetPacksDir, sessionId.toString()) +fun Context.getModuleDir(sessionId: Int, moduleName: String): File = + File(getSessionDir(sessionId), moduleName) +fun Context.getSliceDir(sessionId: Int, moduleName: String, sliceId: String) = + File(getModuleDir(sessionId, moduleName), sliceId) +fun Context.getChunkFile(sessionId: Int, moduleName: String, sliceId: String, chunkNumber: Int): File = + File(getSliceDir(sessionId, moduleName, sliceId), chunkNumber.toString()) -fun HttpClient.initAssetModuleData( +data class AssetModuleOptions(val playCoreVersionCode: Int, val supportedCompressionFormats: List, val supportedPatchFormats: List) + +suspend fun HttpClient.initAssetModuleData( context: Context, packageName: String, accountManager: AccountManager, requestedAssetModuleNames: List, - options: Options, + options: AssetModuleOptions, playCoreVersionCode: Int = options.playCoreVersionCode, - supportedCompressionFormats: List = options.supportedCompressionFormats.takeIf { it.isNotEmpty() } ?: listOf(0, 3), - supportedPatchFormats: List = options.supportedPatchFormats.takeIf { it.isNotEmpty() } ?: listOf(1, 2), + supportedCompressionFormats: List = options.supportedCompressionFormats.takeIf { it.isNotEmpty() } ?: listOf(CompressionFormat.UNSPECIFIED, CompressionFormat.CHUNKED_GZIP), + supportedPatchFormats: List = options.supportedPatchFormats.takeIf { it.isNotEmpty() } ?: listOf(PatchFormat.PATCH_GDIFF, PatchFormat.GZIPPED_GDIFF), ): DownloadData? { val accounts = accountManager.getAccountsByType(AuthConstants.DEFAULT_ACCOUNT_TYPE) var oauthToken: String? = null @@ -70,9 +85,7 @@ fun HttpClient.initAssetModuleData( return null } else { for (account: Account in accounts) { - oauthToken = runBlocking { - accountManager.getAuthToken(account, AUTH_TOKEN_SCOPE, false).getString(AccountManager.KEY_AUTHTOKEN) - } + oauthToken = accountManager.getAuthToken(account, AUTH_TOKEN_SCOPE, false).getString(AccountManager.KEY_AUTHTOKEN) if (oauthToken != null) { break } @@ -92,17 +105,14 @@ fun HttpClient.initAssetModuleData( val androidId = GServices.getString(context.contentResolver, "android_id", "0")?.toLong() ?: 1 - // FIXME: Don't runBlocking, use async - val moduleDeliveryInfo = runBlocking { - runCatching { - post( - url = ASSET_MODULE_DELIVERY_URL, - headers = getLicenseRequestHeaders(oauthToken, androidId), - payload = requestPayload, - adapter = AssetModuleDeliveryResponse.ADAPTER - ).wrapper?.deliveryInfo - }.getOrNull() - } + val moduleDeliveryInfo = runCatching { + post( + url = ASSET_MODULE_DELIVERY_URL, + headers = getLicenseRequestHeaders(oauthToken, androidId), + payload = requestPayload, + adapter = AssetModuleDeliveryResponse.ADAPTER + ).wrapper?.deliveryInfo + }.getOrNull() Log.d(TAG, "initAssetModuleData: moduleDeliveryInfo-> $moduleDeliveryInfo") return initModuleDownloadInfo(packageName, appVersionCode, moduleDeliveryInfo) } @@ -164,7 +174,7 @@ private fun initModuleDownloadInfo(packageName: String, appVersionCode: Long?, d val numberOfChunks = chunks.size val uncompressedSize = sliceInfo.fullDownloadInfo.uncompressedSize val uncompressedHashSha256 = sliceInfo.fullDownloadInfo.uncompressedHashSha256 - val sliceId = sliceInfo.metadata.sliceId?.also { sliceIds.add(it) } + val sliceId = sliceInfo.metadata.sliceId?.also { sliceIds.add(it) } ?: continue var sliceBytesToDownload = 0L for (chunkIndex in chunks.indices) { val dResource: ChunkInfo = chunks[chunkIndex] @@ -209,15 +219,14 @@ private fun initModuleDownloadInfo(packageName: String, appVersionCode: Long?, d ) } -fun buildDownloadBundle(downloadData: DownloadData, list: List? = null): Bundle { +fun buildDownloadBundle(downloadData: DownloadData, list: List? = null): Bundle { val bundleData = Bundle() val arrayList = arrayListOf() var totalBytesToDownload = 0L var bytesDownloaded = 0L - list?.forEach { - val moduleName = it?.get(BundleKeys.MODULE_NAME) - val packData = downloadData.getModuleData(moduleName!!) + list?.forEach { moduleName -> + val packData = downloadData.getModuleData(moduleName) bundleData.put(BundleKeys.STATUS, packData.status) downloadData.sessionIds[moduleName]?.let { sessionId -> bundleData.put(BundleKeys.SESSION_ID, sessionId) diff --git a/vending-app/src/main/res/drawable/ic_cancel.xml b/vending-app/src/main/res/drawable/ic_cancel.xml new file mode 100644 index 0000000000..55a4375b31 --- /dev/null +++ b/vending-app/src/main/res/drawable/ic_cancel.xml @@ -0,0 +1,17 @@ + + + + + + + From eb484ba5be843d6412985e8cf9488ff32894a872 Mon Sep 17 00:00:00 2001 From: Marvin W Date: Tue, 3 Dec 2024 14:07:24 +0100 Subject: [PATCH 098/132] Asset Modules: Improve error handling and add compression format --- .../core/assetpacks/protocol/BundleKeys.java | 4 +- .../assetmoduleservice/AssetModuleService.kt | 36 +++++++------ .../finsky/assetmoduleservice/DownloadData.kt | 2 + .../com/google/android/finsky/extensions.kt | 54 +++++++++---------- vending-app/src/main/proto/AssetModule.proto | 2 +- 5 files changed, 49 insertions(+), 49 deletions(-) diff --git a/vending-app/src/main/java/com/google/android/play/core/assetpacks/protocol/BundleKeys.java b/vending-app/src/main/java/com/google/android/play/core/assetpacks/protocol/BundleKeys.java index 8293f80358..0abba2f3b8 100644 --- a/vending-app/src/main/java/com/google/android/play/core/assetpacks/protocol/BundleKeys.java +++ b/vending-app/src/main/java/com/google/android/play/core/assetpacks/protocol/BundleKeys.java @@ -47,8 +47,8 @@ public final class BundleKeys { public static PackKey> SLICE_IDS = new PackKey.StringArrayList("slice_ids"); public static SliceKey> CHUNK_INTENTS = new SliceKey.ParcelableArrayList<>("chunk_intents", Intent.class); - public static SliceKey COMPRESSION_FORMAT = new SliceKey.Int("compression_format"); - public static SliceKey PATCH_FORMAT = new SliceKey.Int("patch_format"); + public static SliceKey<@CompressionFormat Integer> COMPRESSION_FORMAT = new SliceKey.Int("compression_format"); + public static SliceKey<@PatchFormat Integer> PATCH_FORMAT = new SliceKey.Int("patch_format"); public static SliceKey UNCOMPRESSED_HASH_SHA256 = new SliceKey.String("uncompressed_hash_sha256"); public static SliceKey UNCOMPRESSED_SIZE = new SliceKey.Long("uncompressed_size"); diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/AssetModuleService.kt b/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/AssetModuleService.kt index 02e90a84f6..cde513190e 100644 --- a/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/AssetModuleService.kt +++ b/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/AssetModuleService.kt @@ -107,20 +107,20 @@ class AssetModuleServiceImpl( override suspend fun getSessionStates(params: GetSessionStatesParameters, packageName: String, callback: IAssetModuleServiceCallback?) { val listBundleData: MutableList = mutableListOf() - packageName.takeIf { it == packageDownloadData[packageName]?.packageName }?.let { - packageDownloadData[packageName]?.moduleNames?.forEach { moduleName -> - if (moduleName in params.installedAssetModules) return@forEach - listBundleData.add(sendBroadcastForExistingFile(context, packageDownloadData[packageName]!!, moduleName, null, null)) + packageDownloadData[packageName]?.moduleNames?.forEach { moduleName -> + if (moduleName in params.installedAssetModules) return@forEach - packageDownloadData[packageName]?.getModuleData(moduleName)?.chunks?.forEach { chunkData -> - val destination = chunkData.getChunkFile(context) - if (destination.exists() && destination.length() == chunkData.chunkBytesToDownload) { - sendBroadcastForExistingFile(context, packageDownloadData[packageName]!!, moduleName, chunkData, destination) - } + listBundleData.add(sendBroadcastForExistingFile(context, packageDownloadData[packageName]!!, moduleName, null, null)) + + packageDownloadData[packageName]?.getModuleData(moduleName)?.chunks?.forEach { chunkData -> + val destination = chunkData.getChunkFile(context) + if (destination.exists() && destination.length() == chunkData.chunkBytesToDownload) { + sendBroadcastForExistingFile(context, packageDownloadData[packageName]!!, moduleName, chunkData, destination) } } } + Log.d(TAG, "getSessionStates: $listBundleData") callback?.onGetSessionStates(listBundleData) } @@ -165,7 +165,8 @@ class AssetModuleServiceImpl( checkSessionValid(packageName, params.sessionId) // TODO: Implement - throw UnsupportedOperationException() + callback?.onNotifySessionFailed(bundleOf(BundleKeys.SESSION_ID to params.sessionId)) + //throw UnsupportedOperationException() } override suspend fun keepAlive(params: KeepAliveParameters, packageName: String, callback: IAssetModuleServiceCallback?) { @@ -176,13 +177,12 @@ class AssetModuleServiceImpl( override suspend fun getChunkFileDescriptor(params: GetChunkFileDescriptorParameters, packageName: String, callback: IAssetModuleServiceCallback?) { checkSessionValid(packageName, params.sessionId) - val parcelFileDescriptor = runCatching { - val downLoadFile = context.getChunkFile(params.sessionId, params.moduleName, params.sliceId, params.chunkNumber) - ParcelFileDescriptor.open(downLoadFile, ParcelFileDescriptor.MODE_READ_ONLY).also { - fileDescriptorMap[downLoadFile] = it - } - }.getOrNull() + val downLoadFile = context.getChunkFile(params.sessionId, params.moduleName, params.sliceId, params.chunkNumber) + val parcelFileDescriptor = ParcelFileDescriptor.open(downLoadFile, ParcelFileDescriptor.MODE_READ_ONLY).also { + fileDescriptorMap[downLoadFile] = it + } + Log.d(TAG, "getChunkFileDescriptor -> $parcelFileDescriptor") callback?.onGetChunkFileDescriptor( bundleOf(BundleKeys.CHUNK_FILE_DESCRIPTOR to parcelFileDescriptor), bundleOf(BundleKeys.ERROR_CODE to AssetPackErrorCode.NO_ERROR) @@ -206,6 +206,7 @@ class AssetModuleServiceImpl( } } val bundleData = buildDownloadBundle(packageDownloadData[packageName]!!, params.moduleNames) + Log.d(TAG, "requestDownloadInfo -> $bundleData") callback?.onRequestDownloadInfo(bundleData, bundleData) } @@ -217,6 +218,7 @@ class AssetModuleServiceImpl( override suspend fun cancelDownloads(params: CancelDownloadsParameters, packageName: String, callback: IAssetModuleServiceCallback?) { // TODO: Implement - throw UnsupportedOperationException() + callback?.onCancelDownloads(bundleOf(BundleKeys.ERROR_CODE to AssetPackErrorCode.NO_ERROR)) + //throw UnsupportedOperationException() } } \ No newline at end of file diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/DownloadData.kt b/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/DownloadData.kt index 137930242a..ccc3f7c339 100644 --- a/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/DownloadData.kt +++ b/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/DownloadData.kt @@ -7,6 +7,7 @@ package com.google.android.finsky.assetmoduleservice import android.content.Context import com.google.android.finsky.getChunkFile +import com.google.android.play.core.assetpacks.protocol.CompressionFormat import java.io.File import java.io.Serializable @@ -66,6 +67,7 @@ data class ChunkData( val chunkSourceUri: String?, val chunkBytesToDownload: Long, val chunkIndex: Int, + val sliceCompressionFormat: @CompressionFormat Int, val sliceUncompressedSize: Long, val sliceUncompressedHashSha256: String?, val numberOfChunksInSlice: Int diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/extensions.kt b/vending-app/src/main/kotlin/com/google/android/finsky/extensions.kt index ddb3e20493..4a1e53664d 100644 --- a/vending-app/src/main/kotlin/com/google/android/finsky/extensions.kt +++ b/vending-app/src/main/kotlin/com/google/android/finsky/extensions.kt @@ -9,6 +9,7 @@ import android.accounts.Account import android.accounts.AccountManager import android.content.Context import android.content.Intent +import android.content.pm.PackageManager import android.net.Uri import android.os.Bundle import android.util.Log @@ -19,6 +20,7 @@ import androidx.core.content.pm.PackageInfoCompat import com.android.vending.licensing.AUTH_TOKEN_SCOPE import com.android.vending.licensing.getAuthToken import com.android.vending.licensing.getLicenseRequestHeaders +import com.google.android.finsky.assetmoduleservice.AssetPackException import com.google.android.finsky.assetmoduleservice.DownloadData import com.google.android.finsky.assetmoduleservice.ModuleData import com.google.android.finsky.assetmoduleservice.ChunkData @@ -28,7 +30,6 @@ import com.google.android.play.core.assetpacks.protocol.BroadcastConstants import com.google.android.play.core.assetpacks.protocol.BundleKeys import com.google.android.play.core.assetpacks.protocol.CompressionFormat import com.google.android.play.core.assetpacks.protocol.PatchFormat -import kotlinx.coroutines.runBlocking import org.microg.gms.auth.AuthConstants import org.microg.vending.billing.GServices import org.microg.vending.billing.core.HttpClient @@ -44,7 +45,11 @@ private const val ASSET_MODULE_DELIVERY_URL = "https://play-fe.googleapis.com/fd private const val TAG = "AssetModuleRequest" fun getAppVersionCode(context: Context, packageName: String): Long? { - return runCatching { PackageInfoCompat.getLongVersionCode(context.packageManager.getPackageInfo(packageName, 0)) }.getOrNull() + return try { + PackageInfoCompat.getLongVersionCode(context.packageManager.getPackageInfo(packageName, 0)) + } catch (e: PackageManager.NameNotFoundException) { + throw AssetPackException(AssetPackErrorCode.APP_UNAVAILABLE, e.message) + } } fun Bundle?.get(key: BundleKeys.RootKey): T? = if (this == null) null else BundleKeys.get(this, key) @@ -105,14 +110,12 @@ suspend fun HttpClient.initAssetModuleData( val androidId = GServices.getString(context.contentResolver, "android_id", "0")?.toLong() ?: 1 - val moduleDeliveryInfo = runCatching { - post( - url = ASSET_MODULE_DELIVERY_URL, - headers = getLicenseRequestHeaders(oauthToken, androidId), - payload = requestPayload, - adapter = AssetModuleDeliveryResponse.ADAPTER - ).wrapper?.deliveryInfo - }.getOrNull() + val moduleDeliveryInfo = post( + url = ASSET_MODULE_DELIVERY_URL, + headers = getLicenseRequestHeaders(oauthToken, androidId), + payload = requestPayload, + adapter = AssetModuleDeliveryResponse.ADAPTER + ).wrapper?.deliveryInfo Log.d(TAG, "initAssetModuleData: moduleDeliveryInfo-> $moduleDeliveryInfo") return initModuleDownloadInfo(packageName, appVersionCode, moduleDeliveryInfo) } @@ -187,6 +190,7 @@ private fun initModuleDownloadInfo(packageName: String, appVersionCode: Long?, d chunkSourceUri = dResource.sourceUri, chunkBytesToDownload = dResource.bytesToDownload, chunkIndex = chunkIndex, + sliceCompressionFormat = sliceInfo.fullDownloadInfo.compressionFormat ?: CompressionFormat.UNSPECIFIED, sliceUncompressedSize = uncompressedSize ?: 0, sliceUncompressedHashSha256 = uncompressedHashSha256, numberOfChunksInSlice = numberOfChunks @@ -269,34 +273,26 @@ fun sendBroadcastForExistingFile(context: Context, downloadData: DownloadData, m downloadBundle.put(BundleKeys.PACK_BASE_VERSION, moduleName, packData.moduleVersion) downloadBundle.put(BundleKeys.PACK_VERSION_TAG, moduleName, null) packData.chunks.map { it.copy() }.forEach { - val sliceId = it.sliceId ?: "" - val uncompressedSize = it.sliceUncompressedSize - val uncompressedHashSha256 = it.sliceUncompressedHashSha256 - val numberOfChunksInSlice = it.numberOfChunksInSlice - val chunkIntents: ArrayList - if (destination == null) { - chunkIntents = ArrayList(Collections.nCopies(numberOfChunksInSlice, null)) - } else { - val uFile = Uri.parse(destination.absolutePath).path?.let { path -> File(path) } - chunkIntents = ArrayList(Collections.nCopies(numberOfChunksInSlice, null)) - val uri = Uri.fromFile(uFile) + val sliceId = it.sliceId + val chunkIntents = ArrayList(Collections.nCopies(it.numberOfChunksInSlice, null)) + if (chunkData != null && destination != null) { + val uri = Uri.fromFile(destination) context.grantUriPermission(moduleName, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION) val intent = Intent(Intent.ACTION_VIEW) intent.setDataAndType(uri, context.contentResolver.getType(uri)) intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) - val resourceBlockIndex = chunkData?.chunkIndex - if (uFile?.exists() == true && chunkData?.sliceId == sliceId && resourceBlockIndex != null) { - if (chunkIntents[resourceBlockIndex] == null) { - chunkIntents[resourceBlockIndex] = intent + if (destination.exists() && chunkData.moduleName == moduleName && chunkData.sliceId == sliceId) { + if (chunkIntents[chunkData.chunkIndex] == null) { + chunkIntents[chunkData.chunkIndex] = intent } } } downloadBundle.put(BundleKeys.CHUNK_INTENTS, moduleName, sliceId, chunkIntents) - downloadBundle.put(BundleKeys.UNCOMPRESSED_SIZE, moduleName, sliceId, uncompressedSize) - downloadBundle.put(BundleKeys.COMPRESSION_FORMAT, moduleName, sliceId, 1) // TODO - downloadBundle.put(BundleKeys.UNCOMPRESSED_HASH_SHA256, moduleName, sliceId, uncompressedHashSha256) + downloadBundle.put(BundleKeys.UNCOMPRESSED_SIZE, moduleName, sliceId, it.sliceUncompressedSize) + downloadBundle.put(BundleKeys.COMPRESSION_FORMAT, moduleName, sliceId, it.sliceCompressionFormat) + downloadBundle.put(BundleKeys.UNCOMPRESSED_HASH_SHA256, moduleName, sliceId, it.sliceUncompressedHashSha256) } - downloadBundle.put(BundleKeys.SLICE_IDS, moduleName, ArrayList(packData.chunks.mapNotNull { it.sliceId }.distinct())) + downloadBundle.put(BundleKeys.SLICE_IDS, moduleName, ArrayList(packData.chunks.map { it.sliceId }.distinct())) sendBroadCast(context, downloadData, downloadBundle) return downloadBundle } catch (e: Exception) { diff --git a/vending-app/src/main/proto/AssetModule.proto b/vending-app/src/main/proto/AssetModule.proto index 0f51b0815c..deaa3d0c59 100644 --- a/vending-app/src/main/proto/AssetModule.proto +++ b/vending-app/src/main/proto/AssetModule.proto @@ -70,6 +70,6 @@ message PatchInfo { message ChunkInfo { optional int64 bytesToDownload = 1; - optional string unknown = 2; + optional string sha256 = 2; optional string sourceUri = 3; } From c08df0484766e4e8b17461b0e871fb801807eae5 Mon Sep 17 00:00:00 2001 From: Marvin W Date: Thu, 5 Dec 2024 20:41:18 +0100 Subject: [PATCH 099/132] Ads: Add dummy initializer to DynamiteMeasurementManager --- .../measurement/DynamiteMeasurementManager.kt | 16 +++++++++++++++- .../ads/measurement/IAppMeasurementProxy.aidl | 5 +++++ .../gms/ads/measurement/IMeasurementManager.aidl | 5 ++++- 3 files changed, 24 insertions(+), 2 deletions(-) create mode 100644 play-services-ads-lite/src/main/aidl/com/google/android/gms/ads/measurement/IAppMeasurementProxy.aidl diff --git a/play-services-ads-lite/core/src/main/kotlin/com/google/android/gms/ads/measurement/DynamiteMeasurementManager.kt b/play-services-ads-lite/core/src/main/kotlin/com/google/android/gms/ads/measurement/DynamiteMeasurementManager.kt index 0ad53909ac..5959db3af6 100644 --- a/play-services-ads-lite/core/src/main/kotlin/com/google/android/gms/ads/measurement/DynamiteMeasurementManager.kt +++ b/play-services-ads-lite/core/src/main/kotlin/com/google/android/gms/ads/measurement/DynamiteMeasurementManager.kt @@ -4,7 +4,21 @@ */ package com.google.android.gms.ads.measurement +import android.os.Parcel +import android.util.Log import androidx.annotation.Keep +import com.google.android.gms.dynamic.IObjectWrapper +import org.microg.gms.utils.warnOnTransactionIssues + +private const val TAG = "DynamiteMeasurement" @Keep -class DynamiteMeasurementManager : IMeasurementManager.Stub() +class DynamiteMeasurementManager : IMeasurementManager.Stub() { + + override fun initialize(context: IObjectWrapper?, proxy: IAppMeasurementProxy?) { + Log.d(TAG, "Not yet implemented: initialize") + } + + override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean = + warnOnTransactionIssues(code, reply, flags, TAG) { super.onTransact(code, data, reply, flags) } +} diff --git a/play-services-ads-lite/src/main/aidl/com/google/android/gms/ads/measurement/IAppMeasurementProxy.aidl b/play-services-ads-lite/src/main/aidl/com/google/android/gms/ads/measurement/IAppMeasurementProxy.aidl new file mode 100644 index 0000000000..1fc75009af --- /dev/null +++ b/play-services-ads-lite/src/main/aidl/com/google/android/gms/ads/measurement/IAppMeasurementProxy.aidl @@ -0,0 +1,5 @@ +package com.google.android.gms.ads.measurement; + +interface IAppMeasurementProxy { + +} \ No newline at end of file diff --git a/play-services-ads-lite/src/main/aidl/com/google/android/gms/ads/measurement/IMeasurementManager.aidl b/play-services-ads-lite/src/main/aidl/com/google/android/gms/ads/measurement/IMeasurementManager.aidl index e82159df4f..92f267b410 100644 --- a/play-services-ads-lite/src/main/aidl/com/google/android/gms/ads/measurement/IMeasurementManager.aidl +++ b/play-services-ads-lite/src/main/aidl/com/google/android/gms/ads/measurement/IMeasurementManager.aidl @@ -1,5 +1,8 @@ package com.google.android.gms.ads.measurement; -interface IMeasurementManager { +import com.google.android.gms.ads.measurement.IAppMeasurementProxy; +import com.google.android.gms.dynamic.IObjectWrapper; +interface IMeasurementManager { + void initialize(IObjectWrapper context, IAppMeasurementProxy proxy) = 1; } \ No newline at end of file From b96aae4b52063b38815f86e7876c2e89e2c0b14c Mon Sep 17 00:00:00 2001 From: Leo Alvesson Date: Wed, 13 Nov 2024 09:30:28 +0000 Subject: [PATCH 100/132] Translated using Weblate (French) Currently translated at 100.0% (249 of 249 strings) Translation: microG/play-services-core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-core-strings/fr/ Translated using Weblate (French) Currently translated at 100.0% (4 of 4 strings) Translation: microG/play-services-core: plurals Translate-URL: https://hosted.weblate.org/projects/microg/play-services-core-plurals/fr/ Translated using Weblate (French) Currently translated at 100.0% (13 of 13 strings) Translation: microG/play-services-base: core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-base-core-strings/fr/ Translated using Weblate (French) Currently translated at 100.0% (50 of 50 strings) Translation: microG/play-services-location: core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-location-core-strings/fr/ Translated using Weblate (French) Currently translated at 100.0% (249 of 249 strings) Translation: microG/play-services-core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-core-strings/fr/ Translated using Weblate (French) Currently translated at 100.0% (156 of 156 strings) Translation: microG/play-services-core: permissions Translate-URL: https://hosted.weblate.org/projects/microg/play-services-core-permissions/fr/ Translated using Weblate (French) Currently translated at 100.0% (24 of 24 strings) Translation: microG/play-services-fido: core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-fido-core-strings/fr/ Translated using Weblate (French) Currently translated at 100.0% (252 of 252 strings) Translation: microG/play-services-core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-core-strings/fr/ Translated using Weblate (French) Currently translated at 100.0% (21 of 21 strings) Translation: microG/vending-app Translate-URL: https://hosted.weblate.org/projects/microg/vending-app/fr/ --- .../core/src/main/res/values-fr/strings.xml | 4 +-- .../src/main/res/values-fr/permissions.xml | 20 +++++------ .../src/main/res/values-fr/plurals.xml | 2 +- .../src/main/res/values-fr/strings.xml | 35 ++++++++++--------- .../core/src/main/res/values-fr/strings.xml | 2 +- .../core/src/main/res/values-fr/strings.xml | 4 +-- .../src/main/res/values-fr/strings.xml | 2 ++ 7 files changed, 37 insertions(+), 32 deletions(-) diff --git a/play-services-base/core/src/main/res/values-fr/strings.xml b/play-services-base/core/src/main/res/values-fr/strings.xml index b615a4bc86..702a0b68bd 100644 --- a/play-services-base/core/src/main/res/values-fr/strings.xml +++ b/play-services-base/core/src/main/res/values-fr/strings.xml @@ -12,8 +12,8 @@ Actif en arrière-plan Ouvrir Exclure %1$s de l\'optimisation de la batterie ou modifier les paramètres des notifications pour désactiver cette notification. - Act. - Désact. + Actif + Inactif Tout voir %1$s fonctionne en arrière-plan. \ No newline at end of file diff --git a/play-services-core/src/main/res/values-fr/permissions.xml b/play-services-core/src/main/res/values-fr/permissions.xml index a443789139..d28abf2251 100644 --- a/play-services-core/src/main/res/values-fr/permissions.xml +++ b/play-services-core/src/main/res/values-fr/permissions.xml @@ -36,7 +36,7 @@ accéder à Google Finance par le biais de tout compte Google associé. Google Base - service permettant de mettre en ligne et de référencer tout type de contenu en termes d\'information accéder à Google Base par le biais de tout compte Google associé. - Accéder à Google Play Développeur Android + accéder à Google Play Développeur Android Pour les utilisateurs et administrateurs revendeurs, accès lecture/écriture lors de tests dans la sandbox de l\'API, ou accès lecture/écriture lors d\'un appel direct pour une opération API. Accès lecture/écriture à l\'API Groups Migration. Gérer vos calendriers @@ -128,7 +128,7 @@ accéder à la recherche vocale par le biais de tout compte Google associé. Recherche Vocale accéder aux noms d\'utilisateurs YouTube par le biais de tout compte Google associé. - Voir et gérer vos données Ad Exchange + voir et gérer vos données Ad Exchange Gérer vos tâches Voir vos tâches Google Groups - Groupes Google @@ -150,16 +150,16 @@ YouTube accéder à YouTube par le biais de tout compte Google associé. noms d\'utilisateurs YouTube - Voir l\'historique d\'activité de vos applications Google - Gérer votre configuration de compte acheteur Ad Exchange + voir l\'historique d\'activité de vos applications Google + gérer votre configuration de compte acheteur Ad Exchange Voir vos données Ad Exchange - Voir vos données AdSense - Voir et gérer vos données hôte AdSense et comptes associés - Voir et gérer vos données AdSense - Voir vos données Google Analytics - Voir et gérer vos données Google Analytics + voir vos données AdSense + voir et gérer vos données hôte AdSense et comptes associés + voir et gérer vos données AdSense + voir vos données Google Analytics + voir et gérer vos données Google Analytics Périmètre App Engine Admin. - Voir et gérer les paramètres d\'un Groupe Google Apps + voir et gérer les paramètres d\'un Groupe Google Apps En plus du cadre global lecture/écriture OAuth, utiliser le contexte lecture seule OAuth lors de la récupération des données du client. Accès lecture/écriture à l\'API License Manager. Accès à l\'API Admin Audit en lecture seule diff --git a/play-services-core/src/main/res/values-fr/plurals.xml b/play-services-core/src/main/res/values-fr/plurals.xml index 2781c57693..17e717b7c8 100644 --- a/play-services-core/src/main/res/values-fr/plurals.xml +++ b/play-services-core/src/main/res/values-fr/plurals.xml @@ -30,7 +30,7 @@ Plusieurs autorisations requises pour le bon fonctionnement des services microG sont manquantes. - Demander la autorisation manquante + Demander l\'autorisation manquante Demander les autorisations manquantes Demander les autorisations manquantes diff --git a/play-services-core/src/main/res/values-fr/strings.xml b/play-services-core/src/main/res/values-fr/strings.xml index 0f22ea63a4..a5ba282dfd 100644 --- a/play-services-core/src/main/res/values-fr/strings.xml +++ b/play-services-core/src/main/res/values-fr/strings.xml @@ -41,7 +41,7 @@ Ceci peut prendre plusieurs minutes." envoyer des messages C2DM aux autres applications échanger des messages et recevoir des notifications de synchronisation de la part des serveurs de Google Accès étendu aux services Google - Enregistrement du terminal auprès de Google + Enregistrement du terminal Cloud Messaging Google SafetyNet Jeux Google Play @@ -74,7 +74,7 @@ Ceci peut prendre plusieurs minutes." %1$s installé : Installez l’application %1$s ou tout autre compatible. Merci de consulter la documentation pour obtenir la liste des applications compatibles. %1$s dispose de la bonne signature : - Soit l’application %1$s installée n’est pas compatible, soit l’usurpation de signature n’est pas activée pour celle-ci. Merci de consulter la documentation sur les applications et ROMs compatibles. + Soit l’application %1$s installée n’est pas compatible, soit l’usurpation de signature n’est pas active pour celle-ci. Merci de consulter la documentation sur les applications et ROMs compatibles. Optimisations de la batterie ignorées : Appuyez ici pour désactiver les optimisations de la batterie. Des applications peuvent mal fonctionner si vous ne le faites pas. @@ -117,7 +117,7 @@ Ceci peut prendre plusieurs minutes." Enregistrée Enregistrée depuis : %1$s Désenregistrer %1$s ? - Certaines applications ne se réenregistrent pas et/ou ne fournisse pas de moyens de le faire manuellement. Ces applications peuvent ne plus fonctionner correctement après le désenregistrement.\nContinuer ? + Certaines applications ne se réenregistrent pas et/ou ne fournissent pas de moyen de le faire manuellement. Ces applications peuvent ne plus fonctionner correctement après le désenregistrement.\nContinuer ? Vous avez empêché une application déjà enregistrée de s’enregistrer pour recevoir des notifications poussées. \nVoulez-vous la désenregistrer maintenant pour qu’elle ne reçoive plus de notifications poussées à l’avenir ? Messages : %1$d (%2$d octets) @@ -131,7 +131,7 @@ Ceci peut prendre plusieurs minutes." Paramétrer les services microG. Se connecter Vitesse du véhicule - Accéder à la vitesse de votre véhicule + accéder à la vitesse de votre véhicule accéder aux informations relatives au niveau de carburant de votre véhicule Kilométrage du véhicule Compte @@ -139,7 +139,7 @@ Ceci peut prendre plusieurs minutes." Tous les tests passés avec succès Échec : %s Profil appareil - Ajouter et gérer les comptes Google + Ajouter et gérer vos comptes Google Alerte : %s En cours… Réel @@ -147,7 +147,7 @@ Ceci peut prendre plusieurs minutes." Automatique : %s ReCaptcha : %s Copier les données JSON JWS - Conseil + Recommandation %1$s veut accéder à votre compte en tant que %2$s par %3$s. Ceci pourrait lui donner un accès privilégié à votre compte. Choisir un compte Ajouter un autre compte @@ -184,7 +184,7 @@ Ceci peut prendre plusieurs minutes." accéder au informations relatives au kilométrage de votre véhicule Chaine de la marque du véhicule Démarrer l\'application en arrière-plan pour recevoir les messages poussés arrivants. - Applis utilisant les messages poussés + Applis utilisant les notifications poussées Applis enregistrées Test ReCAPTCHA Enterprise L’exécution de DroidGuard n\'est pas supportée sur cet appareil. Les services SafetyNet pourraient mal fonctionner. @@ -206,7 +206,7 @@ Ceci peut prendre plusieurs minutes." \nSi c\'est intentionnel, utilisez le bouton Se connecter pour vous rendre sur la page de connexion Google. \nAutrement, utilisez le bouton Annuler pour retourner dans l\'application qui a affiché cette fenêtre. Dernière utilisation : %1$s - services Play Store + Services Play Store Si désactivé, les requêtes d\'authentification ne seront pas liées à l\'enregistrement du terminal, ce qui peut permettre à des appareils non autorisés à se connecter, mais peut aussi avoir des conséquences imprévisibles. Autoriser %1$s à s\'enregistrer pour recevoir des notifications poussées ? Autoriser l\'enregistrement @@ -245,25 +245,25 @@ Ceci peut prendre plusieurs minutes." Se connecter avec Google Cette fonctionnalité est expérimentale et peut causer des pertes d\'argent. Vous êtes prévenus. En cours de connexion - Récupération de licences actif + Récupération de licences activée Ajouter automatiquement les applis gratuites à la bibliothèque - Les applis gratuites peuvent vérifier si elles ont été téléchargées depuis Google Play. Ajouter automatiquement les applis gratuites à la bibliothèque de votre compte afin que toutes les applis gratuites à votre disposition passent systématiquement ces vérifications. + Certaines applis gratuites vérifient si elles ont été téléchargées depuis Google Play. Ajouter automatiquement à la bibliothèque de votre compte toutes les applis gratuites à votre disposition afin qu\'elles passent systématiquement ces vérifications. Continuer en tant que %1$s Se reconnecter à %1$s avec Google Je comprends Récupération de licences Google Play Sauvegarde actuellement impossible Facturation Google Play - Traiter les requêtes de facturation - Si activé, certaines applis pourront effectuer des achats ou démarrer des abonnements via le service de facturation Google Play. - Certaines applis pourraient nécessiter aussi d\'activer la vérification de licences pour vérifier vos achats. - Certaines applis exigent de vérifier que vous les avez bien achetées sur Google Play. Quand exigé par une appli, microG peut télécharger une preuve d\'achat en provenance de Google. Si désactivé ou aucun compte Google sur l\'appareil, ces requêtes de vérification de licences sont ignorées. + Gérer les requêtes de facturation + Si activé, certaines applis pourront effectuer des achats ou initier des abonnements via le service de facturation Google Play. + Certaines applis peuvent aussi nécessiter d\'activer la récupération de licences pour vérifier vos achats. + Certaines applis exigent de vérifier que vous les avez achetées sur Google Play. Quand exigé par une appli, microG peut télécharger une preuve d\'achat en provenance de Google. Si désactivé ou si aucun compte Google sur l\'appareil, ces requêtes de vérification de licence seront ignorées. Vous utilisez les services limités microG. Contrairement aux services microG standard, cette version fonctionne uniquement avec les librairies microG, et non celles de Google Play. Cela signifie que la plupart des applications ignoreront ces services. Feedback actuellement impossible - Récupération de licences inactif + Récupération de licences désactivée En cours de connexion en tant que %1$s En continuant, Google partagera avec %1$s votre nom, adresse mail et image de profil. Consultez la politique de confidentialité et les conditions générales de %1$s. - Répondre aux requêtes de vérification de licence + Gérer les requêtes de vérification de licence Vous pouvez gérer Se Connecter avec Google dans votre compte Google. Alertes de compte Google Action requise sur le compte @@ -282,4 +282,7 @@ Ceci peut prendre plusieurs minutes." Votre compte Google est géré par votre entreprise ou votre établissement. Votre administrateur a établi que cet appareil nécessite un verrouillage d\'écran sécurisé avant de pouvoir accéder aux données du compte.\n\nVeuillez configurer un mot de passe, un code PIN ou un modèle de verrouillage écran. Étape complétée Terminer + Google Play Asset Delivery + Activer la distribution des packs d\'éléments à la demande + Télécharger les packs d\'éléments additionnels quand requis par les applis utilisant Play Asset Delivery \ No newline at end of file diff --git a/play-services-fido/core/src/main/res/values-fr/strings.xml b/play-services-fido/core/src/main/res/values-fr/strings.xml index 656dcabeb8..a4dae50f8e 100644 --- a/play-services-fido/core/src/main/res/values-fr/strings.xml +++ b/play-services-fido/core/src/main/res/values-fr/strings.xml @@ -12,7 +12,7 @@ Oui, %1$s est mon navigateur de confiance et peut être autorisé à utiliser des clés de sécurité avec des sites web tiers. Choisir comment utiliser votre clé de sécurité Les clés de sécurité fonctionnent via Bluetooth, NFC ou USB. Choisissez comment utiliser votre clé. - Vérifiez votre identité + Confirmez votre identité %1$s nécessite de vérifier que c\'est bien vous. Connecter votre clé de sécurité USB Connectez votre clé de sécurité à un port USB ou connectez-là avec un câble USB. Si votre clé a un bouton ou un disque doré, appuyez dessus. diff --git a/play-services-location/core/src/main/res/values-fr/strings.xml b/play-services-location/core/src/main/res/values-fr/strings.xml index 02b2c0b253..1fde9e4fcd 100644 --- a/play-services-location/core/src/main/res/values-fr/strings.xml +++ b/play-services-location/core/src/main/res/values-fr/strings.xml @@ -7,7 +7,7 @@ Résolution d\'adresses Obtention depuis Mozilla Pour continuer, activez la localisation de l\'appareil, le service de localisation de microG sera utilisé - Obtention depuis un service en ligne + Obtention via un service en ligne Récupérer la localisation basée sur le Wi-Fi depuis un service de localisation en ligne. Récupérer la localisation basée sur le Wi-Fi depuis une connexion hot-spot supportée. Obtention depuis Mozilla @@ -42,7 +42,7 @@ %1$d enregistrements importés. Pour une meilleure expérience, activez la localisation de l\'appareil, le service de localisation de microG sera utilisé Pour plus de détails, consultez les paramètres de localisation. - Obtention depuis un hot-spot + Obtention via hot-spot Utiliser le service de localisation de microG. Via l\'utilisation de ce service, microG peut collecter périodiquement les données de localisation et les utiliser de façon anonyme pour améliorer la précision de la localisation et des services basés sur la localisation. Le chemin de géolocalisation /v1/ est automatiquement attaché. Si le fournisseur de localisation exige une clé, elle peut être rattachée en tant que paramètre de la requête à l\'URL racine. Configurer l\'URL du service diff --git a/vending-app/src/main/res/values-fr/strings.xml b/vending-app/src/main/res/values-fr/strings.xml index c6b1506282..1d7f0f3073 100644 --- a/vending-app/src/main/res/values-fr/strings.xml +++ b/vending-app/src/main/res/values-fr/strings.xml @@ -19,4 +19,6 @@ Vérifier Le compagnon microG ne peut pas être utilisé seul. Merci d\'installer les services microG pour utiliser microG. Le compagnon microG ne peut pas être directement utilisé. Consultez plutôt les paramètres des services microG. + Fichiers additionnels pour %s + Téléchargement en cours \ No newline at end of file From 4c90dd523257ffe47c8427e822875127df66bb13 Mon Sep 17 00:00:00 2001 From: Francesco Saltori Date: Tue, 12 Nov 2024 22:21:34 +0000 Subject: [PATCH 101/132] Translated using Weblate (Italian) Currently translated at 93.1% (232 of 249 strings) Translation: microG/play-services-core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-core-strings/it/ --- play-services-core/src/main/res/values-it/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/play-services-core/src/main/res/values-it/strings.xml b/play-services-core/src/main/res/values-it/strings.xml index 86bb1b21f8..44a4c33ae2 100644 --- a/play-services-core/src/main/res/values-it/strings.xml +++ b/play-services-core/src/main/res/values-it/strings.xml @@ -214,7 +214,7 @@ Questa operazione può richiedere alcuni secondi." Impostazioni Verifica delle licenze di Google Play Non è possibile effettuare backup al momento - Accounts + Account Pagamenti di Google Play Gestisci le richieste di pagamento Se l\'opzione è abilitata, alcune app potranno completare acquisti o sottoscrivere abbonamenti tramite il servizio di pagamenti di Google Play. From 2a359fd025671b198e647035a701ce29031263ca Mon Sep 17 00:00:00 2001 From: gallegonovato Date: Tue, 12 Nov 2024 20:46:44 +0000 Subject: [PATCH 102/132] Translated using Weblate (Spanish) Currently translated at 100.0% (59 of 59 strings) Translation: microG/play-services-nearby: core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-nearby-core-strings/es/ Translated using Weblate (Spanish) Currently translated at 100.0% (252 of 252 strings) Translation: microG/play-services-core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-core-strings/es/ --- .../src/main/res/values-es/strings.xml | 3 +++ .../core/src/main/res/values-es/strings.xml | 12 +++--------- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/play-services-core/src/main/res/values-es/strings.xml b/play-services-core/src/main/res/values-es/strings.xml index a6bfa5ec37..5e26d4838d 100644 --- a/play-services-core/src/main/res/values-es/strings.xml +++ b/play-services-core/src/main/res/values-es/strings.xml @@ -271,4 +271,7 @@ Esto podría tardar algunos minutos." Puede desactivar Cloud Messaging después de que se complete la configuración de la cuenta. Finalizar Su cuenta de Google la administra su lugar de trabajo o institución educativa. Su administrador decidió que los dispositivos necesitan un bloqueo de pantalla seguro antes de poder acceder a los datos de la cuenta.\n\nConfigure una contraseña, PIN o patrón de bloqueo de pantalla. + Entrega de recursos de Google Play + Habilitar la entrega de activos a pedido + Descargar recursos adicionales cuando lo soliciten aplicaciones que utilicen Play Asset Delivery \ No newline at end of file diff --git a/play-services-nearby/core/src/main/res/values-es/strings.xml b/play-services-nearby/core/src/main/res/values-es/strings.xml index dd88fb3dac..5d739baf62 100644 --- a/play-services-nearby/core/src/main/res/values-es/strings.xml +++ b/play-services-nearby/core/src/main/res/values-es/strings.xml @@ -38,12 +38,8 @@ Eliminar de todas formas Exportar Exportar identificaciones recogidas para su análisis ampliado con otra aplicación. - La API de notificaciones de exposición permite que las aplicaciones le notifiquen si ha estado expuesto a alguien con diagnóstico positivo. -\n -\nLa fecha, la duración y la intensidad de la señal asociadas a una exposición se compartirán con la app correspondiente. - Mientras la API de notificación de exposición está activada, el dispositivo recopila de forma pasiva identificadores (denominados identificadores de proximidad móviles o RPI) de los dispositivos cercanos. -\n -\nCuando los propietarios de los dispositivos informan de un diagnóstico positivo, sus ID pueden compartirse. Su dispositivo comprueba si alguno de los ID diagnosticados conocidos coincide con alguno de los ID recopilados y calcula su riesgo de infección. + La API de notificaciones de exposición permite que las aplicaciones le notifiquen si ha estado expuesto a alguien con diagnóstico positivo.\n\nLa fecha, la duración y la intensidad de la señal asociadas a una exposición se compartirán con la app correspondiente. + Mientras la API de notificación de exposición está activada, el dispositivo recopila de forma pasiva identificadores (denominados identificadores de proximidad móviles o RPI) de los dispositivos cercanos.\n\nCuando los propietarios de los dispositivos informan de un diagnóstico positivo, sus ID pueden compartirse. Su dispositivo comprueba si alguno de los ID diagnosticados conocidos coincide con alguno de los ID recopilados y calcula su riesgo de infección. Usar Notificaciones de Exposición ¿Encender las Notificaciones de Exposición? El teléfono debe usar el Bluetooth para recopilar y compartir de manera segura las identificaciones con otros teléfonos que estén cerca. @@ -56,9 +52,7 @@ La fecha, la duración y la intensidad de la señal asociadas a una exposición Después de desactivar las Notificaciones de Exposición, ya no se le notificará cuando haya estado expuesto a alguien que haya informado de que ha sido diagnosticado como positivo. Apagar Comparte tus identificaciones con %1$s? - Tus identificaciones de los últimos 14 días se utilizarán para ayudar a notificar a otras personas que has estado cerca sobre una posible exposición. -\n -\nTu identidad o el resultado de la prueba no se compartirán con otras personas. + Tus identificaciones de los últimos 14 días se utilizarán para ayudar a notificar a otras personas que has estado cerca sobre una posible exposición.\n\nTu identidad o el resultado de la prueba no se compartirán con otras personas. Compartir %1$s necesita permisos adicionales. Conceder From 112260f71d7418322fa84f5a126d2e2f6cedcf48 Mon Sep 17 00:00:00 2001 From: Dan Date: Wed, 13 Nov 2024 16:48:46 +0000 Subject: [PATCH 103/132] Translated using Weblate (Ukrainian) Currently translated at 100.0% (59 of 59 strings) Translation: microG/play-services-nearby: core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-nearby-core-strings/uk/ Translated using Weblate (Ukrainian) Currently translated at 100.0% (252 of 252 strings) Translation: microG/play-services-core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-core-strings/uk/ Translated using Weblate (Ukrainian) Currently translated at 100.0% (21 of 21 strings) Translation: microG/vending-app Translate-URL: https://hosted.weblate.org/projects/microg/vending-app/uk/ --- play-services-core/src/main/res/values-uk/strings.xml | 3 +++ play-services-nearby/core/src/main/res/values-uk/strings.xml | 2 +- vending-app/src/main/res/values-uk/strings.xml | 2 ++ 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/play-services-core/src/main/res/values-uk/strings.xml b/play-services-core/src/main/res/values-uk/strings.xml index 4f44e19296..f39918ca1e 100644 --- a/play-services-core/src/main/res/values-uk/strings.xml +++ b/play-services-core/src/main/res/values-uk/strings.xml @@ -275,4 +275,7 @@ Ваш пристрій повинен зареєструватися в Google принаймні один раз.\n\nВи можете вимкнути реєстрацію пристрою в Google після завершення налаштування облікового запису. Дозволити хмарні повідомлення для microG Ви можете вимкнути хмарні повідомлення після завершення налаштування облікового запису. + Завантажити додаткові ресурси за запитом застосунків, які використовують Play Asset Delivery + Google Play Asset Delivery + Увімкнути доставку ресурсів на вимогу \ No newline at end of file diff --git a/play-services-nearby/core/src/main/res/values-uk/strings.xml b/play-services-nearby/core/src/main/res/values-uk/strings.xml index 9b3c80efa7..5aab04f1f7 100644 --- a/play-services-nearby/core/src/main/res/values-uk/strings.xml +++ b/play-services-nearby/core/src/main/res/values-uk/strings.xml @@ -52,7 +52,7 @@ \nДата, тривалість і рівень сигналу, пов\'язані з контактом, будуть передані відповідному застосунку. Використовувати сповіщення про вплив Увімкнути сповіщення про вплив? - Ваш телефон повинен використовувати Bluetooth для безпечного збору та обміну ідентифікаторами з іншими телефонами, які знаходяться поблизу. \u0020%1$s може сповістити вас, якщо ви зазнали контакту з людиною, яка повідомила про позитивний діагноз. \u0020Дата, тривалість і рівень сигналу, пов\'язані з контактом, будуть передані застосунку. + Ваш телефон повинен використовувати Bluetooth для безпечного збору та обміну ідентифікаторами з іншими телефонами, які знаходяться поблизу. %1$s може сповістити вас, якщо ви зазнали контакту з людиною, яка повідомила про позитивний діагноз. Дата, тривалість і рівень сигналу, пов\'язані з контактом, будуть передані застосунку. Поділітися своїми ідентифікаторами з %1$s\? Ваші ідентифікаційні дані за останні 14 днів будуть використані, щоби повідомити інших людей, які перебували поруч з вами, про потенційний ризик зараження. \n diff --git a/vending-app/src/main/res/values-uk/strings.xml b/vending-app/src/main/res/values-uk/strings.xml index ef391da33a..c501e7f8aa 100644 --- a/vending-app/src/main/res/values-uk/strings.xml +++ b/vending-app/src/main/res/values-uk/strings.xml @@ -19,4 +19,6 @@ %1$s не вдалося підтвердити ліцензію Увійти Ігнорувати + Додаткові файли для %s + Завантаження \ No newline at end of file From 32008feb8fae9092e34d968f56e53ace63ce7971 Mon Sep 17 00:00:00 2001 From: LucasMZ Date: Wed, 13 Nov 2024 17:16:29 +0000 Subject: [PATCH 104/132] Translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (59 of 59 strings) Translation: microG/play-services-nearby: core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-nearby-core-strings/pt_BR/ Translated using Weblate (Portuguese (Brazil)) Currently translated at 99.2% (250 of 252 strings) Translation: microG/play-services-core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-core-strings/pt_BR/ Translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (21 of 21 strings) Translation: microG/vending-app Translate-URL: https://hosted.weblate.org/projects/microg/vending-app/pt_BR/ --- play-services-core/src/main/res/values-pt-rBR/strings.xml | 3 +++ .../core/src/main/res/values-pt-rBR/strings.xml | 2 +- vending-app/src/main/res/values-pt-rBR/strings.xml | 2 ++ 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/play-services-core/src/main/res/values-pt-rBR/strings.xml b/play-services-core/src/main/res/values-pt-rBR/strings.xml index d176b1169e..2fa1d08737 100644 --- a/play-services-core/src/main/res/values-pt-rBR/strings.xml +++ b/play-services-core/src/main/res/values-pt-rBR/strings.xml @@ -280,4 +280,7 @@ Isso pode demorar alguns minutos." Clique para executar este passo Terminar Passo completo + Asset Delivery da Google Play + Ativar entrega de recursos em-demanda + Baixar recursos adicionais quando solicitados por apps que usam o Play Asset Delivery \ No newline at end of file diff --git a/play-services-nearby/core/src/main/res/values-pt-rBR/strings.xml b/play-services-nearby/core/src/main/res/values-pt-rBR/strings.xml index e20a46fcce..fb7c63e454 100644 --- a/play-services-nearby/core/src/main/res/values-pt-rBR/strings.xml +++ b/play-services-nearby/core/src/main/res/values-pt-rBR/strings.xml @@ -18,7 +18,7 @@ Foram processadas %1$d chaves de diagnóstico. %1$s, chance de risco %2$d %1$s IDs por hora - Seu dispositivo precisa usar Bluetooth para coletar e compartilhar IDs com segurança para dispositivos por perto. \u0020%1$s pode te notificar se você foi exposto à alguém que foi diagnosticado positivo. \u0020A data, duração, e força do sinal associada com uma exposição será compartilhada com o app. + Seu dispositivo precisa usar Bluetooth para coletar e compartilhar IDs com segurança para dispositivos por perto. %1$s pode te notificar se você foi exposto à alguém que foi diagnosticado positivo. A data, duração, e força do sinal associada com uma exposição será compartilhada com o app. Notificações de exposição inativas O Bluetooth precisa estar ativado para receber notificações de exposição. Acesso à localização é necessário para receber notificações de exposição. diff --git a/vending-app/src/main/res/values-pt-rBR/strings.xml b/vending-app/src/main/res/values-pt-rBR/strings.xml index da66c469cf..d6ae23fc43 100644 --- a/vending-app/src/main/res/values-pt-rBR/strings.xml +++ b/vending-app/src/main/res/values-pt-rBR/strings.xml @@ -19,4 +19,6 @@ Lembre da minha sessão neste dispositivo Esqueceu a senha? Aprenda mais + Baixando + Arquivos adicionais para %s \ No newline at end of file From b5002699b40a72063829396649571f674b855a42 Mon Sep 17 00:00:00 2001 From: rehork Date: Wed, 13 Nov 2024 18:04:56 +0000 Subject: [PATCH 105/132] Translated using Weblate (Polish) Currently translated at 100.0% (59 of 59 strings) Translation: microG/play-services-nearby: core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-nearby-core-strings/pl/ Translated using Weblate (Polish) Currently translated at 100.0% (252 of 252 strings) Translation: microG/play-services-core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-core-strings/pl/ Translated using Weblate (Polish) Currently translated at 100.0% (21 of 21 strings) Translation: microG/vending-app Translate-URL: https://hosted.weblate.org/projects/microg/vending-app/pl/ --- play-services-core/src/main/res/values-pl/strings.xml | 3 +++ play-services-nearby/core/src/main/res/values-pl/strings.xml | 2 +- vending-app/src/main/res/values-pl/strings.xml | 2 ++ 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/play-services-core/src/main/res/values-pl/strings.xml b/play-services-core/src/main/res/values-pl/strings.xml index ef0cd0499b..73e6431e2a 100644 --- a/play-services-core/src/main/res/values-pl/strings.xml +++ b/play-services-core/src/main/res/values-pl/strings.xml @@ -274,4 +274,7 @@ Spróbuj ponownie później." Dotknij, aby wykonać etap Etap zakończony Zakończ + Włącz dostarczanie zasobów na żądanie + Google Play Asset Delivery + Pobieranie dodatkowych zasobów na żądanie aplikacji korzystających z funkcji Play Asset Delivery \ No newline at end of file diff --git a/play-services-nearby/core/src/main/res/values-pl/strings.xml b/play-services-nearby/core/src/main/res/values-pl/strings.xml index 1b865b9a47..0a2d3822d1 100644 --- a/play-services-nearby/core/src/main/res/values-pl/strings.xml +++ b/play-services-nearby/core/src/main/res/values-pl/strings.xml @@ -53,7 +53,7 @@ Twoje identyfikatory w ciągu ostatnich 14 dni zostaną wykorzystane, aby pomóc powiadamiać innych o możliwym wystąpieniu narażenia na kontakt z wirusem. \n \nZarówno Twoja tożsamość jak i wynik testu nie zostanie udostępniona innym osobom. - Twój telefon musi korzystać z Bluetooth aby bezpiecznie zbierać i udostępniać identyfikatory do innych telefonów w pobliżu. „%1$s” może powiadomić cię o możliwym narażeniu na kontakt z osobami, których wynik testu na obecność wirusa był pozytywny. Data, okres i siła sygnału związana z narażeniem na kontakt zostanie udostępniona tej aplikacji. + Twój telefon musi korzystać z Bluetooth, aby bezpiecznie gromadzić i udostępniać identyfikatory innym telefonom znajdującym się w pobliżu. <xliff:g example=„Corona-Warn”>%1$s</xliff:g> może powiadomić cię o możliwym narażeniu na kontakt z osobami, których wynik testu na obecność wirusa był pozytywny. Data, czas trwania i siła sygnału związane z ekspozycją zostaną udostępnione aplikacji. Brak wpisów Usuń Wymagane nowe uprawnienia diff --git a/vending-app/src/main/res/values-pl/strings.xml b/vending-app/src/main/res/values-pl/strings.xml index e19890dbda..172c7a790f 100644 --- a/vending-app/src/main/res/values-pl/strings.xml +++ b/vending-app/src/main/res/values-pl/strings.xml @@ -19,4 +19,6 @@ %1$s nie może zweryfikować licencji Jeśli aplikacja działa nieprawidłowo, zaloguj się na konto Google, za pomocą którego aplikacja została zakupiona. Zaloguj się + Dodatkowe pliki dla %s + Pobieranie \ No newline at end of file From 943b773b0261c7637b7514200d44473178cdf4a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Engelbrektson?= Date: Wed, 13 Nov 2024 07:44:50 +0000 Subject: [PATCH 106/132] Translated using Weblate (Swedish) Currently translated at 100.0% (59 of 59 strings) Translation: microG/play-services-nearby: core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-nearby-core-strings/sv/ Translated using Weblate (Swedish) Currently translated at 100.0% (249 of 249 strings) Translation: microG/play-services-core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-core-strings/sv/ Translated using Weblate (Swedish) Currently translated at 100.0% (252 of 252 strings) Translation: microG/play-services-core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-core-strings/sv/ Translated using Weblate (Swedish) Currently translated at 100.0% (21 of 21 strings) Translation: microG/vending-app Translate-URL: https://hosted.weblate.org/projects/microg/vending-app/sv/ --- play-services-core/src/main/res/values-sv/strings.xml | 5 ++++- play-services-nearby/core/src/main/res/values-sv/strings.xml | 2 +- vending-app/src/main/res/values-sv/strings.xml | 2 ++ 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/play-services-core/src/main/res/values-sv/strings.xml b/play-services-core/src/main/res/values-sv/strings.xml index e9459d835d..97c364822d 100644 --- a/play-services-core/src/main/res/values-sv/strings.xml +++ b/play-services-core/src/main/res/values-sv/strings.xml @@ -244,7 +244,7 @@ Gratisappar kan kontrollera om de har laddats ner från Google Play. Lägg automatiskt till gratisappar i ditt kontobibliotek för att alltid klara kontrollen för alla gratisappar som för närvarande är tillgängliga för dig. Jag förstår microG Limited Services - Du använder microG Limited Services. Till skillnad från de vanliga mikroG-tjänsterna fungerar denna smak endast med appar som använder mikroG-bibliotek, inte de på Google Play. Detta innebär att de flesta appar ignorerar dessa tjänster. + Du använder microG Limited Services. Till skillnad från de vanliga mikroG-tjänsterna fungerar denna version endast med appar som använder mikroG-bibliotek, inte de på Google Play. Det innebär att de flesta appar ignorerar dessa tjänster. Ditt Google-konto behöver ytterligare inställning. Tillåt Cloud Messaging för microG Konfigurera säkert skärmlås @@ -262,4 +262,7 @@ Slutför Ditt Google-konto hanteras av din arbetsplats eller utbildningsinstitution. Din administratör bestämde att enheter behöver ett säkert skärmlås innan de kan komma åt kontodata.\n\nAnge ett lösenord, PIN eller mönsterlåsskärm. Steg färdigställt + Google Play Asset Delivery + Aktivera tillgångsleverans på begäran + Ladda ner fler tillgångar, på begäran av appar som använder Google Play Asset Delivery \ No newline at end of file diff --git a/play-services-nearby/core/src/main/res/values-sv/strings.xml b/play-services-nearby/core/src/main/res/values-sv/strings.xml index a903c103ee..1262bf3713 100644 --- a/play-services-nearby/core/src/main/res/values-sv/strings.xml +++ b/play-services-nearby/core/src/main/res/values-sv/strings.xml @@ -53,7 +53,7 @@ Dina ID:n från de senaste 14 dagarna kommer att användas för att informera andra i din närhet, om potentiell exponering. \n \nDin identitet eller testresultat kommer inte att delas med andra människor. - Din telefon måste använda Bluetooth för att säkert samla in och dela ID:n med andra telefoner som finns i närheten. %1$s kan meddela dig om du exponerats för någon som rapporterats vara positivt diagnostiserad. Datum, varaktighet och signalstyrka i samband med en exponering kommer att delas med appen. + Din telefon måste använda Bluetooth för att säkert samla in och dela ID:n med andra telefoner som finns i närheten. %1$s kan meddela dig om du exponerats för någon som rapporterats vara positivt diagnostiserad. Datum, varaktighet och signalstyrka i samband med en exponering kommer att delas med appen. Inga poster Ta bort Nya behörigheter krävs diff --git a/vending-app/src/main/res/values-sv/strings.xml b/vending-app/src/main/res/values-sv/strings.xml index ba5313b49c..f0cf90cc79 100644 --- a/vending-app/src/main/res/values-sv/strings.xml +++ b/vending-app/src/main/res/values-sv/strings.xml @@ -19,4 +19,6 @@ Glömt lösenordet? Läs mer Verifiera + Fler filer för %s + Laddar ner \ No newline at end of file From bb7524dc17f51fe47325cdcfc30567c620c88b32 Mon Sep 17 00:00:00 2001 From: searinminecraft <114207889+searinminecraft@users.noreply.github.com> Date: Wed, 13 Nov 2024 00:40:52 +0000 Subject: [PATCH 107/132] Translated using Weblate (Filipino) Currently translated at 100.0% (249 of 249 strings) Translation: microG/play-services-core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-core-strings/fil/ --- .../src/main/res/values-fil/strings.xml | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/play-services-core/src/main/res/values-fil/strings.xml b/play-services-core/src/main/res/values-fil/strings.xml index 5a35b6abf7..a4484fe353 100644 --- a/play-services-core/src/main/res/values-fil/strings.xml +++ b/play-services-core/src/main/res/values-fil/strings.xml @@ -185,7 +185,7 @@ \n \nMaari ito isang pansamantalang problema o ang iyong Android device ay maaring hindi naka-provision para sa mga serbisyo ng data. Subukan muli kapag nakakonekta sa mobile network, o kumonekta sa Wi-Fi network. Magdagdag ng isa pang account - Ang isang app sa device mo ay sinusubukang mag-sign in sa isang Google Account. \u0020Kung sinasadya ito, gamitin ang Mag-sign in na button para kumonekta sa sign-in page ng Google, kung hindi, pindutin ang Kanselahin para bumalik sa application na naging sanhi ng pagpakita ng dialog na ito. + Ang isang app sa device mo ay sinusubukang mag-sign in sa isang Google Account.\n\nKung sinasadya ito, gamitin ang Mag-sign in na button para kumonekta sa sign-in page ng Google, kung hindi, pindutin ang Kanselahin para bumalik sa application na naging sanhi ng pagpakita ng dialog na ito. makinig sa mga internal status broadcast Pumili ng lugar Mga malapit na lugar @@ -242,4 +242,24 @@ Gustong i-access ng %1$s ang iyong account na kung ito ay %2$s ni/ng %3$s. Maaring bigyan ito ng pribilehiyong pag-access sa iyong account. Awtomatikong idagdag ang mga libreng app sa library Maaring suriin ng mga libreng app kapag na-download sila sa Google Play. Awtomatikong dagdagan ang mga libreng app sa iyong account library para palaging mapasa ang pagsuri para sa lahat ng mga libreng app na available sa iyo. + Mga Limitadong Serbisyo ng microG + Nangangailangan ng aksyon sa account + Nangangailangan ng iyong Google account ng karagdagang setup. + Buksan ang pagrehistro ng device + Buksan ang Cloud na Pagmemensahe + Maari mong i-disable ang Cloud na Pagmemensahe pagkatapos ng pag-set up ng account. + Payagan ang Cloud na Pagmemensahe para sa microG + Batay sa iyong mga kagustuhan, kinakailangan ng microG ang iyong pahintulot bago ito irehistro ang sarili para sa Cloud na Pagmemensahe. + Mag-configure ng secure na lock ng screen + Ang iyong Google account ay ipinamamahala ng iyong lugar ng trabaho o ang iyong institusyong pang-edukasyon. Nagpasya ang iyong tagapangasiwa na ang mga device ay kailangan ng secure na screen lock bago nila ma-access ang data ng account.\n\nPaki-set up ang password, PIN, o pattern na lock screen. + I-click para isagawa ang hakbang + Tapos na ang hakbang + Tapos na + Naiintindihan ko + Mga alerto sa Google account + Tapusing i-set up ang iyong Google account + Inaabisuhan kapag ang isa sa iyong mga Google account ay nangangailangan ng karagdagang setup bago ito magamit o kapag ang account ay hindi compatible sa microG. + Kumpletuhin ang mga sumusunod na hakbang upang magamit ang Google account na %s sa device na ito. + Kailangang i-rehistro ang iyong device sa Google kahit isang beses.\n\nMaari mong i-disable ang pagrehistro ng device sa Google pagkatapos ng pag-set up ng account. + Ginagamit mo ang Mga Limitadong Serbisyo ng microG. Hindi tulad ng nakasanayan na Mga Serbisyo ng microG, ang flavor na ito ay gumagana para sa mga app ba gumagamit ng mga library ng microG, hindi ang mula sa Google Play. Nangangahulugan nito na hindi papansinin ng mga karamihang application ang mga serbisyo na ito. \ No newline at end of file From 3e64e50dc633648613e9bb56369fdfd0f0b46fa6 Mon Sep 17 00:00:00 2001 From: Fjuro Date: Tue, 12 Nov 2024 20:34:30 +0000 Subject: [PATCH 108/132] Translated using Weblate (Czech) Currently translated at 100.0% (59 of 59 strings) Translation: microG/play-services-nearby: core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-nearby-core-strings/cs/ Translated using Weblate (Czech) Currently translated at 100.0% (252 of 252 strings) Translation: microG/play-services-core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-core-strings/cs/ Translated using Weblate (Czech) Currently translated at 100.0% (21 of 21 strings) Translation: microG/vending-app Translate-URL: https://hosted.weblate.org/projects/microg/vending-app/cs/ --- play-services-core/src/main/res/values-cs/strings.xml | 3 +++ play-services-nearby/core/src/main/res/values-cs/strings.xml | 2 +- vending-app/src/main/res/values-cs/strings.xml | 2 ++ 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/play-services-core/src/main/res/values-cs/strings.xml b/play-services-core/src/main/res/values-cs/strings.xml index 0c6fb07699..a356349327 100644 --- a/play-services-core/src/main/res/values-cs/strings.xml +++ b/play-services-core/src/main/res/values-cs/strings.xml @@ -262,4 +262,7 @@ Váš účet Google spravuje vaše pracoviště nebo vzdělávací instituce. Váš správce rozhodl, že zařízení musí mít před přístupem k datům účtu bezpečný zámek obrazovky.\n\nNastavte zámek obrazovky s heslem, kódem PIN nebo vzorem. Klepněte pro provedení kroku Nastavte bezpečný zámek obrazovky + Povolit dodávání prostředků na vyžádání + Dodávání prostředků Google Play + Stáhnout další prostředky na žádost aplikací používajících službu Play Asset Delivery \ No newline at end of file diff --git a/play-services-nearby/core/src/main/res/values-cs/strings.xml b/play-services-nearby/core/src/main/res/values-cs/strings.xml index d48fed4f45..fe055d1c81 100644 --- a/play-services-nearby/core/src/main/res/values-cs/strings.xml +++ b/play-services-nearby/core/src/main/res/values-cs/strings.xml @@ -31,7 +31,7 @@ Exportovat nasbíraná ID pro rozšířenou analýzu v jiné aplikaci. Používat oznámení o možném kontaktu Zapnout oznámení o možném kontaktu? - Váš telefon potřebuje používat Bluetooth pro bezpečné sbírání a sdílení ID s ostatními telefony, které jsou v okolí. \u0020Aplikace %1$s vám může oznámit, zda jste se setkali s někým, kdo byl nahlášen jako pozitivní. \u0020Datum, doba trvání a síla signálu spojené s kontaktem budou sdíleny s touto aplikací. + Váš telefon potřebuje používat Bluetooth pro bezpečné sbírání a sdílení ID s ostatními telefony, které jsou v okolí. Aplikace %1$s vám může oznámit, zda jste se setkali s někým, kdo byl nahlášen jako pozitivní. Datum, doba trvání a síla signálu spojené s kontaktem budou sdíleny s touto aplikací. Zapnout Vypnout oznámení o možném kontaktu? Po vypnutí oznámení o možném kontaktu již nebudete upozorněni, zda jste se setkali s někým, kdo byl nahlášen jako pozitivní. diff --git a/vending-app/src/main/res/values-cs/strings.xml b/vending-app/src/main/res/values-cs/strings.xml index 88374392c3..a3c27f55c9 100644 --- a/vending-app/src/main/res/values-cs/strings.xml +++ b/vending-app/src/main/res/values-cs/strings.xml @@ -19,4 +19,6 @@ Přihlásit se Zadejte své heslo Zapomněli jste heslo? + Stahování + Dodatečné soubory pro %s \ No newline at end of file From 3381244e7c0790e1208ea25a24a744eb3d4e9439 Mon Sep 17 00:00:00 2001 From: HARIRAK SRITHAM Date: Wed, 13 Nov 2024 06:00:08 +0000 Subject: [PATCH 109/132] Translated using Weblate (Thai) Currently translated at 99.3% (155 of 156 strings) Translation: microG/play-services-core: permissions Translate-URL: https://hosted.weblate.org/projects/microg/play-services-core-permissions/th/ Translated using Weblate (Thai) Currently translated at 38.5% (96 of 249 strings) Translation: microG/play-services-core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-core-strings/th/ Translated using Weblate (Thai) Currently translated at 100.0% (59 of 59 strings) Translation: microG/play-services-nearby: core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-nearby-core-strings/th/ Translated using Weblate (Thai) Currently translated at 58.2% (145 of 249 strings) Translation: microG/play-services-core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-core-strings/th/ Translated using Weblate (Thai) Currently translated at 100.0% (156 of 156 strings) Translation: microG/play-services-core: permissions Translate-URL: https://hosted.weblate.org/projects/microg/play-services-core-permissions/th/ Translated using Weblate (Thai) Currently translated at 100.0% (249 of 249 strings) Translation: microG/play-services-core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-core-strings/th/ --- .../src/main/res/values-th/permissions.xml | 36 +++- .../src/main/res/values-th/strings.xml | 158 +++++++++++++++++- .../core/src/main/res/values-th/strings.xml | 2 +- 3 files changed, 192 insertions(+), 4 deletions(-) diff --git a/play-services-core/src/main/res/values-th/permissions.xml b/play-services-core/src/main/res/values-th/permissions.xml index b66a26774e..35f784b802 100644 --- a/play-services-core/src/main/res/values-th/permissions.xml +++ b/play-services-core/src/main/res/values-th/permissions.xml @@ -9,10 +9,10 @@ อนุญาตให้แอปเข้าถึง Google App Engine ผ่านทางบัญชี Google ที่เชื่อมโยง AdSense Blogger - Google Calendar + ปฏิทิน Google อนุญาตให้แอปเข้าถึง Google Calendar ผ่านทางบัญชี Google ที่เชื่อมโยง รายชื่อ - Dodgeball + ดอดจ์บอล อนุญาตให้แอปเข้าถึง Dodgeball ผ่านทางบัญชี Google ที่เชื่อมโยง อนุญาตให้แอปเข้าถึงบริการ Android ผ่านทางบัญชี Google ที่เชื่อมโยง อนุญาตให้แอปเข้าถึง Google Ads ผ่านทางบัญชี Google ที่เชื่อมโยง @@ -124,4 +124,36 @@ การจัดการของคุณ บัญชี YouTube ดูและจัดการสินทรัพย์ของคุณและเนื้อหาที่เกี่ยวข้องกับ YouTube ดูบัญชี YouTube ของคุณ + อ่านทรัพยากรทั้งหมดและข้อมูลเมตาของทรัพยากรเหล่านั้น—ไม่มีการดำเนินการเขียน + การเข้าถึงนักพัฒนา Android ของ Google Play + ดูข้อมูล Google Analytics ของคุณ + ดูและจัดการข้อมูล Google Analytics ของคุณ + ลงชื่อเข้าใช้ Firebase ด้วยบัญชีของคุณ + จัดการตารางฟิวชันของคุณ + ดูตารางฟิวชันของคุณ + ขอบเขตการเข้าถึงข้อมูลจาก Google Play Games + จัดการข้อมูล GAN ของคุณ + ดูข้อมูล GAN ของคุณ + CloudMessaging สำหรับ Chrome + ขอบเขตของ Glass timeline + สร้าง อ่าน อัปเดต และลบฉบับร่าง ส่งข้อความและฉบับร่าง + การดำเนินการอ่าน/เขียนทั้งหมด ยกเว้นการลบเธรดและข้อความทันทีและถาวร โดยข้ามถังขยะ + จัดการตำแหน่งที่ดีที่สุดที่มีอยู่และประวัติตำแหน่งของคุณ + จัดการตำแหน่งระดับเมืองและประวัติตำแหน่งของคุณ + จัดการตำแหน่งที่ดีที่สุดที่มีอยู่ของคุณ + จัดการตำแหน่งระดับเมืองของคุณ + ดูและจัดการข้อมูล Google Maps Engine ของคุณ + ดูข้อมูล Google Maps Engine ของคุณ + ดูและจัดการ Google Maps ของคุณสำหรับประสบการณ์มือถือ + จัดการกิจกรรม Orkut ของคุณ + ดูข้อมูล Orkut ของคุณ + ทราบชื่อของคุณ ข้อมูลพื้นฐาน และรายชื่อบุคคลที่คุณเชื่อมต่อด้วยบน Google+ + รู้ว่าคุณเป็นใครบน Google + จัดการข้อมูลของคุณใน Google Prediction API + ดูข้อมูลผลิตภัณฑ์ของคุณ + จัดการรายการไซต์และโดเมนที่คุณควบคุม + จัดการการตรวจสอบไซต์ใหม่ของคุณด้วย Google + การเข้าถึงแบบอ่าน/เขียนสำหรับ Shopping Content API + ดูรายงานทางการเงินของ YouTube Analytics สำหรับเนื้อหา YouTube ของคุณ + ดูรายงาน YouTube Analytics สำหรับเนื้อหา YouTube ของคุณ \ No newline at end of file diff --git a/play-services-core/src/main/res/values-th/strings.xml b/play-services-core/src/main/res/values-th/strings.xml index 7ad59fe224..9831d6ea86 100644 --- a/play-services-core/src/main/res/values-th/strings.xml +++ b/play-services-core/src/main/res/values-th/strings.xml @@ -14,7 +14,7 @@ ตั้งค่าบริการ microG การดำเนินการต่อถือว่าคุณยินยอมให้แอปนี้และ Google ใช้ข้อมูลของคุณตามข้อกำหนดในการให้บริการและนโยบายความเป็นส่วนตัวของแต่ละแอป %1$s ต้องการ: - แอปบนอุปกรณ์ของคุณกำลังพยายามลงชื่อเข้าใช้บัญชี Google หากตั้งใจ ให้ใช้ปุ่ม ลงชื่อเข้าใช้ เพื่อเชื่อมต่อกับหน้าลงชื่อเข้าใช้ของ Google หากไม่ได้ตั้งใจ ให้กด ยกเลิก เพื่อกลับไปยังแอปพลิเคชันที่ทำให้กล่องโต้ตอบนี้ปรากฏขึ้น + แอปบนอุปกรณ์ของคุณกำลังพยายามลงชื่อเข้าใช้บัญชี Google\n\nหากตั้งใจ ให้ใช้ปุ่ม ลงชื่อเข้าใช้ เพื่อเชื่อมต่อกับหน้าลงชื่อเข้าใช้ของ Google หากไม่ได้ตั้งใจ ให้กด ยกเลิก เพื่อกลับไปยังแอปพลิเคชันที่ทำให้กล่องโต้ตอบนี้ปรากฏขึ้น อุปกรณ์ของคุณกำลังสร้างการเชื่อมต่อกับเซิร์ฟเวอร์ของ Google เพื่อลงชื่อเข้าใช้ให้คุณ\n\nอาจใช้เวลาสักครู่ คุณไม่ได้เชื่อมต่อกับเครือข่าย\n\nนี่อาจเป็นปัญหาชั่วคราวหรืออุปกรณ์ Android ของคุณอาจไม่มีการให้บริการข้อมูล ลองอีกครั้งเมื่อเชื่อมต่อกับเครือข่ายมือถือหรือเชื่อมต่อกับเครือข่าย Wi-Fi microG Limited Services @@ -93,4 +93,160 @@ บริการ ทดสอบ เปิดใช้งานการเพิ่มประสิทธิภาพแบตเตอรี่แล้ว + การแจ้งเตือนบัญชี Google + จำเป็นต้องดำเนินการกับบัญชี + แจ้งเตือนเมื่อบัญชี Google ของคุณต้องมีการตั้งค่าเพิ่มเติมก่อนจึงจะสามารถใช้งานได้ หรือเมื่อบัญชีเข้ากันไม่ได้กับ microG + บัญชี Google ของคุณต้องมีการตั้งค่าเพิ่มเติม + เสร็จสิ้นการตั้งค่าบัญชี Google ของคุณ + เปิดใช้งานการลงทะเบียนอุปกรณ์ + เปิดใช้งานการส่งข้อความบนคลาวด์ + คุณสามารถปิดการใช้งาน Cloud Messaging ได้หลังจากตั้งค่าบัญชีเสร็จสิ้น + อนุญาตให้มีการส่งข้อความบนคลาวด์สำหรับ microG + กำหนดค่าการล็อคหน้าจอที่ปลอดภัย + คลิกเพื่อดำเนินการขั้นตอน + ขั้นตอนเสร็จสมบูรณ์ + เสร็จ + การตั้งค่าบัญชี + ข้อมูลส่วนตัวและความเป็นส่วนตัว + การลงชื่อเข้าใช้และความปลอดภัย + เชื่อถือ Google สำหรับการอนุญาตสิทธิ์แอป + หากปิดใช้งาน ผู้ใช้จะถูกถามก่อนที่จะส่งคำขออนุญาตของแอปไปยัง Google แอปพลิเคชันบางตัวจะไม่สามารถใช้บัญชี Google ได้หากปิดใช้งานตัวเลือกนี้ + อนุญาตให้แอปค้นหาบัญชี + ยืนยันตัวตนด้วยการลงทะเบียนอุปกรณ์ + ถอดชื่ออุปกรณ์ออกเพื่อยืนยันตัวตน + ลงทะเบียนอุปกรณ์ของคุณกับบริการของ Google และสร้างตัวระบุอุปกรณ์เฉพาะ microG จะลบบิตที่ระบุอื่นๆ นอกเหนือจากชื่อบัญชี Google ของคุณออกจากข้อมูลการลงทะเบียน + Android ID + ไม่ได้ลงทะเบียน + ลงทะเบียนล่าสุด: %1$s + ลงทะเบียนอุปกรณ์ + สถานะ + มากกว่า + บัญชี Google + เพิ่มและจัดการบัญชี Google + ตั้งค่า + บัญชี + บัญชี + เพิ่มบัญชี Google + แอปที่ใช้ระบบ Cloud Messaging + รายการแอปที่ลงทะเบียนสำหรับ Cloud Messaging ในปัจจุบัน + ยืนยันแอปใหม่ + ถามก่อนลงทะเบียนแอพใหม่เพื่อรับการแจ้งเตือนแบบพุช + ดำเนินการตามขั้นตอนต่อไปนี้เพื่อจะใช้งานบัญชี Google %s ของคุณบนอุปกรณ์นี้ได้ + อุปกรณ์ของคุณจำเป็นต้องลงทะเบียนกับ Google อย่างน้อยหนึ่งครั้ง\n\nคุณสามารถปิดใช้งานการลงทะเบียนอุปกรณ์ Google ได้หลังจากตั้งค่าบัญชีเสร็จสิ้น + ตามการตั้งค่าของคุณ microG จะต้องได้รับอนุญาตจากคุณก่อนจึงจะสามารถลงทะเบียนสำหรับ Cloud Messaging ได้ + เมื่อเปิดใช้งานแล้ว แอปพลิเคชันทั้งหมดในอุปกรณ์นี้จะสามารถเห็นที่อยู่อีเมลของบัญชี Google ของคุณได้ โดยไม่ต้องได้รับอนุญาตก่อน + บัญชี Google ของคุณได้รับการจัดการโดยที่ทำงานหรือสถาบันการศึกษาของคุณ ผู้ดูแลระบบของคุณตัดสินใจว่าอุปกรณ์จำเป็นต้องมีการล็อกหน้าจอที่ปลอดภัยก่อนจึงจะสามารถเข้าถึงข้อมูลบัญชีได้\n\nโปรดตั้งรหัสผ่าน PIN หรือรูปแบบการล็อกหน้าจอ + เมื่อเปิดใช้งาน การร้องขอการตรวจสอบสิทธิ์จะไม่รวมชื่ออุปกรณ์ ซึ่งอาจทำให้อุปกรณ์ที่ไม่ได้รับอนุญาตลงชื่อเข้าใช้ได้ แต่ก็อาจส่งผลที่ไม่คาดคิดได้ + เมื่อปิดใช้งาน คำขอการตรวจสอบสิทธิ์จะไม่เชื่อมโยงกับการลงทะเบียนอุปกรณ์ ซึ่งอาจทำให้อุปกรณ์ที่ไม่ได้รับอนุญาตลงชื่อเข้าใช้ได้ แต่ก็อาจส่งผลที่ไม่คาดคิดได้ + ระยะเวลาเป็นวินาทีที่ระบบจะส่งสัญญาณไปยังเซิร์ฟเวอร์ของ Google การเพิ่มจำนวนนี้จะช่วยลดการใช้แบตเตอรี่ แต่ข้อความพุชอาจเกิดความล่าช้าได้\nไม่สนับสนุนอีกต่อไป แต่จะถูกแทนที่ในรุ่นถัดไป + ช่วงเวลาปิง: %1$s + คุณเปิดใช้งาน Cloud Messaging แต่มีการเพิ่มประสิทธิภาพแบตเตอรี่สำหรับบริการ microG หากต้องการให้การแจ้งเตือนแบบพุชมาถึงคุณ ควรละเว้นการเพิ่มประสิทธิภาพแบตเตอรี่ + ละเว้นการเพิ่มประสิทธิภาพ + ขาดการอนุญาต + Cloud Messaging คือผู้ให้บริการการแจ้งเตือนแบบพุชที่แอปพลิเคชันของบุคคลที่สามจำนวนมากใช้ หากต้องการใช้งาน คุณต้องเปิดใช้งานการลงทะเบียนอุปกรณ์ + รอบในการส่งข้อความบนคลาวด์ + เกี่ยวกับบริการของ microG + เกิดข้อผิดพลาดในการยกเลิกการลงทะเบียน + ไม่ติดตั้งอีกต่อไป + ไม่ได้ลงทะเบียน + ไม่ได้รับข้อความใดๆจนถึงขณะนี้ + ลงทะเบียนแล้ว + ลงทะเบียนตั้งแต่: %1$s + ยกเลิกการลงทะเบียน %1$s\? + คุณปฏิเสธไม่ให้แอปลงทะเบียนรับข้อความแจ้งเตือนแบบพุชที่ลงทะเบียนไปแล้ว\nคุณต้องการยกเลิกการลงทะเบียนทันทีเพื่อไม่ให้รับข้อความแจ้งเตือนแบบพุชในอนาคตหรือไม่? + อนุญาตให้แอปลงทะเบียนเพื่อรับการแจ้งเตือนแบบพุช + เริ่มการทำงานของแอปด้วยข้อความแบบพุช + เริ่มการทำงานของแอปในขณะที่อยู่ในพื้นหลังเพื่อรับข้อความพุชที่เข้ามา + แอปกำลังใช้การแจ้งเตือนแบบพุช + แอปที่ลงทะเบียนแล้ว + แอปที่ไม่ได้ลงทะเบียน + เครือข่ายที่จะใช้สำหรับการแจ้งเตือนแบบพุช + ล้มเหลว: %s + คำเตือน: %s + กำลังทำงาน… + โหมดการทำงาน + ล้างคำร้องขอล่าสุด + ใช้งานล่าสุด: %1$s + จริง + กำหนดเอง: %s + อัตโนมัติ: %s + ระบบ: %s + นำเข้าโปรไฟล์ที่กำหนดเอง + เลือกโปรไฟล์ + โปรไฟล์อุปกรณ์ + การใช้งานล่าสุด + การรับรอง: %s + ReCaptcha: %s + ReCaptcha Enterprise: %s + คัดลอกข้อมูล JSON JWS + คำแนะนำ + ประเภทการประเมิน + สถานะการตอบกลับ + สถานะการตอบกลับ + ร้องขอข้อมูล + Nonce (เลขฐานสิบหก) + CTS ล้มเหลว + การยืนยันล้มเหลว + ยังไม่เสร็จสมบูรณ์ + ไม่มีผลลัพธ์ + JSON ไม่ถูกต้อง + เปิด / อัตโนมัติ: %s + เปิด / แมนนวล: %s + %s วินาที + %s นาที + เปิดใบอนุญาต + ปิดใบอนุญาต + การอนุญาตสิทธิ์ใช้งาน Google Play + ตอบคำขอตรวจสอบใบอนุญาต + เพิ่มแอปฟรีลงในไลบรารีโดยอัตโนมัติ + ขณะนี้ไม่สามารถให้ข้อเสนอแนะได้ + ไม่สามารถสำรองข้อมูลได้ในขณะนี้ + การเรียกเก็บเงินของ Google Play + จัดการคำขอเรียกเก็บเงิน + เมื่อเปิดใช้งานแล้ว แอปบางตัวสามารถทำการซื้อหรือเริ่มการสมัครสมาชิกผ่านบริการ Play Billing ของ Google ได้ + แอปบางตัวอาจต้องการให้คุณเปิดใช้การตรวจสอบใบอนุญาตเพื่อยืนยันการซื้อของคุณด้วย + ยกเลิก + ดำเนินการต่อ + การลงชื่อเข้าใช้ของคุณ + ดำเนินการต่อเป็น %1$s + ลงชื่อเข้าใช้ %1$s อีกครั้งด้วย Google + ลงนามในฐานะ %1$s + คุณสามารถจัดการการลงชื่อเข้าใช้ด้วย Google ในบัญชี Google ของคุณได้ + เลือกบัญชี + เพื่อดำเนินการต่อ %1$s + ข้อมูลเวอร์ชันและไลบรารีที่ใช้ + ยกเลิกการลงทะเบียน + ข้อความล่าสุด: %1$s + แอปบางตัวจะไม่ลงทะเบียนใหม่โดยอัตโนมัติและ/หรือไม่มีตัวเลือกให้ดำเนินการดังกล่าวด้วยตนเอง แอปเหล่านี้อาจไม่ทำงานอย่างถูกต้องหลังจากยกเลิกการลงทะเบียน\nดำเนินการต่อหรือไม่? + Google SafetyNet เป็นระบบการรับรองอุปกรณ์ ซึ่งรับรองว่าอุปกรณ์ได้รับการรักษาความปลอดภัยอย่างเหมาะสมและเข้ากันได้กับ Android CTS แอปพลิเคชันบางตัวใช้ SafetyNet เพื่อเหตุผลด้านความปลอดภัยหรือเป็นข้อกำหนดเบื้องต้นสำหรับการป้องกันการงัดแงะ\n\nmicroG GmsCore มีการนำ SafetyNet มาใช้ได้ฟรี แต่เซิร์ฟเวอร์อย่างเป็นทางการต้องการให้มีการลงนามคำขอ SafetyNet โดยใช้ระบบ ที่เป็นกรรมสิทธิ์ ของDroidGuard + แอปฟรีสามารถตรวจสอบว่าดาวน์โหลดมาจาก Google Play หรือไม่ เพิ่มแอปฟรีลงในไลบรารีบัญชีของคุณโดยอัตโนมัติเพื่อให้ผ่านการตรวจสอบสำหรับแอปฟรีทั้งหมดที่มีให้คุณใช้งานในปัจจุบัน + แอปบางตัวต้องได้รับการยืนยันว่าคุณซื้อแอปเหล่านั้นจาก Google Play เมื่อแอปร้องขอ microG จะสามารถดาวน์โหลดหลักฐานการซื้อจาก Google ได้ หากปิดใช้งานหรือไม่ได้เพิ่มบัญชี Google คำขอตรวจสอบใบอนุญาตจะถูกละเว้น + คุณกำลังใช้บริการ microG Limited ซึ่งต่างจากบริการ microG ทั่วไป ฟังก์ชันนี้จะทำงานได้เฉพาะกับแอปที่ใช้ไลบรารี microG เท่านั้น ไม่ใช่กับแอปบน Google Play ซึ่งหมายความว่าแอปพลิเคชันส่วนใหญ่จะละเว้นบริการเหล่านี้ + ฟีเจอร์นี้อยู่ในขั้นทดลองและอาจนำไปสู่การสูญเสียเงิน คุณได้รับคำเตือนแล้ว + หากดำเนินการต่อ Google จะแชร์ชื่อ ที่อยู่อีเมล และรูปโปรไฟล์ของคุณกับ %1$s ดูนโยบายความเป็นส่วนตัวและข้อกำหนดในการให้บริการของ %1$s + ลงชื่อเข้าใช้ด้วย Google + รับการแจ้งเตือนแบบพุช + อนุญาตให้ลงทะเบียน + อนุญาตให้ %1$s ลงทะเบียนเพื่อรับการแจ้งเตือนแบบพุชหรือไม่? + ข้อความ: %1$d (%2$d ไบต์) + ถูกตัดการเชื่อมต่อ + เชื่อมต่อตั้งแต่ %1$s + อนุญาติให้รับรองอุปกรณ์ + ทดสอบ ReCAPTCHA + ทดสอบ ReCAPTCHA Enterprise + ผ่านการทดสอบทั้งหมด + ทดสอบการรับรอง SafetyNet + อุปกรณ์นี้ไม่รองรับการทำงานของ DroidGuard บริการ SafetyNet อาจทำงานไม่ถูกต้อง + แอปกำลังใช้งาน SafetyNet + เนทีฟ + ซีเรียล + นำเข้าโปรไฟล์อุปกรณ์จากไฟล์ + ประเภทคำร้องขอ + ข้อมูลพื้นฐาน + โทเค็น + ยืนยันเข้ากันได้และผ่าน CTS + คัดลอกไปยังคลิปบอร์ดแล้ว! + เวลาการร้องขอ + ปิด + ฉันเข้าใจ \ No newline at end of file diff --git a/play-services-nearby/core/src/main/res/values-th/strings.xml b/play-services-nearby/core/src/main/res/values-th/strings.xml index c8b17abff0..9e423d1abd 100644 --- a/play-services-nearby/core/src/main/res/values-th/strings.xml +++ b/play-services-nearby/core/src/main/res/values-th/strings.xml @@ -54,7 +54,7 @@ การลบ รหัสประจำตัว ที่เก็บรวบรวมไว้จะทำให้ไม่สามารถแจ้งให้คุณทราบได้ในกรณีที่ผู้ติดต่อของคุณในช่วง 14 วันล่าสุดได้รับการวินิจฉัย API การแจ้งเตือนการสัมผัสช่วยให้แอปสามารถแจ้งให้คุณทราบหากคุณสัมผัสกับบุคคลที่รายงานว่าได้รับการวินิจฉัยว่าติดเชื้อ\n\nวันที่ ระยะเวลา และความแรงของสัญญาณที่เกี่ยวข้องกับการสัมผัสจะถูกแชร์กับแอปที่เกี่ยวข้อง ขณะที่เปิดใช้งาน API “การแจ้งเตือนการสัมผัส” อุปกรณ์ของคุณจะทำการรวบรวม รหัสประจำตัว (เรียกว่า Rolling Proximity Identifiers หรือ RPI) จากอุปกรณ์ใกล้เคียงโดยอัตโนมัติ\n\nเมื่อเจ้าของอุปกรณ์รายงานว่าได้รับการวินิจฉัยว่าเป็นบวก รหัสประจำตัว ของพวกเขาจะถูกแชร์ได้ อุปกรณ์ของคุณจะทำการตรวจสอบว่ามี รหัสประจำตัว ที่ได้รับการวินิจฉัยที่ทราบแล้วตรงกับ รหัสประจำตัว ที่รวบรวมไว้หรือไม่ และคำนวณความเสี่ยงต่อการติดเชื้อของคุณ - โทรศัพท์ของคุณจำเป็นต้องใช้ Bluetooth เพื่อรวบรวมและแบ่งปัน รหัสประจำตัว กับโทรศัพท์เครื่องอื่นที่อยู่ใกล้เคียงอย่างปลอดภัย %1$s สามารถแจ้งเตือนคุณได้หากคุณสัมผัสกับบุคคลที่รายงานว่าได้รับการวินิจฉัยว่าติดเชื้อ วันที่ ระยะเวลา และความแรงของสัญญาณที่เกี่ยวข้องกับการสัมผัสจะถูกแชร์กับแอป + โทรศัพท์ของคุณต้องใช้บลูทูธเพื่อรวบรวมและแบ่งปัน ID กับโทรศัพท์เครื่องอื่นที่อยู่ใกล้เคียงอย่างปลอดภัย %1$s สามารถแจ้งเตือนคุณได้หากคุณสัมผัสกับบุคคลที่รายงานว่าได้รับการวินิจฉัยว่าติดเชื้อ วันที่ ระยะเวลา และความแรงของสัญญาณที่เกี่ยวข้องกับการสัมผัสจะถูกแชร์กับแอป อัปเดตการตั้งค่า %1$s ต้องได้รับการอนุญาตเพิ่มเติม จำเป็นต้องมีการเข้าถึงตำแหน่ง From 6363f2bb54eb3a2c894b48f06209a0dec5fdc52e Mon Sep 17 00:00:00 2001 From: Idcrafter Date: Fri, 15 Nov 2024 21:19:16 +0000 Subject: [PATCH 110/132] Translated using Weblate (German) Currently translated at 91.5% (228 of 249 strings) Translation: microG/play-services-core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-core-strings/de/ Translated using Weblate (German) Currently translated at 100.0% (5 of 5 strings) Translation: microG/play-services-droidguard: core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-droidguard-core-strings/de/ --- play-services-core/src/main/res/values-de/strings.xml | 3 ++- .../core/src/main/res/values-de/strings.xml | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/play-services-core/src/main/res/values-de/strings.xml b/play-services-core/src/main/res/values-de/strings.xml index 03e39e0d2a..c2742746ce 100644 --- a/play-services-core/src/main/res/values-de/strings.xml +++ b/play-services-core/src/main/res/values-de/strings.xml @@ -24,7 +24,7 @@ %1$s versucht zu nutzen: Google Konto Manager Entschuldigung… - Eine App hat versucht, sich bei einem Google-Konto anzumelden. \n \nFalls dies beabsichtigt war, nutze die Schaltfläche Anmelden-, um Googles Anmeldeseite aufzurufen, andernfalls drücke Abbrechen, um zur App, die diesen Dialog verursacht hat, zurückzukehren. + Eine App auf ihren Gerät hat versucht, sich bei einem Google-Konto anzumelden. \n \nFalls dies beabsichtigt war, nutze die Schaltfläche Anmelden-, um Googles Anmeldeseite aufzurufen, andernfalls drücke Abbrechen, um zur App, die diesen Dialog verursacht hat, zurückzukehren. Einloggen "Dein Gerät verbindet sich mit den Google-Servern, um dich einzuloggen @@ -256,4 +256,5 @@ Versuche es später noch einmal." %1$s möchte auf Ihr Konto zugreifen, als ob es %2$s von %3$s wäre. Dies könnte einen privilegierten Zugang zu Ihrem Konto gewähren. Erlauben Sie %1$s privilegierten Zugriff auf %2$s\? Rechnungsanfragen abwickeln + Limitierte microG Dienste \ No newline at end of file diff --git a/play-services-droidguard/core/src/main/res/values-de/strings.xml b/play-services-droidguard/core/src/main/res/values-de/strings.xml index 27f1230162..8d02a63d6c 100644 --- a/play-services-droidguard/core/src/main/res/values-de/strings.xml +++ b/play-services-droidguard/core/src/main/res/values-de/strings.xml @@ -4,4 +4,5 @@ Eingebettet Lokale DroidGuard-Laufzeit verwenden Über das Netzwerk mit der DroidGuard-Laufzeit verbinden + Externe \ No newline at end of file From 552e59c0aab36f2a04b393e6b853934ee8b8d09d Mon Sep 17 00:00:00 2001 From: Angelo Schirinzi Date: Fri, 15 Nov 2024 20:19:07 +0000 Subject: [PATCH 111/132] Translated using Weblate (Italian) Currently translated at 100.0% (249 of 249 strings) Translation: microG/play-services-core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-core-strings/it/ --- .../src/main/res/values-it/strings.xml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/play-services-core/src/main/res/values-it/strings.xml b/play-services-core/src/main/res/values-it/strings.xml index 44a4c33ae2..43e1e787c5 100644 --- a/play-services-core/src/main/res/values-it/strings.xml +++ b/play-services-core/src/main/res/values-it/strings.xml @@ -250,4 +250,21 @@ Questa operazione può richiedere alcuni secondi." Ho capito %1$s wants to access your account as if it was %2$s by %3$s. Ciò potrebbe garantirgli un accesso privilegiato al tuo account. Stai utilizzando microG Limited Services. A differenza dei soliti servizi microG, questa versione funziona solo con le app che utilizzano le librerie microG, non quelle su Google Play. Ciò significa che la maggior parte delle applicazioni ignorerà questi servizi. + Avvisi dell\'account Google + Azione richiesta sull\'account + Il tuo account Google necessita di ulteriori configurazioni. + Completa la configurazione del tuo account Google + Completa i seguenti passaggi per poter utilizzare il tuo account Google %s su questo dispositivo. + Il tuo dispositivo deve registrarsi su Google almeno una volta.\n\nPuoi disattivare la registrazione del dispositivo Google dopo aver completato la configurazione dell\'account. + Abilita la registrazione del dispositivo + Abilita Cloud Messaging + Una volta completata la configurazione dell\'account, puoi disattivare Cloud Messaging. + Consenti Cloud Messaging per microG + Cliccare per confermare + Operazione completata + Fine + Ti avvisa quando uno dei tuoi account Google richiede una configurazione aggiuntiva prima di poter essere utilizzato o quando un account non è compatibile con microG. + In base alle tue preferenze, microG ha bisogno della tua autorizzazione prima di potersi registrare su Cloud Messaging. + Configurare il blocco schermo sicuro + Il tuo account Google è gestito dal tuo posto di lavoro o istituto scolastico. Il tuo amministratore ha deciso che i dispositivi necessitano di un blocco schermo sicuro prima di poter accedere ai dati dell\'account.\n\nImposta una password, un PIN o una sequenza per la schermata di blocco. \ No newline at end of file From cd89c364c1d1e3497d9f1f2e9fda06eff0db88f0 Mon Sep 17 00:00:00 2001 From: NEXI Date: Fri, 15 Nov 2024 18:02:25 +0000 Subject: [PATCH 112/132] Translated using Weblate (Serbian) Currently translated at 100.0% (59 of 59 strings) Translation: microG/play-services-nearby: core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-nearby-core-strings/sr/ Translated using Weblate (Serbian) Currently translated at 100.0% (252 of 252 strings) Translation: microG/play-services-core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-core-strings/sr/ --- play-services-core/src/main/res/values-sr/strings.xml | 3 +++ play-services-nearby/core/src/main/res/values-sr/strings.xml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/play-services-core/src/main/res/values-sr/strings.xml b/play-services-core/src/main/res/values-sr/strings.xml index 42b6efa488..d39ad845a0 100644 --- a/play-services-core/src/main/res/values-sr/strings.xml +++ b/play-services-core/src/main/res/values-sr/strings.xml @@ -278,4 +278,7 @@ Можете онемогућити размену порука у облаку након што је подешавање налога завршено. Подесите безбедно закључавање екрана Вашим Google налогом управља ваше радно место или образовна институција. Ваш администратор је одлучио да је уређајима потребно безбедно закључавање екрана да би могли да приступе подацима налога.\n\nПодесите лозинку, PIN или шаблон за закључавање екрана. + Омогући испоруку средстава на захтев + Преузмите додатна средства када то затраже апликације које користе Play Asset Delivery + Google Play Asset Delivery \ No newline at end of file diff --git a/play-services-nearby/core/src/main/res/values-sr/strings.xml b/play-services-nearby/core/src/main/res/values-sr/strings.xml index 7aea5e51d5..4dfb13ae28 100644 --- a/play-services-nearby/core/src/main/res/values-sr/strings.xml +++ b/play-services-nearby/core/src/main/res/values-sr/strings.xml @@ -53,7 +53,7 @@ Ваши ID-ови из последњих 14 дана ће се користити за обавештавање других да сте били у близини о потенцијалној изложености. \n \nВаш идентитет или резултат теста се неће делити са другим људима. - Ваш телефон мора да користи Bluetooth за безбедно прикупљање и дељење ID-ова са другим телефонима који су у близини. %1$s може да вас обавести ако сте били изложени некоме ко је пријавио да је позитиван. Датум, трајање и јачина сигнала повезани са изложеношћу ће се делити са апликацијом. + Ваш телефон мора да користи Bluetooth за безбедно прикупљање и дељење ID-ова са другим телефонима који су у близини. %1$s може да вас обавести ако сте били изложени некоме ко је пријавио да је позитиван. Датум, трајање и јачина сигнала повезани са изложеношћу ће се делити са апликацијом. Нема записа Избриши Нове дозволе су неопходне From b9c38e964cd3ac5634a1758fc66ed07ac8db3714 Mon Sep 17 00:00:00 2001 From: Rudi Timmermans Date: Fri, 15 Nov 2024 13:24:32 +0000 Subject: [PATCH 113/132] Translated using Weblate (Dutch) Currently translated at 87.1% (217 of 249 strings) Translation: microG/play-services-core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-core-strings/nl/ Translated using Weblate (Dutch) Currently translated at 100.0% (3 of 3 strings) Translation: microG/play-services-auth-api-phone: core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-auth-api-phone-core-strings/nl/ Translated using Weblate (Dutch) Currently translated at 100.0% (13 of 13 strings) Translation: microG/play-services-base: core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-base-core-strings/nl/ Translated using Weblate (Dutch) Currently translated at 89.1% (222 of 249 strings) Translation: microG/play-services-core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-core-strings/nl/ --- .../core/src/main/res/values-nl/strings.xml | 6 +- .../core/src/main/res/values-nl/strings.xml | 16 ++++- .../src/main/res/values-nl/strings.xml | 62 ++++++++++--------- 3 files changed, 52 insertions(+), 32 deletions(-) diff --git a/play-services-auth-api-phone/core/src/main/res/values-nl/strings.xml b/play-services-auth-api-phone/core/src/main/res/values-nl/strings.xml index a6b3daec93..cb45f89b6e 100644 --- a/play-services-auth-api-phone/core/src/main/res/values-nl/strings.xml +++ b/play-services-auth-api-phone/core/src/main/res/values-nl/strings.xml @@ -1,2 +1,6 @@ - \ No newline at end of file + + %s toestaan het onderstaande bericht te lezen en de code in te voeren? + Toestaan + Niet toestaan + \ No newline at end of file diff --git a/play-services-base/core/src/main/res/values-nl/strings.xml b/play-services-base/core/src/main/res/values-nl/strings.xml index a6b3daec93..910f1c35be 100644 --- a/play-services-base/core/src/main/res/values-nl/strings.xml +++ b/play-services-base/core/src/main/res/values-nl/strings.xml @@ -1,2 +1,16 @@ - \ No newline at end of file + + Actief op achtergrond + %1$s draait op de achtergrond. + Sluit <xliff:g example=“microG Services”>%1$s</xliff:g> uit van batterijoptimalisaties of wijzig de instellingen voor meldingen om deze melding te verbergen. + Geavanceerd + Geen + Zie alles + Open + Uitgeschakeld + Ingeschakeld + Automatisch + Manueel + Aan + Uit + \ No newline at end of file diff --git a/play-services-core/src/main/res/values-nl/strings.xml b/play-services-core/src/main/res/values-nl/strings.xml index 5ed68eb232..d58e5bf3ea 100644 --- a/play-services-core/src/main/res/values-nl/strings.xml +++ b/play-services-core/src/main/res/values-nl/strings.xml @@ -1,7 +1,7 @@ Google-accountbeheerder - Sign in + Aanmelden System has signature spoofing support: System grants signature spoofing permission: Add Google account @@ -11,27 +11,19 @@ microG-diensten microG-instellingen MicroG-services instellen. - Even… + Wacht even… Als u doorgaat, geeft u deze app en Google toestemming uw gegevens te gebruiken in overeenstemming met hun respectieve servicevoorwaarden en privacybeleid. Google Sorry… - An app on your device is trying to sign in to a Google account. \n \nIf this was intentional, use the Sign in button to connect to Google’s sign-in page, if not, press Cancel to go back to the application that caused this dialog to show up. - Your device is establishing a connection to Google’s servers to sign you in. -\n -\nThis can take a few seconds. - You don’t have a network connection. -\n -\nThis could be a temporary problem or your Android device may not be provisioned for data services. Try again when connected to a mobile network, or connect to a Wi-Fi network. - There was a problem communicating with Google servers. -\n -\nTry again later. - Your device is contacting Google to save information to your account. -\n -\nThis can take a couple of minutes. - Allow - Deny - Authentication required - <xliff:g example=F-Droid xmlns:xliff=urn:oasis:names:tc:xliff:document:1.2>%1$s</xliff:g> requires your authorization to access your Google account. + Een app op uw apparaat probeert zich aan te melden bij een Google-account.\n\nAls dit de bedoeling was, gebruik dan de knop Aanmelden om verbinding te maken met de aanmeldingspagina van Google. Zo niet, druk dan op Annuleren om terug te gaan naar de toepassing die dit dialoogvenster heeft veroorzaakt. + Je apparaat maakt verbinding met de servers van Google om je aan te melden.\n\nDit kan enkele seconden duren. + Je hebt geen netwerkverbinding. \n \nDit kan een tijdelijk probleem zijn of je Android-toestel is mogelijk niet ingesteld op dataservices. Probeer het opnieuw als je verbinding hebt met een mobiel netwerk of maak verbinding met een Wi-Fi-netwerk. + Er is een probleem opgetreden bij de communicatie met de Google-servers. \n \nProbeer het later opnieuw. + Uw apparaat neemt contact op met Google om informatie op te slaan in uw account. \n \nDit kan een paar minuten duren. + Toestaan + Niet toestaan + Verificatie vereist + <xliff:g example=F-Droid xmlns:xliff=urn:oasis:names:tc:xliff:document:1.2>%1$s</xliff:g> heeft uw autorisatie nodig om toegang te krijgen tot uw Google-account. Choose an account to continue to <xliff:g example=F-Droid xmlns:xliff=urn:oasis:names:tc:xliff:document:1.2>%1$s</xliff:g> Add another account @@ -59,16 +51,16 @@ Car vendor channel Access your car\'s mileage information Access your car\'s vendor channel to exchange car-specific information - Google device registration - Cloud Messaging - Google SafetyNet - Play Store services + Google apparaat registratie + Cloud berichtenuitwisseling + Google Beveiligingsnet + Play Store diensten Google Play Games <xliff:g example=F-Droid xmlns:xliff=urn:oasis:names:tc:xliff:document:1.2>%1$s</xliff:g> would like to use Play Games To use Play Games it is required to install the Google Play Games app. The application might continue without Play Games, but it is possible that it will behave unexpectedly. Pick a place Place picker is not yet available. - Select this location + Selecteer deze locatie Nearby places (%1$.7f, %2$.7f) microG Services: Lacking permission to <xliff:g example=have full network acccess xmlns:xliff=urn:oasis:names:tc:xliff:document:1.2>%1$s</xliff:g> @@ -92,11 +84,11 @@ Either the installed <xliff:g example=F-Droid xmlns:xliff=urn:oasis:names:tc:xliff:document:1.2>%1$s</xliff:g> is not compatible or signature spoofing is not active for it. Please check the documentation on which applications and ROMs are compatible. Battery optimizations ignored: Touch here to disable battery optimizations. Not doing this may result in misbehaving applications. - About + Over Components Configuration Google Services - Location service + Locatiedienst Services Test Battery optimizations enabled @@ -109,7 +101,7 @@ Trust Google for app permissions When disabled, the user is asked before an app\'s authorization request is sent to Google. Some applications will fail to use the Google account if this is disabled. Allow apps to find accounts - When enabled, all applications on this device will be able to see email address of your Google Accounts without prior authorization. + Als deze optie is ingeschakeld, kunnen alle applicaties op dit apparaat e-mailadressen van uw Google-accounts zien zonder voorafgaande toestemming. Authenticate with device registration When disabled, authentication requests won\'t be linked to the device registration, which may allow unauthorized devices to sign in, but may have unforeseen consequences. Strip device name for authentication @@ -122,7 +114,7 @@ Status More Google Accounts - Add and manage Google accounts + Google-accounts toevoegen en beheren Settings Accounts Account @@ -135,8 +127,8 @@ Confirm new apps Ask before registering a new app to receive push notifications Ping interval: <xliff:g example=10 minutes xmlns:xliff=urn:oasis:names:tc:xliff:document:1.2>%1$s</xliff:g> - About microG Services - Version information and used libraries + Over microG Diensten + Versie informatie en gebruikte bibliotheken Error unregistering No longer installed Unregister @@ -227,4 +219,14 @@ Some apps may require you to also enable license verification to verify your purchases. %1$s wil het volgende: %1$s wil het volgende gebruiken: + microG Beperkte diensten + Google accountmeldingen + Geeft een melding wanneer een van uw Google-accounts extra moet worden ingesteld voordat deze kan worden gebruikt of wanneer een account niet compatibel is met microG. + Accountactie vereist + <b><xliff:g example=“F-Droid”>%1$s</xliff:g></b> geprivilegieerde toegang toestaan tot <xliff:g example=“account@example.com”>%2$s</xliff:g>? + <b><xliff:g example=“F-Droid”>%1$s</xliff:g></b> wants to access your account as if it was <b><xliff:g example=“F-Droid”>%2$s</xliff:g> by <xliff:g example=“F-Droid Inc.”>%3$s</xliff:g></b>. This might grant it privileged access to your account. + Voer de volgende stappen uit om uw Google-account %s op dit apparaat te kunnen gebruiken. + Je apparaat moet zich minstens één keer registreren bij Google.\n\nU kunt de registratie van Google-apparaten uitschakelen nadat de account is ingesteld. + Cloud Messaging inschakelen + U kunt Aanmelden met Google beheren in uw Google-accounts. \ No newline at end of file From 5731a44d7232d955121afd206902726f443e2da3 Mon Sep 17 00:00:00 2001 From: Ettore Atalan Date: Fri, 22 Nov 2024 01:44:23 +0000 Subject: [PATCH 114/132] Translated using Weblate (German) Currently translated at 100.0% (249 of 249 strings) Translation: microG/play-services-core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-core-strings/de/ --- .../src/main/res/values-de/strings.xml | 31 +++++++++++++++---- 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/play-services-core/src/main/res/values-de/strings.xml b/play-services-core/src/main/res/values-de/strings.xml index c2742746ce..64515ea682 100644 --- a/play-services-core/src/main/res/values-de/strings.xml +++ b/play-services-core/src/main/res/values-de/strings.xml @@ -16,7 +16,7 @@ --> microG-Dienste microG-Einstellungen - microG-Dienste einrichten + microG-Dienste einrichten. Einen kurzen Moment… Google Durch Fortsetzen erlaubst du dieser App und Google, deine Informationen nach ihren entsprechenden AGB und Datenschutzrichtlinien zu nutzen. @@ -50,13 +50,13 @@ Versuche es später noch einmal." Stelle microG-Dienste bereit Erlaubt der app, microG-Dienste ohne Benutzerinteraktion zu konfigurieren Google-Geräte-Registrierung - Cloud Messaging + Cloud-Messaging Google SafetyNet - Google Play Games + Google Play Spiele %1$s möchte Play Games benutzen Um Play Games zu nutzen, ist die Installation der Google-Play-Games-App erforderlich. Diese App funktioniert eventuell auch ohne Play Games, verhält sich dabei unter Umständen aber ungewöhnlich. Ort auswählen - Ortsauswahl ist noch nicht verfügbar + Ortsauswahl ist noch nicht verfügbar. Diesen Ort auswählen Orte in der Nähe (%1$.7f, %2$.7f) @@ -140,7 +140,7 @@ Versuche es später noch einmal." Verbunden seit %1$s Push-Nachrichten erhalten Registrierung erlauben - Erlaubt es der App, sich für Push-Nachrichten zu registrieren + Erlaubt es der App, sich für Push-Nachrichten zu registrieren. App beim Erhalt einer Push-Nachricht starten App starten, während im Hintergrund die eingehenden Nachrichten abgerufen werden. Apps, die Push-Nachrichten benutzen @@ -254,7 +254,26 @@ Versuche es später noch einmal." Sie werden angemeldet weiter zu %1$s %1$s möchte auf Ihr Konto zugreifen, als ob es %2$s von %3$s wäre. Dies könnte einen privilegierten Zugang zu Ihrem Konto gewähren. - Erlauben Sie %1$s privilegierten Zugriff auf %2$s\? + Erlaube %1$s privilegierten Zugriff auf %2$s\? Rechnungsanfragen abwickeln Limitierte microG Dienste + Dein Google-Konto wird von deinem Arbeitsplatz oder deiner Bildungseinrichtung verwaltet. Dein Administrator hat entschieden, dass Geräte eine sichere Bildschirmsperre benötigen, bevor sie auf Kontodaten zugreifen können.\n\nBitte richte eine Passwort-, PIN- oder Muster-Bildschirmsperre ein. + Google-Konto-Warnungen + Kontoaktion erforderlich + Benachrichtigt dich, wenn eines deiner Google-Konten zusätzliche Einrichtung erfordert, bevor es verwendet werden kann, oder wenn ein Konto nicht mit microG kompatibel ist. + Dein Google-Konto muss zusätzlich eingerichtet werden. + Beende das Einrichten deines Google-Kontos + Führe die folgenden Schritte aus, um dein Google-Konto %s auf diesem Gerät verwenden zu können. + Geräteregistrierung aktivieren + Cloud-Messaging aktivieren + Du kannst Cloud-Messaging nach Abschluss der Kontoeinrichtung deaktivieren. + Cloud-Messaging für microG zulassen + Dein Gerät muss sich mindestens einmal bei Google registrieren.\n\nDu kannst die Google-Geräteregistrierung deaktivieren, nachdem die Kontoeinrichtung abgeschlossen ist. + Gemäß deinen Einstellungen benötigt microG eine Erlaubnis von dir, bevor es sich für Cloud-Messaging registrieren kann. + Sichere Bildschirmsperre konfigurieren + Zum Ausführen des Schritts anklicken + Schritt abgeschlossen + Fertigstellen + Du verwendest die begrenzten microG-Dienste. Im Gegensatz zu den normalen microG-Diensten funktioniert diese Variante nur mit Apps, die microG-Bibliotheken verwenden, nicht mit denen von Google Play. Das bedeutet, dass die meisten Anwendungen diese Dienste ignorieren werden. + Ich habe es verstanden \ No newline at end of file From 42e517b5fa6d233f171db97ad52b72d09465d29c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ph=E1=BA=A1m=20Ng=E1=BB=8Dc=20V=C5=A9?= Date: Fri, 22 Nov 2024 16:13:04 +0000 Subject: [PATCH 115/132] Translated using Weblate (Vietnamese) Currently translated at 100.0% (59 of 59 strings) Translation: microG/play-services-nearby: core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-nearby-core-strings/vi/ --- play-services-nearby/core/src/main/res/values-vi/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/play-services-nearby/core/src/main/res/values-vi/strings.xml b/play-services-nearby/core/src/main/res/values-vi/strings.xml index 0384023aea..c6c788491e 100644 --- a/play-services-nearby/core/src/main/res/values-vi/strings.xml +++ b/play-services-nearby/core/src/main/res/values-vi/strings.xml @@ -57,6 +57,6 @@ Rất tiếc, thiết bị của bạn chỉ tương thích một phần với Thông báo Tiếp xúc. Bạn có thể nhận thông báo về các tiếp xúc có nguy cơ, nhưng sẽ không thể thông báo cho người khác. %1$d ID trong giờ qua API Thông báo Tiếp xúc cho phép các ứng dụng thông báo cho bạn nếu bạn đã tiếp xúc với ai đó được chẩn đoán là dương tính.\n\nNgày, thời gian, và cường độ tín hiệu liên quan đến lần tiếp xúc sẽ được chia sẻ với ứng dụng tương ứng. - Điện thoại của bạn cần sử dụng Bluetooth để thu thập và chia sẻ an toàn các ID với các điện thoại khác ở gần. \u0020%1$s có thể thông báo cho bạn nếu bạn đã tiếp xúc với ai đó được chẩn đoán dương tính. \u0020Ngày, thời gian và cường độ tín hiệu liên quan đến một lần tiếp xúc sẽ được chia sẻ với ứng dụng. + Điện thoại của bạn cần sử dụng Bluetooth để thu thập và chia sẻ an toàn các ID với các điện thoại khác ở gần. %1$s có thể thông báo cho bạn nếu bạn đã tiếp xúc với ai đó được chẩn đoán dương tính. Ngày, thời gian và cường độ tín hiệu liên quan đến một lần tiếp xúc sẽ được chia sẻ với ứng dụng. Cần có Quyền mới \ No newline at end of file From 5474569a8d847e182e837fa1776db3545d1eb32d Mon Sep 17 00:00:00 2001 From: Yusuf Cihan Date: Fri, 6 Dec 2024 12:17:22 +0000 Subject: [PATCH 116/132] Translated using Weblate (Turkish) Currently translated at 100.0% (4 of 4 strings) Translation: microG/play-services-core: plurals Translate-URL: https://hosted.weblate.org/projects/microg/play-services-core-plurals/tr/ Translated using Weblate (Turkish) Currently translated at 100.0% (249 of 249 strings) Translation: microG/play-services-core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-core-strings/tr/ Translated using Weblate (Turkish) Currently translated at 100.0% (5 of 5 strings) Translation: microG/play-services-droidguard: core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-droidguard-core-strings/tr/ Translated using Weblate (Turkish) Currently translated at 100.0% (6 of 6 strings) Translation: microG/play-services-oss-licenses: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-oss-licenses-strings/tr/ Translated using Weblate (Turkish) Currently translated at 100.0% (4 of 4 strings) Translation: microG/play-services-ads-identifier: core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-ads-identifier-core-strings/tr/ Translated using Weblate (Turkish) Currently translated at 100.0% (19 of 19 strings) Translation: microG/vending-app Translate-URL: https://hosted.weblate.org/projects/microg/vending-app/tr/ Translated using Weblate (Turkish) Currently translated at 100.0% (3 of 3 strings) Translation: microG/play-services-auth-api-phone: core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-auth-api-phone-core-strings/tr/ Translated using Weblate (Turkish) Currently translated at 100.0% (156 of 156 strings) Translation: microG/play-services-core: permissions Translate-URL: https://hosted.weblate.org/projects/microg/play-services-core-permissions/tr/ Translated using Weblate (Turkish) Currently translated at 100.0% (59 of 59 strings) Translation: microG/play-services-nearby: core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-nearby-core-strings/tr/ Translated using Weblate (Turkish) Currently translated at 100.0% (50 of 50 strings) Translation: microG/play-services-location: core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-location-core-strings/tr/ --- .../core/src/main/res/values-tr/strings.xml | 7 ++- .../core/src/main/res/values-tr/strings.xml | 6 +- .../src/main/res/values-tr/permissions.xml | 53 +++++++++++++++- .../src/main/res/values-tr/plurals.xml | 4 +- .../src/main/res/values-tr/strings.xml | 22 ++++++- .../core/src/main/res/values-tr/strings.xml | 4 +- .../core/src/main/res/values-tr/strings.xml | 53 +++++++++++++++- .../core/src/main/res/values-tr/strings.xml | 62 ++++++++++++++++++- .../src/main/res/values-tr/strings.xml | 9 ++- .../src/main/res/values-tr/strings.xml | 22 ++++++- 10 files changed, 228 insertions(+), 14 deletions(-) diff --git a/play-services-ads-identifier/core/src/main/res/values-tr/strings.xml b/play-services-ads-identifier/core/src/main/res/values-tr/strings.xml index a6b3daec93..c505b7cddd 100644 --- a/play-services-ads-identifier/core/src/main/res/values-tr/strings.xml +++ b/play-services-ads-identifier/core/src/main/res/values-tr/strings.xml @@ -1,2 +1,7 @@ - \ No newline at end of file + + Reklam kimliği izni + Reklam kimliği bildirimi + Bir uygulamanın, kullanıcının reklam takibini kısıtlama ayarını veya reklam kimliğini değiştirdiğinde bildirim almasına izin verir. + Bir uygulamanın geçerli bir reklam kimliğine doğrudan veya dolaylı olarak erişmesine izin verir. + \ No newline at end of file diff --git a/play-services-auth-api-phone/core/src/main/res/values-tr/strings.xml b/play-services-auth-api-phone/core/src/main/res/values-tr/strings.xml index a6b3daec93..30f73cd4b3 100644 --- a/play-services-auth-api-phone/core/src/main/res/values-tr/strings.xml +++ b/play-services-auth-api-phone/core/src/main/res/values-tr/strings.xml @@ -1,2 +1,6 @@ - \ No newline at end of file + + İzin ver + %s uygulamasının aşağıdaki mesajı okumasına ve kodu girmesine izin veriyor musunuz? + Reddet + \ No newline at end of file diff --git a/play-services-core/src/main/res/values-tr/permissions.xml b/play-services-core/src/main/res/values-tr/permissions.xml index f4cd11b64d..bc955e5588 100644 --- a/play-services-core/src/main/res/values-tr/permissions.xml +++ b/play-services-core/src/main/res/values-tr/permissions.xml @@ -99,9 +99,9 @@ Google Koordinatör işlerinizi görüntüleyin Google Haritalar Koordinatör işlerinizi görüntüleyin ve yönetin Google Bulut Depolama\'daki verilerinizi ve izinlerinizi yönetin - Verilerinizi Google Bulut Depolama\'da yönetin - DoubleClick for Advertisers raporlarını görüntüleyin ve yönetin - Application Data klasörüne erişime izin verir + Google Bulut Depolama\'daki verilerinizi yönetme + DoubleClick for Advertisers raporlarını görüntüleme ve yönetme + Application Data klasörüne erişme izni Google Drive uygulamalarınızı görüntüleyin Bu uygulamayla açtığınız veya oluşturduğunuz Google Drive dosyalarını görüntüleyin ve yönetin Google Drive\'ınızdaki dosya ve dokümanlara ilişkin meta verileri görüntüleyin @@ -109,4 +109,51 @@ Google Drive\'ınızdaki dosya ve belgeleri görüntüleyin ve yönetin Verilerinizi Google Bulut Depolama\'da görüntüleyin Kullanıcıların bir uygulamanın kurulumunu onaylamasına izin vermek için kullanılan özel kapsam + App Engine yönetici erişimi. + Genel okuma/yazma OAuth yetkisinin yanı sıra, müşterinin verisini getirirken yalnızca okuma OAuth yetkisini kullan. + Google BigQuery\'deki verinizi görme + Admin Audit API sadece okuma izni + App State servisini kullanma yetkisi. + Freebase hesabınızı görüntüleme + Freebase\'e kendi hesabınız ile giriş yapma + Fusion Tables öğelerinizi görüntüleme + GAN verinizi görme + Chrome için Cloud Messaging + Glass zaman çizelgesi yetkisi + Taslak oluştur, oku, düzenle ve sil. Taslak ve mesaj gönder. + Tüm yazma/okuma işlemleri fakat derhal, çöp kutusuna atmadan kalıcı silme işlemleri hariç. + Kullanılabilir en iyi konumunuzu ve konum geçmişinizi yönetme + Şehir seviyesindeki konumunuzu ve konum geçmişinizi yönetme + Google Maps Engine verinizi görüntüleme ve yönetme + Google Maps Engine verinizi görüntüleme + Mobil deneyim için Google Haritalar\'ınızı görüntüleme ve yönetme + Orkut aktivitenizi yönetme + Orkut verinizi görme + Adınızı, temel bilginizi ve Google+\'daki bağlantılarınızı öğrenme + Google\'da kim olduğunuzu öğrenme + Google Prediction API verinizi yönetme + Ürün verinizi yönetme + Yetkilisi olduğunuz siteleri ve alan adlarını yönetme + Görev kuyruklarınızdaki görevlerinizi kullanma + Görevlerinizi yönetme + Görevlerinizi yönetme + goo.gl kısa bağlantılarınızı yönetme + E-posta adresinizi görüntüleme + Hesabınız hakkında temel bilgileri görüntüleme + YouTube hesabınızı yönetme + YouTube\'daki varlıklarınızı ve ilişkili içeriklerinizi görüntüleme ve yönetme + YouTube içeriğinizdeki YouTube Analytics parasal raporları görüntüleme + YouTube içeriğinizdeki YouTube Analytics raporları görüntüleme + Kullanılabilir en iyi konumunuzu yönetme + Şehir seviyesindeki konumunuzu yönetme + Fusion Tables öğelerinizi yönetme + GAN verinizi yönetme + Tüm kaynakları ve metaverilerini oku — yazma erişimi olmadan. + Google BigQuery\'deki verinizi görme veya yönetme + Google Apps Script komutlarının davranışını değiştirme + Google Play Oyunlar verinize erişme yetkisi. + Görevlerinizi görüntüleme + Shopping Content API\'a okuma/yazma erişimi. + Google Maps Tracks API, Bu yetki, projenizin verisine okuma ve yazma erişimi sağlar. + Google ile yeni site doğrulamalarınızı yönetme \ No newline at end of file diff --git a/play-services-core/src/main/res/values-tr/plurals.xml b/play-services-core/src/main/res/values-tr/plurals.xml index 6354030dfa..e5b8da147c 100644 --- a/play-services-core/src/main/res/values-tr/plurals.xml +++ b/play-services-core/src/main/res/values-tr/plurals.xml @@ -5,8 +5,8 @@ %1$d backend yapılandırıldı - <xliff:g example=“1”>%1$d</xliff:g> kayıtlı Uygulama - <xliff:g example=“123”>%1$d</xliff:g> kayıtlı Uygulama + %1$d kayıtlı uygulama + %1$d kayıtlı uygulama Eksik izin talebi diff --git a/play-services-core/src/main/res/values-tr/strings.xml b/play-services-core/src/main/res/values-tr/strings.xml index fb94c7eb04..ba0ee677e6 100644 --- a/play-services-core/src/main/res/values-tr/strings.xml +++ b/play-services-core/src/main/res/values-tr/strings.xml @@ -25,7 +25,7 @@ %1$s şunu istiyor: %1$s şunu kullanmak istiyor: Google Hesap Yöneticisi - Cihazınızdaki bir uygulama, Google hesabına giriş yapmaya çalışıyor. \n \nEğer bu istediğiniz bir şey ise, Oturum aç tuşunu kullanarak Google\'ın oturum açma sayfasına bağlanın, eğer değilse, İptal tuşuna basarak bu menünün çıkmasına neden olan uygulamaya geri dönün. + Cihazınızdaki bir uygulama, Google hesabına giriş yapmaya çalışıyor.\n\nEğer bu istenilen bir şey ise, Oturum aç tuşunu kullanarak Google\'ın oturum açma sayfasına giriş yapın, eğer değilse, İptal tuşuna basarak oturum açmak isteyen uygulamaya geri dönün. Oturum aç Hay aksi… Cihazınız, oturum açmanız için Google sunucuları ile iletişim kuruyor. @@ -242,4 +242,24 @@ Hesap seç Özgür uygulamaları otomatik olarak kütüphaneye ekle Özgür uygulamalar Google Play\'den indirilip indirilmediklerini denetleyebilir. Şu anda kullanabileceğiniz tüm özgür uygulamaların denetimini her zaman geçmek için özgür uygulamaları otomatik olarak hesap kütüphanenize ekleyin. + microG Kısıtlı Servisleri + Google hesap uyarıları + Hesaba eylem gerekiyor + Google hesabınızın ek kuruluma ihtiyacı var. + Google hesabınızı ayarlamayı bitirin + Google hesaplarınızdan biri kullanılmadan önce ek kurulum gerektirdiğinde veya bir hesap, microG ile uyumsuz olduğunda bildirimde bulunur. + Bu cihazda %s Google hesabınızı kullanmak için verilen adımları tamamlayın. + Cihazınızın, en azından bir seferliğine Google\'a kaydettirilmesi gerekiyor.\n\nHesap kurulumu tamamlandıktan sonra Google cihaz kaydını devre dışı bırakabilirsiniz. + Cloud Messaging\'i etkinleştir + Güvenli ekran kilidi ayarla + Hesap kurulumu tamamlandıktan sonra Cloud Messaging\'i geri kapatabilirsiniz. + microG için Cloud Messaging\'e izin ver + Tercihlerinize göre, microG\'nin kendisini Cloud Messaging\'e kaydetmesi için sizin yetki vermeniz gerekiyor. + Adım tamamlandı + Adımı gerçekleştirmek için dokunun + Bitir + Anlaşıldı + Cihaz kaydını etkinleştir + Google hesabınız, eğitim kurumunuz veya çalışma ortamınız tarafından yönetiliyor. Yöneticiniz, hesap verilerine erişilmeden önce cihazların bir ekran kilidine sahip olmasını zorunlu kıldı.\n\nBundan dolayı, lütfen cihazınızı bir şifre, PIN veya desen kilidi ile koruyun. + microG Kısıtlı Servisleri\'ni kullanıyorsunuz. Normal microG Servisleri\'ne kıyasla, microG\'nin bu varyasyonu, Google Play\'deki uygulamalarla değil, özellikle microG kütüphanelerini kullanan uygulamalarla çalışacaktır. Diğer bir deyişle, çoğu uygulama, bu servisleri görmezden gelecektir. \ No newline at end of file diff --git a/play-services-droidguard/core/src/main/res/values-tr/strings.xml b/play-services-droidguard/core/src/main/res/values-tr/strings.xml index c051d60522..fe14e104a5 100644 --- a/play-services-droidguard/core/src/main/res/values-tr/strings.xml +++ b/play-services-droidguard/core/src/main/res/values-tr/strings.xml @@ -2,7 +2,7 @@ DroidGuard çalışma modu Gömülü - Yerel DroidGuard çalışma zamanını kullan + Yerel DroidGuard çalıştırıcısını kullan Uzak - Ağ üzerinden DroidGuard çalışma zamanına bağlan + Ağ üzerinden DroidGuard çalıştırıcısına bağlan \ No newline at end of file diff --git a/play-services-location/core/src/main/res/values-tr/strings.xml b/play-services-location/core/src/main/res/values-tr/strings.xml index a6b3daec93..791bcfc7c2 100644 --- a/play-services-location/core/src/main/res/values-tr/strings.xml +++ b/play-services-location/core/src/main/res/values-tr/strings.xml @@ -1,2 +1,53 @@ - \ No newline at end of file + + Konum + Wi-Fi üzerinden konum + Mobil ağ üzerinden konum + Adres çözümleyicisi + Mozilla\'dan iste + Mozilla Konum Hizmeti\'nden Wi-Fi tabanlı konumu iste. + Çevrimiçi servisten iste + GPS\'ten hatırla + Mobil erişim noktalarından iste + GPS kullanıldığında Wi-Fi konumlarını yerelde sakla. + Çevrimiçi servisten Wi-Fi tabanlı konumu iste. + Çevrimiçi servisten mobil ağ istasyonlarının konumunu iste. + Nominatim\'i kullan + Konum erişimine sahip uygulamalar + GPS kullanıldığında mobil şebeke konumlarını yerel olarak sakla. + Cihazınız şun(lar)a ihtiyaç duyuyor: + Yaklaşık konum vermeye zorla + Uygulamaya verilen izinden bağımsız olarak, bu uygulamaya her zaman yaklaşık konum sağla. + Daha iyi bir deneyim için, microG\'nin konum servislerinden yararlanan cihaz konumunu etkinleştirin + GPS, Wi-Fi, mobil ağlar ve sensörleri kullanmasına + Detaylar için, konum ayarlarına gidin. + Hayır, teşekkürler + Tamam + Hizmet sağlayıcı URL\'sini değiştir + Sıfırla + Bu, özel bir servis sağlayıcısı URL\'si belirtmenize olanak tanır. Yanlış ayarlama sonucunda, konum servisleri yanıt vermeyi durdurabilir veya tamamen mevcut olmayabilir. + microG konum servislerini kullanın; bu servisin bir parçası olarak, microG periyodik olarak konum verisi toplayabilir ve bu verileri anonim bir şekilde konum doğruluğunu iyileştirmek ve daha iyi bir konum tabanlı servis sunmak için kullanabilir. + Koşullar / Gizlilik + Önerilen + Belirtilen URL\'ye /v1/geolocate yolu kendiliğinden eklenecektir. Konum sağlayıcısı, eğer bir erişim anahtarına ihtiyaç duyuyor ise kök URL\'ye sorgu parametresi olarak eklenebilir. + Çevrimiçi konum hizmetlerini kullanmak için öncelikle bir konum veri sağlayıcısı seçmeniz gerekiyor. + Yerel Wi-Fi konum veritabanını dışarı aktar + Yerel baz istasyonu konum veritabanını dışarı aktar + Dosyadan konum verisini içeri aktar + %1$d kayıt içe aktarıldı. + Son erişim + Çevrimiçi servisten iste + Son erişim: %1$s + Mozilla\'dan iste + Mozilla Konum Hizmeti\'nden baz istasyonların konumunu iste. + Özel + Konum verisini içe veya dışarı aktar + microG Servisleri\'ne konum izninin verilmesine + Özel hizmet URL\'si + Ayarlama gerekiyor + Çevrimiçi konum sağlayıcısı seç + Adresleri OpenStreetMap Nominatim hizmeti ile çöz. + GPS\'ten hatırla + Bağlanıldığında Wi-Fi konumunu direkt olarak desteklenen mobil erişim noktalarından (hotspot) al. + Devam etmek için, microG\'nin konum servislerinden yararlanan cihaz konumunu etkinleştirin + \ No newline at end of file diff --git a/play-services-nearby/core/src/main/res/values-tr/strings.xml b/play-services-nearby/core/src/main/res/values-tr/strings.xml index a6b3daec93..3ba5cb67d7 100644 --- a/play-services-nearby/core/src/main/res/values-tr/strings.xml +++ b/play-services-nearby/core/src/main/res/values-tr/strings.xml @@ -1,2 +1,62 @@ - \ No newline at end of file + + Bluetooth\'u etkinleştir + Temas Bildirimleri devre dışı + Temas Bildirimleri\'ni almak için Bluetooth\'un etkinleştirilmesi gerekiyor. + Temas Bildirimleri\'ni almak için konum izinleri gerekiyor. + Temas Bildirimleri\'ni almak için hem Bluetooth hem de konum izinlerinin etkinleştirilmesi gerekiyor. + Temas Bildirimleri\'ni etkinleştirmek için, bu özelliği kullanan bir uygulamayı açın. + Konum ayarlarını aç + Temas Bildirimleri kullanan uygulamalar + Toplanan kimlikler + Ne yazık ki, cihazınız Temas Bildirimleri\'ni sadece kısmen destekliyor. Bu da, kendi riskli temaslarınızdan bilgilendirileceğiniz ancak başkalarını bilgilendiremeyeceğiniz anlamına geliyor. + Şu anki yayınlanan kimlik + 5 dakikadan az + yakın temas + Rapor edilen temaslar + Güncellendi: %1$s + %1$s, %2$s + %1$d teşhis anahtarı işlendi. + Son 14 gün içinde API kullanımı + Kayıt yok + %1$d kimlik toplandı + %1$d temas karşılaşmaları raporlandı: + %1$s, risk skoru %2$d + Sil + Toplanan tüm kimlikleri sil + Yine de sil + Saat başı %1$s kimlik + Toplanan kimliklerin silinmesi, son 14 gün içindeki kişilerinizden herhangi birinin teşhis edilmesi durumunda bilgilendirilmenizi imkansız hale getirecektir. + Veriler ile analiz yapmak için kullanmak üzere toplanan kimlikleri başka bir uygulama ile dışa aktarın. + Temas Bildirimleri API\'si, uygulamaların, daha önceden pozitif teşhis olduğu bildirilen kişilerle temas halinde olmanız durumunda sizi bilgilendirmesine olanak tanır.\n\nBulunduğunuz temasın, tarihi, süresi, ve sinyal menzili, ilgili uygulama ile paylaşılır. + Temas Bildirimleri\'ni etkinleştir? + Temas Bildirimleri\'ni kullan + Kimlikleri güvenli bir şekilde toplamak ve yakındaki diğer cihazlarla paylaşmak için cihazınızın Bluetooth\'u kullanması gerekir. %1$s, pozitif teşhis konulduğunu bildiren biri ile temas halinde kalırsanız sizi bilgilendirebilir. Temas ile ilişkili tarih, süre ve sinyal gücü uygulama ile paylaşılacaktır. + Etkinleştir + Temas Bildirimleri\'ni devre dışı bırak? + Temas Bildirimleri\'ni devre dışı bırakmanız halinde, pozitif teşhis konulduğu bilinen biri ile temasa geçmeniz durumunda bilgilendirilmeyeceksiniz. + Kimliklerinizi %1$s ile paylaşmak istiyor musunuz? + İzin ver + Konum erişimi gerekiyor. + Yeni izinler gerekiyor + Temas Bildirimleri\'ne gerekli izinleri vermek için dokunun + Ayarları değiştir + %1$s ek izinlere ihtiyaç duyuyor. + Son saat içinde %1$d kimlik + Dışa aktar + Etkinleştir + Devre dışı bırak + Bluetooth\'un etkinleştirilmesi gerekiyor. + Ne yazık ki, cihazınız Temas Bildirimleri için uyumlu değil. + Paylaş + Temas Bildirimleri + uzak temas + Bildirilen bir temas yok. + Temas Bildirimleri\'nin çalışması için ek izinlere ihtiyaç var + Yaklaşık %1$d dakika + Not: Risk skoru uygulama tarafından belirlenir. Yüksek sayılar düşük risk anlamına veya tam tersini olabilir. + Temas Bildirimleri API\'si etkin iken, cihazınız yakındaki diğer cihazlardan pasif olarak kimlik toplar. (bu kimlikler, Rolling Identifiers, veya RPI olarak da bilinir)\n\nCihaz sahiplerin testi pozitif çıktığı bilindiğinde, kendilerinin kimlikleri paylaşılabilir. Bu sayede, cihazınız, bilinen kimlikler ile toplanan kimlikleri eşleştirir ve enfeksiyon riskinizi hesaplar. + Neredeyse bitti! Bir sonraki ekranda, arkaplanda konum erişimini vermek için \'Her zaman izin ver\' seçeneğini seçin. Ardından geri dönün. + %2$s için %1$d çağrı + Son 14 gün içindeki kimlikleriniz, çevrenizdekilere potansiyel temasta bulunduğunuz konusunda haber vermeye yardımcı olmak için kullanılır.\n\nKişisel bilgileriniz veya test sonuçlarınız başkaları ile paylaşılmaz. + \ No newline at end of file diff --git a/play-services-oss-licenses/src/main/res/values-tr/strings.xml b/play-services-oss-licenses/src/main/res/values-tr/strings.xml index a6b3daec93..416ea3a4c5 100644 --- a/play-services-oss-licenses/src/main/res/values-tr/strings.xml +++ b/play-services-oss-licenses/src/main/res/values-tr/strings.xml @@ -1,2 +1,9 @@ - \ No newline at end of file + + Lisansı yüklemeye çalışırken bir sorunla karşılaşıldı. + Bu uygulamanın herhangi bir açık kaynak lisansı yok. + Açık kaynak lisansları + Lisans listesi yükleniyor. + Açık kaynak yazılım için lisans bilgileri + Lisans bilgisi yükleniyor. + \ No newline at end of file diff --git a/vending-app/src/main/res/values-tr/strings.xml b/vending-app/src/main/res/values-tr/strings.xml index a6b3daec93..af2251627e 100644 --- a/vending-app/src/main/res/values-tr/strings.xml +++ b/vending-app/src/main/res/values-tr/strings.xml @@ -1,2 +1,22 @@ - \ No newline at end of file + + microG Servisleri tek başına kullanılamaz. microG\'yi kullanmak için lütfen microG Servisleri\'ni yükleyin. + %1$s lisansı doğrulanamadı + Eğer uygulama hatalı davranıyorsa, uygulamayı satın aldığınız Google hesabınız ile giriş yapın. + Yoksay + Ödeme henüz mümkün değil + Ödemeyi onayla + İnternete bağlı değilsiniz. Lütfen Wi-Fi veya mobil verinin açık olduğundan emin olun ve ardından tekrar deneyin. + Bilinmeyen hata, lütfen çıkın ve tekrar deneyin. + Şifrenizi girin + Bu cihazdaki oturumumu hatırla + Şifrenizi mi unuttunuz? + Daha fazla bilgi + Bir uygulama lisansını doğrulamaya çalıştığında ancak siz herhangi bir Google hesabında oturum açmadığınızda bildirim gönderir. + Giriş yap + microG Eşlikçisi tek başına kullanılamaz. Onun yerine microG Servisleri ayarları açılıyor. + Lisans bildirimleri + Girdiğiniz şifre yanlış. + Onayla + microG Eşlikçisi + \ No newline at end of file From f02ed1950e9a5f951ed7db7d538f79e78bab6274 Mon Sep 17 00:00:00 2001 From: Igor Sorocean Date: Mon, 9 Dec 2024 06:31:11 +0000 Subject: [PATCH 117/132] Translated using Weblate (Romanian) Currently translated at 100.0% (252 of 252 strings) Translation: microG/play-services-core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-core-strings/ro/ Translated using Weblate (Romanian) Currently translated at 100.0% (59 of 59 strings) Translation: microG/play-services-nearby: core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-nearby-core-strings/ro/ Translated using Weblate (Romanian) Currently translated at 100.0% (21 of 21 strings) Translation: microG/vending-app Translate-URL: https://hosted.weblate.org/projects/microg/vending-app/ro/ --- play-services-core/src/main/res/values-ro/strings.xml | 3 +++ play-services-nearby/core/src/main/res/values-ro/strings.xml | 2 +- vending-app/src/main/res/values-ro/strings.xml | 2 ++ 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/play-services-core/src/main/res/values-ro/strings.xml b/play-services-core/src/main/res/values-ro/strings.xml index e14fcaa99e..c388b6ce8f 100644 --- a/play-services-core/src/main/res/values-ro/strings.xml +++ b/play-services-core/src/main/res/values-ro/strings.xml @@ -276,4 +276,7 @@ Poți dezactiva mesageria în cloud după finalizarea configurării contului. Activează mesageria în cloud Contul Google este gestionat de locul de muncă sau de instituția de învățământ. Administratorul a decis că dispozitivele au nevoie de o blocare securizată a ecranului înainte de a putea accesa datele contului.\n\nConfigurează o parolă, un cod PIN sau un model de deblocare al ecranului. + Livrare de active Google Play + Activează livrarea de active la cerere + Descarcă materiale suplimentare atunci când sunt solicitate de aplicațiile care folosesc Play Asset Delivery \ No newline at end of file diff --git a/play-services-nearby/core/src/main/res/values-ro/strings.xml b/play-services-nearby/core/src/main/res/values-ro/strings.xml index 63af794c61..65e4631ad0 100644 --- a/play-services-nearby/core/src/main/res/values-ro/strings.xml +++ b/play-services-nearby/core/src/main/res/values-ro/strings.xml @@ -46,7 +46,7 @@ În timp ce API-ul Exposure Notification este activat, dispozitivul colectează pasiv ID-uri (numite Rolling Proximity Identifiers sau RPI) de la dispozitivele din apropiere. \n \nCând proprietarii de dispozitive raportează că sunt diagnosticați pozitivi, ID-urile lor pot fi partajate. Dispozitivul verifică dacă vreunul dintre ID-urile diagnosticate cunoscute se potrivește cu oricare dintre ID-urile colectate și calculează riscul de infecție. - Telefonul trebuie să utilizeze Bluetooth pentru a colecta și a partaja ID-uri în siguranță cu alte telefoane din apropiere.%1$s poate anunța dacă ai fost expus la cineva care a raportat că a fost diagnosticat pozitiv. Data, durata și puterea semnalului asociate cu o expunere vor fi partajate cu aplicația. + Telefonul trebuie să utilizeze Bluetooth pentru a colecta și a partaja ID-uri în siguranță cu alte telefoane din apropiere. %1$s poate anunța dacă ai fost expus la cineva care a raportat că a fost diagnosticat pozitiv. Data, durata și puterea semnalului asociate cu o expunere vor fi partajate cu aplicația. Dezactivezi notificările de expunere? Oprire După dezactivarea notificărilor de expunere, nu vei mai fi notificat când ai fost expus la cineva care a raportat că a fost diagnosticat pozitiv. diff --git a/vending-app/src/main/res/values-ro/strings.xml b/vending-app/src/main/res/values-ro/strings.xml index 294959707a..fa9518eb05 100644 --- a/vending-app/src/main/res/values-ro/strings.xml +++ b/vending-app/src/main/res/values-ro/strings.xml @@ -19,4 +19,6 @@ Memorizează autentificarea mea pe acest dispozitiv Află mai multe Verifică + Se descarcă + Fișiere suplimentare pentru %s \ No newline at end of file From cd87be3726b7f987132f627f7ba4ec710eaebeb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A4=A7=E7=8E=8B=E5=8F=AB=E6=88=91=E6=9D=A5=E5=B7=A1?= =?UTF-8?q?=E5=B1=B1?= Date: Mon, 9 Dec 2024 00:55:10 +0000 Subject: [PATCH 118/132] Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 100.0% (252 of 252 strings) Translation: microG/play-services-core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-core-strings/zh_Hans/ --- play-services-core/src/main/res/values-zh-rCN/strings.xml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/play-services-core/src/main/res/values-zh-rCN/strings.xml b/play-services-core/src/main/res/values-zh-rCN/strings.xml index 64e3a71af2..c9607f5c37 100644 --- a/play-services-core/src/main/res/values-zh-rCN/strings.xml +++ b/play-services-core/src/main/res/values-zh-rCN/strings.xml @@ -261,4 +261,7 @@ microG GmsCore 内置一套自由的 SafetyNet 实现,但是官方服务器要 你的 Google 账户需要额外设置。 要能在这台设备上使用你的 Google 账户 %s 请完成下列步骤。 你的 Google 账户受工作场所或教育机构管理。你的管理员决定设备在可以访问账户数据前需要设置安全屏幕锁。\n\n请设置一个密码、PIN或手势屏幕锁。 + 当使用 Play 资产传递的应用请求时下载额外的资产 + Google Play 资产传递 + 启用按需资产传递 \ No newline at end of file From ed3a8b3905cfac55daf68ef53126fb5152d2e07d Mon Sep 17 00:00:00 2001 From: ngocanhtve Date: Mon, 9 Dec 2024 02:32:38 +0000 Subject: [PATCH 119/132] Translated using Weblate (Vietnamese) Currently translated at 100.0% (252 of 252 strings) Translation: microG/play-services-core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-core-strings/vi/ Translated using Weblate (Vietnamese) Currently translated at 100.0% (21 of 21 strings) Translation: microG/vending-app Translate-URL: https://hosted.weblate.org/projects/microg/vending-app/vi/ --- play-services-core/src/main/res/values-vi/strings.xml | 3 +++ vending-app/src/main/res/values-vi/strings.xml | 2 ++ 2 files changed, 5 insertions(+) diff --git a/play-services-core/src/main/res/values-vi/strings.xml b/play-services-core/src/main/res/values-vi/strings.xml index 92f0ca8fb8..6f33e44c63 100644 --- a/play-services-core/src/main/res/values-vi/strings.xml +++ b/play-services-core/src/main/res/values-vi/strings.xml @@ -257,4 +257,7 @@ Hoàn tất Nhấn để thực hiện Bước đã hoàn thành + Phân phối tài sản của Google Play + Cho phép phân phối tài sản theo yêu cầu + Tải xuống các tài sản bổ sung khi được các ứng dụng sử dụng Play Asset Delivery yêu cầu \ No newline at end of file diff --git a/vending-app/src/main/res/values-vi/strings.xml b/vending-app/src/main/res/values-vi/strings.xml index d198e898ca..35e42d3617 100644 --- a/vending-app/src/main/res/values-vi/strings.xml +++ b/vending-app/src/main/res/values-vi/strings.xml @@ -19,4 +19,6 @@ Quên mật khẩu? Tìm hiểu thêm Xác minh + Các tệp tin bổ sung cho %s + Đang tải xuống \ No newline at end of file From e5acc7322aa28ffa97e746125b61113eb3163209 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aindri=C3=BA=20Mac=20Giolla=20Eoin?= Date: Mon, 9 Dec 2024 09:04:09 +0000 Subject: [PATCH 120/132] Translated using Weblate (Irish) Currently translated at 100.0% (252 of 252 strings) Translation: microG/play-services-core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-core-strings/ga/ Translated using Weblate (Irish) Currently translated at 100.0% (21 of 21 strings) Translation: microG/vending-app Translate-URL: https://hosted.weblate.org/projects/microg/vending-app/ga/ --- play-services-core/src/main/res/values-ga/strings.xml | 3 +++ vending-app/src/main/res/values-ga/strings.xml | 2 ++ 2 files changed, 5 insertions(+) diff --git a/play-services-core/src/main/res/values-ga/strings.xml b/play-services-core/src/main/res/values-ga/strings.xml index 304e485a74..5b77607de9 100644 --- a/play-services-core/src/main/res/values-ga/strings.xml +++ b/play-services-core/src/main/res/values-ga/strings.xml @@ -262,4 +262,7 @@ Is féidir leat Cloud Messaging a dhíchumasú nuair a bhíonn socrú an chuntais críochnaithe. Ceadaigh Cloud Messaging le haghaidh microG Críochnaigh + Íoslódáil sócmhainní breise nuair a iarrann apps a úsáideann Seachadadh Sócmhainní Súgartha + Cumasaigh seachadadh sócmhainní ar éileamh + Seachadadh Sócmhainní Google Play \ No newline at end of file diff --git a/vending-app/src/main/res/values-ga/strings.xml b/vending-app/src/main/res/values-ga/strings.xml index cdbf50124e..21fd1c76ed 100644 --- a/vending-app/src/main/res/values-ga/strings.xml +++ b/vending-app/src/main/res/values-ga/strings.xml @@ -19,4 +19,6 @@ Tá an pasfhocal a d\'iontráil tú mícheart. Cuimhnigh ar mo logáil isteach ar an ngléas seo Fíoraigh + Ag íosluchtú + Comhaid bhreise le haghaidh %s \ No newline at end of file From 2101b80a6f15a0cc0f32f41fb4096050f7adaf3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sveinn=20=C3=AD=20Felli?= Date: Mon, 9 Dec 2024 10:50:56 +0000 Subject: [PATCH 121/132] Translated using Weblate (Icelandic) Currently translated at 64.0% (32 of 50 strings) Translation: microG/play-services-location: core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-location-core-strings/is/ Translated using Weblate (Icelandic) Currently translated at 78.0% (39 of 50 strings) Translation: microG/play-services-location: core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-location-core-strings/is/ --- .../core/src/main/res/values-is/strings.xml | 42 ++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/play-services-location/core/src/main/res/values-is/strings.xml b/play-services-location/core/src/main/res/values-is/strings.xml index a6b3daec93..2afef667bd 100644 --- a/play-services-location/core/src/main/res/values-is/strings.xml +++ b/play-services-location/core/src/main/res/values-is/strings.xml @@ -1,2 +1,42 @@ - \ No newline at end of file + + Staðsetning + Nýlegur aðgangur + Wi-Fi staðsetning + Staðsetning farsímanets + Biðja um frá netþjónustu + Biðja um frá Mozilla + Biðja um frá netþjónustu + Tækið þitt mun þurfa að: + Nota GPS, Wi‑Fi, farsímanet og skynjara + Til að skoða nánar skaltu fara í staðsetningarstillingar. + Veldu staðsetningaþjónustu + Flytja staðsetningargögn inn úr skrá + Flutti inn %1$d færslur. + Uppfletting heimilisfanga + Biðja um frá Mozilla + Biðja um frá tengipunkti + Muna frá GPS + Muna frá GPS + Nota Nominatim + Forrit með aðgang að staðsetningu + Þvinga grófa staðsetningu + Virkja heimildir fyrir staðsetningu til microG-þjónustu + Nei takk + Í lagi + Stilla slóð þjónustu + Endurstilla + Sérsniðin slóð þjónustu + Sérsniðið + Skilmálar og meðferð persónuupplýsinga + Tillaga + Flytja inn/út staðsetningargögn + Staðfestingar krafist + Fletta upp heimilisföngum með OpenStreetMap Nominatim. + Síðasti aðgangur: %1$s + Til að hlutirnir virki betur, skaltu kveikja á staðsetningu tækisins, sem notar staðsetningaþjónustu microG + Flytja út gagnagrunn Wi-Fi-staðsetninga af tölvunni + Til að halda áfram, skaltu kveikja á staðsetningu tækisins, sem notar staðsetningaþjónustu microG + Til að halda áfram að nota staðsetningaþjónustur á netinu, þarftu að velja þjónustu fyrir staðsetningagögn. + Flytja út gagnagrunn endurvarpastaðsetninga af tölvunni + \ No newline at end of file From b09aaed2881200015c9738e53d39b2fa044c5c87 Mon Sep 17 00:00:00 2001 From: Leo Alvesson Date: Tue, 10 Dec 2024 10:08:04 +0000 Subject: [PATCH 122/132] Translated using Weblate (Spanish) Currently translated at 100.0% (4 of 4 strings) Translation: microG/play-services-ads-identifier: core: strings Translate-URL: https://hosted.weblate.org/projects/microg/play-services-ads-identifier-core-strings/es/ --- .../core/src/main/res/values-es/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/play-services-ads-identifier/core/src/main/res/values-es/strings.xml b/play-services-ads-identifier/core/src/main/res/values-es/strings.xml index 8f45e6af02..90c47f7266 100644 --- a/play-services-ads-identifier/core/src/main/res/values-es/strings.xml +++ b/play-services-ads-identifier/core/src/main/res/values-es/strings.xml @@ -3,5 +3,5 @@ Permiso de identificación publicitaria Permite que una aplicación de editor acceda directa o indirectamente a un ID de publicidad válido. Notificación del ID de publicidad - Permite que una aplicación reciba una notificación cuando se actualiza la preferencia de ID de publicidad o de seguimiento de anuncios del usuario. + Permite que una aplicación reciba una notificación cuando se actualiza el ID de publicidad o la preferencia de limitar el seguimiento de anuncios del usuario. \ No newline at end of file From 6a941cddfb7ebf2dae6d25399ebde1ce8a1c449d Mon Sep 17 00:00:00 2001 From: gallegonovato Date: Mon, 9 Dec 2024 20:31:18 +0000 Subject: [PATCH 123/132] Translated using Weblate (Spanish) Currently translated at 100.0% (21 of 21 strings) Translation: microG/vending-app Translate-URL: https://hosted.weblate.org/projects/microg/vending-app/es/ --- vending-app/src/main/res/values-es/strings.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/vending-app/src/main/res/values-es/strings.xml b/vending-app/src/main/res/values-es/strings.xml index 2807722f08..60f8e9cca2 100644 --- a/vending-app/src/main/res/values-es/strings.xml +++ b/vending-app/src/main/res/values-es/strings.xml @@ -19,4 +19,6 @@ Ingresa tu contraseña Recordar el inicio de sesión en este dispositivo Verificar + Archivos adicionales para %s + Descargando \ No newline at end of file From 2f90ea69976d19873f5ba31ed34c074d698066b3 Mon Sep 17 00:00:00 2001 From: DaVinci9196 <150454414+DaVinci9196@users.noreply.github.com> Date: Wed, 11 Dec 2024 02:28:08 +0800 Subject: [PATCH 124/132] Don't set install_referrer to the public store URL (#2622) Google doesn't set this either if the referrer is the store app itself. Some apps (Twitch) show confusing information if we do. --- .../finsky/externalreferrer/GetInstallReferrerService.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/vending-app/src/main/java/com/google/android/finsky/externalreferrer/GetInstallReferrerService.java b/vending-app/src/main/java/com/google/android/finsky/externalreferrer/GetInstallReferrerService.java index fd67908a0c..a59ad44f9f 100644 --- a/vending-app/src/main/java/com/google/android/finsky/externalreferrer/GetInstallReferrerService.java +++ b/vending-app/src/main/java/com/google/android/finsky/externalreferrer/GetInstallReferrerService.java @@ -18,9 +18,8 @@ public class GetInstallReferrerService extends Service { // https://developer.android.com/google/play/installreferrer/igetinstallreferrerservice @Override public Bundle getInstallReferrer(Bundle request) throws RemoteException { - String packageName = request.getString("package_name"); Bundle result = new Bundle(); - result.putString("install_referrer", "https://play.google.com/store/apps/details?utm_source=google-play&utm_medium=organic&id="+packageName); + result.putString("install_referrer", "utm_source=google-play&utm_medium=organic"); result.putLong("referrer_click_timestamp_seconds", 0); result.putLong("referrer_click_timestamp_server_seconds", 0); result.putLong("install_begin_timestamp_seconds", 0); From b1f0706a1b4f9502742a838337761bbb7d82d8d6 Mon Sep 17 00:00:00 2001 From: ale5000 <15793015+ale5000-git@users.noreply.github.com> Date: Tue, 10 Dec 2024 19:28:37 +0100 Subject: [PATCH 125/132] Bump GMS version to 24.47.35 (#2643) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 92339b13f2..07307bbf5c 100644 --- a/build.gradle +++ b/build.gradle @@ -68,7 +68,7 @@ def execResult(... args) { } def ignoreGit = providers.environmentVariable('GRADLE_MICROG_VERSION_WITHOUT_GIT').getOrElse('0') == '1' -def gmsVersion = "24.09.13" +def gmsVersion = "24.47.35" def gmsVersionCode = Integer.parseInt(gmsVersion.replaceAll('\\.', '')) def vendingVersion = "40.2.26" def vendingVersionCode = Integer.parseInt(vendingVersion.replaceAll('\\.', '')) From cc03a587b9becee5218ddfee111742b2168463ef Mon Sep 17 00:00:00 2001 From: ale5000 <15793015+ale5000-git@users.noreply.github.com> Date: Tue, 10 Dec 2024 19:29:05 +0100 Subject: [PATCH 126/132] Add Activity recognition permission (#2645) --- play-services-core/src/main/AndroidManifest.xml | 6 ++++++ play-services-core/src/main/res/values/permissions.xml | 3 +++ 2 files changed, 9 insertions(+) diff --git a/play-services-core/src/main/AndroidManifest.xml b/play-services-core/src/main/AndroidManifest.xml index 0c606960c5..0dbd7c07e2 100644 --- a/play-services-core/src/main/AndroidManifest.xml +++ b/play-services-core/src/main/AndroidManifest.xml @@ -61,6 +61,12 @@ android:label="@string/permission_service_writely_label" android:protectionLevel="dangerous" /> + + YouTube usernames Allows app to access YouTube username(s) used with any associated Google account. + Activity recognition + Allows an app to receive periodic updates of your activity level from Google, for example, if you are walking, driving, cycling, or stationary. + View the activity history of your Google Apps Manage your Ad Exchange buyer account configuration View your Ad Exchange data From b9b72785d3d13c4ecc0baaddb7bee08a909eeef9 Mon Sep 17 00:00:00 2001 From: Marvin W Date: Thu, 12 Dec 2024 11:48:01 +0100 Subject: [PATCH 127/132] Self-Check: Also verify that PackageInfo.signingInfo was spoofed. See https://github.com/microg/GmsCore/issues/2680#issuecomment-2538425189 --- .../org/microg/gms/common/PackageUtils.java | 59 +++++++++++++++++-- .../selfcheck/InstalledPackagesChecks.java | 3 +- .../selfcheck/RomSpoofSignatureChecks.java | 3 +- 3 files changed, 58 insertions(+), 7 deletions(-) diff --git a/play-services-base/core/src/main/java/org/microg/gms/common/PackageUtils.java b/play-services-base/core/src/main/java/org/microg/gms/common/PackageUtils.java index 328ebdc5a0..4fb60df084 100644 --- a/play-services-base/core/src/main/java/org/microg/gms/common/PackageUtils.java +++ b/play-services-base/core/src/main/java/org/microg/gms/common/PackageUtils.java @@ -94,7 +94,16 @@ public static void checkPackageUid(Context context, String packageName, int call @Deprecated @Nullable public static String firstSignatureDigest(Context context, String packageName) { - return firstSignatureDigest(context.getPackageManager(), packageName); + return firstSignatureDigest(context, packageName, false); + } + + /** + * @deprecated We should stop using SHA-1 for certificate fingerprints! + */ + @Deprecated + @Nullable + public static String firstSignatureDigest(Context context, String packageName, boolean useSigningInfo) { + return firstSignatureDigest(context.getPackageManager(), packageName, useSigningInfo); } /** @@ -103,13 +112,33 @@ public static String firstSignatureDigest(Context context, String packageName) { @Deprecated @Nullable public static String firstSignatureDigest(PackageManager packageManager, String packageName) { + return firstSignatureDigest(packageManager, packageName, false); + } + + /** + * @deprecated We should stop using SHA-1 for certificate fingerprints! + */ + @Deprecated + @Nullable + public static String firstSignatureDigest(PackageManager packageManager, String packageName, boolean useSigningInfo) { final PackageInfo info; try { - info = packageManager.getPackageInfo(packageName, PackageManager.GET_SIGNATURES); + info = packageManager.getPackageInfo(packageName, PackageManager.GET_SIGNATURES | (useSigningInfo && SDK_INT >= 28 ? PackageManager.GET_SIGNING_CERTIFICATES : 0)); } catch (PackageManager.NameNotFoundException e) { return null; } - if (info != null && info.signatures != null && info.signatures.length > 0) { + if (info == null) return null; + if (SDK_INT >= 28 && useSigningInfo && info.signingInfo != null) { + if (!info.signingInfo.hasMultipleSigners()) { + for (Signature sig : info.signingInfo.getSigningCertificateHistory()) { + String digest = sha1sum(sig.toByteArray()); + if (digest != null) { + return digest; + } + } + } + } + if (info.signatures != null) { for (Signature sig : info.signatures) { String digest = sha1sum(sig.toByteArray()); if (digest != null) { @@ -135,13 +164,33 @@ public static byte[] firstSignatureDigestBytes(Context context, String packageNa @Deprecated @Nullable public static byte[] firstSignatureDigestBytes(PackageManager packageManager, String packageName) { + return firstSignatureDigestBytes(packageManager, packageName, false); + } + + /** + * @deprecated We should stop using SHA-1 for certificate fingerprints! + */ + @Deprecated + @Nullable + public static byte[] firstSignatureDigestBytes(PackageManager packageManager, String packageName, boolean useSigningInfo) { final PackageInfo info; try { - info = packageManager.getPackageInfo(packageName, PackageManager.GET_SIGNATURES); + info = packageManager.getPackageInfo(packageName, PackageManager.GET_SIGNATURES | (useSigningInfo && SDK_INT >= 28 ? PackageManager.GET_SIGNING_CERTIFICATES : 0)); } catch (PackageManager.NameNotFoundException e) { return null; } - if (info != null && info.signatures != null && info.signatures.length > 0) { + if (info == null) return null; + if (SDK_INT >= 28 && useSigningInfo && info.signingInfo != null) { + if (!info.signingInfo.hasMultipleSigners()) { + for (Signature sig : info.signingInfo.getSigningCertificateHistory()) { + byte[] digest = sha1bytes(sig.toByteArray()); + if (digest != null) { + return digest; + } + } + } + } + if (info.signatures != null) { for (Signature sig : info.signatures) { byte[] digest = sha1bytes(sig.toByteArray()); if (digest != null) { diff --git a/play-services-core/src/main/java/org/microg/tools/selfcheck/InstalledPackagesChecks.java b/play-services-core/src/main/java/org/microg/tools/selfcheck/InstalledPackagesChecks.java index 7b685fb6a1..9f66a7bc2f 100644 --- a/play-services-core/src/main/java/org/microg/tools/selfcheck/InstalledPackagesChecks.java +++ b/play-services-core/src/main/java/org/microg/tools/selfcheck/InstalledPackagesChecks.java @@ -53,7 +53,8 @@ private void addPackageInstalledAndSignedResult(Context context, ResultCollector } private boolean addPackageSignedResult(Context context, ResultCollector collector, String nicePackageName, String androidPackageName, String signatureHash) { - boolean hashMatches = signatureHash.equals(PackageUtils.firstSignatureDigest(context, androidPackageName)); + boolean hashMatches = signatureHash.equals(PackageUtils.firstSignatureDigest(context, androidPackageName, true)) && + signatureHash.equals(PackageUtils.firstSignatureDigest(context, androidPackageName, false)); collector.addResult(context.getString(R.string.self_check_name_correct_sig, nicePackageName), hashMatches ? Positive : Negative, context.getString(R.string.self_check_resolution_correct_sig, nicePackageName), diff --git a/play-services-core/src/main/java/org/microg/tools/selfcheck/RomSpoofSignatureChecks.java b/play-services-core/src/main/java/org/microg/tools/selfcheck/RomSpoofSignatureChecks.java index 96d3f142d1..2e3ea64460 100644 --- a/play-services-core/src/main/java/org/microg/tools/selfcheck/RomSpoofSignatureChecks.java +++ b/play-services-core/src/main/java/org/microg/tools/selfcheck/RomSpoofSignatureChecks.java @@ -58,7 +58,8 @@ private boolean addSystemSpoofsSignature(Context context, ResultCollector collec if (knowsPermission) { grantsPermission = ContextCompat.checkSelfPermission(context, FAKE_SIGNATURE_PERMISSION) == PERMISSION_GRANTED; } - boolean spoofsSignature = GMS_PACKAGE_SIGNATURE_SHA1.equals(PackageUtils.firstSignatureDigest(context, Constants.GMS_PACKAGE_NAME)); + boolean spoofsSignature = GMS_PACKAGE_SIGNATURE_SHA1.equals(PackageUtils.firstSignatureDigest(context, Constants.GMS_PACKAGE_NAME, true)) && + GMS_PACKAGE_SIGNATURE_SHA1.equals(PackageUtils.firstSignatureDigest(context, Constants.GMS_PACKAGE_NAME, false)); if (knowsPermission && !spoofsSignature && !grantsPermission) { collector.addResult( context.getString(R.string.self_check_name_system_spoofs), From f0698a19cdd7d2466e33bf4ed4e272c3e197267a Mon Sep 17 00:00:00 2001 From: DaVinci9196 <150454414+DaVinci9196@users.noreply.github.com> Date: Sun, 15 Dec 2024 02:15:41 +0800 Subject: [PATCH 128/132] Fitness: Add FITNESS_CONFIG & FITNESS_SESSIONS dummy (#2600) Co-authored-by: Marvin W --- play-services-core/build.gradle | 1 + .../src/main/AndroidManifest.xml | 13 - .../org/microg/gms/signin/SignInService.kt | 10 +- play-services-fitness/build.gradle | 4 +- play-services-fitness/core/build.gradle | 50 ++ .../core/src/main/AndroidManifest.xml | 37 ++ .../fitness/service/config/FitConfigBroker.kt | 51 ++ .../service/history/FitHistoryBroker.kt | 4 + .../service/sessions/FitSessionsBroker.kt | 59 +++ .../fitness/internal/IDataTypeCallback.aidl | 12 + .../fitness/internal/IGoogleFitConfigApi.aidl | 16 + .../internal/IGoogleFitSessionsApi.aidl | 21 + .../internal/ISessionReadCallback.aidl | 11 + .../internal/ISessionStopCallback.aidl | 11 + .../gms/fitness/internal/IStatusCallback.aidl | 2 +- .../request/DataTypeCreateRequest.aidl | 8 + .../fitness/request/DisableFitRequest.aidl | 8 + .../fitness/request/ReadDataTypeRequest.aidl | 8 + .../fitness/request/SessionInsertRequest.aidl | 8 + .../fitness/request/SessionReadRequest.aidl | 8 + .../request/SessionRegistrationRequest.aidl | 8 + .../fitness/request/SessionStartRequest.aidl | 8 + .../fitness/request/SessionStopRequest.aidl | 8 + .../request/SessionUnregistrationRequest.aidl | 8 + .../gms/fitness/result/DataTypeResult.aidl | 8 + .../gms/fitness/result/SessionReadResult.aidl | 8 + .../gms/fitness/result/SessionStopResult.aidl | 8 + .../data/{AppInfo.java => Application.java} | 27 +- .../android/gms/fitness/data/Bucket.java | 121 ++++- .../android/gms/fitness/data/DataPoint.java | 394 ++++++++++++++ .../android/gms/fitness/data/DataSet.java | 186 ++++++- .../android/gms/fitness/data/DataSource.java | 267 +++++++++- .../android/gms/fitness/data/DataType.java | 225 +++++--- .../android/gms/fitness/data/Device.java | 159 +++++- .../android/gms/fitness/data/Field.java | 494 +++++++++++++++--- .../android/gms/fitness/data/MapValue.java | 60 +++ .../android/gms/fitness/data/Session.java | 188 +++++++ .../gms/fitness/data/SessionDataSet.java | 37 ++ .../android/gms/fitness/data/Value.java | 270 ++++++++++ .../request/DataTypeCreateRequest.java | 35 ++ .../fitness/request/DisableFitRequest.java | 29 + .../fitness/request/ReadDataTypeRequest.java | 31 ++ .../fitness/request/SessionInsertRequest.java | 42 ++ .../fitness/request/SessionReadRequest.java | 54 ++ .../request/SessionRegistrationRequest.java | 32 ++ .../fitness/request/SessionStartRequest.java | 32 ++ .../fitness/request/SessionStopRequest.java | 33 ++ .../request/SessionUnregistrationRequest.java | 31 ++ .../fitness/result/DataSourceStatsResult.java | 2 + .../gms/fitness/result/DataStatsResult.java | 2 + .../gms/fitness/result/DataTypeResult.java | 76 +++ .../gms/fitness/result/SessionReadResult.java | 133 +++++ .../gms/fitness/result/SessionStopResult.java | 74 +++ settings.gradle | 1 + 54 files changed, 3231 insertions(+), 202 deletions(-) create mode 100644 play-services-fitness/core/build.gradle create mode 100644 play-services-fitness/core/src/main/AndroidManifest.xml create mode 100644 play-services-fitness/core/src/main/kotlin/com/google/android/gms/fitness/service/config/FitConfigBroker.kt rename {play-services-core => play-services-fitness/core}/src/main/kotlin/com/google/android/gms/fitness/service/history/FitHistoryBroker.kt (93%) create mode 100644 play-services-fitness/core/src/main/kotlin/com/google/android/gms/fitness/service/sessions/FitSessionsBroker.kt create mode 100644 play-services-fitness/src/main/aidl/com/google/android/gms/fitness/internal/IDataTypeCallback.aidl create mode 100644 play-services-fitness/src/main/aidl/com/google/android/gms/fitness/internal/IGoogleFitConfigApi.aidl create mode 100644 play-services-fitness/src/main/aidl/com/google/android/gms/fitness/internal/IGoogleFitSessionsApi.aidl create mode 100644 play-services-fitness/src/main/aidl/com/google/android/gms/fitness/internal/ISessionReadCallback.aidl create mode 100644 play-services-fitness/src/main/aidl/com/google/android/gms/fitness/internal/ISessionStopCallback.aidl create mode 100644 play-services-fitness/src/main/aidl/com/google/android/gms/fitness/request/DataTypeCreateRequest.aidl create mode 100644 play-services-fitness/src/main/aidl/com/google/android/gms/fitness/request/DisableFitRequest.aidl create mode 100644 play-services-fitness/src/main/aidl/com/google/android/gms/fitness/request/ReadDataTypeRequest.aidl create mode 100644 play-services-fitness/src/main/aidl/com/google/android/gms/fitness/request/SessionInsertRequest.aidl create mode 100644 play-services-fitness/src/main/aidl/com/google/android/gms/fitness/request/SessionReadRequest.aidl create mode 100644 play-services-fitness/src/main/aidl/com/google/android/gms/fitness/request/SessionRegistrationRequest.aidl create mode 100644 play-services-fitness/src/main/aidl/com/google/android/gms/fitness/request/SessionStartRequest.aidl create mode 100644 play-services-fitness/src/main/aidl/com/google/android/gms/fitness/request/SessionStopRequest.aidl create mode 100644 play-services-fitness/src/main/aidl/com/google/android/gms/fitness/request/SessionUnregistrationRequest.aidl create mode 100644 play-services-fitness/src/main/aidl/com/google/android/gms/fitness/result/DataTypeResult.aidl create mode 100644 play-services-fitness/src/main/aidl/com/google/android/gms/fitness/result/SessionReadResult.aidl create mode 100644 play-services-fitness/src/main/aidl/com/google/android/gms/fitness/result/SessionStopResult.aidl rename play-services-fitness/src/main/java/com/google/android/gms/fitness/data/{AppInfo.java => Application.java} (53%) create mode 100644 play-services-fitness/src/main/java/com/google/android/gms/fitness/data/DataPoint.java create mode 100644 play-services-fitness/src/main/java/com/google/android/gms/fitness/data/MapValue.java create mode 100644 play-services-fitness/src/main/java/com/google/android/gms/fitness/data/SessionDataSet.java create mode 100644 play-services-fitness/src/main/java/com/google/android/gms/fitness/data/Value.java create mode 100644 play-services-fitness/src/main/java/com/google/android/gms/fitness/request/DataTypeCreateRequest.java create mode 100644 play-services-fitness/src/main/java/com/google/android/gms/fitness/request/DisableFitRequest.java create mode 100644 play-services-fitness/src/main/java/com/google/android/gms/fitness/request/ReadDataTypeRequest.java create mode 100644 play-services-fitness/src/main/java/com/google/android/gms/fitness/request/SessionInsertRequest.java create mode 100644 play-services-fitness/src/main/java/com/google/android/gms/fitness/request/SessionReadRequest.java create mode 100644 play-services-fitness/src/main/java/com/google/android/gms/fitness/request/SessionRegistrationRequest.java create mode 100644 play-services-fitness/src/main/java/com/google/android/gms/fitness/request/SessionStartRequest.java create mode 100644 play-services-fitness/src/main/java/com/google/android/gms/fitness/request/SessionStopRequest.java create mode 100644 play-services-fitness/src/main/java/com/google/android/gms/fitness/request/SessionUnregistrationRequest.java create mode 100644 play-services-fitness/src/main/java/com/google/android/gms/fitness/result/DataTypeResult.java create mode 100644 play-services-fitness/src/main/java/com/google/android/gms/fitness/result/SessionReadResult.java create mode 100644 play-services-fitness/src/main/java/com/google/android/gms/fitness/result/SessionStopResult.java diff --git a/play-services-core/build.gradle b/play-services-core/build.gradle index b776816c2b..793795e4ac 100644 --- a/play-services-core/build.gradle +++ b/play-services-core/build.gradle @@ -33,6 +33,7 @@ dependencies { implementation project(':play-services-cronet-core') implementation project(':play-services-droidguard-core') implementation project(':play-services-fido-core') + implementation project(':play-services-fitness-core') implementation project(':play-services-gmscompliance-core') implementation project(':play-services-location-core') implementation project(':play-services-location-core-base') diff --git a/play-services-core/src/main/AndroidManifest.xml b/play-services-core/src/main/AndroidManifest.xml index 0dbd7c07e2..b9d69cb41a 100644 --- a/play-services-core/src/main/AndroidManifest.xml +++ b/play-services-core/src/main/AndroidManifest.xml @@ -831,17 +831,6 @@ - - - - - - - - - - diff --git a/play-services-core/src/main/kotlin/org/microg/gms/signin/SignInService.kt b/play-services-core/src/main/kotlin/org/microg/gms/signin/SignInService.kt index 14bad51a23..b6368ab7b7 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/signin/SignInService.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/signin/SignInService.kt @@ -11,7 +11,6 @@ import android.content.Context import android.os.Bundle import android.os.Parcel import android.util.Log -import androidx.core.content.getSystemService import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner import com.google.android.gms.common.ConnectionResult @@ -20,6 +19,7 @@ import com.google.android.gms.common.api.Scope import com.google.android.gms.common.internal.* import com.google.android.gms.signin.internal.* import org.microg.gms.BaseService +import org.microg.gms.auth.AuthConstants import org.microg.gms.common.GmsService import org.microg.gms.common.PackageUtils import org.microg.gms.utils.warnOnTransactionIssues @@ -60,10 +60,12 @@ class SignInServiceImpl(val context: Context, override val lifecycle: Lifecycle, override fun signIn(request: SignInRequest?, callbacks: ISignInCallbacks?) { Log.d(TAG, "signIn($request)") - val account = request?.request?.account - val result = if (account == null || context.getSystemService()?.getAccountsByType(account.type)?.contains(account) != true) - ConnectionResult(ConnectionResult.SIGN_IN_REQUIRED) else ConnectionResult(ConnectionResult.SUCCESS) runCatching { + val accountManager = AccountManager.get(context) + val account = request?.request?.account?.let { if (it.name == AuthConstants.DEFAULT_ACCOUNT) accountManager.getAccountsByType(it.type).firstOrNull() else it } + val result = if (account == null || !accountManager.getAccountsByType(account.type).contains(account)) + ConnectionResult(ConnectionResult.SIGN_IN_REQUIRED) else ConnectionResult(ConnectionResult.SUCCESS) + Log.d(TAG, "signIn: account -> ${account?.name}") callbacks?.onSignIn(SignInResponse().apply { connectionResult = result response = ResolveAccountResponse().apply { diff --git a/play-services-fitness/build.gradle b/play-services-fitness/build.gradle index 600a0d69f2..54ad5c74f5 100644 --- a/play-services-fitness/build.gradle +++ b/play-services-fitness/build.gradle @@ -26,9 +26,11 @@ android { } dependencies { + // Dependencies from play-services-fitness:21.2.0 + api 'androidx.collection:collection:1.0.0' api project(':play-services-base') - api project(':play-services-base-core') api project(':play-services-basement') + api project(':play-services-tasks') annotationProcessor project(':safe-parcel-processor') } \ No newline at end of file diff --git a/play-services-fitness/core/build.gradle b/play-services-fitness/core/build.gradle new file mode 100644 index 0000000000..c58d517f73 --- /dev/null +++ b/play-services-fitness/core/build.gradle @@ -0,0 +1,50 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' +apply plugin: 'maven-publish' +apply plugin: 'signing' + +dependencies { + api project(':play-services-fitness') + + implementation project(':play-services-base-core') +} + +android { + namespace "org.microg.gms.fitness.core" + + compileSdkVersion androidCompileSdk + buildToolsVersion "$androidBuildVersionTools" + + defaultConfig { + versionName version + minSdkVersion androidMinSdk + targetSdkVersion androidTargetSdk + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + lintOptions { + disable 'MissingTranslation', 'GetLocales' + } + + compileOptions { + sourceCompatibility = 1.8 + targetCompatibility = 1.8 + } + + kotlinOptions { + jvmTarget = 1.8 + } +} + +// Not publishable yet +// apply from: '../../gradle/publish-android.gradle' + +description = 'microG service implementation for play-services-fitness' \ No newline at end of file diff --git a/play-services-fitness/core/src/main/AndroidManifest.xml b/play-services-fitness/core/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..161741daa0 --- /dev/null +++ b/play-services-fitness/core/src/main/AndroidManifest.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/play-services-fitness/core/src/main/kotlin/com/google/android/gms/fitness/service/config/FitConfigBroker.kt b/play-services-fitness/core/src/main/kotlin/com/google/android/gms/fitness/service/config/FitConfigBroker.kt new file mode 100644 index 0000000000..99cb3819c2 --- /dev/null +++ b/play-services-fitness/core/src/main/kotlin/com/google/android/gms/fitness/service/config/FitConfigBroker.kt @@ -0,0 +1,51 @@ +/** + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.fitness.service.config + +import android.os.Parcel +import android.util.Log +import com.google.android.gms.common.api.CommonStatusCodes +import com.google.android.gms.common.api.Status +import com.google.android.gms.common.internal.GetServiceRequest +import com.google.android.gms.common.internal.IGmsCallbacks +import com.google.android.gms.fitness.internal.IGoogleFitConfigApi +import com.google.android.gms.fitness.request.DataTypeCreateRequest +import com.google.android.gms.fitness.request.DisableFitRequest +import com.google.android.gms.fitness.request.ReadDataTypeRequest +import org.microg.gms.BaseService +import org.microg.gms.common.GmsService +import org.microg.gms.utils.warnOnTransactionIssues + +private const val TAG = "FitConfigBroker" + +class FitConfigBroker : BaseService(TAG, GmsService.FITNESS_CONFIG) { + override fun handleServiceRequest(callback: IGmsCallbacks, request: GetServiceRequest, service: GmsService) { + callback.onPostInitComplete(CommonStatusCodes.SUCCESS, FitConfigBrokerImpl(), null) + } +} + +class FitConfigBrokerImpl : IGoogleFitConfigApi.Stub() { + + override fun createCustomDataType(request: DataTypeCreateRequest?) { + Log.d(TAG, "Not implemented createCustomDataType: $request") + } + + override fun readDataType(request: ReadDataTypeRequest?) { + Log.d(TAG, "Not implemented readDataType: $request") + } + + override fun disableFit(request: DisableFitRequest?) { + Log.d(TAG, "Method Called: $request") + try { + request?.callback?.onResult(Status.SUCCESS) + } catch (e: Exception) { + Log.w(TAG, "disableFit Error $e") + } + } + + override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean = + warnOnTransactionIssues(code, reply, flags, TAG) { super.onTransact(code, data, reply, flags) } +} \ No newline at end of file diff --git a/play-services-core/src/main/kotlin/com/google/android/gms/fitness/service/history/FitHistoryBroker.kt b/play-services-fitness/core/src/main/kotlin/com/google/android/gms/fitness/service/history/FitHistoryBroker.kt similarity index 93% rename from play-services-core/src/main/kotlin/com/google/android/gms/fitness/service/history/FitHistoryBroker.kt rename to play-services-fitness/core/src/main/kotlin/com/google/android/gms/fitness/service/history/FitHistoryBroker.kt index 1fefa2c219..349f48b395 100644 --- a/play-services-core/src/main/kotlin/com/google/android/gms/fitness/service/history/FitHistoryBroker.kt +++ b/play-services-fitness/core/src/main/kotlin/com/google/android/gms/fitness/service/history/FitHistoryBroker.kt @@ -6,6 +6,7 @@ package com.google.android.gms.fitness.service.history import android.os.Bundle +import android.os.Parcel import android.util.Log import com.google.android.gms.common.api.CommonStatusCodes import com.google.android.gms.common.internal.GetServiceRequest @@ -27,6 +28,7 @@ import com.google.android.gms.fitness.request.ReadStatsRequest import com.google.android.gms.fitness.request.SessionChangesRequest import org.microg.gms.BaseService import org.microg.gms.common.GmsService +import org.microg.gms.utils.warnOnTransactionIssues private const val TAG = "FitHistoryBroker" @@ -98,4 +100,6 @@ class FitHistoryBrokerImpl : IGoogleFitHistoryApi.Stub() { Log.d(TAG, "Not implemented getSessionChanges: $request") } + override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean = + warnOnTransactionIssues(code, reply, flags, TAG) { super.onTransact(code, data, reply, flags) } } diff --git a/play-services-fitness/core/src/main/kotlin/com/google/android/gms/fitness/service/sessions/FitSessionsBroker.kt b/play-services-fitness/core/src/main/kotlin/com/google/android/gms/fitness/service/sessions/FitSessionsBroker.kt new file mode 100644 index 0000000000..987801c12d --- /dev/null +++ b/play-services-fitness/core/src/main/kotlin/com/google/android/gms/fitness/service/sessions/FitSessionsBroker.kt @@ -0,0 +1,59 @@ +/** + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.fitness.service.sessions; + +import android.os.Parcel +import android.util.Log +import com.google.android.gms.common.api.CommonStatusCodes +import com.google.android.gms.common.internal.GetServiceRequest +import com.google.android.gms.common.internal.IGmsCallbacks +import com.google.android.gms.fitness.internal.IGoogleFitSessionsApi +import com.google.android.gms.fitness.request.SessionInsertRequest +import com.google.android.gms.fitness.request.SessionReadRequest +import com.google.android.gms.fitness.request.SessionRegistrationRequest +import com.google.android.gms.fitness.request.SessionStartRequest +import com.google.android.gms.fitness.request.SessionStopRequest +import com.google.android.gms.fitness.request.SessionUnregistrationRequest +import org.microg.gms.BaseService +import org.microg.gms.common.GmsService +import org.microg.gms.utils.warnOnTransactionIssues + +private const val TAG = "FitSessionsBroker" + +class FitSessionsBroker : BaseService(TAG, GmsService.FITNESS_SESSIONS) { + override fun handleServiceRequest(callback: IGmsCallbacks, request: GetServiceRequest, service: GmsService) { + callback.onPostInitComplete(CommonStatusCodes.SUCCESS, FitSessionsBrokerImpl(), null) + } +} + +class FitSessionsBrokerImpl : IGoogleFitSessionsApi.Stub() { + override fun startRequest(startRequest: SessionStartRequest?) { + Log.d(TAG, "Not implemented startRequest: $startRequest") + } + + override fun stopRequest(stopRequest: SessionStopRequest?) { + Log.d(TAG, "Not implemented stopRequest: $stopRequest") + } + + override fun insertRequest(insetRequest: SessionInsertRequest?) { + Log.d(TAG, "Not implemented insertRequest: $insetRequest") + } + + override fun readRequest(readRequest: SessionReadRequest?) { + Log.d(TAG, "Not implemented readRequest: $readRequest") + } + + override fun registrationRequest(registrationRequest: SessionRegistrationRequest?) { + Log.d(TAG, "Not implemented registrationRequest: $registrationRequest") + } + + override fun unRegistrationRequest(unRegistrationRequest: SessionUnregistrationRequest?) { + Log.d(TAG, "Not implemented unRegistrationRequest: $unRegistrationRequest") + } + + override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean = + warnOnTransactionIssues(code, reply, flags, TAG) { super.onTransact(code, data, reply, flags) } +} \ No newline at end of file diff --git a/play-services-fitness/src/main/aidl/com/google/android/gms/fitness/internal/IDataTypeCallback.aidl b/play-services-fitness/src/main/aidl/com/google/android/gms/fitness/internal/IDataTypeCallback.aidl new file mode 100644 index 0000000000..f1b9995c6e --- /dev/null +++ b/play-services-fitness/src/main/aidl/com/google/android/gms/fitness/internal/IDataTypeCallback.aidl @@ -0,0 +1,12 @@ +/** + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.fitness.internal; + +import com.google.android.gms.fitness.result.DataTypeResult; + +interface IDataTypeCallback { + void onDataType(in DataTypeResult dataTypeResult) = 0; +} \ No newline at end of file diff --git a/play-services-fitness/src/main/aidl/com/google/android/gms/fitness/internal/IGoogleFitConfigApi.aidl b/play-services-fitness/src/main/aidl/com/google/android/gms/fitness/internal/IGoogleFitConfigApi.aidl new file mode 100644 index 0000000000..2ab86aa9e9 --- /dev/null +++ b/play-services-fitness/src/main/aidl/com/google/android/gms/fitness/internal/IGoogleFitConfigApi.aidl @@ -0,0 +1,16 @@ +/** + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.fitness.internal; + +import com.google.android.gms.fitness.request.DataTypeCreateRequest; +import com.google.android.gms.fitness.request.DisableFitRequest; +import com.google.android.gms.fitness.request.ReadDataTypeRequest; + +interface IGoogleFitConfigApi { + void createCustomDataType(in DataTypeCreateRequest request) = 0; + void readDataType(in ReadDataTypeRequest request) = 1; + void disableFit(in DisableFitRequest request) = 21; +} diff --git a/play-services-fitness/src/main/aidl/com/google/android/gms/fitness/internal/IGoogleFitSessionsApi.aidl b/play-services-fitness/src/main/aidl/com/google/android/gms/fitness/internal/IGoogleFitSessionsApi.aidl new file mode 100644 index 0000000000..45433abc56 --- /dev/null +++ b/play-services-fitness/src/main/aidl/com/google/android/gms/fitness/internal/IGoogleFitSessionsApi.aidl @@ -0,0 +1,21 @@ +/** + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ +package com.google.android.gms.fitness.internal; + +import com.google.android.gms.fitness.request.SessionStartRequest; +import com.google.android.gms.fitness.request.SessionStopRequest; +import com.google.android.gms.fitness.request.SessionInsertRequest; +import com.google.android.gms.fitness.request.SessionReadRequest; +import com.google.android.gms.fitness.request.SessionRegistrationRequest; +import com.google.android.gms.fitness.request.SessionUnregistrationRequest; + +interface IGoogleFitSessionsApi { + void startRequest(in SessionStartRequest startRequest) = 0; + void stopRequest(in SessionStopRequest stopRequest) = 1; + void insertRequest(in SessionInsertRequest insetRequest) = 2; + void readRequest(in SessionReadRequest readRequest) = 3; + void registrationRequest(in SessionRegistrationRequest registrationRequest) = 4; + void unRegistrationRequest(in SessionUnregistrationRequest unRegistrationRequest) = 5; +} \ No newline at end of file diff --git a/play-services-fitness/src/main/aidl/com/google/android/gms/fitness/internal/ISessionReadCallback.aidl b/play-services-fitness/src/main/aidl/com/google/android/gms/fitness/internal/ISessionReadCallback.aidl new file mode 100644 index 0000000000..eb85d8b370 --- /dev/null +++ b/play-services-fitness/src/main/aidl/com/google/android/gms/fitness/internal/ISessionReadCallback.aidl @@ -0,0 +1,11 @@ +/** + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ +package com.google.android.gms.fitness.internal; + +import com.google.android.gms.fitness.result.SessionReadResult; + +interface ISessionReadCallback { + void onResult(in SessionReadResult sessionReadResult) = 0; +} \ No newline at end of file diff --git a/play-services-fitness/src/main/aidl/com/google/android/gms/fitness/internal/ISessionStopCallback.aidl b/play-services-fitness/src/main/aidl/com/google/android/gms/fitness/internal/ISessionStopCallback.aidl new file mode 100644 index 0000000000..23a925432b --- /dev/null +++ b/play-services-fitness/src/main/aidl/com/google/android/gms/fitness/internal/ISessionStopCallback.aidl @@ -0,0 +1,11 @@ +/** + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ +package com.google.android.gms.fitness.internal; + +import com.google.android.gms.fitness.result.SessionStopResult; + +interface ISessionStopCallback { + void onResult(in SessionStopResult sessionStopReult) = 0; +} \ No newline at end of file diff --git a/play-services-fitness/src/main/aidl/com/google/android/gms/fitness/internal/IStatusCallback.aidl b/play-services-fitness/src/main/aidl/com/google/android/gms/fitness/internal/IStatusCallback.aidl index 99544d738d..63e0483bd4 100644 --- a/play-services-fitness/src/main/aidl/com/google/android/gms/fitness/internal/IStatusCallback.aidl +++ b/play-services-fitness/src/main/aidl/com/google/android/gms/fitness/internal/IStatusCallback.aidl @@ -8,5 +8,5 @@ package com.google.android.gms.fitness.internal; import com.google.android.gms.common.api.Status; interface IStatusCallback { - void onResult(in Status status) = 1; + void onResult(in Status status) = 0; } \ No newline at end of file diff --git a/play-services-fitness/src/main/aidl/com/google/android/gms/fitness/request/DataTypeCreateRequest.aidl b/play-services-fitness/src/main/aidl/com/google/android/gms/fitness/request/DataTypeCreateRequest.aidl new file mode 100644 index 0000000000..c786ea07f7 --- /dev/null +++ b/play-services-fitness/src/main/aidl/com/google/android/gms/fitness/request/DataTypeCreateRequest.aidl @@ -0,0 +1,8 @@ +/** + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.fitness.request; + +parcelable DataTypeCreateRequest; \ No newline at end of file diff --git a/play-services-fitness/src/main/aidl/com/google/android/gms/fitness/request/DisableFitRequest.aidl b/play-services-fitness/src/main/aidl/com/google/android/gms/fitness/request/DisableFitRequest.aidl new file mode 100644 index 0000000000..5011dad1bf --- /dev/null +++ b/play-services-fitness/src/main/aidl/com/google/android/gms/fitness/request/DisableFitRequest.aidl @@ -0,0 +1,8 @@ +/** + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.fitness.request; + +parcelable DisableFitRequest; \ No newline at end of file diff --git a/play-services-fitness/src/main/aidl/com/google/android/gms/fitness/request/ReadDataTypeRequest.aidl b/play-services-fitness/src/main/aidl/com/google/android/gms/fitness/request/ReadDataTypeRequest.aidl new file mode 100644 index 0000000000..774883a504 --- /dev/null +++ b/play-services-fitness/src/main/aidl/com/google/android/gms/fitness/request/ReadDataTypeRequest.aidl @@ -0,0 +1,8 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.fitness.request; + +parcelable ReadDataTypeRequest; \ No newline at end of file diff --git a/play-services-fitness/src/main/aidl/com/google/android/gms/fitness/request/SessionInsertRequest.aidl b/play-services-fitness/src/main/aidl/com/google/android/gms/fitness/request/SessionInsertRequest.aidl new file mode 100644 index 0000000000..62c1211ef2 --- /dev/null +++ b/play-services-fitness/src/main/aidl/com/google/android/gms/fitness/request/SessionInsertRequest.aidl @@ -0,0 +1,8 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.fitness.request; + +parcelable SessionInsertRequest; \ No newline at end of file diff --git a/play-services-fitness/src/main/aidl/com/google/android/gms/fitness/request/SessionReadRequest.aidl b/play-services-fitness/src/main/aidl/com/google/android/gms/fitness/request/SessionReadRequest.aidl new file mode 100644 index 0000000000..e419c939a1 --- /dev/null +++ b/play-services-fitness/src/main/aidl/com/google/android/gms/fitness/request/SessionReadRequest.aidl @@ -0,0 +1,8 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.fitness.request; + +parcelable SessionReadRequest; \ No newline at end of file diff --git a/play-services-fitness/src/main/aidl/com/google/android/gms/fitness/request/SessionRegistrationRequest.aidl b/play-services-fitness/src/main/aidl/com/google/android/gms/fitness/request/SessionRegistrationRequest.aidl new file mode 100644 index 0000000000..4163a42c1c --- /dev/null +++ b/play-services-fitness/src/main/aidl/com/google/android/gms/fitness/request/SessionRegistrationRequest.aidl @@ -0,0 +1,8 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.fitness.request; + +parcelable SessionRegistrationRequest; \ No newline at end of file diff --git a/play-services-fitness/src/main/aidl/com/google/android/gms/fitness/request/SessionStartRequest.aidl b/play-services-fitness/src/main/aidl/com/google/android/gms/fitness/request/SessionStartRequest.aidl new file mode 100644 index 0000000000..d56ebc2a64 --- /dev/null +++ b/play-services-fitness/src/main/aidl/com/google/android/gms/fitness/request/SessionStartRequest.aidl @@ -0,0 +1,8 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.fitness.request; + +parcelable SessionStartRequest; \ No newline at end of file diff --git a/play-services-fitness/src/main/aidl/com/google/android/gms/fitness/request/SessionStopRequest.aidl b/play-services-fitness/src/main/aidl/com/google/android/gms/fitness/request/SessionStopRequest.aidl new file mode 100644 index 0000000000..bf428185c9 --- /dev/null +++ b/play-services-fitness/src/main/aidl/com/google/android/gms/fitness/request/SessionStopRequest.aidl @@ -0,0 +1,8 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.fitness.request; + +parcelable SessionStopRequest; \ No newline at end of file diff --git a/play-services-fitness/src/main/aidl/com/google/android/gms/fitness/request/SessionUnregistrationRequest.aidl b/play-services-fitness/src/main/aidl/com/google/android/gms/fitness/request/SessionUnregistrationRequest.aidl new file mode 100644 index 0000000000..2b14eb5c90 --- /dev/null +++ b/play-services-fitness/src/main/aidl/com/google/android/gms/fitness/request/SessionUnregistrationRequest.aidl @@ -0,0 +1,8 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.fitness.request; + +parcelable SessionUnregistrationRequest; \ No newline at end of file diff --git a/play-services-fitness/src/main/aidl/com/google/android/gms/fitness/result/DataTypeResult.aidl b/play-services-fitness/src/main/aidl/com/google/android/gms/fitness/result/DataTypeResult.aidl new file mode 100644 index 0000000000..9d5a0b4837 --- /dev/null +++ b/play-services-fitness/src/main/aidl/com/google/android/gms/fitness/result/DataTypeResult.aidl @@ -0,0 +1,8 @@ +/** + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.fitness.result; + +parcelable DataTypeResult; \ No newline at end of file diff --git a/play-services-fitness/src/main/aidl/com/google/android/gms/fitness/result/SessionReadResult.aidl b/play-services-fitness/src/main/aidl/com/google/android/gms/fitness/result/SessionReadResult.aidl new file mode 100644 index 0000000000..b0b4c0fd0b --- /dev/null +++ b/play-services-fitness/src/main/aidl/com/google/android/gms/fitness/result/SessionReadResult.aidl @@ -0,0 +1,8 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.fitness.result; + +parcelable SessionReadResult; \ No newline at end of file diff --git a/play-services-fitness/src/main/aidl/com/google/android/gms/fitness/result/SessionStopResult.aidl b/play-services-fitness/src/main/aidl/com/google/android/gms/fitness/result/SessionStopResult.aidl new file mode 100644 index 0000000000..eabd31d4d9 --- /dev/null +++ b/play-services-fitness/src/main/aidl/com/google/android/gms/fitness/result/SessionStopResult.aidl @@ -0,0 +1,8 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.fitness.result; + +parcelable SessionStopResult; \ No newline at end of file diff --git a/play-services-fitness/src/main/java/com/google/android/gms/fitness/data/AppInfo.java b/play-services-fitness/src/main/java/com/google/android/gms/fitness/data/Application.java similarity index 53% rename from play-services-fitness/src/main/java/com/google/android/gms/fitness/data/AppInfo.java rename to play-services-fitness/src/main/java/com/google/android/gms/fitness/data/Application.java index b4dea5d67d..cff0c6206f 100644 --- a/play-services-fitness/src/main/java/com/google/android/gms/fitness/data/AppInfo.java +++ b/play-services-fitness/src/main/java/com/google/android/gms/fitness/data/Application.java @@ -13,29 +13,34 @@ import com.google.android.gms.common.internal.safeparcel.SafeParcelable; import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter; +import org.microg.gms.common.Constants; +import org.microg.gms.common.Hide; import org.microg.gms.utils.ToStringHelper; @SafeParcelable.Class -public class AppInfo extends AbstractSafeParcelable { +@Hide +public class Application extends AbstractSafeParcelable { - public static final AppInfo DEFAULT = new AppInfo("com.google.android.gms"); + public static final Application GMS_APP = new Application(Constants.GMS_PACKAGE_NAME); - @Field(1) - public String packageName; + @Field(value = 1, getterName = "getPackageName") + @NonNull + private final String packageName; - public AppInfo() { + @Constructor + public Application(@Param(1) @NonNull String packageName) { + this.packageName = packageName; } - public AppInfo(String packageName) { - this.packageName = packageName; + @NonNull + public String getPackageName() { + return packageName; } @NonNull @Override public String toString() { - return ToStringHelper.name("AppInfo") - .field("packageName", packageName) - .end(); + return ToStringHelper.name("Application").value(packageName).end(); } @Override @@ -43,6 +48,6 @@ public void writeToParcel(@NonNull Parcel dest, int flags) { CREATOR.writeToParcel(this, dest, flags); } - public static final SafeParcelableCreatorAndWriter CREATOR = findCreator(AppInfo.class); + public static final SafeParcelableCreatorAndWriter CREATOR = findCreator(Application.class); } diff --git a/play-services-fitness/src/main/java/com/google/android/gms/fitness/data/Bucket.java b/play-services-fitness/src/main/java/com/google/android/gms/fitness/data/Bucket.java index 1eaf0c49e7..a2cc6f33ee 100644 --- a/play-services-fitness/src/main/java/com/google/android/gms/fitness/data/Bucket.java +++ b/play-services-fitness/src/main/java/com/google/android/gms/fitness/data/Bucket.java @@ -1,6 +1,9 @@ /* * SPDX-FileCopyrightText: 2023 microG Project Team * SPDX-License-Identifier: Apache-2.0 + * Notice: Portions of this file are reproduced from work created and shared by Google and used + * according to terms described in the Creative Commons 4.0 Attribution License. + * See https://developers.google.com/readme/policies for details. */ package com.google.android.gms.fitness.data; @@ -9,6 +12,7 @@ import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import com.google.android.gms.common.internal.safeparcel.AbstractSafeParcelable; import com.google.android.gms.common.internal.safeparcel.SafeParcelable; import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter; @@ -16,27 +20,120 @@ import org.microg.gms.utils.ToStringHelper; import java.util.List; +import java.util.concurrent.TimeUnit; @SafeParcelable.Class public class Bucket extends AbstractSafeParcelable { + /** + * Type constant denoting that bucketing by time is requested. + */ public static final int TYPE_TIME = 1; + /** + * Type constant denoting that bucketing by session is requested. + */ public static final int TYPE_SESSION = 2; + /** + * Type constant denoting that bucketing by activity type is requested. + */ public static final int TYPE_ACTIVITY_TYPE = 3; + /** + * Type constant denoting that bucketing by individual activity segment is requested. + */ public static final int TYPE_ACTIVITY_SEGMENT = 4; - @Field(1) - public long startTimeMillis; - @Field(2) - public long endTimeMillis; - @Field(3) - public Session session; - @Field(4) - public int activityType; - @Field(5) - public List dataSets; - @Field(6) - public int bucketType; + @Field(value = 1, getterName = "getStartTimeMillis") + private final long startTimeMillis; + @Field(value = 2, getterName = "getEndTimeMillis") + private final long endTimeMillis; + @Field(value = 3, getterName = "getSession") + @Nullable + private final Session session; + @Field(value = 4, getterName = "getActivityType") + private final int activityType; + @Field(value = 5, getterName = "getDataSets") + private final List dataSets; + @Field(value = 6, getterName = "getBucketType") + private final int bucketType; + + @Constructor + public Bucket(@Param(1) long startTimeMillis, @Param(2) long endTimeMillis, @Nullable @Param(3) Session session, @Param(4) int activityType, @Param(5) List dataSets, @Param(6) int bucketType) { + this.startTimeMillis = startTimeMillis; + this.endTimeMillis = endTimeMillis; + this.session = session; + this.activityType = activityType; + this.dataSets = dataSets; + this.bucketType = bucketType; + } + + /** + * Returns the activity of the bucket if bucketing by activity was requested, or {@link FitnessActivities#UNKNOWN} otherwise. + */ + @NonNull + public String getActivity() { + // TODO + return null; + } + + /** + * Returns the type of the bucket. + */ + public int getBucketType() { + return bucketType; + } + + /** + * Returns the data set of requested data type over the time interval of the bucket. Returns null, if data set for the requested type is not found. + */ + public DataSet getDataSet(@NonNull DataType dataType) { + for (DataSet dataSet : this.dataSets) { + if (dataSet.getDataType().equals(dataType)) { + return dataSet; + } + } + return null; + } + + /** + * Returns the requested data sets over the time interval of the bucket. + */ + public List getDataSets() { + return dataSets; + } + + /** + * Returns the end time of the bucket, in the given time unit since epoch. + */ + public long getEndTime(TimeUnit timeUnit) { + return timeUnit.convert(this.endTimeMillis, TimeUnit.MILLISECONDS); + } + + /** + * Returns the session of the bucket if bucketing by session was requested, {@code null} otherwise. + */ + @Nullable + public Session getSession() { + return session; + } + + /** + * Returns the start time of the bucket, in the given time unit since epoch. + */ + public long getStartTime(@NonNull TimeUnit timeUnit) { + return timeUnit.convert(this.startTimeMillis, TimeUnit.MILLISECONDS); + } + + int getActivityType() { + return activityType; + } + + long getEndTimeMillis() { + return endTimeMillis; + } + + long getStartTimeMillis() { + return startTimeMillis; + } @Override public void writeToParcel(@NonNull Parcel dest, int flags) { diff --git a/play-services-fitness/src/main/java/com/google/android/gms/fitness/data/DataPoint.java b/play-services-fitness/src/main/java/com/google/android/gms/fitness/data/DataPoint.java new file mode 100644 index 0000000000..573815313c --- /dev/null +++ b/play-services-fitness/src/main/java/com/google/android/gms/fitness/data/DataPoint.java @@ -0,0 +1,394 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + * Notice: Portions of this file are reproduced from work created and shared by Google and used + * according to terms described in the Creative Commons 4.0 Attribution License. + * See https://developers.google.com/readme/policies for details. + */ + +package com.google.android.gms.fitness.data; + +import android.content.Intent; +import android.os.Parcel; + +import androidx.annotation.NonNull; + +import androidx.annotation.Nullable; +import com.google.android.gms.common.internal.safeparcel.AbstractSafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter; +import com.google.android.gms.common.internal.safeparcel.SafeParcelableSerializer; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +@SafeParcelable.Class +public class DataPoint extends AbstractSafeParcelable { + @Field(value = 1, getterName = "getDataSource") + @NonNull + private final DataSource dataSource; + @Field(value = 3, getterName = "getTimestampNanos") + private long timestampNanos; + @Field(value = 4, getterName = "getStartTimeNanos") + private long startTimeNanos; + @Field(value = 5, getterName = "getValues") + private final Value[] values; + @Field(value = 6, getterName = "getOriginalDataSourceIfSet") + @Nullable + private DataSource originalDataSource; + @Field(value = 7, getterName = "getRawTimestamp") + private final long rawTimestamp; + + DataPoint(DataSource dataSource) { + this.dataSource = dataSource; + List fields = dataSource.getDataType().getFields(); + this.values = new Value[fields.size()]; + for (int i = 0; i < fields.size(); i++) { + values[i] = new Value(fields.get(i).getFormat()); + } + this.rawTimestamp = 0; + } + + @Constructor + DataPoint(@Param(1) @NonNull DataSource dataSource, @Param(3) long timestampNanos, @Param(4) long startTimeNanos, @Param(5) Value[] values, @Param(6) @Nullable DataSource originalDataSource, @Param(7) long rawTimestamp) { + this.dataSource = dataSource; + this.timestampNanos = timestampNanos; + this.startTimeNanos = startTimeNanos; + this.values = values; + this.originalDataSource = originalDataSource; + this.rawTimestamp = rawTimestamp; + } + + /** + * Returns the data source for the data point. If the data point is part of a {@link DataSet}, this will correspond to the data set's data source. + */ + @NonNull + public DataSource getDataSource() { + return dataSource; + } + + /** + * Returns the data type defining the format of the values in this data point. + */ + @NonNull + public DataType getDataType() { + return dataSource.getDataType(); + } + + /** + * Returns the end time of the interval represented by this data point, in the given unit since epoch. This method is equivalent to + * {@link #getTimestamp(TimeUnit)} + */ + public long getEndTime(@NonNull TimeUnit timeUnit) { + return timeUnit.convert(this.timestampNanos, TimeUnit.NANOSECONDS); + } + + /** + * Returns the original data source for this data point. The original data source helps identify the source of the data point as it gets merged and + * transformed into different streams. + *

+ * Note that, if this data point is part of a {@link DataSet}, the data source returned here may be different from the data set's data source. In case of + * transformed or merged data sets, each data point's original data source will retain the original attribution as much as possible, while the + * data set's data source will represent the merged or transformed stream. + *

+ * WARNING: do not rely on this field for anything other than debugging. The value of this field, if it is set at all, is an implementation detail and + * is not guaranteed to remain consistent. + */ + @NonNull + public DataSource getOriginalDataSource() { + if (originalDataSource != null) return originalDataSource; + return dataSource; + } + + /** + * Returns the start time of the interval represented by this data point, in the given unit since epoch. + */ + public long getStartTime(@NonNull TimeUnit timeUnit) { + return timeUnit.convert(this.startTimeNanos, TimeUnit.NANOSECONDS); + } + + /** + * Returns the timestamp of the data point, in the given unit since epoch. For data points that represent intervals, this method will return the + * end time. + */ + public long getTimestamp(@NonNull TimeUnit timeUnit) { + return timeUnit.convert(this.timestampNanos, TimeUnit.NANOSECONDS); + } + + /** + * Returns the value holder for the field with the given name. This method can be used both to query the value and to set it. + * + * @param field One of the fields of this data type. + * @return The Value associated with the given field. + * @throws IllegalArgumentException If the given field doesn't match any of the fields for this DataPoint's data type. + */ + @NonNull + public Value getValue(com.google.android.gms.fitness.data.Field field) { + return this.values[getDataType().indexOf(field)]; + } + + long getTimestampNanos() { + return timestampNanos; + } + + long getStartTimeNanos() { + return startTimeNanos; + } + + Value[] getValues() { + return values; + } + + DataSource getOriginalDataSourceIfSet() { + return originalDataSource; + } + + long getRawTimestamp() { + return rawTimestamp; + } + + /** + * Sets the values of this data point, where the format for all of its values is float. + * + * @param values The value for each field of the data point, in order. + * @deprecated Use {@link DataPoint.Builder} to create {@link DataPoint} instances. + */ + @Deprecated + public DataPoint setFloatValues(float... values) { + if (values.length != this.getDataType().getFields().size()) + throw new IllegalArgumentException("The number of values does not match the number of fields"); + for (int i = 0; i < values.length; i++) { + this.values[i].setFloat(values[i]); + } + return this; + } + + /** + * Sets the values of this data point, where the format for all of its values is int. + * + * @param values The value for each field of the data point, in order. + * @deprecated Use {@link DataPoint.Builder} to create {@link DataPoint} instances. + */ + @Deprecated + public DataPoint setIntValues(int... values) { + if (values.length != this.getDataType().getFields().size()) + throw new IllegalArgumentException("The number of values does not match the number of fields"); + for (int i = 0; i < values.length; i++) { + this.values[i].setInt(values[i]); + } + return this; + } + + /** + * Sets the time interval of a data point that represents an interval of time. For data points that represent instantaneous readings, + * {@link #setTimestamp(long, TimeUnit)} should be used. + * + * @param startTime The start time in the given unit, representing elapsed time since epoch. + * @param endTime The end time in the given unit, representing elapsed time since epoch. + * @param timeUnit The time unit of both start and end timestamps. + * @deprecated Use {@link DataPoint.Builder} to create {@link DataPoint} instances. + */ + @Deprecated + public DataPoint setTimeInterval(long startTime, long endTime, TimeUnit timeUnit) { + this.startTimeNanos = timeUnit.toNanos(startTime); + this.timestampNanos = timeUnit.toNanos(endTime); + return this; + } + + /** + * Sets the timestamp of a data point that represent an instantaneous reading, measurement, or input. For data points that represent intervals, + * {@link #setTimeInterval(long, long, TimeUnit)} should be used. + * + * @param timestamp The timestamp in the given unit, representing elapsed time since epoch. + * @param timeUnit The unit of the given timestamp. + * @deprecated Use {@link DataPoint.Builder} to create {@link DataPoint} instances. + */ + @Deprecated + public DataPoint setTimestamp(long timestamp, TimeUnit timeUnit) { + this.timestampNanos = timeUnit.toNanos(timestamp); + return this; + } + + /** + * Creates a new builder for a {@link DataPoint} with the given {@code dataSource}. + * + * @throws NullPointerException If specified data source is null. + */ + @NonNull + public static Builder builder(@NonNull DataSource dataSource) { + return new Builder(dataSource); + } + + /** + * Creates a new data point for the given dataSource. An unset {@link Value} is created for each field of the data source's data type. + * + * @return An empty data point instance. + * @deprecated Use {@link DataPoint.Builder} to create {@link DataPoint} instances. + */ + @NonNull + @Deprecated + public static DataPoint create(@NonNull DataSource dataSource) { + return new DataPoint(dataSource); + } + + /** + * Extracts a data point from a callback intent received after registering to a data source with a PendingIntent. + * + * @return The extracted DataPoint, or {@code null} if the given intent does not contain a DataPoint + */ + @Nullable + public static DataPoint extract(@NonNull Intent intent) { + return SafeParcelableSerializer.deserializeFromBytes(intent.getByteArrayExtra("com.google.android.gms.fitness.EXTRA_DATA_POINT"), CREATOR); + } + + /** + * Builder for {@link DataPoint} instances. + */ + public static class Builder { + private final DataPoint dataPoint; + private boolean built = false; + + Builder(DataSource dataSource) { + this.dataPoint = DataPoint.create(dataSource); + } + + /** + * Builds and returns the {@link DataPoint}. + */ + @NonNull + public DataPoint build() { + if (built) throw new IllegalStateException("DataPoint already built"); + this.built = true; + return this.dataPoint; + } + + /** + * Sets the value of an activity field to {@code activity}. + * + * @throws IllegalArgumentException If the given index is out of the range for this data type. + * @throws IllegalStateException If the field isn't of format {@link com.google.android.gms.fitness.data.Field#FORMAT_INT32}. + */ + @NonNull + public Builder setActivityField(@NonNull com.google.android.gms.fitness.data.Field field, @NonNull String activity) { + if (built) throw new IllegalStateException("DataPoint already built"); + this.dataPoint.getValue(field).setActivity(activity); + return this; + } + + /** + * Sets the floating point value of the given {@code field} to {@code value}. + * + * @throws IllegalArgumentException If the given index is out of the range for this data type. + * @throws IllegalStateException If the field isn't of format {@link com.google.android.gms.fitness.data.Field#FORMAT_FLOAT}. + */ + @NonNull + public Builder setField(@NonNull com.google.android.gms.fitness.data.Field field, float value) { + if (built) throw new IllegalStateException("DataPoint already built"); + this.dataPoint.getValue(field).setFloat(value); + return this; + } + + + /** + * Sets the map value of the given {@code field} to {@code value}. + * + * @throws IllegalArgumentException If the given index is out of the range for this data type. + * @throws IllegalStateException If the field isn't of format {@link com.google.android.gms.fitness.data.Field#FORMAT_MAP}. + */ + @NonNull + public Builder setField(@NonNull com.google.android.gms.fitness.data.Field field, @NonNull Map map) { + if (built) throw new IllegalStateException("DataPoint already built"); + this.dataPoint.getValue(field).setMap(map); + return this; + } + + + /** + * Sets the integer value of the given {@code field} to {@code value}. + * + * @throws IllegalArgumentException If the given index is out of the range for this data type. + * @throws IllegalStateException If the field isn't of format {@link com.google.android.gms.fitness.data.Field#FORMAT_INT32}. + */ + @NonNull + public Builder setField(@NonNull com.google.android.gms.fitness.data.Field field, int value) { + if (built) throw new IllegalStateException("DataPoint already built"); + this.dataPoint.getValue(field).setInt(value); + return this; + } + + /** + * Sets the string value of the given {@code field} to {@code value}. + * + * @throws IllegalArgumentException If the given index is out of the range for this data type. + * @throws IllegalStateException If the field isn't of format {@link com.google.android.gms.fitness.data.Field#FORMAT_STRING}. + */ + @NonNull + public Builder setField(@NonNull com.google.android.gms.fitness.data.Field field, @NonNull String value) { + if (built) throw new IllegalStateException("DataPoint already built"); + this.dataPoint.getValue(field).setString(value); + return this; + } + + /** + * Sets the values of the data point, where the format for all of its values is float. + * + * @param values The value for each field of the data point, in order. + */ + @NonNull + public Builder setFloatValues(@NonNull float... values) { + if (built) throw new IllegalStateException("DataPoint already built"); + this.dataPoint.setFloatValues(values); + return this; + } + + /** + * Sets the values of the data point, where the format for all of its values is int. + * + * @param values The value for each field of the data point, in order. + */ + @NonNull + public Builder setIntValues(@NonNull int... values) { + if (built) throw new IllegalStateException("DataPoint already built"); + this.dataPoint.setIntValues(values); + return this; + } + + /** + * Sets the time interval of a data point that represents an interval of time. For data points that represent instantaneous readings, + * {@link #setTimestamp(long, TimeUnit)} should be used. + * + * @param startTime The start time in the given unit, representing elapsed time since epoch. + * @param endTime The end time in the given unit, representing elapsed time since epoch. + * @param timeUnit The time unit of both start and end timestamps. + */ + @NonNull + public Builder setTimeInterval(long startTime, long endTime, @NonNull TimeUnit timeUnit) { + if (built) throw new IllegalStateException("DataPoint already built"); + this.dataPoint.setTimeInterval(startTime, endTime, timeUnit); + return this; + } + + /** + * Sets the timestamp of a data point that represent an instantaneous reading, measurement, or input. For data points that represent intervals, + * {@link #setTimeInterval(long, long, TimeUnit)} should be used. + * + * @param timestamp The timestamp in the given unit, representing elapsed time since epoch. + * @param timeUnit The unit of the given timestamp. + */ + @NonNull + public Builder setTimestamp(long timestamp, @NonNull TimeUnit timeUnit) { + if (built) throw new IllegalStateException("DataPoint already built"); + this.dataPoint.setTimestamp(timestamp, timeUnit); + return this; + } + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + CREATOR.writeToParcel(this, dest, flags); + } + + public static final SafeParcelableCreatorAndWriter CREATOR = findCreator(DataPoint.class); + +} diff --git a/play-services-fitness/src/main/java/com/google/android/gms/fitness/data/DataSet.java b/play-services-fitness/src/main/java/com/google/android/gms/fitness/data/DataSet.java index e96d311ed6..ab0a51cbbc 100644 --- a/play-services-fitness/src/main/java/com/google/android/gms/fitness/data/DataSet.java +++ b/play-services-fitness/src/main/java/com/google/android/gms/fitness/data/DataSet.java @@ -1,6 +1,9 @@ /* * SPDX-FileCopyrightText: 2023 microG Project Team * SPDX-License-Identifier: Apache-2.0 + * Notice: Portions of this file are reproduced from work created and shared by Google and used + * according to terms described in the Creative Commons 4.0 Attribution License. + * See https://developers.google.com/readme/policies for details. */ package com.google.android.gms.fitness.data; @@ -13,19 +16,196 @@ import com.google.android.gms.common.internal.safeparcel.SafeParcelable; import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Represents a fixed set of data points in a data type's stream from a particular data source. A data set usually represents data at + * fixed time boundaries, and can be used both for batch data insertion and as a result of read requests. + */ @SafeParcelable.Class public class DataSet extends AbstractSafeParcelable { @Field(1000) - public int versionCode; - @Field(1) - public DataSource dataSource; + final int versionCode; + @Field(value = 1, getterName = "getDataSource") + @NonNull + private final DataSource dataSource; + @Field(value = 3, getterName = "getRawDataPoints") + @NonNull + private final List rawDataPoints; + @Field(value = 4, getterName = "getUniqueDataSources") + @NonNull + private final List uniqueDataSources; + + @Constructor + DataSet(@Param(1000) int versionCode, @Param(1) @NonNull DataSource dataSource, @Param(3) @NonNull List rawDataPoints, @Param(4) List uniqueDataSources) { + this.versionCode = versionCode; + this.dataSource = dataSource; + this.rawDataPoints = rawDataPoints; + this.uniqueDataSources = versionCode < 2 ? Collections.singletonList(dataSource) : uniqueDataSources; + } + + DataSet(@NonNull DataSource dataSource) { + this.versionCode = 3; + this.dataSource = dataSource; + this.rawDataPoints = new ArrayList<>(); + this.uniqueDataSources = new ArrayList<>(); + uniqueDataSources.add(dataSource); + } + + /** + * Adds a data point to this data set. The data points should be for the correct data type and data source, and should have its timestamp + * already set. + * + * @throws IllegalArgumentException If dataPoint has invalid data. + * @deprecated Build {@link DataSet} instances using the builder. + */ + @Deprecated + public void add(@NonNull DataPoint dataPoint) { + if (!dataPoint.getDataSource().getStreamIdentifier().equals(dataSource.getStreamIdentifier())) + throw new IllegalArgumentException("Conflicting data sources found"); + // TODO + rawDataPoints.add(dataPoint); + } + + /** + * Adds a list of data points to this data set in bulk. All data points should be for the correct data type and data source, and should have their + * timestamp already set. + * + * @deprecated Build {@link DataSet} instances using the builder. + */ + @Deprecated + public void addAll(@NonNull Iterable iterable) { + for (DataPoint dataPoint : iterable) { + add(dataPoint); + } + } + + /** + * Creates an empty data point for this data set's data source. The new data point is not added to the data set by this method. After the data + * point is initialized, {@link #add(DataPoint)} should be called. + */ + @NonNull + public DataPoint createDataPoint() { + return DataPoint.create(this.dataSource); + } + + /** + * Returns the list of data points represented by this data set. The data points will preserve the same order in which they were inserted. + *

+ * Certain APIs that return a DataSet might insert data points in chronological order, but this isn't enforced. + */ + @NonNull + public List getDataPoints() { + return Collections.unmodifiableList(rawDataPoints); + } + + /** + * Returns the data source which this data set represents. All of the data points in the data set are from this data source. + */ + @NonNull + public DataSource getDataSource() { + return dataSource; + } + + /** + * Returns the data type this data set represents. All of the data points in the data set are of this data type. + */ + @NonNull + public DataType getDataType() { + return dataSource.getDataType(); + } + + @NonNull + List getRawDataPoints() { + return rawDataPoints; + } + + @NonNull + List getUniqueDataSources() { + return uniqueDataSources; + } + + /** + * Creates a new builder for a {@link DataSet} with the given {@code dataSource}. + * + * @throws NullPointerException If specified data source is null. + */ + @NonNull + public static Builder builder(@NonNull DataSource dataSource) { + return new Builder(dataSource); + } + + /** + * Creates a new data set to hold data points for the given {@code dataSource}. + *

+ * Data points with the matching data source can be created using {@link #createDataPoint()}, and after having the values set added to the data set + * via {@link #add(DataPoint)}. + * + * @throws NullPointerException If specified data source is null. + */ + @NonNull + public static DataSet create(@NonNull DataSource dataSource) { + return new DataSet(dataSource); + } @Override public void writeToParcel(@NonNull Parcel dest, int flags) { CREATOR.writeToParcel(this, dest, flags); } + /** + * Builder used to create new data sets. + */ + public static class Builder { + private final DataSet dataSet; + private boolean built = false; + + Builder(DataSource dataSource) { + this.dataSet = DataSet.create(dataSource); + } + + /** + * Adds a data point to this data set. The data points should be for the correct data type and data source, and should have its timestamp + * already set. + * + * @throws IllegalArgumentException If dataPoint has the wrong {@link DataSource}, or contain invalid data. + */ + @NonNull + public Builder add(@NonNull DataPoint dataPoint) { + if (built) throw new IllegalStateException("DataSet has already been built."); + this.dataSet.add(dataPoint); + return this; + } + + /** + * Adds a list of data points to this data set in bulk. All data points should be for the correct data type and data source, and should have their + * timestamp already set. + * + * @throws IllegalArgumentException If the {@code dataPoints} have the wrong source, or contain invalid data. + */ + @NonNull + public Builder addAll(@NonNull Iterable iterable) { + if (built) throw new IllegalStateException("DataSet has already been built."); + this.dataSet.addAll(iterable); + return this; + } + + /** + * Finishes building and returns the {@link DataSet}. + * + * @throws IllegalStateException If called more than once. + */ + @NonNull + public DataSet build() { + if (built) throw new IllegalStateException("DataSet has already been built."); + this.built = true; + return this.dataSet; + } + } + public static final SafeParcelableCreatorAndWriter CREATOR = findCreator(DataSet.class); } diff --git a/play-services-fitness/src/main/java/com/google/android/gms/fitness/data/DataSource.java b/play-services-fitness/src/main/java/com/google/android/gms/fitness/data/DataSource.java index 93d0aa6f89..8c6e2d7a60 100644 --- a/play-services-fitness/src/main/java/com/google/android/gms/fitness/data/DataSource.java +++ b/play-services-fitness/src/main/java/com/google/android/gms/fitness/data/DataSource.java @@ -1,37 +1,284 @@ /* * SPDX-FileCopyrightText: 2023 microG Project Team * SPDX-License-Identifier: Apache-2.0 + * Notice: Portions of this file are reproduced from work created and shared by Google and used + * according to terms described in the Creative Commons 4.0 Attribution License. + * See https://developers.google.com/readme/policies for details. */ package com.google.android.gms.fitness.data; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; import android.os.Parcel; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import com.google.android.gms.common.internal.safeparcel.AbstractSafeParcelable; import com.google.android.gms.common.internal.safeparcel.SafeParcelable; import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter; +import com.google.android.gms.common.internal.safeparcel.SafeParcelableSerializer; +import org.microg.gms.common.Constants; +import org.microg.gms.utils.ToStringHelper; +/** + * Definition of a unique source of sensor data. Data sources can expose raw data coming from hardware sensors on local or companion + * devices. They can also expose derived data, created by transforming or merging other data sources. Multiple data sources can exist for the + * same data type. Every data point inserted into or read from Google Fit has an associated data source. + *

+ * The data source contains enough information to uniquely identify its data, including the hardware device and the application that + * collected and/or transformed the data. It also holds useful metadata, such as a stream name and the device type. + *

+ * The data source's data stream can be accessed in a live fashion by registering a data source listener, or via queries over fixed time intervals. + *

+ * An end-user-visible name for the data stream can be set by calling {@link DataSource.Builder.setStreamName(String)} or otherwise computed + * from the device model and application name. + */ @SafeParcelable.Class public class DataSource extends AbstractSafeParcelable { - @Field(1) - public DataType dataType; - @Field(3) - public int type; - @Field(4) - public Device device; - @Field(5) - public AppInfo appInfo; - @Field(6) - public String info; + /** + * Name for the parcelable intent extra containing a data source. It can be extracted using {@link #extract(Intent)}. + */ + public static final String EXTRA_DATA_SOURCE = "vnd.google.fitness.data_source"; + + /** + * Type constant for a data source which exposes original, raw data from an external source such as a hardware sensor, a wearable device, or + * user input. + */ + public static final int TYPE_RAW = 0; + + /** + * Type constant for a data source which exposes data which is derived from one or more existing data sources by performing + * transformations on the original data. + */ + public static final int TYPE_DERIVED = 1; + + @Field(value = 1, getterName = "getDataType") + @NonNull + private final DataType dataType; + @Field(value = 3, getterName = "getType") + private final int type; + @Field(value = 4, getterName = "getDevice") + @Nullable + private final Device device; + @Field(value = 5, getterName = "getApplication") + @Nullable + final Application application; + @Field(value = 6, getterName = "getStreamName") + private final String streamName; + + @Constructor + DataSource(@Param(1) @NonNull DataType dataType, @Param(3) int type, @Param(4) @Nullable Device device, @Param(5) @Nullable Application application, @Param(6) String streamName) { + this.dataType = dataType; + this.type = type; + this.device = device; + this.application = application; + this.streamName = streamName; + } + + @Nullable + public Application getApplication() { + return application; + } + + /** + * Returns the package name for the application responsible for setting the data, or {@code null} if unset/unknown. {@link PackageManager} can be used to + * query relevant information about the application, such as the name, icon, and logo. + *

+ * Data coming from local sensors or BLE devices will not have a corresponding application. + */ + @Nullable + public String getAppPackageName() { + if (application == null) return null; + return application.getPackageName(); + } + + /** + * Returns the data type for data coming from this data source. Knowing the type of a data source can be useful to perform transformations on + * top of raw data without using sources that are themselves computed by transforming raw data. + */ + @NonNull + public DataType getDataType() { + return dataType; + } + + /** + * Returns the device where data is being collected, or {@code null} if unset. + */ + @Nullable + public Device getDevice() { + return device; + } + + /** + * Returns a unique identifier for the data stream produced by this data source. The identifier includes, in order: + *

    + *
  1. the data source's type (raw or derived)
  2. + *
  3. the data source's data type
  4. + *
  5. the application's package name (unique for a given application)
  6. + *
  7. the physical device's manufacturer, model, and serial number (UID)
  8. + *
  9. the data source's stream name.
  10. + *
+ */ + @NonNull + public String getStreamIdentifier() { + StringBuilder sb = new StringBuilder(); + sb.append(type == TYPE_RAW ? "raw" : "derived"); + sb.append(":").append(dataType.getName()); + if (application != null) sb.append(":").append(application.getPackageName()); + if (device != null) sb.append(":").append(device.getDeviceId()); + if (streamName != null) sb.append(":").append(streamName); + return sb.toString(); + } + + /** + * Returns the specific stream name for the stream coming from this data source, or an empty string if unset. + */ + @NonNull + public String getStreamName() { + return streamName; + } + + /** + * Returns the constant describing the type of this data source. + * + * @return One of the constant values ({@link #TYPE_DERIVED} or {@link #TYPE_RAW}), zero if unset. Values outside of this range should be treated as + * unset/unknown. + */ + public int getType() { + return type; + } + + @Override + public int hashCode() { + return getStreamIdentifier().hashCode(); + } + + @NonNull + @Override + public String toString() { + return ToStringHelper.name("DataSource").value(getStreamIdentifier()).end(); + } @Override public void writeToParcel(@NonNull Parcel dest, int flags) { CREATOR.writeToParcel(this, dest, flags); } + /** + * Extracts the data source extra from the given intent, such as an intent to view user's data. + * + * @return The data source, or {@code null} if not found. + */ + @Nullable + public static DataSource extract(@NonNull Intent intent) { + return SafeParcelableSerializer.deserializeFromBytes(intent.getByteArrayExtra(EXTRA_DATA_SOURCE), CREATOR); + } + + /** + * A builder that can be used to construct new data source objects. In general, a built data source should be saved in memory to avoid the cost + * of re-constructing it for every request. + */ + public static class Builder { + private DataType dataType; + private Device device; + private Application application; + private int type = -1; + private String streamName = ""; + + /** + * Finishes building the data source and returns a DataSource object. + * + * @throws IllegalStateException If the builder didn't have enough data to build a valid data source. + */ + @NonNull + public DataSource build() { + if (dataType == null) throw new IllegalStateException("dataType must be set"); + if (type < 0) throw new IllegalStateException("type must be set"); + return new DataSource(dataType, type, device, application, streamName); + } + + /** + * Sets the package name for the application that is recording or computing the data. Used for data sources that aren't built into the platform + * (local sensors and BLE sensors are built-in). It can be used to identify the data source, to disambiguate between data from different + * applications, and also to link back to the original application for a detailed view. + */ + @NonNull + public Builder setAppPackageName(@NonNull String packageName) { + Application application = Application.GMS_APP; + this.application = Constants.GMS_PACKAGE_NAME.equals(packageName) ? Application.GMS_APP : new Application(packageName); + return this; + } + + /** + * Sets the package name for the application that is recording or computing the data based on the app's context. This method should be + * preferred when an application is creating a data source that represents its own data. When creating a data source to query data from other + * apps, {@link #setAppPackageName(String)} should be used. + */ + @NonNull + public Builder setAppPackageName(@NonNull Context appContext) { + setAppPackageName(appContext.getPackageName()); + return this; + } + + /** + * Sets the data type for the data source. Every data source is required to have a data type. + * + * @param dataType One of the data types defined in {@link DataType}, or a custom data type. + */ + @NonNull + public Builder setDataType(@NonNull DataType dataType) { + this.dataType = dataType; + return this; + } + + /** + * Sets the integrated device where data is being recorded (for instance, a phone that has sensors, or a wearable). Can be useful to identify the + * data source, and to disambiguate between data from different devices. If the data is coming from the local device, use + * {@link Device#getLocalDevice(Context)}. + *

+ * Note that it may be useful to set the device even if the data is not coming from a hardware sensor on the device. For instance, if the user + * installs an application which generates sensor data in two separate devices, the only way to differentiate the two data sources is using the + * device. This can be specially important if both devices are used at the same time. + */ + @NonNull + public Builder setDevice(@NonNull Device device) { + this.device = device; + return this; + } + + /** + * The stream name uniquely identifies this particular data source among other data sources of the same type from the same underlying + * producer. Setting the stream name is optional, but should be done whenever an application exposes two streams for the same data type, or + * when a device has two equivalent sensors. + *

+ * The stream name is used by {@link DataSource#getStreamIdentifier()} to make sure the different streams are properly separated when + * querying or persisting data. + * + * @throws IllegalArgumentException If the specified stream name is null. + */ + @NonNull + public Builder setStreamName(@NonNull String streamName) { + //noinspection ConstantValue + if (streamName == null) throw new IllegalArgumentException("streamName must be set"); + this.streamName = streamName; + return this; + } + + /** + * Sets the type of the data source. {@link DataSource#TYPE_DERIVED} should be used if any other data source is used in generating the data. + * {@link DataSource#TYPE_RAW} should be used if the data comes completely from outside of Google Fit. + */ + @NonNull + public Builder setType(int type) { + this.type = type; + return this; + } + } + public static final SafeParcelableCreatorAndWriter CREATOR = findCreator(DataSource.class); } diff --git a/play-services-fitness/src/main/java/com/google/android/gms/fitness/data/DataType.java b/play-services-fitness/src/main/java/com/google/android/gms/fitness/data/DataType.java index 10cbe66472..a66a9dccd6 100644 --- a/play-services-fitness/src/main/java/com/google/android/gms/fitness/data/DataType.java +++ b/play-services-fitness/src/main/java/com/google/android/gms/fitness/data/DataType.java @@ -1,6 +1,9 @@ /* * SPDX-FileCopyrightText: 2023 microG Project Team * SPDX-License-Identifier: Apache-2.0 + * Notice: Portions of this file are reproduced from work created and shared by Google and used + * according to terms described in the Creative Commons 4.0 Attribution License. + * See https://developers.google.com/readme/policies for details. */ package com.google.android.gms.fitness.data; @@ -9,6 +12,7 @@ import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import com.google.android.gms.common.internal.safeparcel.AbstractSafeParcelable; import com.google.android.gms.common.internal.safeparcel.SafeParcelable; import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter; @@ -17,79 +21,178 @@ import java.util.Collections; import java.util.List; +import static com.google.android.gms.fitness.data.Field.*; + +/** + * The data type defines the schema for a stream of data being collected by, inserted into, or queried from Google Fit. The data type defines + * only the representation and format of the data, and not how it's being collected, the sensor being used, or the parameters of the collection. + *

+ * A data type contains one or more fields. In case of multi-dimensional data (such as location with latitude, longitude, and accuracy) each + * field represents one dimension. Each data type field has a unique name which identifies it. The field also defines the format of the data + * (such as int or float). + *

+ * The data types in the {@code com.google} namespace are shared with any app with the user consent. These are fixed and can only be updated in + * new releases of the platform. This class contains constants representing each of the {@code com.google} data types, each prefixed with {@code TYPE_}. + * Custom data types can be accessed via the {@link ConfigClient}. + *

+ * Certain data types can represent aggregates, and can be computed as part of read requests by calling + * {@link DataReadRequest.Builder#aggregate(DataType)}. This class contains constants for all the valid aggregates, each prefixed with + * {@code AGGREGATE_}. The aggregates for each input type can be queried via {@link #getAggregatesForInput(DataType)}. + */ @SafeParcelable.Class public class DataType extends AbstractSafeParcelable { + /** + * The common prefix for data type MIME types, for use in intents. The MIME type for a particular data type will be this prefix followed by + * the data type name. + *

+ * The data type's name is returned by {@link #getName()}. The full MIME type can be computed by {@link #getMimeType(DataType)}. + */ + public static final String MIME_TYPE_PREFIX = "vnd.google.fitness.data_type/"; - public static final DataType TYPE_STEP_COUNT_DELTA = new DataType("com.google.step_count.delta", "https://www.googleapis.com/auth/fitness.activity.read", "https://www.googleapis.com/auth/fitness.activity.write", com.google.android.gms.fitness.data.Field.FIELD_STEPS); - public static final DataType TYPE_STEP_COUNT_CUMULATIVE = new DataType("com.google.step_count.cumulative", "https://www.googleapis.com/auth/fitness.activity.read", "https://www.googleapis.com/auth/fitness.activity.write", com.google.android.gms.fitness.data.Field.FIELD_STEPS); - public static final DataType TYPE_STEP_COUNT_CADENCE = new DataType("com.google.step_count.cadence", "https://www.googleapis.com/auth/fitness.activity.read", "https://www.googleapis.com/auth/fitness.activity.write", com.google.android.gms.fitness.data.Field.FIELD_RPM); - public static final DataType TYPE_INTERNAL_GOAL = new DataType("com.google.internal.goal", "https://www.googleapis.com/auth/fitness.activity.read", "https://www.googleapis.com/auth/fitness.activity.write", com.google.android.gms.fitness.data.Field.FIELD_FITNESS_GOAL_V2); - public static final DataType TYPE_ACTIVITY_SEGMENT = new DataType("com.google.activity.segment", "https://www.googleapis.com/auth/fitness.activity.read", "https://www.googleapis.com/auth/fitness.activity.write", com.google.android.gms.fitness.data.Field.FIELD_ACTIVITY); - public static final DataType TYPE_SLEEP_SEGMENT = new DataType("com.google.sleep.segment", "https://www.googleapis.com/auth/fitness.sleep.read", "https://www.googleapis.com/auth/fitness.sleep.write", com.google.android.gms.fitness.data.Field.FIELD_SLEEP_SEGMENT_TYPE); - public static final DataType TYPE_CALORIES_EXPENDED = new DataType("com.google.calories.expended", "https://www.googleapis.com/auth/fitness.activity.read", "https://www.googleapis.com/auth/fitness.activity.write", com.google.android.gms.fitness.data.Field.FIELD_CALORIES); - public static final DataType TYPE_BASAL_METABOLIC_RATE = new DataType("com.google.calories.bmr", "https://www.googleapis.com/auth/fitness.activity.read", "https://www.googleapis.com/auth/fitness.activity.write", com.google.android.gms.fitness.data.Field.FIELD_CALORIES); - public static final DataType TYPE_POWER_SAMPLE = new DataType("com.google.power.sample", "https://www.googleapis.com/auth/fitness.activity.read", "https://www.googleapis.com/auth/fitness.activity.write", com.google.android.gms.fitness.data.Field.FIELD_WATTS); - public static final DataType TYPE_SENSOR_EVENTS = new DataType("com.google.sensor.events", "https://www.googleapis.com/auth/fitness.activity.read", "https://www.googleapis.com/auth/fitness.activity.write", com.google.android.gms.fitness.data.Field.FIELD_SENSOR_TYPE, com.google.android.gms.fitness.data.Field.FIELD_TIMESTAMPS, com.google.android.gms.fitness.data.Field.FIELD_SENSOR_VALUES); - public static final DataType TYPE_HEART_RATE_BPM = new DataType("com.google.heart_rate.bpm", "https://www.googleapis.com/auth/fitness.heart_rate.read", "https://www.googleapis.com/auth/fitness.heart_rate.write", com.google.android.gms.fitness.data.Field.FIELD_BPM); - public static final DataType TYPE_RESPIRATORY_RATE = new DataType("com.google.respiratory_rate", "https://www.googleapis.com/auth/fitness.respiratory_rate.read", "https://www.googleapis.com/auth/fitness.respiratory_rate.write", com.google.android.gms.fitness.data.Field.FIELD_RESPIRATORY_RATE); - public static final DataType TYPE_LOCATION_SAMPLE = new DataType("com.google.location.sample", "https://www.googleapis.com/auth/fitness.location.read", "https://www.googleapis.com/auth/fitness.location.write", com.google.android.gms.fitness.data.Field.FIELD_LATITUDE, com.google.android.gms.fitness.data.Field.FIELD_LONGITUDE, com.google.android.gms.fitness.data.Field.FIELD_ACCURACY, com.google.android.gms.fitness.data.Field.FIELD_ALTITUDE); + public static final DataType TYPE_ACTIVITY_SEGMENT = new DataType("com.google.activity.segment", "https://www.googleapis.com/auth/fitness.activity.read", "https://www.googleapis.com/auth/fitness.activity.write", FIELD_ACTIVITY); + public static final DataType TYPE_BASAL_METABOLIC_RATE = new DataType("com.google.calories.bmr", "https://www.googleapis.com/auth/fitness.activity.read", "https://www.googleapis.com/auth/fitness.activity.write", FIELD_CALORIES); + public static final DataType TYPE_BODY_FAT_PERCENTAGE = new DataType("com.google.body.fat.percentage", "https://www.googleapis.com/auth/fitness.body.read", "https://www.googleapis.com/auth/fitness.body.write", FIELD_PERCENTAGE); + public static final DataType TYPE_CALORIES_EXPENDED = new DataType("com.google.calories.expended", "https://www.googleapis.com/auth/fitness.activity.read", "https://www.googleapis.com/auth/fitness.activity.write", FIELD_CALORIES); + public static final DataType TYPE_CYCLING_PEDALING_CADENCE = new DataType("com.google.cycling.pedaling.cadence", "https://www.googleapis.com/auth/fitness.activity.read", "https://www.googleapis.com/auth/fitness.activity.write", FIELD_RPM); + public static final DataType TYPE_CYCLING_PEDALING_CUMULATIVE = new DataType("com.google.cycling.pedaling.cumulative", "https://www.googleapis.com/auth/fitness.activity.read", "https://www.googleapis.com/auth/fitness.activity.write", FIELD_REVOLUTIONS); + public static final DataType TYPE_CYCLING_WHEEL_REVOLUTION = new DataType("com.google.cycling.wheel_revolution.cumulative", "https://www.googleapis.com/auth/fitness.location.read", "https://www.googleapis.com/auth/fitness.location.write", FIELD_REVOLUTIONS); + public static final DataType TYPE_CYCLING_WHEEL_RPM = new DataType("com.google.cycling.wheel_revolution.rpm", "https://www.googleapis.com/auth/fitness.location.read", "https://www.googleapis.com/auth/fitness.location.write", FIELD_RPM); + public static final DataType TYPE_DISTANCE_DELTA = new DataType("com.google.distance.delta", "https://www.googleapis.com/auth/fitness.location.read", "https://www.googleapis.com/auth/fitness.location.write", FIELD_DISTANCE); + public static final DataType TYPE_HEART_POINTS = new DataType("com.google.heart_minutes", "https://www.googleapis.com/auth/fitness.activity.read", "https://www.googleapis.com/auth/fitness.activity.write", FIELD_INTENSITY); + public static final DataType TYPE_HEART_RATE_BPM = new DataType("com.google.heart_rate.bpm", "https://www.googleapis.com/auth/fitness.heart_rate.read", "https://www.googleapis.com/auth/fitness.heart_rate.write", FIELD_BPM); + public static final DataType TYPE_HEIGHT = new DataType("com.google.height", "https://www.googleapis.com/auth/fitness.body.read", "https://www.googleapis.com/auth/fitness.body.write", FIELD_HEIGHT); + public static final DataType TYPE_HYDRATION = new DataType("com.google.hydration", "https://www.googleapis.com/auth/fitness.nutrition.read", "https://www.googleapis.com/auth/fitness.nutrition.write", FIELD_VOLUME); + public static final DataType TYPE_LOCATION_SAMPLE = new DataType("com.google.location.sample", "https://www.googleapis.com/auth/fitness.location.read", "https://www.googleapis.com/auth/fitness.location.write", FIELD_LATITUDE, FIELD_LONGITUDE, FIELD_ACCURACY, FIELD_ALTITUDE); @Deprecated - public static final DataType TYPE_LOCATION_TRACK = new DataType("com.google.location.track", "https://www.googleapis.com/auth/fitness.location.read", "https://www.googleapis.com/auth/fitness.location.write", com.google.android.gms.fitness.data.Field.FIELD_LATITUDE, com.google.android.gms.fitness.data.Field.FIELD_LONGITUDE, com.google.android.gms.fitness.data.Field.FIELD_ACCURACY, com.google.android.gms.fitness.data.Field.FIELD_ALTITUDE); - public static final DataType TYPE_DISTANCE_DELTA = new DataType("com.google.distance.delta", "https://www.googleapis.com/auth/fitness.location.read", "https://www.googleapis.com/auth/fitness.location.write", com.google.android.gms.fitness.data.Field.FIELD_DISTANCE); - public static final DataType TYPE_SPEED = new DataType("com.google.speed", "https://www.googleapis.com/auth/fitness.location.read", "https://www.googleapis.com/auth/fitness.location.write", com.google.android.gms.fitness.data.Field.FIELD_SPEED); - public static final DataType TYPE_CYCLING_WHEEL_REVOLUTION = new DataType("com.google.cycling.wheel_revolution.cumulative", "https://www.googleapis.com/auth/fitness.location.read", "https://www.googleapis.com/auth/fitness.location.write", com.google.android.gms.fitness.data.Field.FIELD_REVOLUTIONS); - public static final DataType TYPE_CYCLING_WHEEL_RPM = new DataType("com.google.cycling.wheel_revolution.rpm", "https://www.googleapis.com/auth/fitness.location.read", "https://www.googleapis.com/auth/fitness.location.write", com.google.android.gms.fitness.data.Field.FIELD_RPM); - public static final DataType TYPE_CYCLING_PEDALING_CUMULATIVE = new DataType("com.google.cycling.pedaling.cumulative", "https://www.googleapis.com/auth/fitness.activity.read", "https://www.googleapis.com/auth/fitness.activity.write", com.google.android.gms.fitness.data.Field.FIELD_REVOLUTIONS); - public static final DataType TYPE_CYCLING_PEDALING_CADENCE = new DataType("com.google.cycling.pedaling.cadence", "https://www.googleapis.com/auth/fitness.activity.read", "https://www.googleapis.com/auth/fitness.activity.write", com.google.android.gms.fitness.data.Field.FIELD_RPM); - public static final DataType TYPE_HEIGHT = new DataType("com.google.height", "https://www.googleapis.com/auth/fitness.body.read", "https://www.googleapis.com/auth/fitness.body.write", com.google.android.gms.fitness.data.Field.FIELD_HEIGHT); - public static final DataType TYPE_WEIGHT = new DataType("com.google.weight", "https://www.googleapis.com/auth/fitness.body.read", "https://www.googleapis.com/auth/fitness.body.write", com.google.android.gms.fitness.data.Field.FIELD_WEIGHT); - public static final DataType TYPE_BODY_FAT_PERCENTAGE = new DataType("com.google.body.fat.percentage", "https://www.googleapis.com/auth/fitness.body.read", "https://www.googleapis.com/auth/fitness.body.write", com.google.android.gms.fitness.data.Field.FIELD_PERCENTAGE); - public static final DataType TYPE_NUTRITION = new DataType("com.google.nutrition", "https://www.googleapis.com/auth/fitness.nutrition.read", "https://www.googleapis.com/auth/fitness.nutrition.write", com.google.android.gms.fitness.data.Field.FIELD_NUTRIENTS, com.google.android.gms.fitness.data.Field.FIELD_MEAL_TYPE, com.google.android.gms.fitness.data.Field.FIELD_FOOD_ITEM); - public static final DataType AGGREGATE_HYDRATION = new DataType("com.google.hydration", "https://www.googleapis.com/auth/fitness.nutrition.read", "https://www.googleapis.com/auth/fitness.nutrition.write", com.google.android.gms.fitness.data.Field.FIELD_VOLUME); - public static final DataType TYPE_WORKOUT_EXERCISE = new DataType("com.google.activity.exercise", "https://www.googleapis.com/auth/fitness.activity.read", "https://www.googleapis.com/auth/fitness.activity.write", com.google.android.gms.fitness.data.Field.FIELD_EXERCISE, com.google.android.gms.fitness.data.Field.FIELD_REPETITIONS, com.google.android.gms.fitness.data.Field.FIELD_DURATION_OPTIONAL, com.google.android.gms.fitness.data.Field.FIELD_RESISTANCE_TYPE, com.google.android.gms.fitness.data.Field.FIELD_RESISTANCE); - public static final DataType TYPE_MOVE_MINUTES = new DataType("com.google.active_minutes", "https://www.googleapis.com/auth/fitness.activity.read", "https://www.googleapis.com/auth/fitness.activity.write", com.google.android.gms.fitness.data.Field.FIELD_DURATION); + public static final DataType TYPE_LOCATION_TRACK = new DataType("com.google.location.track", "https://www.googleapis.com/auth/fitness.location.read", "https://www.googleapis.com/auth/fitness.location.write", FIELD_LATITUDE, FIELD_LONGITUDE, FIELD_ACCURACY, FIELD_ALTITUDE); + public static final DataType TYPE_MOVE_MINUTES = new DataType("com.google.active_minutes", "https://www.googleapis.com/auth/fitness.activity.read", "https://www.googleapis.com/auth/fitness.activity.write", FIELD_DURATION); + public static final DataType TYPE_NUTRITION = new DataType("com.google.nutrition", "https://www.googleapis.com/auth/fitness.nutrition.read", "https://www.googleapis.com/auth/fitness.nutrition.write", FIELD_NUTRIENTS, FIELD_MEAL_TYPE, FIELD_FOOD_ITEM); + public static final DataType TYPE_POWER_SAMPLE = new DataType("com.google.power.sample", "https://www.googleapis.com/auth/fitness.activity.read", "https://www.googleapis.com/auth/fitness.activity.write", FIELD_WATTS); + public static final DataType TYPE_SLEEP_SEGMENT = new DataType("com.google.sleep.segment", "https://www.googleapis.com/auth/fitness.sleep.read", "https://www.googleapis.com/auth/fitness.sleep.write", FIELD_SLEEP_SEGMENT_TYPE); + public static final DataType TYPE_SPEED = new DataType("com.google.speed", "https://www.googleapis.com/auth/fitness.location.read", "https://www.googleapis.com/auth/fitness.location.write", FIELD_SPEED); + public static final DataType TYPE_STEP_COUNT_CADENCE = new DataType("com.google.step_count.cadence", "https://www.googleapis.com/auth/fitness.activity.read", "https://www.googleapis.com/auth/fitness.activity.write", FIELD_RPM); + public static final DataType TYPE_STEP_COUNT_DELTA = new DataType("com.google.step_count.delta", "https://www.googleapis.com/auth/fitness.activity.read", "https://www.googleapis.com/auth/fitness.activity.write", FIELD_STEPS); + public static final DataType TYPE_WEIGHT = new DataType("com.google.weight", "https://www.googleapis.com/auth/fitness.body.read", "https://www.googleapis.com/auth/fitness.body.write", FIELD_WEIGHT); + public static final DataType TYPE_WORKOUT_EXERCISE = new DataType("com.google.activity.exercise", "https://www.googleapis.com/auth/fitness.activity.read", "https://www.googleapis.com/auth/fitness.activity.write", FIELD_EXERCISE, FIELD_REPETITIONS, FIELD_DURATION_OPTIONAL, FIELD_RESISTANCE_TYPE, FIELD_RESISTANCE); + + public static final DataType TYPE_DEVICE_ON_BODY = new DataType("com.google.device_on_body", "https://www.googleapis.com/auth/fitness.activity.read", "https://www.googleapis.com/auth/fitness.activity.write", FIELD_PROBABILITY); + public static final DataType TYPE_INTERNAL_GOAL = new DataType("com.google.internal.goal", "https://www.googleapis.com/auth/fitness.activity.read", "https://www.googleapis.com/auth/fitness.activity.write", FIELD_FITNESS_GOAL_V2); + public static final DataType TYPE_MET = new DataType("com.google.internal.met", "https://www.googleapis.com/auth/fitness.location.read", "https://www.googleapis.com/auth/fitness.location.write", FIELD_MET); + public static final DataType TYPE_PACED_WALKING_ATTRIBUTES = new DataType("com.google.internal.paced_walking_attributes", "https://www.googleapis.com/auth/fitness.activity.read", "https://www.googleapis.com/auth/fitness.activity.write", FIELD_FITNESS_PACED_WALKING_ATTRIBUTES); + public static final DataType TYPE_RESPIRATORY_RATE = new DataType("com.google.respiratory_rate", "https://www.googleapis.com/auth/fitness.respiratory_rate.read", "https://www.googleapis.com/auth/fitness.respiratory_rate.write", FIELD_RESPIRATORY_RATE); + public static final DataType TYPE_SENSOR_EVENTS = new DataType("com.google.sensor.events", "https://www.googleapis.com/auth/fitness.activity.read", "https://www.googleapis.com/auth/fitness.activity.write", FIELD_SENSOR_TYPE, FIELD_TIMESTAMPS, FIELD_SENSOR_VALUES); + public static final DataType TYPE_SLEEP_ATTRIBUTES = new DataType("com.google.internal.sleep_attributes", "https://www.googleapis.com/auth/fitness.sleep.read", "https://www.googleapis.com/auth/fitness.sleep.write", FIELD_FITNESS_SLEEP_ATTRIBUTES); + public static final DataType TYPE_SLEEP_SCHEDULE = new DataType("com.google.internal.sleep_schedule", "https://www.googleapis.com/auth/fitness.sleep.read", "https://www.googleapis.com/auth/fitness.sleep.write", FIELD_FITNESS_SLEEP_SCHEDULE); + public static final DataType TYPE_STEP_COUNT_CUMULATIVE = new DataType("com.google.step_count.cumulative", "https://www.googleapis.com/auth/fitness.activity.read", "https://www.googleapis.com/auth/fitness.activity.write", FIELD_STEPS); + public static final DataType TYPE_TIME_ZONE_CHANGE = new DataType("com.google.time_zone_change", "https://www.googleapis.com/auth/fitness.location.read", "https://www.googleapis.com/auth/fitness.location.write", FIELD_ZONE_ID); + public static final DataType TYPE_WORKOUT_SAMPLES = new DataType("com.google.activity.samples", "https://www.googleapis.com/auth/fitness.activity.read", "https://www.googleapis.com/auth/fitness.activity.write", FIELD_ACTIVITY_CONFIDENCE); + + + public static final DataType AGGREGATE_ACTIVITY_SUMMARY = new DataType("com.google.activity.summary", "https://www.googleapis.com/auth/fitness.activity.read", "https://www.googleapis.com/auth/fitness.activity.write", FIELD_ACTIVITY, FIELD_DURATION, FIELD_NUM_SEGMENTS); + public static final DataType AGGREGATE_BASAL_METABOLIC_RATE_SUMMARY = new DataType("com.google.calories.bmr.summary", "https://www.googleapis.com/auth/fitness.activity.read", "https://www.googleapis.com/auth/fitness.activity.write", FIELD_AVERAGE, FIELD_MAX, FIELD_MIN); + public static final DataType AGGREGATE_BODY_FAT_PERCENTAGE_SUMMARY = new DataType("com.google.body.fat.percentage.summary", "https://www.googleapis.com/auth/fitness.body.read", "https://www.googleapis.com/auth/fitness.body.write", FIELD_AVERAGE, FIELD_MAX, FIELD_MIN); + public static final DataType AGGREGATE_CALORIES_EXPENDED = TYPE_CALORIES_EXPENDED; + public static final DataType AGGREGATE_DISTANCE_DELTA = TYPE_DISTANCE_DELTA; + public static final DataType AGGREGATE_HEART_POINTS = new DataType("com.google.heart_minutes.summary", "https://www.googleapis.com/auth/fitness.activity.read", "https://www.googleapis.com/auth/fitness.activity.write", FIELD_INTENSITY, FIELD_DURATION); + public static final DataType AGGREGATE_HEART_RATE_SUMMARY = new DataType("com.google.heart_rate.summary", "https://www.googleapis.com/auth/fitness.heart_rate.read", "https://www.googleapis.com/auth/fitness.heart_rate.write", FIELD_AVERAGE, FIELD_MAX, FIELD_MIN); + public static final DataType AGGREGATE_HEIGHT_SUMMARY = new DataType("com.google.height.summary", "https://www.googleapis.com/auth/fitness.body.read", "https://www.googleapis.com/auth/fitness.body.write", FIELD_AVERAGE, FIELD_MAX, FIELD_MIN); + public static final DataType AGGREGATE_HYDRATION = TYPE_HYDRATION; + public static final DataType AGGREGATE_LOCATION_BOUNDING_BOX = new DataType("com.google.location.bounding_box", "https://www.googleapis.com/auth/fitness.location.read", "https://www.googleapis.com/auth/fitness.location.write", FIELD_LOW_LATITUDE, FIELD_LOW_LONGITUDE, FIELD_HIGH_LATITUDE, FIELD_HIGH_LONGITUDE); public static final DataType AGGREGATE_MOVE_MINUTES = TYPE_MOVE_MINUTES; - public static final DataType TYPE_DEVICE_ON_BODY = new DataType("com.google.device_on_body", "https://www.googleapis.com/auth/fitness.activity.read", "https://www.googleapis.com/auth/fitness.activity.write", com.google.android.gms.fitness.data.Field.FIELD_PROBABILITY); - public static final DataType AGGREGATE_ACTIVITY_SUMMARY = new DataType("com.google.activity.summary", "https://www.googleapis.com/auth/fitness.activity.read", "https://www.googleapis.com/auth/fitness.activity.write", com.google.android.gms.fitness.data.Field.FIELD_ACTIVITY, com.google.android.gms.fitness.data.Field.FIELD_DURATION, com.google.android.gms.fitness.data.Field.FIELD_NUM_SEGMENTS); - public static final DataType AGGREGATE_BASAL_METABOLIC_RATE_SUMMARY = new DataType("com.google.calories.bmr.summary", "https://www.googleapis.com/auth/fitness.activity.read", "https://www.googleapis.com/auth/fitness.activity.write", com.google.android.gms.fitness.data.Field.FIELD_AVERAGE, com.google.android.gms.fitness.data.Field.FIELD_MAX, com.google.android.gms.fitness.data.Field.FIELD_MIN); + public static final DataType AGGREGATE_NUTRITION_SUMMARY = new DataType("com.google.nutrition.summary", "https://www.googleapis.com/auth/fitness.nutrition.read", "https://www.googleapis.com/auth/fitness.nutrition.write", FIELD_NUTRIENTS, FIELD_MEAL_TYPE); + public static final DataType AGGREGATE_POWER_SUMMARY = new DataType("com.google.power.summary", "https://www.googleapis.com/auth/fitness.activity.read", "https://www.googleapis.com/auth/fitness.activity.write", FIELD_AVERAGE, FIELD_MAX, FIELD_MIN); + public static final DataType AGGREGATE_SPEED_SUMMARY = new DataType("com.google.speed.summary", "https://www.googleapis.com/auth/fitness.location.read", "https://www.googleapis.com/auth/fitness.location.write", FIELD_AVERAGE, FIELD_MAX, FIELD_MIN); public static final DataType AGGREGATE_STEP_COUNT_DELTA = TYPE_STEP_COUNT_DELTA; - public static final DataType AGGREGATE_DISTANCE_DELTA = TYPE_DISTANCE_DELTA; - public static final DataType AGGREGATE_CALORIES_EXPENDED = TYPE_CALORIES_EXPENDED; - public static final DataType TYPE_HEART_POINTS = new DataType("com.google.heart_minutes", "https://www.googleapis.com/auth/fitness.activity.read", "https://www.googleapis.com/auth/fitness.activity.write", com.google.android.gms.fitness.data.Field.FIELD_INTENSITY); - public static final DataType AGGREGATE_HEART_POINTS = new DataType("com.google.heart_minutes.summary", "https://www.googleapis.com/auth/fitness.activity.read", "https://www.googleapis.com/auth/fitness.activity.write", com.google.android.gms.fitness.data.Field.FIELD_INTENSITY, com.google.android.gms.fitness.data.Field.FIELD_DURATION); - public static final DataType AGGREGATE_HEART_RATE_SUMMARY = new DataType("com.google.heart_rate.summary", "https://www.googleapis.com/auth/fitness.heart_rate.read", "https://www.googleapis.com/auth/fitness.heart_rate.write", com.google.android.gms.fitness.data.Field.FIELD_AVERAGE, com.google.android.gms.fitness.data.Field.FIELD_MAX, com.google.android.gms.fitness.data.Field.FIELD_MIN); - public static final DataType AGGREGATE_LOCATION_BOUNDING_BOX = new DataType("com.google.location.bounding_box", "https://www.googleapis.com/auth/fitness.location.read", "https://www.googleapis.com/auth/fitness.location.write", com.google.android.gms.fitness.data.Field.FIELD_LOW_LATITUDE, com.google.android.gms.fitness.data.Field.FIELD_LOW_LONGITUDE, com.google.android.gms.fitness.data.Field.FIELD_HIGH_LATITUDE, com.google.android.gms.fitness.data.Field.FIELD_HIGH_LONGITUDE); - public static final DataType AGGREGATE_POWER_SUMMARY = new DataType("com.google.power.summary", "https://www.googleapis.com/auth/fitness.activity.read", "https://www.googleapis.com/auth/fitness.activity.write", com.google.android.gms.fitness.data.Field.FIELD_AVERAGE, com.google.android.gms.fitness.data.Field.FIELD_MAX, com.google.android.gms.fitness.data.Field.FIELD_MIN); - public static final DataType AGGREGATE_SPEED_SUMMARY = new DataType("com.google.speed.summary", "https://www.googleapis.com/auth/fitness.location.read", "https://www.googleapis.com/auth/fitness.location.write", com.google.android.gms.fitness.data.Field.FIELD_AVERAGE, com.google.android.gms.fitness.data.Field.FIELD_MAX, com.google.android.gms.fitness.data.Field.FIELD_MIN); - public static final DataType AGGREGATE_BODY_FAT_PERCENTAGE_SUMMARY = new DataType("com.google.body.fat.percentage.summary", "https://www.googleapis.com/auth/fitness.body.read", "https://www.googleapis.com/auth/fitness.body.write", com.google.android.gms.fitness.data.Field.FIELD_AVERAGE, com.google.android.gms.fitness.data.Field.FIELD_MAX, com.google.android.gms.fitness.data.Field.FIELD_MIN); - public static final DataType AGGREGATE_WEIGHT_SUMMARY = new DataType("com.google.weight.summary", "https://www.googleapis.com/auth/fitness.body.read", "https://www.googleapis.com/auth/fitness.body.write", com.google.android.gms.fitness.data.Field.FIELD_AVERAGE, com.google.android.gms.fitness.data.Field.FIELD_MAX, com.google.android.gms.fitness.data.Field.FIELD_MIN); - public static final DataType AGGREGATE_HEIGHT_SUMMARY = new DataType("com.google.height.summary", "https://www.googleapis.com/auth/fitness.body.read", "https://www.googleapis.com/auth/fitness.body.write", com.google.android.gms.fitness.data.Field.FIELD_AVERAGE, com.google.android.gms.fitness.data.Field.FIELD_MAX, com.google.android.gms.fitness.data.Field.FIELD_MIN); - public static final DataType AGGREGATE_NUTRITION_SUMMARY = new DataType("com.google.nutrition.summary", "https://www.googleapis.com/auth/fitness.nutrition.read", "https://www.googleapis.com/auth/fitness.nutrition.write", com.google.android.gms.fitness.data.Field.FIELD_NUTRIENTS, com.google.android.gms.fitness.data.Field.FIELD_MEAL_TYPE); - public static final DataType TYPE_HYDRATION = AGGREGATE_HYDRATION; - public static final DataType TYPE_WORKOUT_SAMPLES = new DataType("com.google.activity.samples", "https://www.googleapis.com/auth/fitness.activity.read", "https://www.googleapis.com/auth/fitness.activity.write", com.google.android.gms.fitness.data.Field.FIELD_ACTIVITY_CONFIDENCE); - public static final DataType TYPE_SLEEP_ATTRIBUTES = new DataType("com.google.internal.sleep_attributes", "https://www.googleapis.com/auth/fitness.sleep.read", "https://www.googleapis.com/auth/fitness.sleep.write", com.google.android.gms.fitness.data.Field.FIELD_FITNESS_SLEEP_ATTRIBUTES); - public static final DataType TYPE_SLEEP_SCHEDULE = new DataType("com.google.internal.sleep_schedule", "https://www.googleapis.com/auth/fitness.sleep.read", "https://www.googleapis.com/auth/fitness.sleep.write", com.google.android.gms.fitness.data.Field.FIELD_FITNESS_SLEEP_SCHEDULE); - public static final DataType TYPE_PACED_WALKING_ATTRIBUTES = new DataType("com.google.internal.paced_walking_attributes", "https://www.googleapis.com/auth/fitness.activity.read", "https://www.googleapis.com/auth/fitness.activity.write", com.google.android.gms.fitness.data.Field.FIELD_FITNESS_PACED_WALKING_ATTRIBUTES); - public static final DataType TYPE_TIME_ZONE_CHANGE = new DataType("com.google.time_zone_change", "https://www.googleapis.com/auth/fitness.location.read", "https://www.googleapis.com/auth/fitness.location.write", com.google.android.gms.fitness.data.Field.FIELD_ZONE_ID); - public static final DataType TYPE_MET = new DataType("com.google.internal.met", "https://www.googleapis.com/auth/fitness.location.read", "https://www.googleapis.com/auth/fitness.location.write", com.google.android.gms.fitness.data.Field.FIELD_MET); - - @Field(1) - public String packageName; - @Field(2) - public List fields; + public static final DataType AGGREGATE_WEIGHT_SUMMARY = new DataType("com.google.weight.summary", "https://www.googleapis.com/auth/fitness.body.read", "https://www.googleapis.com/auth/fitness.body.write", FIELD_AVERAGE, FIELD_MAX, FIELD_MIN); + + @Field(value = 1, getterName = "getName") + @NonNull + private final String name; + @Field(value = 2, getterName = "getFields") + @NonNull + private final List fields; @Field(3) - public String name; + @Nullable + final String readScope; @Field(4) - public String value; + @Nullable + final String writeScope; + + DataType(@NonNull String name, @Nullable String readScope, @Nullable String writeScope, com.google.android.gms.fitness.data.Field... fields) { + this.name = name; + this.readScope = readScope; + this.writeScope = writeScope; + this.fields = Collections.unmodifiableList(Arrays.asList(fields)); + } - public DataType(String packageName, String name, String value, com.google.android.gms.fitness.data.Field... fieldArr) { - this.packageName = packageName; - this.fields = Collections.unmodifiableList(Arrays.asList(fieldArr)); + @Constructor + DataType(@Param(1) @NonNull String name, @Param(2) @NonNull List fields, @Param(3) @Nullable String readScope, @Param(4) @Nullable String writeScope) { this.name = name; - this.value = value; + this.fields = fields; + this.readScope = readScope; + this.writeScope = writeScope; + } + + /** + * Returns the aggregate output type for this type, or {@code null} if the type does not support aggregation. + *

+ * To check if a data type is supported for aggregation, check that the returned type is non-null. + */ + @Nullable + public DataType getAggregateType() { + // TODO + return null; + } + + /** + * Returns the ordered list of fields for the data type. + */ + @NonNull + public List getFields() { + return fields; + } + + /** + * Returns the namespaced name which uniquely identifies this data type. + */ + @NonNull + public String getName() { + return name; + } + + /** + * Returns the index of a field. + * + * @throws IllegalArgumentException If field isn't defined for this data type. + */ + public int indexOf(@NonNull com.google.android.gms.fitness.data.Field field) { + int indexOf = this.fields.indexOf(field); + if (indexOf < 0) throw new IllegalArgumentException("Field not found"); + return indexOf; + } + + /** + * Returns a list of output aggregate data types for the specified {@code inputDataType}. + *

+ * To check if a data type is supported for aggregation, check that the returned list is not empty + * {@code DataType.getAggregatesForInput(dataType).isEmpty()}. + * + * @deprecated Use {@link #getAggregateType()} instead. + */ + @NonNull + @Deprecated + public static List getAggregatesForInput(@NonNull DataType inputDataType) { + DataType aggregateType = inputDataType.getAggregateType(); + if (aggregateType == null) return Collections.emptyList(); + return Collections.singletonList(aggregateType); } - public DataType() { + /** + * Returns the MIME type for a particular {@link DataType}. The MIME type is used in intents such as the data view intent. + */ + @NonNull + public static String getMimeType(@NonNull DataType dataType) { + return MIME_TYPE_PREFIX + dataType.getName(); } @Override diff --git a/play-services-fitness/src/main/java/com/google/android/gms/fitness/data/Device.java b/play-services-fitness/src/main/java/com/google/android/gms/fitness/data/Device.java index 558569cde7..846d3b6edd 100644 --- a/play-services-fitness/src/main/java/com/google/android/gms/fitness/data/Device.java +++ b/play-services-fitness/src/main/java/com/google/android/gms/fitness/data/Device.java @@ -1,29 +1,172 @@ /* * SPDX-FileCopyrightText: 2023 microG Project Team * SPDX-License-Identifier: Apache-2.0 + * Notice: Portions of this file are reproduced from work created and shared by Google and used + * according to terms described in the Creative Commons 4.0 Attribution License. + * See https://developers.google.com/readme/policies for details. */ package com.google.android.gms.fitness.data; +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.pm.PackageManager; +import android.content.res.Configuration; +import android.os.Build; import android.os.Parcel; +import android.provider.Settings; import androidx.annotation.NonNull; import com.google.android.gms.common.internal.safeparcel.AbstractSafeParcelable; import com.google.android.gms.common.internal.safeparcel.SafeParcelable; import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter; +import org.microg.gms.utils.ToStringHelper; +import static android.os.Build.VERSION.SDK_INT; + +/** + * Representation of an integrated device (such as a phone or a wearable) that can hold sensors. Each sensor is exposed as a {@link DataSource}. + */ @SafeParcelable.Class public class Device extends AbstractSafeParcelable { - @Field(1) - public String manufacturer; - @Field(2) - public String model; - @Field(4) - public String uid; - @Field(5) - public int type; + /** + * Constant indicating the device type is not known. + */ + public static final int TYPE_UNKNOWN = 0; + /** + * Constant indicating the device is an Android phone. + */ + public static final int TYPE_PHONE = 1; + /** + * Constant indicating the device is an Android tablet. + */ + public static final int TYPE_TABLET = 2; + /** + * Constant indicating the device is a watch or other wrist-mounted band. + */ + public static final int TYPE_WATCH = 3; + /** + * Constant indicating the device is a chest strap. + */ + public static final int TYPE_CHEST_STRAP = 4; + /** + * Constant indicating the device is a scale or similar device the user steps on. + */ + public static final int TYPE_SCALE = 5; + /** + * Constant indicating the device is a headset, pair of glasses, or other head-mounted device + */ + public static final int TYPE_HEAD_MOUNTED = 6; + + @Field(value = 1, getterName = "getManufacturer") + @NonNull + private final String manufacturer; + @Field(value = 2, getterName = "getModel") + @NonNull + private final String model; + @Field(value = 4, getterName = "getUid") + @NonNull + private final String uid; + @Field(value = 5, getterName = "getType") + private final int type; + @Field(value = 6, getterName = "getPlatformType") + private final int platformType; + + @Constructor + Device(@NonNull @Param(1) String manufacturer, @NonNull @Param(2) String model, @NonNull @Param(4) String uid, @Param(5) int type, @Param(6) int platformType) { + this.manufacturer = manufacturer; + this.model = model; + this.uid = uid; + this.type = type; + this.platformType = platformType; + } + + /** + * Creates a new device. + * + * @param manufacturer The manufacturer of the product/hardware. + * @param model The end-user-visible name for the end product. + * @param uid A serial number or other unique identifier for the particular device hardware. + * @param type The type of device. One of the type constants. + */ + public Device(@NonNull String manufacturer, @NonNull String model, @NonNull String uid, int type) { + this(manufacturer, model, uid, type, 0); + } + + /** + * Returns the manufacturer of the product/hardware. + */ + @NonNull + public String getManufacturer() { + return manufacturer; + } + + /** + * Returns the end-user-visible model name for the device. + */ + @NonNull + public String getModel() { + return model; + } + + /** + * Returns the constant representing the type of the device. This will usually be one of the values from the type constants in this class, but it's + * not required to be. Any other value should be treated as {@link #TYPE_UNKNOWN}. + */ + public int getType() { + return type; + } + + /** + * Returns the serial number or other unique ID for the hardware. + *

+ * Device UIDs are obfuscated based on the calling application's package name. Different applications will see different UIDs for the same + * {@link Device}. If two {@link Device} instances have the same underlying UID, they'll also have the same obfuscated UID within each app (but not across + * apps). + */ + @NonNull + public String getUid() { + return uid; + } + + String getDeviceId() { + return manufacturer + ":" + model + ":" + uid; + } + + int getPlatformType() { + return platformType; + } + + /** + * Returns the Device representation of the local device, which can be used when defining local data sources. + * + * @noinspection deprecation + */ + public static Device getLocalDevice(Context context) { + @SuppressLint("HardwareIds") String uid = Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID); + int type = TYPE_PHONE; + Configuration configuration = context.getResources().getConfiguration(); + PackageManager packageManager = context.getPackageManager(); + if (SDK_INT >= 20 && packageManager.hasSystemFeature(PackageManager.FEATURE_WATCH)) + type = TYPE_WATCH; + else if (packageManager.hasSystemFeature(PackageManager.FEATURE_TELEVISION) || packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)) + type = TYPE_UNKNOWN; // TV + else if (packageManager.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE)) + type = TYPE_UNKNOWN; // Car + else if ((configuration.screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK) > Configuration.SCREENLAYOUT_SIZE_LARGE && configuration.smallestScreenWidthDp >= 600) + type = TYPE_TABLET; + else if (Build.PRODUCT.startsWith("glass_")) + type = TYPE_HEAD_MOUNTED; + return new Device(Build.MANUFACTURER, Build.MODEL, uid, type, 2); + } + + @NonNull + @Override + public String toString() { + return ToStringHelper.name("Device").value(getDeviceId() + ":" + type + ":" + platformType).end(); + } @Override public void writeToParcel(@NonNull Parcel dest, int flags) { diff --git a/play-services-fitness/src/main/java/com/google/android/gms/fitness/data/Field.java b/play-services-fitness/src/main/java/com/google/android/gms/fitness/data/Field.java index a8b951a1d6..3971ecce5e 100644 --- a/play-services-fitness/src/main/java/com/google/android/gms/fitness/data/Field.java +++ b/play-services-fitness/src/main/java/com/google/android/gms/fitness/data/Field.java @@ -1,6 +1,9 @@ /* * SPDX-FileCopyrightText: 2023 microG Project Team * SPDX-License-Identifier: Apache-2.0 + * Notice: Portions of this file are reproduced from work created and shared by Google and used + * according to terms described in the Creative Commons 4.0 Attribution License. + * See https://developers.google.com/readme/policies for details. */ package com.google.android.gms.fitness.data; @@ -9,136 +12,471 @@ import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import com.google.android.gms.common.internal.safeparcel.AbstractSafeParcelable; import com.google.android.gms.common.internal.safeparcel.SafeParcelable; import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter; +import org.microg.gms.common.Hide; +/** + * A field represents one dimension of a data type. It defines the name and format of data. Unlike data type names, field names are not + * namespaced, and only need to be unique within the data type. + *

+ * This class contains constants representing the field names of common data types, each prefixed with {@code FIELD_}. These can be used to + * access and set the fields via {@link DataPoint#getValue(com.google.android.gms.fitness.data.Field)}. + *

+ * Fields for custom data types can be created using {@link DataTypeCreateRequest.Builder#addField(String, int)}. + */ @SafeParcelable.Class public class Field extends AbstractSafeParcelable { - public static final com.google.android.gms.fitness.data.Field FIELD_ACTIVITY = formatIntField("activity"); - public static final com.google.android.gms.fitness.data.Field FIELD_SLEEP_SEGMENT_TYPE = formatIntField("sleep_segment_type"); - public static final com.google.android.gms.fitness.data.Field FIELD_CONFIDENCE = formatFloatField("confidence"); - public static final com.google.android.gms.fitness.data.Field FIELD_STEPS = formatIntField("steps"); - @Deprecated - public static final com.google.android.gms.fitness.data.Field FIELD_STEP_LENGTH = formatFloatField("step_length"); - public static final com.google.android.gms.fitness.data.Field FIELD_DURATION = formatIntField("duration"); - public static final com.google.android.gms.fitness.data.Field FIELD_DURATION_OPTIONAL = formatIntOptionalField("duration"); - public static final com.google.android.gms.fitness.data.Field FIELD_ACTIVITY_DURATION_ASCENDING = formatMapField("activity_duration.ascending"); - public static final com.google.android.gms.fitness.data.Field FIELD_ACTIVITY_DURATION_DESCENDING = formatMapField("activity_duration.descending"); - public static final com.google.android.gms.fitness.data.Field FIELD_BPM = formatFloatField("bpm"); - public static final com.google.android.gms.fitness.data.Field FIELD_RESPIRATORY_RATE = formatFloatField("respiratory_rate"); - public static final com.google.android.gms.fitness.data.Field FIELD_LATITUDE = formatFloatField("latitude"); - public static final com.google.android.gms.fitness.data.Field FIELD_LONGITUDE = formatFloatField("longitude"); - public static final com.google.android.gms.fitness.data.Field FIELD_ACCURACY = formatFloatField("accuracy"); - public static final com.google.android.gms.fitness.data.Field FIELD_ALTITUDE = formatFloatOptionalField("altitude"); - public static final com.google.android.gms.fitness.data.Field FIELD_DISTANCE = formatFloatField("distance"); - public static final com.google.android.gms.fitness.data.Field FIELD_HEIGHT = formatFloatField("height"); - public static final com.google.android.gms.fitness.data.Field FIELD_WEIGHT = formatFloatField("weight"); - public static final com.google.android.gms.fitness.data.Field FIELD_PERCENTAGE = formatFloatField("percentage"); - public static final com.google.android.gms.fitness.data.Field FIELD_SPEED = formatFloatField("speed"); - public static final com.google.android.gms.fitness.data.Field FIELD_RPM = formatFloatField("rpm"); - public static final com.google.android.gms.fitness.data.Field FIELD_FITNESS_GOAL_V2 = formatObjectField("google.android.fitness.GoalV2"); - public static final com.google.android.gms.fitness.data.Field FIELD_FITNESS_DEVICE = formatObjectField("google.android.fitness.Device"); - public static final com.google.android.gms.fitness.data.Field FIELD_REVOLUTIONS = formatIntField("revolutions"); - public static final com.google.android.gms.fitness.data.Field FIELD_CALORIES = formatFloatField("calories"); - public static final com.google.android.gms.fitness.data.Field FIELD_WATTS = formatFloatField("watts"); - public static final com.google.android.gms.fitness.data.Field FIELD_VOLUME = formatFloatField("volume"); - public static final com.google.android.gms.fitness.data.Field FIELD_MEAL_TYPE = formatIntOptionalField("meal_type"); - public static final com.google.android.gms.fitness.data.Field FIELD_FOOD_ITEM = formatStringOptionalField("food_item"); - public static final com.google.android.gms.fitness.data.Field FIELD_NUTRIENTS = formatMapField("nutrients"); - public static final com.google.android.gms.fitness.data.Field FIELD_EXERCISE = formatStringField("exercise"); - public static final com.google.android.gms.fitness.data.Field FIELD_REPETITIONS = formatIntOptionalField("repetitions"); - public static final com.google.android.gms.fitness.data.Field FIELD_RESISTANCE = formatFloatOptionalField("resistance"); - public static final com.google.android.gms.fitness.data.Field FIELD_RESISTANCE_TYPE = formatIntOptionalField("resistance_type"); - public static final com.google.android.gms.fitness.data.Field FIELD_NUM_SEGMENTS = formatIntField("num_segments"); - public static final com.google.android.gms.fitness.data.Field FIELD_AVERAGE = formatFloatField("average"); - public static final com.google.android.gms.fitness.data.Field FIELD_MAX = formatFloatField("max"); - public static final com.google.android.gms.fitness.data.Field FIELD_MIN = formatFloatField("min"); - public static final com.google.android.gms.fitness.data.Field FIELD_LOW_LATITUDE = formatFloatField("low_latitude"); - public static final com.google.android.gms.fitness.data.Field FIELD_LOW_LONGITUDE = formatFloatField("low_longitude"); - public static final com.google.android.gms.fitness.data.Field FIELD_HIGH_LATITUDE = formatFloatField("high_latitude"); - public static final com.google.android.gms.fitness.data.Field FIELD_HIGH_LONGITUDE = formatFloatField("high_longitude"); - public static final com.google.android.gms.fitness.data.Field FIELD_OCCURRENCES = formatIntField("occurrences"); - public static final com.google.android.gms.fitness.data.Field FIELD_SENSOR_TYPE = formatIntField("sensor_type"); - public static final com.google.android.gms.fitness.data.Field FIELD_TIMESTAMPS = formatLongField("timestamps"); - public static final com.google.android.gms.fitness.data.Field FIELD_SENSOR_VALUES = formatDoubleField("sensor_values"); - public static final com.google.android.gms.fitness.data.Field FIELD_INTENSITY = formatFloatField("intensity"); - public static final com.google.android.gms.fitness.data.Field FIELD_ACTIVITY_CONFIDENCE = formatMapField("activity_confidence"); - public static final com.google.android.gms.fitness.data.Field FIELD_PROBABILITY = formatFloatField("probability"); - public static final com.google.android.gms.fitness.data.Field FIELD_FITNESS_SLEEP_ATTRIBUTES = formatObjectField("google.android.fitness.SleepAttributes"); - public static final com.google.android.gms.fitness.data.Field FIELD_FITNESS_SLEEP_SCHEDULE = formatObjectField("google.android.fitness.SleepSchedule"); - @Deprecated - public static final com.google.android.gms.fitness.data.Field FIELD_CIRCUMFERENCE = formatFloatField("circumference"); - public static final com.google.android.gms.fitness.data.Field FIELD_FITNESS_PACED_WALKING_ATTRIBUTES = formatObjectField("google.android.fitness.PacedWalkingAttributes"); - public static final com.google.android.gms.fitness.data.Field FIELD_ZONE_ID = formatStringField("zone_id"); - public static final com.google.android.gms.fitness.data.Field FIELD_MET = formatFloatField("met"); - + /** + * Format constant indicating the field holds integer values. + */ public static final int FORMAT_INT32 = 1; + /** + * Format constant indicating the field holds float values. + */ public static final int FORMAT_FLOAT = 2; + /** + * Format constant indicating the field holds string values. Strings should be kept small whenever possible. Data streams with large string + * values and high data frequency may be down sampled. + */ public static final int FORMAT_STRING = 3; + /** + * Format constant indicating the field holds a map of string keys to values. The valid key space and units for the corresponding value should + * be documented as part of the data type definition. + *

+ * Map values can be set using {@link DataPoint.Builder#setField(com.google.android.gms.fitness.data.Field, java.util.Map)}. + *

+ * Keys should be kept small whenever possible. Data streams with large keys and high data frequency may be down sampled. + */ public static final int FORMAT_MAP = 4; + public static final int FORMAT_LONG = 5; public static final int FORMAT_DOUBLE = 6; public static final int FORMAT_OBJECT = 7; - @Field(1) - public String name; - @Field(2) - public int format; - @Field(3) - public Boolean optional; + /** + * Meal type constant representing that the meal type is unknown. + */ + public static final int MEAL_TYPE_UNKNOWN = 0; + /** + * Meal type constant representing a breakfast meal. + */ + public static final int MEAL_TYPE_BREAKFAST = 1; + /** + * Meal type constant representing a lunch meal. + */ + public static final int MEAL_TYPE_LUNCH = 2; + /** + * Meal type constant representing a dinner meal. + */ + public static final int MEAL_TYPE_DINNER = 3; + /** + * Meal type constant representing a snack meal. + */ + public static final int MEAL_TYPE_SNACK = 4; + /** + * Calcium amount in milligrams. + */ + @NonNull + public static final String NUTRIENT_CALCIUM = "calcium"; + /** + * Calories in kcal. + */ + @NonNull + public static final String NUTRIENT_CALORIES = "calories"; + /** + * Cholesterol in milligrams. + */ + @NonNull + public static final String NUTRIENT_CHOLESTEROL = "cholesterol"; + /** + * Dietary fiber in grams. + */ + @NonNull + public static final String NUTRIENT_DIETARY_FIBER = "dietary_fiber"; + /** + * Iron amount in milligrams. + */ + @NonNull + public static final String NUTRIENT_IRON = "iron"; + /** + * Monounsaturated fat in grams. + */ + @NonNull + public static final String NUTRIENT_MONOUNSATURATED_FAT = "fat.monounsaturated"; + /** + * Polyunsaturated fat in grams. + */ + @NonNull + public static final String NUTRIENT_POLYUNSATURATED_FAT = "fat.polyunsaturated"; + /** + * Potassium in milligrams. + */ + @NonNull + public static final String NUTRIENT_POTASSIUM = "potassium"; + /** + * Protein amount in grams. + */ + @NonNull + public static final String NUTRIENT_PROTEIN = "protein"; + /** + * Saturated fat in grams. + */ + @NonNull + public static final String NUTRIENT_SATURATED_FAT = "fat.saturated"; + /** + * Sodium in milligrams. + */ + @NonNull + public static final String NUTRIENT_SODIUM = "sodium"; + /** + * Sugar amount in grams. + */ + @NonNull + public static final String NUTRIENT_SUGAR = "sugar"; + /** + * Total carbohydrates in grams. + */ + @NonNull + public static final String NUTRIENT_TOTAL_CARBS = "carbs.total"; + /** + * Total fat in grams. + */ + @NonNull + public static final String NUTRIENT_TOTAL_FAT = "fat.total"; + /** + * Trans fat in grams. + */ + @NonNull + public static final String NUTRIENT_TRANS_FAT = "fat.trans"; + /** + * Unsaturated fat in grams. + */ + @NonNull + public static final String NUTRIENT_UNSATURATED_FAT = "fat.unsaturated"; + /** + * Vitamin A amount in International Units (IU). For converting from daily percentages, the FDA recommended 5000 IUs Daily Value can be + * used. + */ + @NonNull + public static final String NUTRIENT_VITAMIN_A = "vitamin_a"; + /** + * Vitamin C amount in milligrams. + */ + @NonNull + public static final String NUTRIENT_VITAMIN_C = "vitamin_c"; + /** + * The resistance type is unknown, unspecified, or not represented by any canonical values. + */ + public static final int RESISTANCE_TYPE_UNKNOWN = 0; + /** + * The user is using a barbell for resistance. The specified resistance should include the weight of the bar, as well as weights added to both + * sides. + */ + public static final int RESISTANCE_TYPE_BARBELL = 1; + /** + * The user is using a cable for resistance. When two cables are being used (one for each arm), the specified resistance should include the + * weight being pulled by one cable. + */ + public static final int RESISTANCE_TYPE_CABLE = 2; + /** + * The user is using dumbells for resistance. The specified resistance should include the weight of a single dumbell. + */ + public static final int RESISTANCE_TYPE_DUMBBELL = 3; + /** + * The user is using a kettlebell for resistance. + */ + public static final int RESISTANCE_TYPE_KETTLEBELL = 4; + /** + * The user is performing the exercise in a machine. The specified resistance should match the weight specified by the machine. + */ + public static final int RESISTANCE_TYPE_MACHINE = 5; + /** + * The user is using their own body weight for resistance. + */ + public static final int RESISTANCE_TYPE_BODY = 6; + /** + * The accuracy of an accompanied value (such as location). + */ + public static final com.google.android.gms.fitness.data.Field FIELD_ACCURACY = createFloatField("accuracy"); + /** + * An activity type of {@link FitnessActivities}, encoded as an integer for efficiency. The activity value should be stored using + * {@link DataPoint.Builder#setActivityField(Field, String)}. + */ + public static final com.google.android.gms.fitness.data.Field FIELD_ACTIVITY = createIntField("activity"); + /** + * An altitude of a location represented as a float, in meters above sea level. Some location samples don't have an altitude value so this field + * might not be set. + */ + public static final com.google.android.gms.fitness.data.Field FIELD_ALTITUDE = createOptionalFloatField("altitude"); + /** + * An average value. + */ + public static final com.google.android.gms.fitness.data.Field FIELD_AVERAGE = createFloatField("average"); + /** + * A heart rate in beats per minute. + */ + public static final com.google.android.gms.fitness.data.Field FIELD_BPM = createFloatField("bpm"); + /** + * Calories in kcal. + */ + public static final com.google.android.gms.fitness.data.Field FIELD_CALORIES = createFloatField("calories"); + /** + * Circumference of a body part, in centimeters. + * + * @deprecated There is no applicable replacement field. + */ + @Deprecated + public static final com.google.android.gms.fitness.data.Field FIELD_CIRCUMFERENCE = createFloatField("circumference"); + /** + * The confidence of an accompanied value, specified as a value between 0.0 and 100.0. + */ + public static final com.google.android.gms.fitness.data.Field FIELD_CONFIDENCE = createFloatField("confidence"); + /** + * A distance in meters. + */ + public static final com.google.android.gms.fitness.data.Field FIELD_DISTANCE = createFloatField("distance"); + /** + * A field containing duration. The units of the field are defined by the outer data type. + */ + public static final com.google.android.gms.fitness.data.Field FIELD_DURATION = createIntField("duration"); + /** + * A workout exercise, as represented by one of the constants in {@link WorkoutExercises}. + */ + public static final com.google.android.gms.fitness.data.Field FIELD_EXERCISE = createStringField("exercise"); + /** + * The corresponding food item for a nutrition entry. + */ + public static final com.google.android.gms.fitness.data.Field FIELD_FOOD_ITEM = createOptionalStringField("food_item"); + /** + * A height in meters. + */ + public static final com.google.android.gms.fitness.data.Field FIELD_HEIGHT = createFloatField("height"); + /** + * A high latitude of a location bounding box represented as a float, in degrees. + */ + public static final com.google.android.gms.fitness.data.Field FIELD_HIGH_LATITUDE = createFloatField("high_latitude"); + /** + * A high longitude of a location bounding box represented as a float, in degrees. + */ + public static final com.google.android.gms.fitness.data.Field FIELD_HIGH_LONGITUDE = createFloatField("high_longitude"); + /** + * Intensity of user activity, represented as a float. + */ + public static final com.google.android.gms.fitness.data.Field FIELD_INTENSITY = createFloatField("intensity"); + /** + * A latitude of a location represented as a float, in degrees. + */ + public static final com.google.android.gms.fitness.data.Field FIELD_LATITUDE = createFloatField("latitude"); + /** + * A longitude of a location represented as a float, in degrees. + */ + public static final com.google.android.gms.fitness.data.Field FIELD_LONGITUDE = createFloatField("longitude"); + /** + * A low latitude of a location bounding box represented as a float, in degrees. + */ + public static final com.google.android.gms.fitness.data.Field FIELD_LOW_LATITUDE = createFloatField("low_latitude"); + /** + * A low longitude of a location bounding box represented as a float, in degrees. + */ + public static final com.google.android.gms.fitness.data.Field FIELD_LOW_LONGITUDE = createFloatField("low_longitude"); + /** + * A maximum value. + */ + public static final com.google.android.gms.fitness.data.Field FIELD_MAX = createFloatField("max"); + /** + * A maximum int value. + */ + public static final com.google.android.gms.fitness.data.Field FIELD_MAX_INT = createIntField("max"); + /** + * Type of meal, represented as the appropriate int constant. + */ + public static final com.google.android.gms.fitness.data.Field FIELD_MEAL_TYPE = createOptionalIntField("meal_type"); + /** + * A minimum value. + */ + public static final com.google.android.gms.fitness.data.Field FIELD_MIN = createFloatField("min"); + /** + * A minimum int value. + */ + public static final com.google.android.gms.fitness.data.Field FIELD_MIN_INT = createIntField("min"); + /** + * A number of segments. + */ + public static final com.google.android.gms.fitness.data.Field FIELD_NUM_SEGMENTS = createIntField("num_segments"); + /** + * Nutrients ingested by the user, represented as a float map of nutrient key to quantity. The valid keys of the map are listed in this class using + * the {@code NUTRIENT_} prefix. The documentation for each key describes the unit of its value. + */ + public static final com.google.android.gms.fitness.data.Field FIELD_NUTRIENTS = createMapField("nutrients"); + /** + * How many occurrences of an event there were in a time range. For sample data types this should not be set to more than one. + */ + public static final com.google.android.gms.fitness.data.Field FIELD_OCCURRENCES = createIntField("occurrences"); + /** + * A percentage value, between 0 and 100. + */ + public static final com.google.android.gms.fitness.data.Field FIELD_PERCENTAGE = createFloatField("percentage"); + /** + * A count of repetitions for a single set of a workout exercise. + */ + public static final com.google.android.gms.fitness.data.Field FIELD_REPETITIONS = createOptionalIntField("repetitions"); + /** + * The resistance of the exercise (or weight), in kg. + */ + public static final com.google.android.gms.fitness.data.Field FIELD_RESISTANCE = createOptionalFloatField("resistance"); + /** + * The type of resistance used in this exercise, represented as the appropriate int constant. + */ + public static final com.google.android.gms.fitness.data.Field FIELD_RESISTANCE_TYPE = createOptionalIntField("resistance_type"); + /** + * A count of revolutions. + */ + public static final com.google.android.gms.fitness.data.Field FIELD_REVOLUTIONS = createIntField("revolutions"); + /** + * Revolutions per minute or rate per minute. + */ + public static final com.google.android.gms.fitness.data.Field FIELD_RPM = createFloatField("rpm"); + /** + * Sleep Segment type defined in {@link SleepStages}. + */ + public static final com.google.android.gms.fitness.data.Field FIELD_SLEEP_SEGMENT_TYPE = createIntField("sleep_segment_type"); + /** + * A speed in meter/sec. + */ + public static final com.google.android.gms.fitness.data.Field FIELD_SPEED = createFloatField("speed"); + /** + * A count of steps. + */ + public static final com.google.android.gms.fitness.data.Field FIELD_STEPS = createIntField("steps"); + /** + * Distance between steps in meters. + * + * @deprecated There is no applicable replacement field. + */ + @Deprecated + public static final com.google.android.gms.fitness.data.Field FIELD_STEP_LENGTH = createFloatField("step_length"); + /** + * Volume in liters. + */ + public static final com.google.android.gms.fitness.data.Field FIELD_VOLUME = createFloatField("volume"); + /** + * Power in watts. + */ + public static final com.google.android.gms.fitness.data.Field FIELD_WATTS = createFloatField("watts"); + /** + * A weight in kilograms. + */ + public static final com.google.android.gms.fitness.data.Field FIELD_WEIGHT = createFloatField("weight"); - public Field() { - } + public static final com.google.android.gms.fitness.data.Field FIELD_ACTIVITY_CONFIDENCE = createMapField("activity_confidence"); + public static final com.google.android.gms.fitness.data.Field FIELD_ACTIVITY_DURATION_ASCENDING = createMapField("activity_duration.ascending"); + public static final com.google.android.gms.fitness.data.Field FIELD_ACTIVITY_DURATION_DESCENDING = createMapField("activity_duration.descending"); + public static final com.google.android.gms.fitness.data.Field FIELD_DURATION_OPTIONAL = createOptionalIntField("duration"); + public static final com.google.android.gms.fitness.data.Field FIELD_FITNESS_DEVICE = createObjectField("google.android.fitness.Device"); + public static final com.google.android.gms.fitness.data.Field FIELD_FITNESS_GOAL_V2 = createObjectField("google.android.fitness.GoalV2"); + public static final com.google.android.gms.fitness.data.Field FIELD_FITNESS_SLEEP_ATTRIBUTES = createObjectField("google.android.fitness.SleepAttributes"); + public static final com.google.android.gms.fitness.data.Field FIELD_FITNESS_SLEEP_SCHEDULE = createObjectField("google.android.fitness.SleepSchedule"); + public static final com.google.android.gms.fitness.data.Field FIELD_FITNESS_PACED_WALKING_ATTRIBUTES = createObjectField("google.android.fitness.PacedWalkingAttributes"); + public static final com.google.android.gms.fitness.data.Field FIELD_MET = createFloatField("met"); + public static final com.google.android.gms.fitness.data.Field FIELD_PROBABILITY = createFloatField("probability"); + public static final com.google.android.gms.fitness.data.Field FIELD_RESPIRATORY_RATE = createFloatField("respiratory_rate"); + public static final com.google.android.gms.fitness.data.Field FIELD_SENSOR_TYPE = createIntField("sensor_type"); + public static final com.google.android.gms.fitness.data.Field FIELD_SENSOR_VALUES = createDoubleField("sensor_values"); + public static final com.google.android.gms.fitness.data.Field FIELD_TIMESTAMPS = createLongField("timestamps"); + public static final com.google.android.gms.fitness.data.Field FIELD_ZONE_ID = createStringField("zone_id"); + + @Field(value = 1, getterName = "getName") + @NonNull + private final String name; + @Field(value = 2, getterName = "getFormat") + private final int format; + @Field(value = 3, getterName = "isOptional") + @Nullable + private final Boolean optional; - public Field(String name, int format, Boolean optional) { + @Constructor + public Field(@Param(1) @NonNull String name, @Param(2) int format, @Param(3) @Nullable Boolean optional) { this.name = name; this.format = format; this.optional = optional; } - public Field(String name, int format) { + public Field(@NonNull String name, int format) { this(name, format, null); } - public static com.google.android.gms.fitness.data.Field formatIntField(String name) { + /** + * Returns the format of the field, as one of the format constant values. + */ + public int getFormat() { + return format; + } + + /** + * Returns the name of the field. + */ + public String getName() { + return name; + } + + public Boolean isOptional() { + return optional; + } + + @Override + public int hashCode() { + return name.hashCode(); + } + + @Override + public final boolean equals(Object o) { + if (!(o instanceof com.google.android.gms.fitness.data.Field)) return false; + + com.google.android.gms.fitness.data.Field field = (com.google.android.gms.fitness.data.Field) o; + return format == field.format && name.equals(field.name); + } + + public static com.google.android.gms.fitness.data.Field createIntField(String name) { return new com.google.android.gms.fitness.data.Field(name, FORMAT_INT32); } - public static com.google.android.gms.fitness.data.Field formatFloatField(String name) { + public static com.google.android.gms.fitness.data.Field createFloatField(String name) { return new com.google.android.gms.fitness.data.Field(name, FORMAT_FLOAT); } - public static com.google.android.gms.fitness.data.Field formatStringField(String name) { + public static com.google.android.gms.fitness.data.Field createStringField(String name) { return new com.google.android.gms.fitness.data.Field(name, FORMAT_STRING); } - public static com.google.android.gms.fitness.data.Field formatMapField(String name) { + public static com.google.android.gms.fitness.data.Field createMapField(String name) { return new com.google.android.gms.fitness.data.Field(name, FORMAT_MAP); } - public static com.google.android.gms.fitness.data.Field formatLongField(String name) { + public static com.google.android.gms.fitness.data.Field createLongField(String name) { return new com.google.android.gms.fitness.data.Field(name, FORMAT_LONG); } - public static com.google.android.gms.fitness.data.Field formatDoubleField(String name) { + public static com.google.android.gms.fitness.data.Field createDoubleField(String name) { return new com.google.android.gms.fitness.data.Field(name, FORMAT_DOUBLE); } - public static com.google.android.gms.fitness.data.Field formatObjectField(String name) { + public static com.google.android.gms.fitness.data.Field createObjectField(String name) { return new com.google.android.gms.fitness.data.Field(name, FORMAT_OBJECT); } - public static com.google.android.gms.fitness.data.Field formatIntOptionalField(String name) { + public static com.google.android.gms.fitness.data.Field createOptionalIntField(String name) { return new com.google.android.gms.fitness.data.Field(name, FORMAT_INT32, true); } - public static com.google.android.gms.fitness.data.Field formatFloatOptionalField(String name) { + public static com.google.android.gms.fitness.data.Field createOptionalFloatField(String name) { return new com.google.android.gms.fitness.data.Field(name, FORMAT_FLOAT, true); } - public static com.google.android.gms.fitness.data.Field formatStringOptionalField(String name) { + public static com.google.android.gms.fitness.data.Field createOptionalStringField(String name) { return new com.google.android.gms.fitness.data.Field(name, FORMAT_STRING, true); } diff --git a/play-services-fitness/src/main/java/com/google/android/gms/fitness/data/MapValue.java b/play-services-fitness/src/main/java/com/google/android/gms/fitness/data/MapValue.java new file mode 100644 index 0000000000..f03711f100 --- /dev/null +++ b/play-services-fitness/src/main/java/com/google/android/gms/fitness/data/MapValue.java @@ -0,0 +1,60 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.fitness.data; + +import android.os.Parcel; +import androidx.annotation.NonNull; +import com.google.android.gms.common.internal.safeparcel.AbstractSafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter; +import org.microg.gms.common.Hide; + +import static com.google.android.gms.fitness.data.Field.FORMAT_FLOAT; + +@Hide +@SafeParcelable.Class +public class MapValue extends AbstractSafeParcelable { + @Field(value = 1, getterName = "getFormat") + private final int format; + @Field(value = 2, getterName = "getValue") + private final float value; + + @Constructor + public MapValue(@Param(1) int format, @Param(2) float value) { + this.format = format; + this.value = value; + } + + @NonNull + public static MapValue ofFloat(float value) { + return new MapValue(FORMAT_FLOAT, value); + } + + public int getFormat() { + return format; + } + + float getValue() { + return value; + } + + public float asFloat() { + if (format != FORMAT_FLOAT) throw new IllegalStateException("MapValue is not a float"); + return value; + } + + @Override + public int hashCode() { + return (int) value; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + CREATOR.writeToParcel(this, dest, flags); + } + + public static final SafeParcelableCreatorAndWriter CREATOR = findCreator(MapValue.class); +} diff --git a/play-services-fitness/src/main/java/com/google/android/gms/fitness/data/Session.java b/play-services-fitness/src/main/java/com/google/android/gms/fitness/data/Session.java index 33132cb9b3..8082ca3435 100644 --- a/play-services-fitness/src/main/java/com/google/android/gms/fitness/data/Session.java +++ b/play-services-fitness/src/main/java/com/google/android/gms/fitness/data/Session.java @@ -1,20 +1,208 @@ /* * SPDX-FileCopyrightText: 2023 microG Project Team * SPDX-License-Identifier: Apache-2.0 + * Notice: Portions of this file are reproduced from work created and shared by Google and used + * according to terms described in the Creative Commons 4.0 Attribution License. + * See https://developers.google.com/readme/policies for details. */ package com.google.android.gms.fitness.data; +import android.content.Intent; +import android.content.pm.PackageManager; import android.os.Parcel; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import com.google.android.gms.common.internal.safeparcel.AbstractSafeParcelable; import com.google.android.gms.common.internal.safeparcel.SafeParcelable; import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter; +import com.google.android.gms.common.internal.safeparcel.SafeParcelableSerializer; +import java.util.concurrent.TimeUnit; + +/** + * A Session represents a time interval with associated metadata. Sessions provide a mechanism to store user-visible groups of related + * stream data in a useful and shareable manner, and allows for easy querying of the data in a detailed or aggregated fashion. The start and + * end times for sessions will be controlled by applications, and can be used to represent user-friendly groupings of activities, such as "bike + * ride", "marathon training run". Any data in Google Fit which falls within this time range is implicitly associated with the session. + */ @SafeParcelable.Class public class Session extends AbstractSafeParcelable { + + /** + * Name for the parcelable intent extra containing a session. It can be extracted using {@link #extract(Intent)}. + */ + @NonNull + public static final String EXTRA_SESSION = "vnd.google.fitness.session"; + + /** + * The common prefix for session MIME types. The MIME type for a particular session will be this prefix followed by the session's activity + * name. + *

+ * The session's activity type is returned by {@link #getActivity()}. The MIME type can be computed from the activity using {@link #getMimeType(String)} + */ + @NonNull + public static final String MIME_TYPE_PREFIX = "vnd.google.fitness.session/"; + + @Field(value = 1, getterName = "getStartTimeMillis") + private final long startTimeMillis; + @Field(value = 2, getterName = "getEndTimeMillis") + private final long endTimeMillis; + @Field(value = 3, getterName = "getName") + @Nullable + private final String name; + @Field(value = 4, getterName = "getIdentifier") + @NonNull + private final String identifier; + @Field(value = 5, getterName = "getDescription") + @NonNull + private final String description; + @Field(value = 7, getterName = "getActivityType") + private final int activityType; + @Field(value = 8, getterName = "getApplication") + private final Application application; + @Field(value = 9, getterName = "getActiveTimeMillis") + @Nullable + private final Long activeTimeMillis; + + @Constructor + Session(@Param(1) long startTimeMillis, @Param(2) long endTimeMillis, @Param(3) @Nullable String name, @Param(4) @NonNull String identifier, @Param(5) @NonNull String description, @Param(7) int activityType, @Param(8) Application application, @Param(9) @Nullable Long activeTimeMillis) { + this.startTimeMillis = startTimeMillis; + this.endTimeMillis = endTimeMillis; + this.name = name; + this.identifier = identifier; + this.description = description; + this.activityType = activityType; + this.application = application; + this.activeTimeMillis = activeTimeMillis; + } + + /** + * Returns the active time period of the session. + *

+ * Make sure to use {@link #hasActiveTime()} before using this method. + * + * @throws IllegalStateException {@link #hasActiveTime()} returns false. + */ + public long getActiveTime(@NonNull TimeUnit timeUnit) { + if (activeTimeMillis == null) throw new IllegalStateException("Active time is not set"); + return timeUnit.convert(activeTimeMillis, TimeUnit.MILLISECONDS); + } + + /** + * Returns the activity associated with this session, if set. Else returns {@link FitnessActivities#UNKNOWN}. + */ + @NonNull + public String getActivity() { + return null; // TODO + } + + /** + * Returns the package name for the application responsible for adding the session. or {@code null} if unset/unknown. The {@link PackageManager} can be + * used to query relevant data on the application, such as the name, icon, or logo. + */ + @Nullable + public String getAppPackageName() { + if (application == null) return null; + return application.getPackageName(); + } + + /** + * Returns the description for this session. + */ + @NonNull + public String getDescription() { + return description; + } + + /** + * Returns the end time for the session, in the given unit since epoch. If the session is ongoing (it hasn't ended yet), this will return 0. + */ + public long getEndTime(@NonNull TimeUnit timeUnit) { + return timeUnit.convert(endTimeMillis, TimeUnit.MILLISECONDS); + } + + /** + * Returns the identifier for this session. + */ + @NonNull + public String getIdentifier() { + return identifier; + } + + /** + * Returns the name for this session, if set. + */ + @Nullable + public String getName() { + return name; + } + + /** + * Returns the start time for the session, in the given time unit since epoch. A valid start time is always set. + */ + public long getStartTime(@NonNull TimeUnit timeUnit) { + return timeUnit.convert(startTimeMillis, TimeUnit.MILLISECONDS); + } + + /** + * Returns whether the session active time is set. + */ + public boolean hasActiveTime() { + return activeTimeMillis != null; + } + + /** + * Returns whether the session is ongoing. If the session has ended, this will return false. + */ + public boolean isOngoing() { + return endTimeMillis == 0; + } + + Application getApplication() { + return application; + } + + int getActivityType() { + return activityType; + } + + long getStartTimeMillis() { + return startTimeMillis; + } + + long getEndTimeMillis() { + return endTimeMillis; + } + + @Nullable + Long getActiveTimeMillis() { + return activeTimeMillis; + } + + /** + * Extracts the session extra from the given intent, such as a callback intent received after registering to session start/end notifications, or an intent to view a session. + * + * @param intent The extracted Session, or {@code null} if the given intent does not contain a Session. + */ + @Nullable + public static Session extract(@NonNull Intent intent) { + return SafeParcelableSerializer.deserializeFromBytes(intent.getByteArrayExtra(EXTRA_SESSION), CREATOR); + } + + /** + * Returns the MIME type which describes a Session for a particular activity. The MIME type is used in intents such as the session view + * intent. + * + * @param activity One of the activities in {@link FitnessActivities}. + */ + @NonNull + public static String getMimeType(@NonNull String activity) { + return MIME_TYPE_PREFIX + activity; + } + @Override public void writeToParcel(@NonNull Parcel dest, int flags) { CREATOR.writeToParcel(this, dest, flags); diff --git a/play-services-fitness/src/main/java/com/google/android/gms/fitness/data/SessionDataSet.java b/play-services-fitness/src/main/java/com/google/android/gms/fitness/data/SessionDataSet.java new file mode 100644 index 0000000000..7037956f24 --- /dev/null +++ b/play-services-fitness/src/main/java/com/google/android/gms/fitness/data/SessionDataSet.java @@ -0,0 +1,37 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.fitness.data; + +import android.os.Parcel; + +import androidx.annotation.NonNull; + +import com.google.android.gms.common.internal.safeparcel.AbstractSafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter; +import org.microg.gms.common.Hide; + +@SafeParcelable.Class +@Hide +public class SessionDataSet extends AbstractSafeParcelable { + public static final SafeParcelableCreatorAndWriter CREATOR = findCreator(SessionDataSet.class); + + @Field(1) + public final Session session; + @Field(2) + public final DataSet dataSet; + + @Constructor + public SessionDataSet(@Param(1) Session session, @Param(2) DataSet dataSet) { + this.session = session; + this.dataSet = dataSet; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + CREATOR.writeToParcel(this, dest, flags); + } +} diff --git a/play-services-fitness/src/main/java/com/google/android/gms/fitness/data/Value.java b/play-services-fitness/src/main/java/com/google/android/gms/fitness/data/Value.java new file mode 100644 index 0000000000..357a462228 --- /dev/null +++ b/play-services-fitness/src/main/java/com/google/android/gms/fitness/data/Value.java @@ -0,0 +1,270 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + * Notice: Portions of this file are reproduced from work created and shared by Google and used + * according to terms described in the Creative Commons 4.0 Attribution License. + * See https://developers.google.com/readme/policies for details. + */ + +package com.google.android.gms.fitness.data; + +import android.os.Bundle; +import android.os.Build.VERSION; +import android.os.Parcel; + +import androidx.annotation.NonNull; + +import androidx.annotation.Nullable; +import com.google.android.gms.common.internal.safeparcel.AbstractSafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter; + +import java.util.Map; + +import static com.google.android.gms.fitness.data.Field.*; + +/** + * Holder object for the value of a single field in a data point. Values are not constructed directly; a value for each field of the data type + * is created for each data point. + *

+ * A field value has a particular format, and should be set and read using the format-specific methods. For instance, a float value should be set + * via {@link #setFloat(float)} and read via {@link #asFloat()}. Formats are defined as constants in {@link com.google.android.gms.fitness.data.Field}. + */ +@SafeParcelable.Class +public final class Value extends AbstractSafeParcelable { + @Field(value = 1, getterName = "getFormat") + public int format; + @Field(value = 2, getterName = "isSet") + public boolean set; + @Field(value = 3, getterName = "getValue") + public float value; + @Field(value = 4, getterName = "getStringValue") + public String stringValue; + @Field(value = 5, getterName = "getMapValue") + @Nullable + public Bundle mapValue; + @Field(value = 6, getterName = "getIntArrayValue") + public int[] intArrayValue; + @Field(value = 7, getterName = "getFloatArrayValue") + public float[] floatArrayValue; + @Field(value = 8, getterName = "getBlob") + public byte[] blob; + + @Constructor + public Value(@Param(1) int format, @Param(2) boolean set, @Param(3) float value, @Param(4) String stringValue, @Param(5) @Nullable Bundle mapValue, @Param(6) int[] intArrayValue, @Param(7) float[] floatArrayValue, @Param(8) byte[] blob) { + this.format = format; + this.set = set; + this.value = value; + this.stringValue = stringValue; + this.mapValue = mapValue; + this.intArrayValue = intArrayValue; + this.floatArrayValue = floatArrayValue; + this.blob = blob; + } + + Value(int format) { + this(format, false, 0f, null, null, null, null, null); + } + + /** + * Returns the value of this object as an activity. The integer representation of the activity is converted to a String prior to returning. + * + * @return One of the constants from {@link FitnessActivities}; {@link FitnessActivities#UNKNOWN} if the object does not hold a valid activity + * representation + * @throws IllegalStateException If this {@link Value} does not correspond to a {@link com.google.android.gms.fitness.data.Field#FORMAT_INT32} + */ + public String asActivity() { + return null; // TODO + } + + /** + * Returns the value of this object as a float. + * + * @throws IllegalStateException If this {@link Value} does not correspond to a {@link com.google.android.gms.fitness.data.Field#FORMAT_FLOAT} + */ + public float asFloat() { + if (format != FORMAT_FLOAT) throw new IllegalStateException("Value is not a float."); + return value; + } + + /** + * Returns the value of this object as a int. + * + * @throws IllegalStateException If this {@link Value} does not correspond to a {@link com.google.android.gms.fitness.data.Field#FORMAT_INT32} + */ + public int asInt() { + if (format != FORMAT_INT32) throw new IllegalStateException("Value is not a int."); + return Float.floatToRawIntBits(this.value); + } + + /** + * Returns the value of this object as a string. + * + * @throws IllegalStateException If this {@link Value} does not correspond to a {@link com.google.android.gms.fitness.data.Field#FORMAT_STRING} + */ + @NonNull + public String asString() { + if (format != FORMAT_STRING) throw new IllegalStateException("Value is not a string."); + if (stringValue == null) return ""; + return stringValue; + } + + /** + * Clears any value currently associated with the given {@code key} in the map. This method can be used only on map values. + * + * @param key The key you're modifying. + * @deprecated Use {@link DataPoint.Builder} to construct new {@link DataPoint} instances. + */ + @Deprecated + public void clearKey(String key) { + if (format != FORMAT_MAP) throw new IllegalStateException("Value is not a map."); + if (mapValue != null) { + mapValue.remove(key); + } + } + + /** + * Returns the format of this value, which matches the appropriate field in the data type definition. + * + * @return One of the format constants from {@link com.google.android.gms.fitness.data.Field}. + */ + public int getFormat() { + return format; + } + + /** + * Returns the value of the given key in the map as a {@link Float}. + * + * @return {@code null} if the key doesn't have a set value in the map. + * @throws IllegalStateException If this {@link Value} does not correspond to a {@link com.google.android.gms.fitness.data.Field#FORMAT_MAP} + */ + @Nullable + public Float getKeyValue(@NonNull String key) { + if (format != FORMAT_MAP) throw new IllegalStateException("Value is not a map."); + if (mapValue == null || !mapValue.containsKey(key)) { + return null; + } + mapValue.setClassLoader(MapValue.class.getClassLoader()); + if (VERSION.SDK_INT >= 33) { + return mapValue.getParcelable(key, MapValue.class).asFloat(); + } else { + //noinspection deprecation + return ((MapValue) mapValue.getParcelable(key)).asFloat(); + } + } + + /** + * Returns {@code true} if this object's value has been set by calling one of the setters. + */ + public boolean isSet() { + return set; + } + + float getValue() { + return value; + } + + String getStringValue() { + return stringValue; + } + + @Nullable + Bundle getMapValue() { + return mapValue; + } + + int[] getIntArrayValue() { + return intArrayValue; + } + + float[] getFloatArrayValue() { + return floatArrayValue; + } + + byte[] getBlob() { + return blob; + } + + /** + * Updates this value object to represent an activity value. Activities are internally represented as integers for storage. + * + * @param activity One of the activities from {@link FitnessActivities} + * @deprecated Use {@link DataPoint.Builder} to construct new {@link DataPoint} instances. + */ + public void setActivity(String activity) { + setInt(0); // TODO + } + + /** + * Updates this value object to represent a float value. Any previous values associated with this object are erased. + * + * @param value The new value that this objects holds. + * @deprecated Use {@link DataPoint.Builder} to construct new {@link DataPoint} instances. + */ + public void setFloat(float value) { + if (format != FORMAT_FLOAT) throw new IllegalStateException("Value is not a float."); + this.set = true; + this.value = value; + } + + /** + * Updates this value object to represent an int value. Any previous values are erased. + * + * @param value The new value that this object holds. + * @deprecated Use {@link DataPoint.Builder} to construct new {@link DataPoint} instances. + */ + public void setInt(int value) { + if (format != FORMAT_INT32) throw new IllegalStateException("Value is not a int."); + this.set = true; + this.value = Float.intBitsToFloat(value); + } + + /** + * Updates the value for a given key in the map to the given float value. Any previous values associated with the key are erased. This method + * can be used only on map values. + *

+ * Key values should be kept small whenever possible. This is specially important for high frequency streams, since large keys may result in + * down sampling. + * + * @param key The key you're modifying. + * @param value The new value for the given key. + * @deprecated Use {@link DataPoint.Builder} to construct new {@link DataPoint} instances. + */ + public void setKeyValue(String key, float value) { + if (format != FORMAT_MAP) throw new IllegalStateException("Value is not a map."); + this.set = true; + if (mapValue == null) mapValue = new Bundle(); + mapValue.putParcelable(key, MapValue.ofFloat(value)); + } + + void setMap(@NonNull Map value) { + if (format != FORMAT_MAP) throw new IllegalStateException("Value is not a map."); + this.set = true; + if (mapValue == null) mapValue = new Bundle(); + for (String key : value.keySet()) { + mapValue.putParcelable(key, MapValue.ofFloat(value.get(key))); + } + } + + /** + * Updates this value object to represent a string value. Any previous values associated with this object are erased. + *

+ * String values should be kept small whenever possible. This is specially important for high frequency streams, since large values may result + * in down sampling. + * + * @param value The new value that this objects holds. + * @deprecated Use {@link DataPoint.Builder} to construct new {@link DataPoint} instances. + */ + public void setString(String value) { + if (format != FORMAT_STRING) throw new IllegalStateException("Value is not a string."); + this.set = true; + this.stringValue = value; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + CREATOR.writeToParcel(this, dest, flags); + } + + public static final SafeParcelableCreatorAndWriter CREATOR = findCreator(Value.class); +} \ No newline at end of file diff --git a/play-services-fitness/src/main/java/com/google/android/gms/fitness/request/DataTypeCreateRequest.java b/play-services-fitness/src/main/java/com/google/android/gms/fitness/request/DataTypeCreateRequest.java new file mode 100644 index 0000000000..21902cadec --- /dev/null +++ b/play-services-fitness/src/main/java/com/google/android/gms/fitness/request/DataTypeCreateRequest.java @@ -0,0 +1,35 @@ +/** + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.fitness.request; + +import android.os.Parcel; + +import androidx.annotation.NonNull; + +import com.google.android.gms.common.internal.safeparcel.AbstractSafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter; +import com.google.android.gms.fitness.internal.IDataTypeCallback; + +import java.util.List; + +@SafeParcelable.Class +public class DataTypeCreateRequest extends AbstractSafeParcelable { + public static final SafeParcelableCreatorAndWriter CREATOR = findCreator(DataTypeCreateRequest.class); + + @Field(1) + public String name; + @Field(2) + public List fields; + @Field(3) + public IDataTypeCallback callback; + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + CREATOR.writeToParcel(this, dest, flags); + } + +} diff --git a/play-services-fitness/src/main/java/com/google/android/gms/fitness/request/DisableFitRequest.java b/play-services-fitness/src/main/java/com/google/android/gms/fitness/request/DisableFitRequest.java new file mode 100644 index 0000000000..5034ea7997 --- /dev/null +++ b/play-services-fitness/src/main/java/com/google/android/gms/fitness/request/DisableFitRequest.java @@ -0,0 +1,29 @@ +/** + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.fitness.request; + +import android.os.Parcel; + +import androidx.annotation.NonNull; + +import com.google.android.gms.common.internal.safeparcel.AbstractSafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter; +import com.google.android.gms.fitness.internal.IStatusCallback; + +@SafeParcelable.Class +public class DisableFitRequest extends AbstractSafeParcelable { + public static final SafeParcelableCreatorAndWriter CREATOR = findCreator(DisableFitRequest.class); + + @Field(1) + public IStatusCallback callback; + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + CREATOR.writeToParcel(this, dest, flags); + } + +} diff --git a/play-services-fitness/src/main/java/com/google/android/gms/fitness/request/ReadDataTypeRequest.java b/play-services-fitness/src/main/java/com/google/android/gms/fitness/request/ReadDataTypeRequest.java new file mode 100644 index 0000000000..631f28d6e1 --- /dev/null +++ b/play-services-fitness/src/main/java/com/google/android/gms/fitness/request/ReadDataTypeRequest.java @@ -0,0 +1,31 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.fitness.request; + +import android.os.Parcel; + +import androidx.annotation.NonNull; + +import com.google.android.gms.common.internal.safeparcel.AbstractSafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter; +import com.google.android.gms.fitness.internal.IDataTypeCallback; + +@SafeParcelable.Class +public class ReadDataTypeRequest extends AbstractSafeParcelable { + public static final SafeParcelableCreatorAndWriter CREATOR = findCreator(ReadDataTypeRequest.class); + + @Field(1) + public String name; + @Field(3) + public IDataTypeCallback callback; + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + CREATOR.writeToParcel(this, dest, flags); + } + +} diff --git a/play-services-fitness/src/main/java/com/google/android/gms/fitness/request/SessionInsertRequest.java b/play-services-fitness/src/main/java/com/google/android/gms/fitness/request/SessionInsertRequest.java new file mode 100644 index 0000000000..1ca56f2695 --- /dev/null +++ b/play-services-fitness/src/main/java/com/google/android/gms/fitness/request/SessionInsertRequest.java @@ -0,0 +1,42 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.fitness.request; + +import android.os.Parcel; + +import androidx.annotation.NonNull; + +import com.google.android.gms.common.internal.safeparcel.AbstractSafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter; +import com.google.android.gms.fitness.data.DataPoint; +import com.google.android.gms.fitness.data.DataSet; +import com.google.android.gms.fitness.data.Session; +import com.google.android.gms.fitness.internal.IStatusCallback; + +import java.util.List; + +@SafeParcelable.Class +public class SessionInsertRequest extends AbstractSafeParcelable { + + @Field(1) + public Session seesion; + @Field(2) + public List dataSets; + + @Field(3) + public List aggregateDataPoints; + + @Field(4) + public IStatusCallback callback; + + public void writeToParcel(@NonNull Parcel dest, int flags) { + CREATOR.writeToParcel(this, dest, flags); + } + + public static final SafeParcelableCreatorAndWriter CREATOR = findCreator(SessionInsertRequest.class); + +} diff --git a/play-services-fitness/src/main/java/com/google/android/gms/fitness/request/SessionReadRequest.java b/play-services-fitness/src/main/java/com/google/android/gms/fitness/request/SessionReadRequest.java new file mode 100644 index 0000000000..0cbaeb59f4 --- /dev/null +++ b/play-services-fitness/src/main/java/com/google/android/gms/fitness/request/SessionReadRequest.java @@ -0,0 +1,54 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.fitness.request; + +import android.os.Parcel; + +import androidx.annotation.NonNull; + +import com.google.android.gms.common.internal.safeparcel.AbstractSafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter; +import com.google.android.gms.fitness.data.DataSource; +import com.google.android.gms.fitness.data.DataType; +import com.google.android.gms.fitness.internal.ISessionReadCallback; + +import java.util.List; + +@SafeParcelable.Class +public class SessionReadRequest extends AbstractSafeParcelable { + @Field(1) + public String sessionName; + @Field(2) + public String sessionId; + @Field(3) + public long StartTimeMillis; + @Field(4) + public long EndTimeMillis; + @Field(5) + public List dataTypes; + @Field(6) + public List dataSources; + @Field(7) + public boolean includeSessionsFromAllApps; + @Field(8) + public boolean areServerQueriesEnabled; + @Field(9) + public List excludedPackages; + @Field(10) + public ISessionReadCallback callback; + @Field(value = 12, defaultValue = "true") + public boolean areActivitySessionsIncluded; + @Field(value = 13, defaultValue = "false") + public boolean areSleepSessionsIncluded; + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + CREATOR.writeToParcel(this, dest, flags); + } + + public static final SafeParcelableCreatorAndWriter CREATOR = findCreator(SessionReadRequest.class); +} diff --git a/play-services-fitness/src/main/java/com/google/android/gms/fitness/request/SessionRegistrationRequest.java b/play-services-fitness/src/main/java/com/google/android/gms/fitness/request/SessionRegistrationRequest.java new file mode 100644 index 0000000000..e12a001b8f --- /dev/null +++ b/play-services-fitness/src/main/java/com/google/android/gms/fitness/request/SessionRegistrationRequest.java @@ -0,0 +1,32 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.fitness.request; + +import android.app.PendingIntent; +import android.os.Parcel; + +import androidx.annotation.NonNull; + +import com.google.android.gms.common.api.internal.IStatusCallback; +import com.google.android.gms.common.internal.safeparcel.AbstractSafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter; + +@SafeParcelable.Class +public class SessionRegistrationRequest extends AbstractSafeParcelable { + + @Field(1) + public PendingIntent intent; + @Field(2) + public IStatusCallback callback; + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + CREATOR.writeToParcel(this, dest, flags); + } + + public static final SafeParcelableCreatorAndWriter CREATOR = findCreator(SessionRegistrationRequest.class); +} diff --git a/play-services-fitness/src/main/java/com/google/android/gms/fitness/request/SessionStartRequest.java b/play-services-fitness/src/main/java/com/google/android/gms/fitness/request/SessionStartRequest.java new file mode 100644 index 0000000000..5d8fddf34f --- /dev/null +++ b/play-services-fitness/src/main/java/com/google/android/gms/fitness/request/SessionStartRequest.java @@ -0,0 +1,32 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.fitness.request; + +import android.os.Parcel; + +import androidx.annotation.NonNull; + +import com.google.android.gms.common.internal.safeparcel.AbstractSafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter; +import com.google.android.gms.fitness.data.Session; +import com.google.android.gms.fitness.internal.IStatusCallback; + +@SafeParcelable.Class +public class SessionStartRequest extends AbstractSafeParcelable { + @Field(1) + public Session session; + @Field(2) + public IStatusCallback callback; + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + CREATOR.writeToParcel(this, dest, flags); + } + + public static final SafeParcelableCreatorAndWriter CREATOR = findCreator(SessionStartRequest.class); + +} diff --git a/play-services-fitness/src/main/java/com/google/android/gms/fitness/request/SessionStopRequest.java b/play-services-fitness/src/main/java/com/google/android/gms/fitness/request/SessionStopRequest.java new file mode 100644 index 0000000000..729b0bcfaf --- /dev/null +++ b/play-services-fitness/src/main/java/com/google/android/gms/fitness/request/SessionStopRequest.java @@ -0,0 +1,33 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.fitness.request; + +import android.os.Parcel; + +import androidx.annotation.NonNull; + +import com.google.android.gms.common.internal.safeparcel.AbstractSafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter; +import com.google.android.gms.fitness.internal.ISessionStopCallback; + +@SafeParcelable.Class +public class SessionStopRequest extends AbstractSafeParcelable { + @Field(1) + public String name; + @Field(2) + public String identifier; + @Field(3) + public ISessionStopCallback callback; + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + CREATOR.writeToParcel(this, dest, flags); + } + + public static final SafeParcelableCreatorAndWriter CREATOR = findCreator(SessionStopRequest.class); + +} \ No newline at end of file diff --git a/play-services-fitness/src/main/java/com/google/android/gms/fitness/request/SessionUnregistrationRequest.java b/play-services-fitness/src/main/java/com/google/android/gms/fitness/request/SessionUnregistrationRequest.java new file mode 100644 index 0000000000..e7967466c9 --- /dev/null +++ b/play-services-fitness/src/main/java/com/google/android/gms/fitness/request/SessionUnregistrationRequest.java @@ -0,0 +1,31 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.fitness.request; + +import android.app.PendingIntent; +import android.os.Parcel; + +import androidx.annotation.NonNull; + +import com.google.android.gms.common.api.internal.IStatusCallback; +import com.google.android.gms.common.internal.safeparcel.AbstractSafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter; + +@SafeParcelable.Class +public class SessionUnregistrationRequest extends AbstractSafeParcelable { + @Field(1) + public PendingIntent intent; + @Field(2) + public IStatusCallback callback; + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + CREATOR.writeToParcel(this, dest, flags); + } + + public static final SafeParcelableCreatorAndWriter CREATOR = findCreator(SessionUnregistrationRequest.class); +} diff --git a/play-services-fitness/src/main/java/com/google/android/gms/fitness/result/DataSourceStatsResult.java b/play-services-fitness/src/main/java/com/google/android/gms/fitness/result/DataSourceStatsResult.java index 82dd795d7d..d6d79a51b5 100644 --- a/play-services-fitness/src/main/java/com/google/android/gms/fitness/result/DataSourceStatsResult.java +++ b/play-services-fitness/src/main/java/com/google/android/gms/fitness/result/DataSourceStatsResult.java @@ -13,7 +13,9 @@ import com.google.android.gms.common.internal.safeparcel.SafeParcelable; import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter; import com.google.android.gms.fitness.data.DataSource; +import org.microg.gms.common.Hide; +@Hide @SafeParcelable.Class public class DataSourceStatsResult extends AbstractSafeParcelable { @Field(1) diff --git a/play-services-fitness/src/main/java/com/google/android/gms/fitness/result/DataStatsResult.java b/play-services-fitness/src/main/java/com/google/android/gms/fitness/result/DataStatsResult.java index 54b9edb493..68a2c1d259 100644 --- a/play-services-fitness/src/main/java/com/google/android/gms/fitness/result/DataStatsResult.java +++ b/play-services-fitness/src/main/java/com/google/android/gms/fitness/result/DataStatsResult.java @@ -14,10 +14,12 @@ import com.google.android.gms.common.internal.safeparcel.AbstractSafeParcelable; import com.google.android.gms.common.internal.safeparcel.SafeParcelable; import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter; +import org.microg.gms.common.Hide; import java.io.Closeable; import java.util.List; +@Hide @SafeParcelable.Class public class DataStatsResult extends AbstractSafeParcelable implements Closeable { @Field(1) diff --git a/play-services-fitness/src/main/java/com/google/android/gms/fitness/result/DataTypeResult.java b/play-services-fitness/src/main/java/com/google/android/gms/fitness/result/DataTypeResult.java new file mode 100644 index 0000000000..b8956449d3 --- /dev/null +++ b/play-services-fitness/src/main/java/com/google/android/gms/fitness/result/DataTypeResult.java @@ -0,0 +1,76 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + * Notice: Portions of this file are reproduced from work created and shared by Google and used + * according to terms described in the Creative Commons 4.0 Attribution License. + * See https://developers.google.com/readme/policies for details. + */ + +package com.google.android.gms.fitness.result; + +import android.app.Activity; +import android.os.Parcel; + +import androidx.annotation.NonNull; + +import androidx.annotation.Nullable; +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.common.api.Status; +import com.google.android.gms.common.internal.safeparcel.AbstractSafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter; +import com.google.android.gms.fitness.data.DataType; +import org.microg.gms.common.Hide; + +/** + * Result of {@link ConfigApi#readDataType(GoogleApiClient, String)}. + *

+ * The method {@link #getStatus()} can be used to confirm if the request was successful. On success, the returned data type can be accessed + * via {@link #getDataType()}. + *

+ * In case the calling app is missing the required permissions, the returned status has status code set to + * {@link FitnessStatusCodes#NEEDS_OAUTH_PERMISSIONS}. In this case the caller should use {@link Status#startResolutionForResult(Activity, int)} + * to start an intent to get the necessary consent from the user before retrying the request. + *

+ * In case the app attempts to read a custom data type created by other app, the returned status has status code set to + * {@link FitnessStatusCodes#INCONSISTENT_DATA_TYPE}. + * + * @deprecated No replacement. + */ +@Deprecated +@SafeParcelable.Class +public class DataTypeResult extends AbstractSafeParcelable { + + @Field(value = 1, getterName = "getStatus") + private final Status status; + @Field(value = 3, getterName = "getDataType") + @Nullable + private final DataType dataType; + + @Constructor + @Hide + public DataTypeResult(@Param(1) Status status, @Param(3) @Nullable DataType dataType) { + this.status = status; + this.dataType = dataType; + } + + /** + * Returns the new custom data type inserted, or {@code null} if the request failed. + */ + @Nullable + public DataType getDataType() { + return dataType; + } + + public Status getStatus() { + return status; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + CREATOR.writeToParcel(this, dest, flags); + } + + public static final SafeParcelableCreatorAndWriter CREATOR = findCreator(DataTypeResult.class); + +} diff --git a/play-services-fitness/src/main/java/com/google/android/gms/fitness/result/SessionReadResult.java b/play-services-fitness/src/main/java/com/google/android/gms/fitness/result/SessionReadResult.java new file mode 100644 index 0000000000..c81d3a2431 --- /dev/null +++ b/play-services-fitness/src/main/java/com/google/android/gms/fitness/result/SessionReadResult.java @@ -0,0 +1,133 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + * Notice: Portions of this file are reproduced from work created and shared by Google and used + * according to terms described in the Creative Commons 4.0 Attribution License. + * See https://developers.google.com/readme/policies for details. + */ + +package com.google.android.gms.fitness.result; + +import android.app.Activity; +import android.os.Parcel; + +import androidx.annotation.NonNull; + +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.common.api.Status; +import com.google.android.gms.common.internal.safeparcel.AbstractSafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter; +import com.google.android.gms.fitness.data.DataSet; +import com.google.android.gms.fitness.data.DataType; +import com.google.android.gms.fitness.data.Session; +import com.google.android.gms.fitness.data.SessionDataSet; +import com.google.android.gms.fitness.request.SessionReadRequest; +import org.microg.gms.common.Hide; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Result of {@link SessionsApi#readSession(GoogleApiClient, SessionReadRequest)}. + * Contains all Sessions and their corresponding data sets that matched the filters specified in the {@link SessionReadRequest}. + *

+ * The method {@link #getStatus()} can be used to confirm if the request was successful. + *

+ * In case the calling app is missing the required permissions, the returned status has status code set to + * {@link FitnessStatusCodes#NEEDS_OAUTH_PERMISSIONS}. In this case the caller should use {@link Status#startResolutionForResult(Activity, int)} + * to start an intent to get the necessary consent from the user before retrying the request. + *

+ * The method {@link #getSessions()} returns all sessions that are returned for the request. The method {@link #getDataSet(Session, DataType)} returns + * {@link DataSet} for a particular Session and {@link DataType} from the result. + *

+ * In case the app tried to read data for a custom data type created by another app, the returned status has status code set to + * {@link FitnessStatusCodes#INCONSISTENT_DATA_TYPE}. + */ +@SafeParcelable.Class +public class SessionReadResult extends AbstractSafeParcelable { + @Field(value = 1, getterName = "getSessions") + @NonNull + private final List sessions; + @Field(value = 2, getterName = "getSessionDataSets") + @NonNull + private final List sessionDataSets; + @Field(value = 3, getterName = "getStatus") + @NonNull + private final Status status; + + @Constructor + @Hide + public SessionReadResult(@Param(1) @NonNull List sessions, @Param(2) @NonNull List sessionDataSets, @Param(3) @NonNull Status status) { + this.sessions = sessions; + this.sessionDataSets = sessionDataSets; + this.status = status; + } + + /** + * Returns the data sets for a given {@code session} and {@code dataType}. If a specific data source was requested for this data type in the read request, the + * returned data set is from that source. Else, the default data source for this data type is used. Returns empty if no data for the requested data + * type is found. + * + * @return Data sets for the given session and data type, empty if no data was found. Multiple data sets may be returned for a given type, based + * on the read request + * @throws IllegalArgumentException If the given session was not part of getSessions() output. + */ + @NonNull + public List getDataSet(@NonNull Session session, @NonNull DataType dataType) { + if (!sessions.contains(session)) throw new IllegalArgumentException("Attempting to read data for session which was not returned"); + List dataSets = new ArrayList<>(); + for (SessionDataSet sessionDataSet : this.sessionDataSets) { + if (session.equals(sessionDataSet.session) && dataType.equals(sessionDataSet.dataSet.getDataType())) { + dataSets.add(sessionDataSet.dataSet); + } + } + return dataSets; + } + + /** + * Returns the data sets for all data sources for a given {@code session}. If a specific data source was requested for a data type in the read request, + * the returned data set is from that source. Else, the default data source for the requested data type is used. + * + * @return Data sets for the given session for all data sources, empty if no data was found. Multiple data sets may be returned for a given type, + * based on the read request + * @throws IllegalArgumentException If the given session was not part of getSessions() output + */ + @NonNull + public List getDataSet(@NonNull Session session) { + if (!sessions.contains(session)) throw new IllegalArgumentException("Attempting to read data for session which was not returned"); + List dataSets = new ArrayList<>(); + for (SessionDataSet sessionDataSet : sessionDataSets) { + if (session.equals(sessionDataSet.session)) { + dataSets.add(sessionDataSet.dataSet); + } + } + return dataSets; + } + + /** + * Returns all sessions that matched the requested filters. + */ + @NonNull + public List getSessions() { + return this.sessions; + } + + @NonNull + public Status getStatus() { + return this.status; + } + + @NonNull + List getSessionDataSets() { + return sessionDataSets; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + CREATOR.writeToParcel(this, dest, flags); + } + + public static final SafeParcelableCreatorAndWriter CREATOR = findCreator(SessionReadResult.class); +} diff --git a/play-services-fitness/src/main/java/com/google/android/gms/fitness/result/SessionStopResult.java b/play-services-fitness/src/main/java/com/google/android/gms/fitness/result/SessionStopResult.java new file mode 100644 index 0000000000..b4b4e6e5a1 --- /dev/null +++ b/play-services-fitness/src/main/java/com/google/android/gms/fitness/result/SessionStopResult.java @@ -0,0 +1,74 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + * Notice: Portions of this file are reproduced from work created and shared by Google and used + * according to terms described in the Creative Commons 4.0 Attribution License. + * See https://developers.google.com/readme/policies for details. + */ + +package com.google.android.gms.fitness.result; + +import android.app.Activity; +import android.os.Parcel; + +import androidx.annotation.NonNull; + +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.common.api.Status; +import com.google.android.gms.common.internal.safeparcel.AbstractSafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter; +import com.google.android.gms.fitness.data.Session; +import org.microg.gms.common.Hide; + +import java.util.List; + +/** + * Result of {@link SessionsApi#stopSession(GoogleApiClient, String)}. + *

+ * The method {@link #getStatus()} can be used to confirm if the request was successful. + *

+ * In case the calling app is missing the required permissions, the returned status has status code set to + * {@link FitnessStatusCodes#NEEDS_OAUTH_PERMISSIONS}. In this case the caller should use {@link Status#startResolutionForResult(Activity, int)} + * to start an intent to get the necessary consent from the user before retrying the request. + */ +@SafeParcelable.Class +public class SessionStopResult extends AbstractSafeParcelable { + @Field(value = 2, getterName = "getStatus") + @NonNull + private final Status status; + @Field(value = 3, getterName = "getSessions") + @NonNull + private final List sessions; + + @Constructor + @Hide + public SessionStopResult(@Param(2) @NonNull Status status, @Param(3) @NonNull List sessions) { + this.status = status; + this.sessions = sessions; + } + + /** + * Returns the list of sessions that were stopped by the request. Returns an empty list if no active session was stopped. + */ + @NonNull + public List getSessions() { + return sessions; + } + + /** + * Returns the status of the call to Google Fit. {@link Status#isSuccess()} can be used to determine whether the call succeeded. In the case of + * failure, you can inspect the status to determine the reason. + */ + @NonNull + public Status getStatus() { + return status; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + CREATOR.writeToParcel(this, dest, flags); + } + + public static final SafeParcelableCreatorAndWriter CREATOR = findCreator(SessionStopResult.class); +} diff --git a/settings.gradle b/settings.gradle index 0c2ee077ef..eb719860b4 100644 --- a/settings.gradle +++ b/settings.gradle @@ -89,6 +89,7 @@ include ':play-services-conscrypt-provider-core' sublude ':play-services-cronet:core' sublude ':play-services-droidguard:core' sublude ':play-services-fido:core' +sublude ':play-services-fitness:core' sublude ':play-services-gmscompliance:core' sublude ':play-services-location:core' sublude ':play-services-location:core:base' From ad7e79ecdb5efb9a0fe185a066bba7d792bcf8a0 Mon Sep 17 00:00:00 2001 From: DaVinci9196 <150454414+DaVinci9196@users.noreply.github.com> Date: Sun, 15 Dec 2024 04:27:29 +0800 Subject: [PATCH 129/132] DynamiteModule: Added method (#2664) Co-authored-by: Marvin W --- .../gms/ads/dynamite/ModuleDescriptor.java | 17 +++++++++++++++++ .../gms/chimera/container/DynamiteContext.java | 7 +++++-- .../chimera/container/DynamiteModuleInfo.java | 11 +++++++++-- 3 files changed, 31 insertions(+), 4 deletions(-) diff --git a/play-services-ads/core/src/main/java/com/google/android/gms/dynamite/descriptors/com/google/android/gms/ads/dynamite/ModuleDescriptor.java b/play-services-ads/core/src/main/java/com/google/android/gms/dynamite/descriptors/com/google/android/gms/ads/dynamite/ModuleDescriptor.java index 4583d77b57..f147bc76dd 100644 --- a/play-services-ads/core/src/main/java/com/google/android/gms/dynamite/descriptors/com/google/android/gms/ads/dynamite/ModuleDescriptor.java +++ b/play-services-ads/core/src/main/java/com/google/android/gms/dynamite/descriptors/com/google/android/gms/ads/dynamite/ModuleDescriptor.java @@ -5,10 +5,27 @@ package com.google.android.gms.dynamite.descriptors.com.google.android.gms.ads.dynamite; +import android.content.Context; +import android.content.ContextWrapper; +import android.webkit.WebSettings; import androidx.annotation.Keep; @Keep public class ModuleDescriptor { public static final String MODULE_ID = "com.google.android.gms.ads.dynamite"; public static final int MODULE_VERSION = 230500001; + + /** + * The ads module might try to access the user agent, requiring initialization on the main thread, + * which may result in deadlocks when invoked from any other thread. This only happens with microG, + * because we don't use the highly privileged SELinux Sandbox that regular Play Services uses + * (which allows apps to read the user-agent from Play Services instead of the WebView). To prevent + * the issue we pre-emptively initialize the WebView. + */ + public static void init(Context context) { + if (context instanceof ContextWrapper) { + context = ((ContextWrapper) context).getBaseContext(); + } + WebSettings.getDefaultUserAgent(context); + } } diff --git a/play-services-core/src/main/java/com/google/android/gms/chimera/container/DynamiteContext.java b/play-services-core/src/main/java/com/google/android/gms/chimera/container/DynamiteContext.java index cbc93d2777..f93fdae0d3 100644 --- a/play-services-core/src/main/java/com/google/android/gms/chimera/container/DynamiteContext.java +++ b/play-services-core/src/main/java/com/google/android/gms/chimera/container/DynamiteContext.java @@ -88,11 +88,14 @@ public static DynamiteContext create(String moduleId, Context originalContext) { DynamiteModuleInfo moduleInfo = new DynamiteModuleInfo(moduleId); Context gmsContext = originalContext.createPackageContext(Constants.GMS_PACKAGE_NAME, 0); Context originalAppContext = originalContext.getApplicationContext(); + DynamiteContext dynamiteContext; if (originalAppContext == null || originalAppContext == originalContext) { - return new DynamiteContext(moduleInfo, originalContext, gmsContext, null); + dynamiteContext = new DynamiteContext(moduleInfo, originalContext, gmsContext, null); } else { - return new DynamiteContext(moduleInfo, originalContext, gmsContext, new DynamiteContext(moduleInfo, originalAppContext, gmsContext, null)); + dynamiteContext = new DynamiteContext(moduleInfo, originalContext, gmsContext, new DynamiteContext(moduleInfo, originalAppContext, gmsContext, null)); } + moduleInfo.init(dynamiteContext); + return dynamiteContext; } catch (PackageManager.NameNotFoundException e) { Log.w(TAG, e); return null; diff --git a/play-services-core/src/main/java/com/google/android/gms/chimera/container/DynamiteModuleInfo.java b/play-services-core/src/main/java/com/google/android/gms/chimera/container/DynamiteModuleInfo.java index 637a269fca..bad67c00a5 100644 --- a/play-services-core/src/main/java/com/google/android/gms/chimera/container/DynamiteModuleInfo.java +++ b/play-services-core/src/main/java/com/google/android/gms/chimera/container/DynamiteModuleInfo.java @@ -8,8 +8,7 @@ import java.util.Collection; import java.util.Collections; -import static android.content.Context.CONTEXT_IGNORE_SECURITY; -import static android.content.Context.CONTEXT_INCLUDE_CODE; +import android.content.Context; public class DynamiteModuleInfo { private Class descriptor; @@ -51,4 +50,12 @@ public Collection getMergedClasses() { return Collections.emptySet(); } } + + public void init(Context dynamiteContext) { + try { + descriptor.getMethod("init", Context.class).invoke(null, dynamiteContext); + } catch (Exception e) { + // Ignore + } + } } From a33defb154a02b5891442a30aca26ffdd1e5ebc3 Mon Sep 17 00:00:00 2001 From: DaVinci9196 <150454414+DaVinci9196@users.noreply.github.com> Date: Sun, 15 Dec 2024 16:03:27 +0800 Subject: [PATCH 130/132] Google Maps location information sharing page settings button fix (#2634) Co-authored-by: Marvin W --- play-services-core/src/main/AndroidManifest.xml | 4 ++++ .../kotlin/org/microg/gms/accountsettings/ui/MainActivity.kt | 5 ++--- .../kotlin/org/microg/gms/accountsettings/ui/extensions.kt | 1 + 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/play-services-core/src/main/AndroidManifest.xml b/play-services-core/src/main/AndroidManifest.xml index b9d69cb41a..498980221e 100644 --- a/play-services-core/src/main/AndroidManifest.xml +++ b/play-services-core/src/main/AndroidManifest.xml @@ -770,6 +770,10 @@ + + + + 0 } ?: 1 val product = intent?.getStringExtra(EXTRA_SCREEN_MY_ACTIVITY_PRODUCT) val kidOnboardingParams = intent?.getStringExtra(EXTRA_SCREEN_KID_ONBOARDING_PARAMS) diff --git a/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/extensions.kt b/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/extensions.kt index 30a3b01be9..a142321579 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/extensions.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/extensions.kt @@ -10,6 +10,7 @@ const val ACTION_MY_ACCOUNT = "com.google.android.gms.accountsettings.MY_ACCOUNT const val ACTION_ACCOUNT_PREFERENCES_SETTINGS = "com.google.android.gms.accountsettings.ACCOUNT_PREFERENCES_SETTINGS" const val ACTION_PRIVACY_SETTINGS = "com.google.android.gms.accountsettings.PRIVACY_SETTINGS" const val ACTION_SECURITY_SETTINGS = "com.google.android.gms.accountsettings.SECURITY_SETTINGS" +const val ACTION_LOCATION_SHARING = "com.google.android.gms.location.settings.LOCATION_SHARING" const val EXTRA_CALLING_PACKAGE_NAME = "extra.callingPackageName" const val EXTRA_IGNORE_ACCOUNT = "extra.ignoreAccount" From c65410ac05637b59055615dc3e599b2ad78c22d4 Mon Sep 17 00:00:00 2001 From: DaVinci9196 <150454414+DaVinci9196@users.noreply.github.com> Date: Wed, 18 Dec 2024 07:29:52 +0800 Subject: [PATCH 131/132] Added initial support for Play Integrity service (#2599) Co-authored-by: Marvin W --- build.gradle | 1 + play-services-droidguard/build.gradle | 5 + .../droidguard/DroidGuardChimeraService.java | 8 +- .../droidguard/core/DroidGuardHandleImpl.kt | 4 +- .../droidguard/core/DroidGuardPreferences.kt | 2 +- .../core/DroidGuardResultCreator.kt | 2 +- ...actory.kt => NetworkHandleProxyFactory.kt} | 72 +-- .../gms/droidguard/DroidGuardApiClient.java | 15 + .../microg/gms/droidguard}/BytesException.kt | 2 +- .../org/microg/gms/droidguard}/HandleProxy.kt | 26 +- .../gms/droidguard/HandleProxyFactory.kt | 117 +++++ vending-app/build.gradle | 4 + vending-app/src/main/AndroidManifest.xml | 16 + .../protocol/IExpressIntegrityService.aidl | 15 + .../IExpressIntegrityServiceCallback.aidl | 12 + .../integrity/protocol/IIntegrityService.aidl | 14 + .../protocol/IIntegrityServiceCallback.aidl | 10 + .../protocol/IRequestDialogCallback.aidl | 10 + .../vending/licensing/LicenseChecker.kt | 7 +- .../android/vending/VendingRequestHeaders.kt} | 36 +- .../android/finsky/IntegrityExtensions.kt | 460 ++++++++++++++++++ .../DeviceIntegrity.kt | 14 + .../DeviceIntegrityAndExpiredKey.kt | 8 + .../DeviceIntegrityResponse.kt | 9 + .../ExpressIntegrityService.kt | 338 +++++++++++++ .../ExpressIntegritySession.kt | 15 + .../IntermediateIntegrity.kt | 21 + .../IntermediateIntegrityResponse.kt | 10 + .../PackageInformation.kt | 7 + .../com/google/android/finsky/extensions.kt | 6 +- .../integrityservice/IntegrityService.kt | 220 +++++++++ .../finsky/model/IntegrityErrorCode.kt | 172 +++++++ vending-app/src/main/proto/Integrity.proto | 312 ++++++++++++ 33 files changed, 1847 insertions(+), 123 deletions(-) rename play-services-droidguard/core/src/main/kotlin/org/microg/gms/droidguard/core/{HandleProxyFactory.kt => NetworkHandleProxyFactory.kt} (71%) rename play-services-droidguard/{core/src/main/kotlin/org/microg/gms/droidguard/core => src/main/kotlin/org/microg/gms/droidguard}/BytesException.kt (92%) rename play-services-droidguard/{core/src/main/kotlin/org/microg/gms/droidguard/core => src/main/kotlin/org/microg/gms/droidguard}/HandleProxy.kt (62%) create mode 100644 play-services-droidguard/src/main/kotlin/org/microg/gms/droidguard/HandleProxyFactory.kt create mode 100644 vending-app/src/main/aidl/com/google/android/play/core/integrity/protocol/IExpressIntegrityService.aidl create mode 100644 vending-app/src/main/aidl/com/google/android/play/core/integrity/protocol/IExpressIntegrityServiceCallback.aidl create mode 100644 vending-app/src/main/aidl/com/google/android/play/core/integrity/protocol/IIntegrityService.aidl create mode 100644 vending-app/src/main/aidl/com/google/android/play/core/integrity/protocol/IIntegrityServiceCallback.aidl create mode 100644 vending-app/src/main/aidl/com/google/android/play/core/integrity/protocol/IRequestDialogCallback.aidl rename vending-app/src/main/{java/com/android/vending/licensing/LicenseRequestHeaders.kt => kotlin/com/android/vending/VendingRequestHeaders.kt} (82%) create mode 100644 vending-app/src/main/kotlin/com/google/android/finsky/IntegrityExtensions.kt create mode 100644 vending-app/src/main/kotlin/com/google/android/finsky/expressintegrityservice/DeviceIntegrity.kt create mode 100644 vending-app/src/main/kotlin/com/google/android/finsky/expressintegrityservice/DeviceIntegrityAndExpiredKey.kt create mode 100644 vending-app/src/main/kotlin/com/google/android/finsky/expressintegrityservice/DeviceIntegrityResponse.kt create mode 100644 vending-app/src/main/kotlin/com/google/android/finsky/expressintegrityservice/ExpressIntegrityService.kt create mode 100644 vending-app/src/main/kotlin/com/google/android/finsky/expressintegrityservice/ExpressIntegritySession.kt create mode 100644 vending-app/src/main/kotlin/com/google/android/finsky/expressintegrityservice/IntermediateIntegrity.kt create mode 100644 vending-app/src/main/kotlin/com/google/android/finsky/expressintegrityservice/IntermediateIntegrityResponse.kt create mode 100644 vending-app/src/main/kotlin/com/google/android/finsky/expressintegrityservice/PackageInformation.kt create mode 100644 vending-app/src/main/kotlin/com/google/android/finsky/integrityservice/IntegrityService.kt create mode 100644 vending-app/src/main/kotlin/com/google/android/finsky/model/IntegrityErrorCode.kt create mode 100644 vending-app/src/main/proto/Integrity.proto diff --git a/build.gradle b/build.gradle index 07307bbf5c..d4c1a8cefa 100644 --- a/build.gradle +++ b/build.gradle @@ -28,6 +28,7 @@ buildscript { ext.slf4jVersion = '1.7.36' ext.volleyVersion = '1.2.1' ext.wireVersion = '4.8.0' + ext.tinkVersion = '1.13.0' ext.androidBuildGradleVersion = '8.2.2' diff --git a/play-services-droidguard/build.gradle b/play-services-droidguard/build.gradle index a253811a4c..78d9af9a3a 100644 --- a/play-services-droidguard/build.gradle +++ b/play-services-droidguard/build.gradle @@ -5,6 +5,7 @@ apply plugin: 'com.android.library' apply plugin: 'maven-publish' +apply plugin: 'kotlin-android' apply plugin: 'signing' android { @@ -31,6 +32,10 @@ android { sourceCompatibility = 1.8 targetCompatibility = 1.8 } + + kotlinOptions { + jvmTarget = 1.8 + } } apply from: '../gradle/publish-android.gradle' diff --git a/play-services-droidguard/core/src/main/java/com/google/android/gms/droidguard/DroidGuardChimeraService.java b/play-services-droidguard/core/src/main/java/com/google/android/gms/droidguard/DroidGuardChimeraService.java index a25cbd87aa..ed15575a30 100644 --- a/play-services-droidguard/core/src/main/java/com/google/android/gms/droidguard/DroidGuardChimeraService.java +++ b/play-services-droidguard/core/src/main/java/com/google/android/gms/droidguard/DroidGuardChimeraService.java @@ -17,7 +17,7 @@ import org.microg.gms.droidguard.core.DroidGuardServiceBroker; import org.microg.gms.droidguard.GuardCallback; -import org.microg.gms.droidguard.core.HandleProxyFactory; +import org.microg.gms.droidguard.core.NetworkHandleProxyFactory; import org.microg.gms.droidguard.PingData; import org.microg.gms.droidguard.Request; @@ -30,7 +30,7 @@ public class DroidGuardChimeraService extends TracingIntentService { public static final Object a = new Object(); // factory - public HandleProxyFactory b; + public NetworkHandleProxyFactory b; // widevine public Object c; // executor @@ -51,7 +51,7 @@ public DroidGuardChimeraService() { setIntentRedelivery(true); } - public DroidGuardChimeraService(HandleProxyFactory factory, Object ping, Object database) { + public DroidGuardChimeraService(NetworkHandleProxyFactory factory, Object ping, Object database) { super("DG"); setIntentRedelivery(true); this.b = factory; @@ -120,7 +120,7 @@ public final IBinder onBind(Intent intent) { @Override public void onCreate() { this.e = new Object(); - this.b = new HandleProxyFactory(this); + this.b = new NetworkHandleProxyFactory(this); this.g = new Object(); this.h = new Handler(); this.c = new Object(); diff --git a/play-services-droidguard/core/src/main/kotlin/org/microg/gms/droidguard/core/DroidGuardHandleImpl.kt b/play-services-droidguard/core/src/main/kotlin/org/microg/gms/droidguard/core/DroidGuardHandleImpl.kt index 5ce8116564..9dfa0ab9a4 100644 --- a/play-services-droidguard/core/src/main/kotlin/org/microg/gms/droidguard/core/DroidGuardHandleImpl.kt +++ b/play-services-droidguard/core/src/main/kotlin/org/microg/gms/droidguard/core/DroidGuardHandleImpl.kt @@ -14,10 +14,12 @@ import android.util.Log import com.google.android.gms.droidguard.internal.DroidGuardInitReply import com.google.android.gms.droidguard.internal.DroidGuardResultsRequest import com.google.android.gms.droidguard.internal.IDroidGuardHandle +import org.microg.gms.droidguard.BytesException import org.microg.gms.droidguard.GuardCallback +import org.microg.gms.droidguard.HandleProxy import java.io.FileNotFoundException -class DroidGuardHandleImpl(private val context: Context, private val packageName: String, private val factory: HandleProxyFactory, private val callback: GuardCallback) : IDroidGuardHandle.Stub() { +class DroidGuardHandleImpl(private val context: Context, private val packageName: String, private val factory: NetworkHandleProxyFactory, private val callback: GuardCallback) : IDroidGuardHandle.Stub() { private val condition = ConditionVariable() private var flow: String? = null diff --git a/play-services-droidguard/core/src/main/kotlin/org/microg/gms/droidguard/core/DroidGuardPreferences.kt b/play-services-droidguard/core/src/main/kotlin/org/microg/gms/droidguard/core/DroidGuardPreferences.kt index cb0e5eff07..bb96747df2 100644 --- a/play-services-droidguard/core/src/main/kotlin/org/microg/gms/droidguard/core/DroidGuardPreferences.kt +++ b/play-services-droidguard/core/src/main/kotlin/org/microg/gms/droidguard/core/DroidGuardPreferences.kt @@ -38,7 +38,7 @@ object DroidGuardPreferences { fun isAvailable(context: Context): Boolean = isEnabled(context) && (!isForcedLocalDisabled(context) || getMode(context) != Mode.Embedded) @JvmStatic - fun isLocalAvailable(context: Context): Boolean = isEnabled(context) && !isForcedLocalDisabled(context) + fun isLocalAvailable(context: Context): Boolean = isEnabled(context) && !isForcedLocalDisabled(context) && getMode(context) == Mode.Embedded @JvmStatic fun setEnabled(context: Context, enabled: Boolean) = setSettings(context) { put(ENABLED, enabled) } diff --git a/play-services-droidguard/core/src/main/kotlin/org/microg/gms/droidguard/core/DroidGuardResultCreator.kt b/play-services-droidguard/core/src/main/kotlin/org/microg/gms/droidguard/core/DroidGuardResultCreator.kt index a2e7d4d928..f3ae3dc7b6 100644 --- a/play-services-droidguard/core/src/main/kotlin/org/microg/gms/droidguard/core/DroidGuardResultCreator.kt +++ b/play-services-droidguard/core/src/main/kotlin/org/microg/gms/droidguard/core/DroidGuardResultCreator.kt @@ -41,7 +41,7 @@ private class NetworkDroidGuardResultCreator(private val context: Context) : Dro get() = DroidGuardPreferences.getNetworkServerUrl(context) ?: throw IllegalStateException("Network URL required") override suspend fun getResults(flow: String, data: Map): String = suspendCoroutine { continuation -> - queue.add(PostParamsStringRequest("$url?flow=$flow", data, { + queue.add(PostParamsStringRequest("$url?flow=$flow&source=${context.packageName}", data, { continuation.resume(it) }, { continuation.resumeWithException(it.cause ?: it) diff --git a/play-services-droidguard/core/src/main/kotlin/org/microg/gms/droidguard/core/HandleProxyFactory.kt b/play-services-droidguard/core/src/main/kotlin/org/microg/gms/droidguard/core/NetworkHandleProxyFactory.kt similarity index 71% rename from play-services-droidguard/core/src/main/kotlin/org/microg/gms/droidguard/core/HandleProxyFactory.kt rename to play-services-droidguard/core/src/main/kotlin/org/microg/gms/droidguard/core/NetworkHandleProxyFactory.kt index b254866478..4e685de383 100644 --- a/play-services-droidguard/core/src/main/kotlin/org/microg/gms/droidguard/core/HandleProxyFactory.kt +++ b/play-services-droidguard/core/src/main/kotlin/org/microg/gms/droidguard/core/NetworkHandleProxyFactory.kt @@ -6,13 +6,11 @@ package org.microg.gms.droidguard.core import android.content.Context -import androidx.annotation.GuardedBy import com.android.volley.NetworkResponse import com.android.volley.VolleyError import com.android.volley.toolbox.RequestFuture import com.android.volley.toolbox.Volley import com.google.android.gms.droidguard.internal.DroidGuardResultsRequest -import dalvik.system.DexClassLoader import okio.ByteString.Companion.decodeHex import okio.ByteString.Companion.of import org.microg.gms.droidguard.* @@ -20,16 +18,11 @@ import org.microg.gms.profile.Build import org.microg.gms.profile.ProfileManager import org.microg.gms.utils.singleInstanceOf import java.io.File -import java.io.IOException -import java.security.MessageDigest -import java.security.cert.Certificate import java.util.* import com.android.volley.Request as VolleyRequest import com.android.volley.Response as VolleyResponse -class HandleProxyFactory(private val context: Context) { - @GuardedBy("CLASS_LOCK") - private val classMap = hashMapOf>() +class NetworkHandleProxyFactory(private val context: Context) : HandleProxyFactory(context) { private val dgDb: DgDatabaseHelper = DgDatabaseHelper(context) private val version = VersionUtil(context) private val queue = singleInstanceOf { Volley.newRequestQueue(context.applicationContext) } @@ -136,7 +129,7 @@ class HandleProxyFactory(private val context: Context) { override fun getHeaders(): Map { return mapOf( - "User-Agent" to "DroidGuard/${version.versionCode}" + "User-Agent" to "DroidGuard/${version.versionCode}" ) } }) @@ -178,68 +171,7 @@ class HandleProxyFactory(private val context: Context) { return HandleProxy(clazz, context, flow, byteCode, callback, vmKey, extra, request?.bundle) } - fun getTheApkFile(vmKey: String) = File(getCacheDir(vmKey), "the.apk") - private fun getCacheDir() = context.getDir(CACHE_FOLDER_NAME, Context.MODE_PRIVATE) - private fun getCacheDir(vmKey: String) = File(getCacheDir(), vmKey) - private fun getOptDir(vmKey: String) = File(getCacheDir(vmKey), "opt") - private fun isValidCache(vmKey: String) = getTheApkFile(vmKey).isFile && getOptDir(vmKey).isDirectory - - private fun updateCacheTimestamp(vmKey: String) { - try { - val timestampFile = File(getCacheDir(vmKey), "t") - if (!timestampFile.exists() && !timestampFile.createNewFile()) { - throw Exception("Failed to touch last-used file for $vmKey.") - } - if (!timestampFile.setLastModified(System.currentTimeMillis())) { - throw Exception("Failed to update last-used timestamp for $vmKey.") - } - } catch (e: IOException) { - throw Exception("Failed to touch last-used file for $vmKey.") - } - } - - private fun verifyApkSignature(apk: File): Boolean { - return true - val certificates: Array = TODO() - if (certificates.size != 1) return false - return Arrays.equals(MessageDigest.getInstance("SHA-256").digest(certificates[0].encoded), PROD_CERT_HASH) - } - - private fun loadClass(vmKey: String, bytes: ByteArray): Class<*> { - synchronized(CLASS_LOCK) { - val cachedClass = classMap[vmKey] - if (cachedClass != null) { - updateCacheTimestamp(vmKey) - return cachedClass - } - val weakClass = weakClassMap[vmKey] - if (weakClass != null) { - classMap[vmKey] = weakClass - updateCacheTimestamp(vmKey) - return weakClass - } - if (!isValidCache(vmKey)) { - throw BytesException(bytes, "VM key $vmKey not found in cache") - } - if (!verifyApkSignature(getTheApkFile(vmKey))) { - getCacheDir(vmKey).deleteRecursively() - throw ClassNotFoundException("APK signature verification failed") - } - val loader = DexClassLoader(getTheApkFile(vmKey).absolutePath, getOptDir(vmKey).absolutePath, null, context.classLoader) - val clazz = loader.loadClass(CLASS_NAME) - classMap[vmKey] = clazz - weakClassMap[vmKey] = clazz - return clazz - } - } - companion object { - const val CLASS_NAME = "com.google.ccc.abuse.droidguard.DroidGuard" const val SERVER_URL = "https://www.googleapis.com/androidantiabuse/v1/x/create?alt=PROTO&key=AIzaSyBofcZsgLSS7BOnBjZPEkk4rYwzOIz-lTI" - const val CACHE_FOLDER_NAME = "cache_dg" - val CLASS_LOCK = Object() - @GuardedBy("CLASS_LOCK") - val weakClassMap = WeakHashMap>() - val PROD_CERT_HASH = byteArrayOf(61, 122, 18, 35, 1, -102, -93, -99, -98, -96, -29, 67, 106, -73, -64, -119, 107, -5, 79, -74, 121, -12, -34, 95, -25, -62, 63, 50, 108, -113, -103, 74) } } diff --git a/play-services-droidguard/src/main/java/org/microg/gms/droidguard/DroidGuardApiClient.java b/play-services-droidguard/src/main/java/org/microg/gms/droidguard/DroidGuardApiClient.java index 5399c347fc..b2baf6b833 100644 --- a/play-services-droidguard/src/main/java/org/microg/gms/droidguard/DroidGuardApiClient.java +++ b/play-services-droidguard/src/main/java/org/microg/gms/droidguard/DroidGuardApiClient.java @@ -11,6 +11,7 @@ import android.os.HandlerThread; import android.os.IBinder; import android.os.Looper; +import android.os.ParcelFileDescriptor; import android.util.Log; import com.google.android.gms.droidguard.DroidGuardHandle; @@ -29,6 +30,7 @@ public class DroidGuardApiClient extends GmsClient { private final Context context; private int openHandles = 0; private Handler handler; + private HandleProxyFactory factory; public DroidGuardApiClient(Context context, ConnectionCallbacks callbacks, OnConnectionFailedListener connectionFailedListener) { super(context, callbacks, connectionFailedListener, GmsService.DROIDGUARD.ACTION); @@ -38,6 +40,8 @@ public DroidGuardApiClient(Context context, ConnectionCallbacks callbacks, OnCon HandlerThread thread = new HandlerThread("DG"); thread.start(); handler = new Handler(thread.getLooper()); + + factory = new HandleProxyFactory(context); } public void setPackageName(String packageName) { @@ -60,6 +64,7 @@ public DroidGuardHandle openHandle(String flow, DroidGuardResultsRequest request for (String key : bundle.keySet()) { Log.d(TAG, "reply.object[" + key + "] = " + bundle.get(key)); } + handleDroidGuardData(reply.pfd, (Bundle) reply.object); } } } @@ -70,6 +75,16 @@ public DroidGuardHandle openHandle(String flow, DroidGuardResultsRequest request } } + private void handleDroidGuardData(ParcelFileDescriptor pfd, Bundle bundle) { + String vmKey = bundle.getString("h"); + if (vmKey == null) { + throw new RuntimeException("Missing vmKey"); + } + HandleProxy proxy = factory.createHandle(vmKey, pfd, bundle); + proxy.init(); + proxy.close(); + } + public void markHandleClosed() { if (openHandles == 0) { Log.w(TAG, "Can't mark handle closed if none is open"); diff --git a/play-services-droidguard/core/src/main/kotlin/org/microg/gms/droidguard/core/BytesException.kt b/play-services-droidguard/src/main/kotlin/org/microg/gms/droidguard/BytesException.kt similarity index 92% rename from play-services-droidguard/core/src/main/kotlin/org/microg/gms/droidguard/core/BytesException.kt rename to play-services-droidguard/src/main/kotlin/org/microg/gms/droidguard/BytesException.kt index 71ef914228..32721ee211 100644 --- a/play-services-droidguard/core/src/main/kotlin/org/microg/gms/droidguard/core/BytesException.kt +++ b/play-services-droidguard/src/main/kotlin/org/microg/gms/droidguard/BytesException.kt @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package org.microg.gms.droidguard.core +package org.microg.gms.droidguard class BytesException : Exception { val bytes: ByteArray diff --git a/play-services-droidguard/core/src/main/kotlin/org/microg/gms/droidguard/core/HandleProxy.kt b/play-services-droidguard/src/main/kotlin/org/microg/gms/droidguard/HandleProxy.kt similarity index 62% rename from play-services-droidguard/core/src/main/kotlin/org/microg/gms/droidguard/core/HandleProxy.kt rename to play-services-droidguard/src/main/kotlin/org/microg/gms/droidguard/HandleProxy.kt index 871468027d..34d1545c68 100644 --- a/play-services-droidguard/core/src/main/kotlin/org/microg/gms/droidguard/core/HandleProxy.kt +++ b/play-services-droidguard/src/main/kotlin/org/microg/gms/droidguard/HandleProxy.kt @@ -1,9 +1,9 @@ /* - * SPDX-FileCopyrightText: 2021 microG Project Team + * SPDX-FileCopyrightText: 2022 microG Project Team * SPDX-License-Identifier: Apache-2.0 */ -package org.microg.gms.droidguard.core +package org.microg.gms.droidguard import android.content.Context import android.os.Bundle @@ -11,20 +11,20 @@ import android.os.Parcelable class HandleProxy(val handle: Any, val vmKey: String, val extra: ByteArray = ByteArray(0)) { constructor(clazz: Class<*>, context: Context, vmKey: String, data: Parcelable) : this( - kotlin.runCatching { - clazz.getDeclaredConstructor(Context::class.java, Parcelable::class.java).newInstance(context, data) - }.getOrElse { - throw BytesException(ByteArray(0), it) - }, - vmKey + kotlin.runCatching { + clazz.getDeclaredConstructor(Context::class.java, Parcelable::class.java).newInstance(context, data) + }.getOrElse { + throw BytesException(ByteArray(0), it) + }, + vmKey ) constructor(clazz: Class<*>, context: Context, flow: String?, byteCode: ByteArray, callback: Any, vmKey: String, extra: ByteArray, bundle: Bundle?) : this( - kotlin.runCatching { - clazz.getDeclaredConstructor(Context::class.java, String::class.java, ByteArray::class.java, Object::class.java, Bundle::class.java).newInstance(context, flow, byteCode, callback, bundle) - }.getOrElse { - throw BytesException(extra, it) - }, vmKey, extra) + kotlin.runCatching { + clazz.getDeclaredConstructor(Context::class.java, String::class.java, ByteArray::class.java, Object::class.java, Bundle::class.java).newInstance(context, flow, byteCode, callback, bundle) + }.getOrElse { + throw BytesException(extra, it) + }, vmKey, extra) fun run(data: Map): ByteArray { try { diff --git a/play-services-droidguard/src/main/kotlin/org/microg/gms/droidguard/HandleProxyFactory.kt b/play-services-droidguard/src/main/kotlin/org/microg/gms/droidguard/HandleProxyFactory.kt new file mode 100644 index 0000000000..e6fc1707b1 --- /dev/null +++ b/play-services-droidguard/src/main/kotlin/org/microg/gms/droidguard/HandleProxyFactory.kt @@ -0,0 +1,117 @@ +/* + * SPDX-FileCopyrightText: 2021 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.droidguard + +import android.content.Context +import android.os.Bundle +import android.os.ParcelFileDescriptor +import android.os.Parcelable +import androidx.annotation.GuardedBy +import dalvik.system.DexClassLoader +import java.io.File +import java.io.IOException +import java.security.MessageDigest +import java.security.cert.Certificate +import java.util.* + +open class HandleProxyFactory(private val context: Context) { + @GuardedBy("CLASS_LOCK") + protected val classMap = hashMapOf>() + + fun createHandle(vmKey: String, pfd: ParcelFileDescriptor, extras: Bundle): HandleProxy { + fetchFromFileDescriptor(pfd, vmKey) + return createHandleProxy(vmKey, extras) + } + + private fun fetchFromFileDescriptor(pfd: ParcelFileDescriptor, vmKey: String) { + if (!isValidCache(vmKey)) { + val auIs = ParcelFileDescriptor.AutoCloseInputStream(pfd) + val temp = File(getCacheDir(), "${UUID.randomUUID()}.apk") + temp.parentFile!!.mkdirs() + temp.writeBytes(auIs.readBytes()) + auIs.close() + getOptDir(vmKey).mkdirs() + temp.renameTo(getTheApkFile(vmKey)) + updateCacheTimestamp(vmKey) + if (!isValidCache(vmKey)) { + getCacheDir(vmKey).deleteRecursively() + throw IllegalStateException("Error ") + } + } + } + + private fun createHandleProxy( + vmKey: String, + extras: Parcelable + ): HandleProxy { + val clazz = loadClass(vmKey) + return HandleProxy(clazz, context, vmKey, extras) + } + + fun getTheApkFile(vmKey: String) = File(getCacheDir(vmKey), "the.apk") + protected fun getCacheDir() = context.getDir(CACHE_FOLDER_NAME, Context.MODE_PRIVATE) + protected fun getCacheDir(vmKey: String) = File(getCacheDir(), vmKey) + protected fun getOptDir(vmKey: String) = File(getCacheDir(vmKey), "opt") + protected fun isValidCache(vmKey: String) = getTheApkFile(vmKey).isFile && getOptDir(vmKey).isDirectory + + protected fun updateCacheTimestamp(vmKey: String) { + try { + val timestampFile = File(getCacheDir(vmKey), "t") + if (!timestampFile.exists() && !timestampFile.createNewFile()) { + throw Exception("Failed to touch last-used file for $vmKey.") + } + if (!timestampFile.setLastModified(System.currentTimeMillis())) { + throw Exception("Failed to update last-used timestamp for $vmKey.") + } + } catch (e: IOException) { + throw Exception("Failed to touch last-used file for $vmKey.") + } + } + + private fun verifyApkSignature(apk: File): Boolean { + return true + val certificates: Array = TODO() + if (certificates.size != 1) return false + return Arrays.equals(MessageDigest.getInstance("SHA-256").digest(certificates[0].encoded), PROD_CERT_HASH) + } + + protected fun loadClass(vmKey: String, bytes: ByteArray = ByteArray(0)): Class<*> { + synchronized(CLASS_LOCK) { + val cachedClass = classMap[vmKey] + if (cachedClass != null) { + updateCacheTimestamp(vmKey) + return cachedClass + } + val weakClass = weakClassMap[vmKey] + if (weakClass != null) { + classMap[vmKey] = weakClass + updateCacheTimestamp(vmKey) + return weakClass + } + if (!isValidCache(vmKey)) { + throw BytesException(bytes, "VM key $vmKey not found in cache") + } + if (!verifyApkSignature(getTheApkFile(vmKey))) { + getCacheDir(vmKey).deleteRecursively() + throw ClassNotFoundException("APK signature verification failed") + } + val loader = DexClassLoader(getTheApkFile(vmKey).absolutePath, getOptDir(vmKey).absolutePath, null, context.classLoader) + val clazz = loader.loadClass(CLASS_NAME) + classMap[vmKey] = clazz + weakClassMap[vmKey] = clazz + return clazz + } + } + + companion object { + const val CLASS_NAME = "com.google.ccc.abuse.droidguard.DroidGuard" + const val CACHE_FOLDER_NAME = "cache_dg" + val CLASS_LOCK = Object() + @GuardedBy("CLASS_LOCK") + val weakClassMap = WeakHashMap>() + val PROD_CERT_HASH = byteArrayOf(61, 122, 18, 35, 1, -102, -93, -99, -98, -96, -29, 67, 106, -73, -64, -119, 107, -5, 79, -74, 121, -12, -34, 95, -25, -62, 63, 50, 108, -113, -103, 74) + } +} diff --git a/vending-app/build.gradle b/vending-app/build.gradle index 7cc40124fe..d256e4f652 100644 --- a/vending-app/build.gradle +++ b/vending-app/build.gradle @@ -112,6 +112,7 @@ dependencies { //droidguard implementation project(':play-services-droidguard') + implementation project(':play-services-tasks-ktx') implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutineVersion" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutineVersion" @@ -122,6 +123,9 @@ dependencies { implementation "androidx.appcompat:appcompat:$appcompatVersion" implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.6.2' implementation "androidx.preference:preference-ktx:$preferenceVersion" + + // tink + implementation "com.google.crypto.tink:tink-android:$tinkVersion" } wire { diff --git a/vending-app/src/main/AndroidManifest.xml b/vending-app/src/main/AndroidManifest.xml index eb12ae7378..0150ecc799 100644 --- a/vending-app/src/main/AndroidManifest.xml +++ b/vending-app/src/main/AndroidManifest.xml @@ -175,5 +175,21 @@ + + + + + + + + + + + + diff --git a/vending-app/src/main/aidl/com/google/android/play/core/integrity/protocol/IExpressIntegrityService.aidl b/vending-app/src/main/aidl/com/google/android/play/core/integrity/protocol/IExpressIntegrityService.aidl new file mode 100644 index 0000000000..7b04d89205 --- /dev/null +++ b/vending-app/src/main/aidl/com/google/android/play/core/integrity/protocol/IExpressIntegrityService.aidl @@ -0,0 +1,15 @@ +/* + * SPDX-FileCopyrightText: 2022 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.play.core.integrity.protocol; + +import com.google.android.play.core.integrity.protocol.IExpressIntegrityServiceCallback; +import com.google.android.play.core.integrity.protocol.IRequestDialogCallback; + +interface IExpressIntegrityService { + void warmUpIntegrityToken(in Bundle bundle, in IExpressIntegrityServiceCallback callback) = 1; + void requestExpressIntegrityToken(in Bundle bundle, in IExpressIntegrityServiceCallback callback) = 2; + void requestAndShowDialog(in Bundle bundle, in IRequestDialogCallback callback) = 5; +} \ No newline at end of file diff --git a/vending-app/src/main/aidl/com/google/android/play/core/integrity/protocol/IExpressIntegrityServiceCallback.aidl b/vending-app/src/main/aidl/com/google/android/play/core/integrity/protocol/IExpressIntegrityServiceCallback.aidl new file mode 100644 index 0000000000..8f4cfbc579 --- /dev/null +++ b/vending-app/src/main/aidl/com/google/android/play/core/integrity/protocol/IExpressIntegrityServiceCallback.aidl @@ -0,0 +1,12 @@ +/* + * SPDX-FileCopyrightText: 2022 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.play.core.integrity.protocol; + +interface IExpressIntegrityServiceCallback { + void OnWarmUpIntegrityTokenCallback(in Bundle bundle) = 1; + void onRequestExpressIntegrityToken(in Bundle bundle) = 2; + void onRequestIntegrityToken(in Bundle bundle) = 3; +} diff --git a/vending-app/src/main/aidl/com/google/android/play/core/integrity/protocol/IIntegrityService.aidl b/vending-app/src/main/aidl/com/google/android/play/core/integrity/protocol/IIntegrityService.aidl new file mode 100644 index 0000000000..a08b4027be --- /dev/null +++ b/vending-app/src/main/aidl/com/google/android/play/core/integrity/protocol/IIntegrityService.aidl @@ -0,0 +1,14 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.play.core.integrity.protocol; + +import com.google.android.play.core.integrity.protocol.IIntegrityServiceCallback; +import com.google.android.play.core.integrity.protocol.IRequestDialogCallback; + +interface IIntegrityService { + void requestDialog(in Bundle bundle, in IRequestDialogCallback callback) = 0; + void requestIntegrityToken(in Bundle request, in IIntegrityServiceCallback callback) = 1; +} \ No newline at end of file diff --git a/vending-app/src/main/aidl/com/google/android/play/core/integrity/protocol/IIntegrityServiceCallback.aidl b/vending-app/src/main/aidl/com/google/android/play/core/integrity/protocol/IIntegrityServiceCallback.aidl new file mode 100644 index 0000000000..5b72b40d10 --- /dev/null +++ b/vending-app/src/main/aidl/com/google/android/play/core/integrity/protocol/IIntegrityServiceCallback.aidl @@ -0,0 +1,10 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.play.core.integrity.protocol; + +interface IIntegrityServiceCallback { + void onResult(in Bundle bundle) = 1; +} \ No newline at end of file diff --git a/vending-app/src/main/aidl/com/google/android/play/core/integrity/protocol/IRequestDialogCallback.aidl b/vending-app/src/main/aidl/com/google/android/play/core/integrity/protocol/IRequestDialogCallback.aidl new file mode 100644 index 0000000000..7dc3e85002 --- /dev/null +++ b/vending-app/src/main/aidl/com/google/android/play/core/integrity/protocol/IRequestDialogCallback.aidl @@ -0,0 +1,10 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.play.core.integrity.protocol; + +interface IRequestDialogCallback { + void onRequestAndShowDialog(in Bundle bundle); +} \ No newline at end of file diff --git a/vending-app/src/main/java/com/android/vending/licensing/LicenseChecker.kt b/vending-app/src/main/java/com/android/vending/licensing/LicenseChecker.kt index 057a709e15..7cd86e1a6a 100644 --- a/vending-app/src/main/java/com/android/vending/licensing/LicenseChecker.kt +++ b/vending-app/src/main/java/com/android/vending/licensing/LicenseChecker.kt @@ -9,7 +9,9 @@ import android.content.pm.PackageInfo import android.os.Bundle import android.os.RemoteException import android.util.Log +import com.android.vending.AUTH_TOKEN_SCOPE import com.android.vending.LicenseResult +import com.android.vending.getRequestHeaders import com.android.volley.VolleyError import org.microg.vending.billing.core.HttpClient import java.io.IOException @@ -69,7 +71,6 @@ const val ERROR_INVALID_PACKAGE_NAME: Int = 0x102 */ const val ERROR_NON_MATCHING_UID: Int = 0x103 -const val AUTH_TOKEN_SCOPE: String = "oauth2:https://www.googleapis.com/auth/googleplay" sealed class LicenseRequestParameters data class V1Parameters( @@ -145,7 +146,7 @@ suspend fun HttpClient.makeLicenseV1Request( packageName: String, auth: String, versionCode: Int, nonce: Long, androidId: Long ): V1Response? = get( url = "https://play-fe.googleapis.com/fdfe/apps/checkLicense?pkgn=$packageName&vc=$versionCode&nnc=$nonce", - headers = getLicenseRequestHeaders(auth, androidId), + headers = getRequestHeaders(auth, androidId), adapter = LicenseResult.ADAPTER ).information?.v1?.let { if (it.result != null && it.signedData != null && it.signature != null) { @@ -160,7 +161,7 @@ suspend fun HttpClient.makeLicenseV2Request( androidId: Long ): V2Response? = get( url = "https://play-fe.googleapis.com/fdfe/apps/checkLicenseServerFallback?pkgn=$packageName&vc=$versionCode", - headers = getLicenseRequestHeaders(auth, androidId), + headers = getRequestHeaders(auth, androidId), adapter = LicenseResult.ADAPTER ).information?.v2?.license?.jwt?.let { // Field present ←→ user has license diff --git a/vending-app/src/main/java/com/android/vending/licensing/LicenseRequestHeaders.kt b/vending-app/src/main/kotlin/com/android/vending/VendingRequestHeaders.kt similarity index 82% rename from vending-app/src/main/java/com/android/vending/licensing/LicenseRequestHeaders.kt rename to vending-app/src/main/kotlin/com/android/vending/VendingRequestHeaders.kt index 157d40efc3..82d5e3c3a3 100644 --- a/vending-app/src/main/java/com/android/vending/licensing/LicenseRequestHeaders.kt +++ b/vending-app/src/main/kotlin/com/android/vending/VendingRequestHeaders.kt @@ -1,26 +1,7 @@ -package com.android.vending.licensing +package com.android.vending import android.util.Base64 import android.util.Log -import com.android.vending.AndroidVersionMeta -import com.android.vending.DeviceMeta -import com.android.vending.EncodedTriple -import com.android.vending.EncodedTripleWrapper -import com.android.vending.IntWrapper -import com.android.vending.LicenseRequestHeader -import com.android.vending.Locality -import com.android.vending.LocalityWrapper -import com.android.vending.StringWrapper -import com.android.vending.Timestamp -import com.android.vending.TimestampContainer -import com.android.vending.TimestampContainer1 -import com.android.vending.TimestampContainer1Wrapper -import com.android.vending.TimestampContainer2 -import com.android.vending.TimestampStringWrapper -import com.android.vending.TimestampWrapper -import com.android.vending.UnknownByte12 -import com.android.vending.UserAgent -import com.android.vending.Uuid import com.google.android.gms.common.BuildConfig import okio.ByteString import org.microg.gms.profile.Build @@ -30,12 +11,14 @@ import java.net.URLEncoder import java.util.UUID import java.util.zip.GZIPOutputStream -private const val TAG = "FakeLicenseRequest" +private const val TAG = "VendingRequestHeaders" + +const val AUTH_TOKEN_SCOPE: String = "oauth2:https://www.googleapis.com/auth/googleplay" private const val BASE64_FLAGS = Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING private const val FINSKY_VERSION = "Finsky/37.5.24-29%20%5B0%5D%20%5BPR%5D%20565477504" -internal fun getLicenseRequestHeaders(auth: String, androidId: Long): Map { +internal fun getRequestHeaders(auth: String, androidId: Long): Map { var millis = System.currentTimeMillis() val timestamp = TimestampContainer.Builder() .container2( @@ -139,16 +122,17 @@ internal fun getLicenseRequestHeaders(auth: String, androidId: Long): Map= 33) { + getPackageInfo(packageName, PackageManager.PackageInfoFlags.of(flags.toLong())) + } else { + getPackageInfo(packageName, flags) + } + }.getOrDefault(getPackageInfo(packageName, flags)) +} + +val SIGNING_FLAGS = if (Build.VERSION.SDK_INT >= 28) { + PackageManager.GET_SIGNING_CERTIFICATES +} else { + @Suppress("DEPRECATION") PackageManager.GET_SIGNATURES +} + +val PackageInfo.signaturesCompat: Array + get() { + return if (Build.VERSION.SDK_INT >= 28) { + if (signingInfo.hasMultipleSigners()) { + signingInfo.apkContentsSigners + } else { + signingInfo.signingCertificateHistory + } + } else { + @Suppress("DEPRECATION") signatures + } + } + +fun ByteArray.encodeBase64(noPadding: Boolean, noWrap: Boolean = true, urlSafe: Boolean = true): String { + var flags = 0 + if (noPadding) flags = flags or Base64.NO_PADDING + if (noWrap) flags = flags or Base64.NO_WRAP + if (urlSafe) flags = flags or Base64.URL_SAFE + return Base64.encodeToString(this, flags) +} + +fun ByteArray.sha256(): ByteArray { + return MessageDigest.getInstance("SHA-256").digest(this) +} + +fun Bundle.buildPlayCoreVersion() = PlayCoreVersion( + major = getInt(KEY_VERSION_MAJOR, 0), minor = getInt(KEY_VERSION_MINOR, 0), patch = getInt(KEY_VERSION_PATCH, 0) +) + +fun readAes128GcmBuilderFromClientKey(clientKey: ClientKey?): Aead? { + if (clientKey == null) { + return null + } + return try { + val keySetHandle = CleartextKeysetHandle.read(BinaryKeysetReader.withBytes(clientKey.keySetHandle?.toByteArray())) + keySetHandle.getPrimitive(Aead::class.java) + } catch (e: Exception) { + null + } +} + +suspend fun getIntegrityRequestWrapper(context: Context, expressIntegritySession: ExpressIntegritySession, accountName: String) = withContext(Dispatchers.IO){ + fun getUpdatedWebViewRequestMode(webViewRequestMode: Int): Int { + return when (webViewRequestMode) { + in 0..2 -> webViewRequestMode + 1 + else -> 1 + } + } + val expressFilePB = FileInputStream(context.getProtoFile()).use { input -> ExpressFilePB.ADAPTER.decode(input) } + expressFilePB.integrityRequestWrapper.filter { item -> + TextUtils.equals(item.packageName, expressIntegritySession.packageName) && item.cloudProjectNumber == expressIntegritySession.cloudProjectVersion && getUpdatedWebViewRequestMode( + expressIntegritySession.webViewRequestMode + ) == getUpdatedWebViewRequestMode( + item.webViewRequestMode ?: 0 + ) + }.firstOrNull { item -> + TextUtils.equals(item.accountName, accountName) + } +} + +fun fetchCertificateChain(context: Context, attestationChallenge: ByteArray?): List { + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) { + val devicePropertiesAttestationIncluded = context.packageManager.hasSystemFeature("android.software.device_id_attestation") + val keyGenParameterSpecBuilder = + KeyGenParameterSpec.Builder("integrity.api.key.alias", KeyProperties.PURPOSE_SIGN).setAlgorithmParameterSpec(ECGenParameterSpec("secp256r1")).setDigests(KeyProperties.DIGEST_SHA512) + .setAttestationChallenge(attestationChallenge) + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) { + keyGenParameterSpecBuilder.setDevicePropertiesAttestationIncluded(devicePropertiesAttestationIncluded) + } + val keyGenParameterSpec = keyGenParameterSpecBuilder.build() + val keyPairGenerator = KeyPairGenerator.getInstance("EC", "AndroidKeyStore").apply { + initialize(keyGenParameterSpec) + } + if (keyPairGenerator.generateKeyPair() == null) { + Log.w(TAG, "Failed to create the key pair.") + return emptyList() + } + val keyStore: KeyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) } + val certificateChainList = keyStore.getCertificateChain(keyGenParameterSpec.keystoreAlias)?.let { chain -> + chain.map { it.encoded.toByteString() } + } + if (certificateChainList.isNullOrEmpty()) { + Log.w(TAG, "Failed to get the certificate chain.") + return emptyList() + } + return certificateChainList + } else { + return emptyList() + } +} + +suspend fun updateLocalExpressFilePB(context: Context, intermediateIntegrityResponseData: IntermediateIntegrityResponseData) = withContext(Dispatchers.IO) { + Log.d(TAG, "Writing AAR to express cache") + val intermediateIntegrity = intermediateIntegrityResponseData.intermediateIntegrity + val expressFilePB = FileInputStream(context.getProtoFile()).use { input -> ExpressFilePB.ADAPTER.decode(input) } + + val integrityResponseWrapper = IntegrityRequestWrapper.Builder().apply { + accountName = intermediateIntegrity.accountName + packageName = intermediateIntegrity.packageName + cloudProjectNumber = intermediateIntegrity.cloudProjectNumber + callerKey = intermediateIntegrity.callerKey + webViewRequestMode = intermediateIntegrity.webViewRequestMode.let { + when (it) { + in 0..2 -> it + 1 + else -> 1 + } + } - 1 + deviceIntegrityWrapper = DeviceIntegrityWrapper.Builder().apply { + creationTime = intermediateIntegrity.callerKey.generated + serverGenerated = intermediateIntegrity.serverGenerated + deviceIntegrityToken = intermediateIntegrity.intermediateToken + }.build() + }.build() + + val requestList = expressFilePB.integrityRequestWrapper.toMutableList() + + for ((index, item) in requestList.withIndex()) { + if (TextUtils.equals(item.packageName, intermediateIntegrity.packageName) && item.cloudProjectNumber == intermediateIntegrity.cloudProjectNumber && TextUtils.equals( + item.accountName, intermediateIntegrity.accountName + ) + ) { + if (integrityResponseWrapper.webViewRequestMode == item.webViewRequestMode) { + requestList[index] = integrityResponseWrapper + val newExpressFilePB = expressFilePB.newBuilder().integrityRequestWrapper(requestList).build() + FileOutputStream(context.getProtoFile()).use { output -> ExpressFilePB.ADAPTER.encode(output, newExpressFilePB) } + return@withContext + } + } + } + requestList.add(integrityResponseWrapper) + val newExpressFilePB = expressFilePB.newBuilder().integrityRequestWrapper(requestList).build() + FileOutputStream(context.getProtoFile()).use { output -> ExpressFilePB.ADAPTER.encode(output, newExpressFilePB) } +} + +suspend fun updateExpressSessionTime(context: Context, expressIntegritySession: ExpressIntegritySession, refreshWarmUpMethodTime: Boolean, refreshRequestMethodTime: Boolean) = + withContext(Dispatchers.IO) { + val packageName = if (expressIntegritySession.webViewRequestMode != 0) { + "WebView_" + expressIntegritySession.packageName + } else { + expressIntegritySession.packageName + } + + val expressFilePB = FileInputStream(context.getProtoFile()).use { input -> ExpressFilePB.ADAPTER.decode(input) } + + val clientKey = expressFilePB.integrityTokenTimeMap ?: IntegrityTokenTimeMap() + val timeMutableMap = clientKey.newBuilder().timeMap.toMutableMap() + + if (refreshWarmUpMethodTime) { + timeMutableMap[packageName] = IntegrityTokenTime.Builder().warmUpTokenTime( + TokenTime.Builder().type(1).timestamp(makeTimestamp(System.currentTimeMillis())).build() + ).build() + } + + if (refreshRequestMethodTime) { + timeMutableMap[packageName] = IntegrityTokenTime.Builder().requestTokenTime( + TokenTime.Builder().type(1).timestamp(makeTimestamp(System.currentTimeMillis())).build() + ).build() + } + + val newExpressFilePB = expressFilePB.newBuilder().integrityTokenTimeMap(IntegrityTokenTimeMap.Builder().timeMap(timeMutableMap).build()).build() + FileOutputStream(context.getProtoFile()).use { output -> ExpressFilePB.ADAPTER.encode(output, newExpressFilePB) } + } + +suspend fun updateExpressClientKey(context: Context) = withContext(Dispatchers.IO) { + val expressFilePB = FileInputStream(context.getProtoFile()).use { input -> ExpressFilePB.ADAPTER.decode(input) } + + val oldClientKey = expressFilePB.clientKey ?: ClientKey() + var clientKey = ClientKey.Builder().apply { + val currentTimeMillis = System.currentTimeMillis() + generated = Timestamp.Builder().seconds(currentTimeMillis / 1000).nanos((Math.floorMod(currentTimeMillis, 1000L) * 1000000).toInt()).build() + val keySetHandle = KeysetHandle.generateNew(AesGcmKeyManager.aes128GcmTemplate()) + val outputStream = ByteArrayOutputStream() + CleartextKeysetHandle.write(keySetHandle, BinaryKeysetWriter.withOutputStream(outputStream)) + this.keySetHandle = ByteBuffer.wrap(outputStream.toByteArray()).toByteString() + }.build() + if (oldClientKey.keySetHandle?.size != 0) { + if (oldClientKey.generated?.seconds != null && clientKey.generated?.seconds != null && oldClientKey.generated.seconds < clientKey.generated?.seconds!!.minus(TEMPORARY_DEVICE_KEY_VALIDITY)) { + clientKey = oldClientKey + } + } + + val newExpressFilePB = expressFilePB.newBuilder().clientKey(clientKey).build() + FileOutputStream(context.getProtoFile()).use { output -> ExpressFilePB.ADAPTER.encode(output, newExpressFilePB) } + clientKey +} + +suspend fun updateExpressAuthTokenWrapper(context: Context, expressIntegritySession: ExpressIntegritySession, authToken: String, clientKey: ClientKey) = withContext(Dispatchers.IO) { + var expressFilePB = FileInputStream(context.getProtoFile()).use { input -> ExpressFilePB.ADAPTER.decode(input) } + + val createTimeSeconds = expressFilePB.tokenWrapper?.deviceIntegrityWrapper?.creationTime?.seconds ?: 0 + val lastManualSoftRefreshTime = expressFilePB.tokenWrapper?.lastManualSoftRefreshTime?.seconds ?: 0 + if (createTimeSeconds < System.currentTimeMillis() - DEVICE_INTEGRITY_HARD_EXPIRATION) { + expressFilePB = expressFilePB.newBuilder().tokenWrapper(regenerateToken(context, authToken, expressIntegritySession.packageName, clientKey)).build() + } else if (lastManualSoftRefreshTime <= System.currentTimeMillis() - DEVICE_INTEGRITY_SOFT_EXPIRATION_CHECK_PERIOD && createTimeSeconds < System.currentTimeMillis() - DEVICE_INTEGRITY_SOFT_EXPIRATION) { + expressFilePB = expressFilePB.newBuilder().tokenWrapper(regenerateToken(context, authToken, expressIntegritySession.packageName, clientKey)).build() + } + + FileOutputStream(context.getProtoFile()).use { output -> ExpressFilePB.ADAPTER.encode(output, expressFilePB) } + + expressFilePB +} + +private suspend fun regenerateToken( + context: Context, authToken: String, packageName: String, clientKey: ClientKey +): AuthTokenWrapper { + try { + Log.d(TAG, "regenerateToken authToken:$authToken, packageName:$packageName, clientKey:$clientKey") + val droidGuardSessionTokenResponse = requestDroidGuardSessionToken(context, authToken) + + if (droidGuardSessionTokenResponse.tokenWrapper == null) { + throw RuntimeException("regenerateToken droidGuardSessionTokenResponse.tokenWrapper is Empty!") + } + + val droidGuardTokenType = droidGuardSessionTokenResponse.tokenWrapper.tokenContent?.tokenType?.firstOrNull { it.type?.toInt() == 5 } + ?: throw RuntimeException("regenerateToken droidGuardTokenType is null!") + + val droidGuardTokenSession = droidGuardTokenType.tokenSessionWrapper?.wrapper?.sessionContent?.session?.id + if (droidGuardTokenSession.isNullOrEmpty()) { + throw RuntimeException("regenerateToken droidGuardTokenSession is null") + } + + val data = mutableMapOf(KEY_DROID_GUARD_SESSION_TOKEN_V1 to droidGuardTokenSession) + val droidGuardData = withContext(Dispatchers.IO) { + val droidGuardResultsRequest = DroidGuardResultsRequest().apply { + bundle.putByteArray(PARAMS_PIA_EXPRESS_DEVICE_KEY, clientKey.keySetHandle?.toByteArray()) + } + Log.d(TAG, "Running DroidGuard (flow: $EXPRESS_INTEGRITY_FLOW_NAME, data: $data)") + DroidGuard.getClient(context).getResults(EXPRESS_INTEGRITY_FLOW_NAME, data, droidGuardResultsRequest).await().encode() + } + + val deviceIntegrityTokenResponse = requestDeviceIntegrityToken(context, authToken, droidGuardTokenSession, droidGuardData) + + val deviceIntegrityTokenType = deviceIntegrityTokenResponse.tokenWrapper?.tokenContent?.tokenType?.firstOrNull { it.type?.toInt() == 5 } + ?: throw RuntimeException("regenerateToken deviceIntegrityTokenType is null!") + + val deviceIntegrityToken = deviceIntegrityTokenType.tokenSessionWrapper?.wrapper?.sessionContent?.tokenContent?.tokenWrapper?.token + + return AuthTokenWrapper.Builder().apply { + this.clientKey = clientKey + this.deviceIntegrityWrapper = DeviceIntegrityWrapper.Builder().apply { + this.deviceIntegrityToken = deviceIntegrityToken ?: ByteString.EMPTY + this.creationTime = makeTimestamp(System.currentTimeMillis()) + }.build() + }.build() + } catch (e: Exception) { + Log.d(TAG, "regenerateToken: error ", e) + return AuthTokenWrapper() + } +} + +private suspend fun requestDroidGuardSessionToken(context: Context, authToken: String): TokenResponse { + val tokenWrapper = TokenRequestWrapper.Builder().apply { + request = mutableListOf(TokenRequest.Builder().apply { + droidGuardBody = DroidGuardBody.Builder().apply { + tokenBody = DroidGuardSessionTokenContent() + }.build() + }.build()) + }.build() + return requestExpressSyncData(context, authToken, tokenWrapper) +} + +private suspend fun requestDeviceIntegrityToken( + context: Context, authToken: String, session: String, token: ByteString +): TokenResponse { + val tokenWrapper = TokenRequestWrapper.Builder().apply { + request = mutableListOf(TokenRequest.Builder().apply { + droidGuardBody = DroidGuardBody.Builder().apply { + deviceBody = DeviceIntegrityTokenContent.Builder().apply { + sessionWrapper = SessionWrapper.Builder().apply { + type = KEY_DROID_GUARD_SESSION_TOKEN_V1 + this.session = Session.Builder().apply { + id = session + }.build() + }.build() + this.token = token.utf8() + flowName = EXPRESS_INTEGRITY_FLOW_NAME + }.build() + }.build() + }.build()) + }.build() + return requestExpressSyncData(context, authToken, tokenWrapper) +} + +suspend fun getAuthToken(context: Context, authTokenType: String): String { + val accountManager = AccountManager.get(context) + val accounts = accountManager.getAccountsByType(DEFAULT_ACCOUNT_TYPE) + var oauthToken = "" + if (accounts.isEmpty()) { + Log.w(TAG, "getAuthToken: No Google account found") + } else for (account in accounts) { + val result = suspendCoroutine { continuation -> + accountManager.getAuthToken(account, authTokenType, false, { future: AccountManagerFuture -> + try { + val result = future.result.getString(AccountManager.KEY_AUTHTOKEN) + continuation.resume(result) + } catch (e: Exception) { + Log.w(TAG, "getAuthToken: ", e) + continuation.resume(null) + } + }, null) + } + if (result != null) { + oauthToken = result + break + } + } + return oauthToken +} + +suspend fun requestIntegritySyncData(context: Context, authToken: String, request: IntegrityRequest): IntegrityResponse { + val androidId = GServices.getString(context.contentResolver, "android_id", "0")?.toLong() ?: 1 + return HttpClient(context).post( + url = "https://play-fe.googleapis.com/fdfe/integrity", + headers = getRequestHeaders(authToken, androidId), + payload = request, + adapter = IntegrityResponse.ADAPTER + ) +} + +suspend fun requestExpressSyncData(context: Context, authToken: String, request: TokenRequestWrapper): TokenResponse { + val androidId = GServices.getString(context.contentResolver, "android_id", "0")?.toLong() ?: 1 + return HttpClient(context).post( + url = "https://play-fe.googleapis.com/fdfe/sync?nocache_qos=lt", + headers = getRequestHeaders(authToken, androidId), + payload = request, + adapter = TokenResponse.ADAPTER + ) +} + +suspend fun requestIntermediateIntegrity( + context: Context, authToken: String, request: IntermediateIntegrityRequest +): IntermediateIntegrityResponseWrapperExtend { + val androidId = GServices.getString(context.contentResolver, "android_id", "0")?.toLong() ?: 1 + return HttpClient(context).post( + url = "https://play-fe.googleapis.com/fdfe/intermediateIntegrity", + headers = getRequestHeaders(authToken, androidId), + payload = request, + adapter = IntermediateIntegrityResponseWrapperExtend.ADAPTER + ) +} diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/expressintegrityservice/DeviceIntegrity.kt b/vending-app/src/main/kotlin/com/google/android/finsky/expressintegrityservice/DeviceIntegrity.kt new file mode 100644 index 0000000000..2a4f058910 --- /dev/null +++ b/vending-app/src/main/kotlin/com/google/android/finsky/expressintegrityservice/DeviceIntegrity.kt @@ -0,0 +1,14 @@ +/* + * SPDX-FileCopyrightText: 2022 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.finsky.expressintegrityservice + +import com.android.vending.Timestamp +import com.google.android.finsky.ClientKey +import okio.ByteString + +data class DeviceIntegrity( + var clientKey: ClientKey?, var deviceIntegrityToken: ByteString?, var creationTime: Timestamp?, var lastManualSoftRefreshTime: Timestamp? +) \ No newline at end of file diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/expressintegrityservice/DeviceIntegrityAndExpiredKey.kt b/vending-app/src/main/kotlin/com/google/android/finsky/expressintegrityservice/DeviceIntegrityAndExpiredKey.kt new file mode 100644 index 0000000000..0a78df4996 --- /dev/null +++ b/vending-app/src/main/kotlin/com/google/android/finsky/expressintegrityservice/DeviceIntegrityAndExpiredKey.kt @@ -0,0 +1,8 @@ +/* + * SPDX-FileCopyrightText: 2022 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.finsky.expressintegrityservice + +data class DeviceIntegrityAndExpiredKey(var deviceIntegrity: DeviceIntegrity, var expiredDeviceKey: Any?) \ No newline at end of file diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/expressintegrityservice/DeviceIntegrityResponse.kt b/vending-app/src/main/kotlin/com/google/android/finsky/expressintegrityservice/DeviceIntegrityResponse.kt new file mode 100644 index 0000000000..fd006209c8 --- /dev/null +++ b/vending-app/src/main/kotlin/com/google/android/finsky/expressintegrityservice/DeviceIntegrityResponse.kt @@ -0,0 +1,9 @@ +/* + * SPDX-FileCopyrightText: 2022 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.finsky.expressintegrityservice +data class DeviceIntegrityResponse( + var deviceIntegrity: DeviceIntegrity, var attemptedDroidGuardTokenRefresh: Boolean, var deviceKeyMd5: String, var expiredDeviceKey: Any? +) diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/expressintegrityservice/ExpressIntegrityService.kt b/vending-app/src/main/kotlin/com/google/android/finsky/expressintegrityservice/ExpressIntegrityService.kt new file mode 100644 index 0000000000..ae758baa1d --- /dev/null +++ b/vending-app/src/main/kotlin/com/google/android/finsky/expressintegrityservice/ExpressIntegrityService.kt @@ -0,0 +1,338 @@ +/** + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.finsky.expressintegrityservice + +import android.accounts.AccountManager +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.os.Bundle +import android.os.IBinder +import android.os.RemoteException +import android.text.TextUtils +import android.util.Base64 +import android.util.Log +import androidx.core.os.bundleOf +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LifecycleService +import androidx.lifecycle.lifecycleScope +import com.android.vending.AUTH_TOKEN_SCOPE +import com.android.vending.Timestamp +import com.android.vending.makeTimestamp +import com.android.volley.AuthFailureError +import com.google.android.finsky.AuthTokenWrapper +import com.google.android.finsky.ClientKey +import com.google.android.finsky.ClientKeyExtend +import com.google.android.finsky.DeviceIntegrityWrapper +import com.google.android.finsky.ExpressIntegrityResponse +import com.google.android.finsky.IntermediateIntegrityRequest +import com.google.android.finsky.IntermediateIntegritySession +import com.google.android.finsky.KEY_CLOUD_PROJECT +import com.google.android.finsky.KEY_NONCE +import com.google.android.finsky.KEY_OPT_PACKAGE +import com.google.android.finsky.KEY_PACKAGE_NAME +import com.google.android.finsky.KEY_REQUEST_MODE +import com.google.android.finsky.KEY_ERROR +import com.google.android.finsky.KEY_REQUEST_TOKEN_SID +import com.google.android.finsky.KEY_REQUEST_VERDICT_OPT_OUT +import com.google.android.finsky.KEY_TOKEN +import com.google.android.finsky.KEY_WARM_UP_SID +import com.google.android.finsky.PlayProtectDetails +import com.google.android.finsky.PlayProtectState +import com.google.android.finsky.RESULT_UN_AUTH +import com.google.android.finsky.RequestMode +import com.google.android.finsky.buildPlayCoreVersion +import com.google.android.finsky.encodeBase64 +import com.google.android.finsky.fetchCertificateChain +import com.google.android.finsky.getAuthToken +import com.google.android.finsky.getIntegrityRequestWrapper +import com.google.android.finsky.getPackageInfoCompat +import com.google.android.finsky.model.IntegrityErrorCode +import com.google.android.finsky.readAes128GcmBuilderFromClientKey +import com.google.android.finsky.requestIntermediateIntegrity +import com.google.android.finsky.sha256 +import com.google.android.finsky.signaturesCompat +import com.google.android.finsky.updateExpressAuthTokenWrapper +import com.google.android.finsky.updateExpressClientKey +import com.google.android.finsky.updateExpressSessionTime +import com.google.android.finsky.updateLocalExpressFilePB +import com.google.android.play.core.integrity.protocol.IExpressIntegrityService +import com.google.android.play.core.integrity.protocol.IExpressIntegrityServiceCallback +import com.google.android.play.core.integrity.protocol.IRequestDialogCallback +import com.google.crypto.tink.config.TinkConfig +import okio.ByteString.Companion.toByteString +import org.microg.gms.profile.ProfileManager +import org.microg.vending.billing.DEFAULT_ACCOUNT_TYPE +import kotlin.random.Random + +private const val TAG = "ExpressIntegrityService" + +class ExpressIntegrityService : LifecycleService() { + + override fun onBind(intent: Intent): IBinder? { + super.onBind(intent) + ProfileManager.ensureInitialized(this) + Log.d(TAG, "onBind") + TinkConfig.register() + return ExpressIntegrityServiceImpl(this, lifecycle).asBinder() + } + + override fun onUnbind(intent: Intent?): Boolean { + Log.d(TAG, "onUnbind") + return super.onUnbind(intent) + } +} + +private class ExpressIntegrityServiceImpl(private val context: Context, override val lifecycle: Lifecycle) : IExpressIntegrityService.Stub(), LifecycleOwner { + + override fun warmUpIntegrityToken(bundle: Bundle, callback: IExpressIntegrityServiceCallback) { + lifecycleScope.launchWhenCreated { + runCatching { + val authToken = getAuthToken(context, AUTH_TOKEN_SCOPE) + if (TextUtils.isEmpty(authToken)) { + Log.w(TAG, "warmUpIntegrityToken: Got null auth token for type: $AUTH_TOKEN_SCOPE") + } + Log.d(TAG, "warmUpIntegrityToken authToken: $authToken") + + val expressIntegritySession = ExpressIntegritySession( + packageName = bundle.getString(KEY_PACKAGE_NAME) ?: "", + cloudProjectVersion = bundle.getLong(KEY_CLOUD_PROJECT, 0L), + sessionId = Random.nextLong(), + null, + 0, + null, + webViewRequestMode = bundle.getInt(KEY_REQUEST_MODE, 0) + ) + updateExpressSessionTime(context, expressIntegritySession, refreshWarmUpMethodTime = true, refreshRequestMethodTime = false) + + val clientKey = updateExpressClientKey(context) + val expressFilePB = updateExpressAuthTokenWrapper(context, expressIntegritySession, authToken, clientKey) + + val tokenWrapper = expressFilePB.tokenWrapper ?: AuthTokenWrapper() + val tokenClientKey = tokenWrapper.clientKey ?: ClientKey() + val deviceIntegrityWrapper = tokenWrapper.deviceIntegrityWrapper ?: DeviceIntegrityWrapper() + val creationTime = tokenWrapper.deviceIntegrityWrapper?.creationTime ?: Timestamp() + val lastManualSoftRefreshTime = tokenWrapper.lastManualSoftRefreshTime ?: Timestamp() + + val deviceIntegrityAndExpiredKey = DeviceIntegrityAndExpiredKey( + deviceIntegrity = DeviceIntegrity( + tokenClientKey, deviceIntegrityWrapper.deviceIntegrityToken, creationTime, lastManualSoftRefreshTime + ), expressFilePB.expiredDeviceKey ?: ClientKey() + ) + + val deviceIntegrity = deviceIntegrityAndExpiredKey.deviceIntegrity + if (deviceIntegrity.deviceIntegrityToken?.size == 0 || deviceIntegrity.clientKey?.keySetHandle?.size == 0) { + throw RuntimeException("DroidGuard token is empty.") + } + + val deviceKeyMd5 = Base64.encodeToString( + deviceIntegrity.clientKey?.keySetHandle?.md5()?.toByteArray(), Base64.NO_PADDING or Base64.NO_WRAP or Base64.URL_SAFE + ) + if (deviceKeyMd5.isNullOrEmpty()) { + throw RuntimeException("Null deviceKeyMd5.") + } + + val deviceIntegrityResponse = DeviceIntegrityResponse( + deviceIntegrity, false, deviceKeyMd5, deviceIntegrityAndExpiredKey.expiredDeviceKey + ) + + val packageInfo = context.packageManager.getPackageInfoCompat( + expressIntegritySession.packageName, PackageManager.GET_SIGNING_CERTIFICATES or PackageManager.GET_SIGNATURES + ) + val certificateSha256Hashes = packageInfo.signaturesCompat.map { + it.toByteArray().sha256().encodeBase64(noPadding = true, noWrap = true, urlSafe = true) + } + + val packageInformation = PackageInformation(certificateSha256Hashes, packageInfo.versionCode) + + val clientKeyExtend = ClientKeyExtend.Builder().apply { + cloudProjectNumber = expressIntegritySession.cloudProjectVersion + keySetHandle = clientKey.keySetHandle + if (expressIntegritySession.webViewRequestMode == 2) { + this.optPackageName = KEY_OPT_PACKAGE + this.versionCode = 0 + } else { + this.optPackageName = expressIntegritySession.packageName + this.versionCode = packageInformation.versionCode + this.certificateSha256Hashes = packageInformation.certificateSha256Hashes + } + }.build() + + val certificateChainList = fetchCertificateChain(context, clientKeyExtend.keySetHandle?.sha256()?.toByteArray()) + + val sessionId = expressIntegritySession.sessionId + val playCoreVersion = bundle.buildPlayCoreVersion() + + Log.d(TAG, "warmUpIntegrityToken sessionId:$sessionId") + + val intermediateIntegrityRequest = IntermediateIntegrityRequest.Builder().apply { + deviceIntegrityToken(deviceIntegrityResponse.deviceIntegrity.deviceIntegrityToken) + readAes128GcmBuilderFromClientKey(deviceIntegrityResponse.deviceIntegrity.clientKey)?.let { + clientKeyExtendBytes(it.encrypt(clientKeyExtend.encode(), null).toByteString()) + } + playCoreVersion(playCoreVersion) + sessionId(sessionId) + certificateChainWrapper(IntermediateIntegrityRequest.CertificateChainWrapper(certificateChainList)) + playProtectDetails(PlayProtectDetails(PlayProtectState.PLAY_PROTECT_STATE_NONE)) + if (expressIntegritySession.webViewRequestMode != 0) { + requestMode(RequestMode.Builder().mode(expressIntegritySession.webViewRequestMode.takeIf { it in 0..2 } ?: 0).build()) + } + }.build() + + Log.d(TAG, "intermediateIntegrityRequest: $intermediateIntegrityRequest") + + val intermediateIntegrityResponse = requestIntermediateIntegrity(context, authToken, intermediateIntegrityRequest).intermediateIntegrityResponseWrapper?.intermediateIntegrityResponse + ?: throw RuntimeException("intermediateIntegrityResponse is null.") + + Log.d(TAG, "requestIntermediateIntegrity: $intermediateIntegrityResponse") + + val defaultAccountName: String = runCatching { + if (expressIntegritySession.webViewRequestMode != 0) { + RESULT_UN_AUTH + } else { + AccountManager.get(context).getAccountsByType(DEFAULT_ACCOUNT_TYPE).firstOrNull()?.name ?: RESULT_UN_AUTH + } + }.getOrDefault(RESULT_UN_AUTH) + + val intermediateIntegrityResponseData = IntermediateIntegrityResponseData( + intermediateIntegrity = IntermediateIntegrity( + expressIntegritySession.packageName, + expressIntegritySession.cloudProjectVersion, + defaultAccountName, + clientKey, + intermediateIntegrityResponse.intermediateToken, + intermediateIntegrityResponse.serverGenerated, + expressIntegritySession.webViewRequestMode, + 0 + ), + callerKeyMd5 = Base64.encodeToString( + clientKey.encode(), Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING + ), + appVersionCode = packageInformation.versionCode, + deviceIntegrityResponse = deviceIntegrityResponse, + appAccessRiskVerdictEnabled = intermediateIntegrityResponse.appAccessRiskVerdictEnabled + ) + + updateLocalExpressFilePB(context, intermediateIntegrityResponseData) + + callback.onWarmResult(bundleOf(KEY_WARM_UP_SID to sessionId)) + }.onFailure { + callback.onWarmResult(bundleOf(KEY_ERROR to IntegrityErrorCode.INTEGRITY_TOKEN_PROVIDER_INVALID)) + } + } + } + + override fun requestExpressIntegrityToken(bundle: Bundle, callback: IExpressIntegrityServiceCallback) { + Log.d(TAG, "requestExpressIntegrityToken bundle:$bundle") + lifecycleScope.launchWhenCreated { + val expressIntegritySession = ExpressIntegritySession( + packageName = bundle.getString(KEY_PACKAGE_NAME) ?: "", + cloudProjectVersion = bundle.getLong(KEY_CLOUD_PROJECT, 0L), + sessionId = Random.nextLong(), + requestHash = bundle.getString(KEY_NONCE), + originatingWarmUpSessionId = bundle.getLong(KEY_WARM_UP_SID, 0), + verdictOptOut = bundle.getIntegerArrayList(KEY_REQUEST_VERDICT_OPT_OUT), + webViewRequestMode = bundle.getInt(KEY_REQUEST_MODE, 0) + ) + + if (TextUtils.isEmpty(expressIntegritySession.packageName)) { + Log.w(TAG, "packageName is empty.") + callback.onRequestResult(bundleOf(KEY_ERROR to IntegrityErrorCode.INTERNAL_ERROR)) + return@launchWhenCreated + } + + if (expressIntegritySession.cloudProjectVersion <= 0L) { + Log.w(TAG, "cloudProjectVersion error") + callback.onRequestResult(bundleOf(KEY_ERROR to IntegrityErrorCode.CLOUD_PROJECT_NUMBER_IS_INVALID)) + return@launchWhenCreated + } + + if (expressIntegritySession.requestHash?.length!! > 500) { + Log.w(TAG, "requestHash error") + callback.onRequestResult(bundleOf(KEY_ERROR to IntegrityErrorCode.REQUEST_HASH_TOO_LONG)) + return@launchWhenCreated + } + + updateExpressSessionTime(context, expressIntegritySession, refreshWarmUpMethodTime = false, refreshRequestMethodTime = true) + + val defaultAccountName: String = runCatching { + if (expressIntegritySession.webViewRequestMode != 0) { + RESULT_UN_AUTH + } else { + AccountManager.get(context).getAccountsByType(DEFAULT_ACCOUNT_TYPE).firstOrNull()?.name ?: RESULT_UN_AUTH + } + }.getOrDefault(RESULT_UN_AUTH) + + val integrityRequestWrapper = getIntegrityRequestWrapper(context, expressIntegritySession, defaultAccountName) + if (integrityRequestWrapper == null) { + Log.w(TAG, "integrityRequestWrapper is null") + callback.onRequestResult(bundleOf(KEY_ERROR to IntegrityErrorCode.INTEGRITY_TOKEN_PROVIDER_INVALID)) + return@launchWhenCreated + } + + try { + val integritySession = IntermediateIntegritySession.Builder().creationTime(makeTimestamp(System.currentTimeMillis())).requestHash(expressIntegritySession.requestHash) + .sessionId(Random.nextBytes(8).toByteString()).timestampMillis(0).build() + + val expressIntegrityResponse = ExpressIntegrityResponse.Builder().apply { + this.deviceIntegrityToken = integrityRequestWrapper.deviceIntegrityWrapper?.deviceIntegrityToken + this.sessionHashAes128 = readAes128GcmBuilderFromClientKey(integrityRequestWrapper.callerKey)?.encrypt( + integritySession.encode(), null + )?.toByteString() + }.build() + + val token = Base64.encodeToString( + expressIntegrityResponse.encode(), Base64.NO_PADDING or Base64.NO_WRAP or Base64.URL_SAFE + ) + + callback.onRequestResult( + bundleOf( + KEY_TOKEN to token, + KEY_REQUEST_TOKEN_SID to expressIntegritySession.sessionId, + KEY_REQUEST_MODE to expressIntegritySession.webViewRequestMode + ) + ) + Log.d(TAG, "requestExpressIntegrityToken token: $token, sid: ${expressIntegritySession.sessionId}, mode: ${expressIntegritySession.webViewRequestMode}") + } catch (exception: RemoteException) { + Log.e(TAG, "requesting token has failed for ${expressIntegritySession.packageName}.") + callback.onRequestResult(bundleOf(KEY_ERROR to IntegrityErrorCode.INTEGRITY_TOKEN_PROVIDER_INVALID)) + } + } + } + + override fun requestAndShowDialog(bundle: Bundle?, callback: IRequestDialogCallback?) { + Log.d(TAG, "requestAndShowDialog bundle:$bundle") + callback?.onRequestAndShowDialog(bundleOf(KEY_ERROR to IntegrityErrorCode.INTERNAL_ERROR)) + } + +} + +private fun IExpressIntegrityServiceCallback.onWarmResult(result: Bundle) { + if (asBinder()?.isBinderAlive == false) { + Log.e(TAG, "onWarmResult IExpressIntegrityServiceCallback Binder died") + return + } + try { + OnWarmUpIntegrityTokenCallback(result) + } catch (e: Exception) { + Log.w(TAG, "error -> $e") + } + Log.d(TAG, "IExpressIntegrityServiceCallback onWarmResult success: $result") +} + +private fun IExpressIntegrityServiceCallback.onRequestResult(result: Bundle) { + if (asBinder()?.isBinderAlive == false) { + Log.e(TAG, "onRequestResult IExpressIntegrityServiceCallback Binder died") + return + } + try { + onRequestExpressIntegrityToken(result) + } catch (e: Exception) { + Log.w(TAG, "error -> $e") + } + Log.d(TAG, "IExpressIntegrityServiceCallback onRequestResult success: $result") +} \ No newline at end of file diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/expressintegrityservice/ExpressIntegritySession.kt b/vending-app/src/main/kotlin/com/google/android/finsky/expressintegrityservice/ExpressIntegritySession.kt new file mode 100644 index 0000000000..9116d4319e --- /dev/null +++ b/vending-app/src/main/kotlin/com/google/android/finsky/expressintegrityservice/ExpressIntegritySession.kt @@ -0,0 +1,15 @@ +/* + * SPDX-FileCopyrightText: 2022 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.finsky.expressintegrityservice +data class ExpressIntegritySession( + var packageName: String, + var cloudProjectVersion: Long, + var sessionId: Long, + var requestHash: String?, + var originatingWarmUpSessionId: Long, + var verdictOptOut: List?, + var webViewRequestMode: Int +) \ No newline at end of file diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/expressintegrityservice/IntermediateIntegrity.kt b/vending-app/src/main/kotlin/com/google/android/finsky/expressintegrityservice/IntermediateIntegrity.kt new file mode 100644 index 0000000000..8011597d5b --- /dev/null +++ b/vending-app/src/main/kotlin/com/google/android/finsky/expressintegrityservice/IntermediateIntegrity.kt @@ -0,0 +1,21 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.finsky.expressintegrityservice + +import com.android.vending.Timestamp +import com.google.android.finsky.ClientKey +import okio.ByteString + +data class IntermediateIntegrity( + var packageName: String, + var cloudProjectNumber: Long, + var accountName: String, + var callerKey: ClientKey, + var intermediateToken: ByteString?, + var serverGenerated: Timestamp?, + var webViewRequestMode: Int, + var testErrorCode: Int +) \ No newline at end of file diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/expressintegrityservice/IntermediateIntegrityResponse.kt b/vending-app/src/main/kotlin/com/google/android/finsky/expressintegrityservice/IntermediateIntegrityResponse.kt new file mode 100644 index 0000000000..be2c9339ae --- /dev/null +++ b/vending-app/src/main/kotlin/com/google/android/finsky/expressintegrityservice/IntermediateIntegrityResponse.kt @@ -0,0 +1,10 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.finsky.expressintegrityservice + +data class IntermediateIntegrityResponseData( + var intermediateIntegrity: IntermediateIntegrity, var callerKeyMd5: String, var appVersionCode: Int, var deviceIntegrityResponse: DeviceIntegrityResponse, var appAccessRiskVerdictEnabled: Boolean? +) diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/expressintegrityservice/PackageInformation.kt b/vending-app/src/main/kotlin/com/google/android/finsky/expressintegrityservice/PackageInformation.kt new file mode 100644 index 0000000000..a5779fee3e --- /dev/null +++ b/vending-app/src/main/kotlin/com/google/android/finsky/expressintegrityservice/PackageInformation.kt @@ -0,0 +1,7 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.finsky.expressintegrityservice +data class PackageInformation(var certificateSha256Hashes: List, var versionCode: Int) diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/extensions.kt b/vending-app/src/main/kotlin/com/google/android/finsky/extensions.kt index 4a1e53664d..8878c80097 100644 --- a/vending-app/src/main/kotlin/com/google/android/finsky/extensions.kt +++ b/vending-app/src/main/kotlin/com/google/android/finsky/extensions.kt @@ -17,9 +17,9 @@ import androidx.collection.ArraySet import androidx.collection.arrayMapOf import androidx.collection.arraySetOf import androidx.core.content.pm.PackageInfoCompat -import com.android.vending.licensing.AUTH_TOKEN_SCOPE +import com.android.vending.AUTH_TOKEN_SCOPE import com.android.vending.licensing.getAuthToken -import com.android.vending.licensing.getLicenseRequestHeaders +import com.android.vending.getRequestHeaders import com.google.android.finsky.assetmoduleservice.AssetPackException import com.google.android.finsky.assetmoduleservice.DownloadData import com.google.android.finsky.assetmoduleservice.ModuleData @@ -112,7 +112,7 @@ suspend fun HttpClient.initAssetModuleData( val moduleDeliveryInfo = post( url = ASSET_MODULE_DELIVERY_URL, - headers = getLicenseRequestHeaders(oauthToken, androidId), + headers = getRequestHeaders(oauthToken, androidId), payload = requestPayload, adapter = AssetModuleDeliveryResponse.ADAPTER ).wrapper?.deliveryInfo diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/integrityservice/IntegrityService.kt b/vending-app/src/main/kotlin/com/google/android/finsky/integrityservice/IntegrityService.kt new file mode 100644 index 0000000000..ff8f44d39d --- /dev/null +++ b/vending-app/src/main/kotlin/com/google/android/finsky/integrityservice/IntegrityService.kt @@ -0,0 +1,220 @@ +/** + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.finsky.integrityservice + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.os.IBinder +import android.os.SystemClock +import android.text.TextUtils +import android.util.Base64 +import android.util.Log +import androidx.core.os.bundleOf +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LifecycleService +import androidx.lifecycle.lifecycleScope +import com.android.vending.AUTH_TOKEN_SCOPE +import com.android.vending.makeTimestamp +import com.google.android.finsky.AccessibilityAbuseSignalDataWrapper +import com.google.android.finsky.AppAccessRiskDetailsResponse +import com.google.android.finsky.DisplayListenerMetadataWrapper +import com.google.android.finsky.INTEGRITY_FLOW_NAME +import com.google.android.finsky.INTEGRITY_PREFIX_ERROR +import com.google.android.finsky.InstalledAppsSignalDataWrapper +import com.google.android.finsky.IntegrityParams +import com.google.android.finsky.IntegrityRequest +import com.google.android.finsky.KEY_CLOUD_PROJECT +import com.google.android.finsky.KEY_NONCE +import com.google.android.finsky.KEY_PACKAGE_NAME +import com.google.android.finsky.PARAMS_BINDING_KEY +import com.google.android.finsky.PARAMS_GCP_N_KEY +import com.google.android.finsky.PARAMS_NONCE_SHA256_KEY +import com.google.android.finsky.PARAMS_PKG_KEY +import com.google.android.finsky.PARAMS_TM_S_KEY +import com.google.android.finsky.PARAMS_VC_KEY +import com.google.android.finsky.PackageNameWrapper +import com.google.android.finsky.PlayProtectDetails +import com.google.android.finsky.PlayProtectState +import com.google.android.finsky.SIGNING_FLAGS +import com.google.android.finsky.ScreenCaptureSignalDataWrapper +import com.google.android.finsky.ScreenOverlaySignalDataWrapper +import com.google.android.finsky.VersionCodeWrapper +import com.google.android.finsky.buildPlayCoreVersion +import com.google.android.finsky.encodeBase64 +import com.google.android.finsky.getAuthToken +import com.google.android.finsky.getPackageInfoCompat +import com.google.android.finsky.model.IntegrityErrorCode +import com.google.android.finsky.requestIntegritySyncData +import com.google.android.finsky.sha256 +import com.google.android.finsky.signaturesCompat +import com.google.android.gms.droidguard.DroidGuard +import com.google.android.gms.droidguard.internal.DroidGuardResultsRequest +import com.google.android.gms.tasks.await +import com.google.android.play.core.integrity.protocol.IIntegrityService +import com.google.android.play.core.integrity.protocol.IIntegrityServiceCallback +import com.google.android.play.core.integrity.protocol.IRequestDialogCallback +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import okio.ByteString.Companion.toByteString +import org.microg.gms.profile.ProfileManager + +private const val TAG = "IntegrityService" + +class IntegrityService : LifecycleService() { + + override fun onBind(intent: Intent): IBinder? { + super.onBind(intent) + ProfileManager.ensureInitialized(this) + Log.d(TAG, "onBind") + return IntegrityServiceImpl(this, lifecycle).asBinder() + } + + override fun onUnbind(intent: Intent?): Boolean { + Log.d(TAG, "onUnbind") + return super.onUnbind(intent) + } +} + +private class IntegrityServiceImpl(private val context: Context, override val lifecycle: Lifecycle) : IIntegrityService.Stub(), LifecycleOwner { + + override fun requestDialog(bundle: Bundle, callback: IRequestDialogCallback) { + Log.d(TAG, "Method (requestDialog) called but not implemented ") + } + + override fun requestIntegrityToken(request: Bundle, callback: IIntegrityServiceCallback) { + Log.d(TAG, "Method (requestIntegrityToken) called") + val packageName = request.getString(KEY_PACKAGE_NAME) + if (packageName == null) { + callback.onError("", IntegrityErrorCode.INTERNAL_ERROR, "Null packageName.") + return + } + val nonceArr = request.getByteArray(KEY_NONCE) + if (nonceArr == null || nonceArr.count() < 16L || nonceArr.count() > 500L) { + callback.onError(packageName, IntegrityErrorCode.INTERNAL_ERROR, "Nonce error.") + return + } + val cloudProjectNumber = request.getLong(KEY_CLOUD_PROJECT, 0L) + val playCoreVersion = request.buildPlayCoreVersion() + Log.d(TAG, "requestIntegrityToken(packageName: $packageName, nonce: ${nonceArr.encodeBase64(false)}, cloudProjectNumber: $cloudProjectNumber, playCoreVersion: $playCoreVersion)") + + val packageInfo = context.packageManager.getPackageInfoCompat(packageName, SIGNING_FLAGS) + val timestamp = makeTimestamp(System.currentTimeMillis()) + val versionCode = packageInfo.versionCode + + val integrityParams = IntegrityParams( + packageName = PackageNameWrapper(packageName), + versionCode = VersionCodeWrapper(versionCode), + nonce = nonceArr.encodeBase64(noPadding = false, noWrap = true, urlSafe = true), + certificateSha256Digests = packageInfo.signaturesCompat.map { + it.toByteArray().sha256().encodeBase64(noPadding = true, noWrap = true, urlSafe = true) + }, + timestampAtRequest = timestamp + ) + + val data = mutableMapOf( + PARAMS_PKG_KEY to packageName, + PARAMS_VC_KEY to versionCode.toString(), + PARAMS_NONCE_SHA256_KEY to nonceArr.sha256().encodeBase64(noPadding = true, noWrap = true, urlSafe = true), + PARAMS_TM_S_KEY to timestamp.seconds.toString(), + PARAMS_BINDING_KEY to integrityParams.encode().encodeBase64(noPadding = false, noWrap = true, urlSafe = true), + ) + if (cloudProjectNumber > 0L) { + data[PARAMS_GCP_N_KEY] = cloudProjectNumber.toString() + } + + var mapSize = 0 + data.entries.forEach { mapSize += it.key.toByteArray().size + it.value.toByteArray().size } + if (mapSize > 65536) { + callback.onError(packageName, IntegrityErrorCode.INTERNAL_ERROR, "Content binding size exceeded maximum allowed size.") + return + } + + lifecycleScope.launchWhenCreated { + runCatching { + val authToken = getAuthToken(context, AUTH_TOKEN_SCOPE) + if (TextUtils.isEmpty(authToken)) { + Log.w(TAG, "requestIntegrityToken: Got null auth token for type: $AUTH_TOKEN_SCOPE") + } + Log.d(TAG, "requestIntegrityToken authToken: $authToken") + + val droidGuardData = withContext(Dispatchers.IO) { + val droidGuardResultsRequest = DroidGuardResultsRequest() + droidGuardResultsRequest.bundle.putString("thirdPartyCallerAppPackageName", packageName) + Log.d(TAG, "Running DroidGuard (flow: $INTEGRITY_FLOW_NAME, data: $data)") + val droidGuardToken = DroidGuard.getClient(context).getResults(INTEGRITY_FLOW_NAME, data, droidGuardResultsRequest).await() + Log.d(TAG, "Running DroidGuard (flow: $INTEGRITY_FLOW_NAME, droidGuardToken: $droidGuardToken)") + Base64.decode(droidGuardToken, Base64.NO_PADDING or Base64.NO_WRAP or Base64.URL_SAFE).toByteString() + } + + if (droidGuardData.utf8().startsWith(INTEGRITY_PREFIX_ERROR)) { + Log.w(TAG, "droidGuardData: ${droidGuardData.utf8()}") + callback.onError(packageName, IntegrityErrorCode.INTERNAL_ERROR, "DroidGuard failed.") + return@launchWhenCreated + } + + val integrityRequest = IntegrityRequest( + params = integrityParams, + flowName = INTEGRITY_FLOW_NAME, + droidGuardTokenRaw = droidGuardData, + playCoreVersion = playCoreVersion, + playProtectDetails = PlayProtectDetails(PlayProtectState.PLAY_PROTECT_STATE_NO_PROBLEMS), + appAccessRiskDetailsResponse = AppAccessRiskDetailsResponse( + installedAppsSignalDataWrapper = InstalledAppsSignalDataWrapper("."), + screenCaptureSignalDataWrapper = ScreenCaptureSignalDataWrapper("."), + screenOverlaySignalDataWrapper = ScreenOverlaySignalDataWrapper("."), + accessibilityAbuseSignalDataWrapper = AccessibilityAbuseSignalDataWrapper(), + displayListenerMetadataWrapper = DisplayListenerMetadataWrapper( + lastDisplayAddedTimeDelta = makeTimestamp(SystemClock.elapsedRealtimeNanos()) + ) + ) + ) + Log.d(TAG, "requestIntegrityToken integrityRequest: $integrityRequest") + val integrityResponse = requestIntegritySyncData(context, authToken, integrityRequest) + Log.d(TAG, "requestIntegrityToken integrityResponse: $integrityResponse") + + val integrityToken = integrityResponse.contentWrapper?.content?.token + if (integrityToken.isNullOrEmpty()) { + callback.onError(packageName, IntegrityErrorCode.INTERNAL_ERROR, "IntegrityResponse didn't have a token") + return@launchWhenCreated + } + + Log.d(TAG, "requestIntegrityToken integrityToken: $integrityToken") + callback.onSuccess(packageName, integrityToken) + }.onFailure { + Log.w(TAG, "requestIntegrityToken has exception: ", it) + callback.onError(packageName, IntegrityErrorCode.INTERNAL_ERROR, it.message ?: "Exception") + } + } + } +} + +private fun IIntegrityServiceCallback.onError(packageName: String?, errorCode: Int, errorMsg: String) { + if (asBinder()?.isBinderAlive == false) { + Log.e(TAG, "IIntegrityServiceCallback onError Binder died") + return + } + try { + onResult(bundleOf("error" to errorCode)) + } catch (e: Exception) { + Log.e(TAG, "exception $packageName error -> $e") + } + Log.d(TAG, "requestIntegrityToken() failed for $packageName error -> $errorMsg") +} + +private fun IIntegrityServiceCallback.onSuccess(packageName: String?, token: String) { + if (asBinder()?.isBinderAlive == false) { + Log.e(TAG, "IIntegrityServiceCallback onSuccess Binder died") + return + } + try { + onResult(bundleOf("token" to token)) + } catch (e: Exception) { + Log.e(TAG, "exception $packageName error -> $e") + } + Log.d(TAG, "requestIntegrityToken() success for $packageName)") +} \ No newline at end of file diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/model/IntegrityErrorCode.kt b/vending-app/src/main/kotlin/com/google/android/finsky/model/IntegrityErrorCode.kt new file mode 100644 index 0000000000..4d9e4bdc70 --- /dev/null +++ b/vending-app/src/main/kotlin/com/google/android/finsky/model/IntegrityErrorCode.kt @@ -0,0 +1,172 @@ +/* + * SPDX-FileCopyrightText: 2022 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.finsky.model + +import org.microg.gms.common.PublicApi + +@PublicApi +annotation class IntegrityErrorCode { + companion object { + /** + * Integrity API is not available. + * + * Integrity API is not enabled, or the Play Store version might be old. + * + * Recommended actions: + * Make sure that Integrity API is enabled in Google Play Console. + * Ask the user to update Play Store. + */ + const val API_NOT_AVAILABLE = -1 + + /** + * PackageManager could not find this app. + * + * Something is wrong (possibly an attack). Non-actionable. + */ + const val APP_NOT_INSTALLED = -5 + + /** + * The calling app UID (user id) does not match the one from Package Manager. + * + * Something is wrong (possibly an attack). Non-actionable. + */ + const val APP_UID_MISMATCH = -7 + + /** + * Binding to the service in the Play Store has failed. + * This can be due to having an old Play Store version installed on the device. + * + * Ask the user to update Play Store. + */ + const val CANNOT_BIND_TO_SERVICE = -9 + + /** + * The provided request hash is too long. + * + * The request hash length must be less than 500 bytes. + * Retry with a shorter request hash. + * */ + const val REQUEST_HASH_TOO_LONG = -17 + + /** + * There is a transient error on the calling device. + * + * Retry with an exponential backoff. + * + * Introduced in Integrity Play Core version 1.1.0 (prior versions returned a token with empty Device Integrity Verdict). + * If the error persists after a few retries, you should assume that the device has failed integrity checks and act accordingly. + */ + const val CLIENT_TRANSIENT_ERROR = -18 + + /** + * The StandardIntegrityTokenProvider is invalid (e.g. it is outdated). + * + * Request a new integrity token provider by calling StandardIntegrityManager#prepareIntegrityToken. + * */ + const val INTEGRITY_TOKEN_PROVIDER_INVALID = -19 + + /** + * The provided cloud project number is invalid. + * + * Use the cloud project number which can be found in Project info in + * your Google Cloud Console for the cloud project where Play Integrity API is enabled. + */ + const val CLOUD_PROJECT_NUMBER_IS_INVALID = -16 + + /** + * Unknown internal Google server error. + * + * Retry with an exponential backoff. Consider filing a bug if fails consistently. + */ + const val GOOGLE_SERVER_UNAVAILABLE = -12 + + /** + * Unknown error processing integrity request. + * + * Retry with an exponential backoff. Consider filing a bug if fails consistently. + */ + const val INTERNAL_ERROR = -100 + + /** + * Network error: unable to obtain integrity details. + * + * Ask the user to check for a connection. + */ + const val NETWORK_ERROR = -3 + + /** + * Nonce is not encoded as a base64 web-safe no-wrap string. + * + * Retry with correct nonce format. + */ + const val NONCE_IS_NOT_BASE64 = -13 + + /** + * Nonce length is too long. The nonce must be less than 500 bytes before base64 encoding. + * + * Retry with a shorter nonce. + */ + const val NONCE_TOO_LONG = -11 + + /** + * Nonce length is too short. The nonce must be a minimum of 16 bytes (before base64 encoding) to allow for a better security. + * + * Retry with a longer nonce. + */ + const val NONCE_TOO_SHORT = -10 + + /** + * No error. + * + * This is the default value. + */ + const val NO_ERROR = 0 + + /** + * Google Play Services is not available or version is too old. + * + * Ask the user to Install or Update Play Services. + */ + const val PLAY_SERVICES_NOT_FOUND = -6 + + /** + * The Play Services needs to be updated. + * + * Ask the user to update Google Play Services. + */ + const val PLAY_SERVICES_VERSION_OUTDATED = -15 + + /** + * No active account found in the Play Store app. + * Note that the Play Integrity API now supports unauthenticated requests. + * This error code is used only for older Play Store versions that lack support. + * + * Ask the user to authenticate in Play Store. + */ + const val PLAY_STORE_ACCOUNT_NOT_FOUND = -4 + + /** + * The Play Store app is either not installed or not the official version. + * + * Ask the user to install an official and recent version of Play Store. + */ + const val PLAY_STORE_NOT_FOUND = -2 + + /** + * The Play Store needs to be updated. + * + * Ask the user to update the Google Play Store. + */ + const val PLAY_STORE_VERSION_OUTDATED = -14 + + /** + * The calling app is making too many requests to the API and hence is throttled. + * + * Retry with an exponential backoff. + */ + const val TOO_MANY_REQUESTS = -8 + } +} \ No newline at end of file diff --git a/vending-app/src/main/proto/Integrity.proto b/vending-app/src/main/proto/Integrity.proto new file mode 100644 index 0000000000..f90e4bac9d --- /dev/null +++ b/vending-app/src/main/proto/Integrity.proto @@ -0,0 +1,312 @@ +option java_package = "com.google.android.finsky"; +option java_multiple_files = true; + +import "Timestamp.proto"; + +message IntegrityRequest { + oneof droidGuardToken { + string droidGuardTokenBase64 = 2; + bytes droidGuardTokenRaw = 5; + } + optional IntegrityParams params = 1; + optional CompressType compressType = 4; + optional string flowName = 3; + optional PlayCoreVersion playCoreVersion = 6; + optional PlayProtectDetails playProtectDetails = 7; + optional AppAccessRiskDetailsResponse appAccessRiskDetailsResponse = 8; +} + +enum CompressType { + UNKNOWN_COMPRESSION_FORMAT = 0; + GZIP = 1; +} + +message PackageNameWrapper { + optional string value = 1; +} + +message VersionCodeWrapper { + optional int32 value = 1; +} + +message IntegrityParams { + optional PackageNameWrapper packageName = 1; + optional VersionCodeWrapper versionCode = 2; + optional string nonce = 3; + repeated string certificateSha256Digests = 4; + optional Timestamp timestampAtRequest = 5; + // optional int64 unknownInt64 = 6; +} + +message InstalledAppsSignalDataWrapper { + optional string installedAppsSignalData = 1; +} + +message ScreenCaptureSignalDataWrapper { + optional string screenCaptureSignalData = 1; +} + +message ScreenOverlaySignalDataWrapper { + optional string screenOverlaySignalData = 1; +} + +message AccessibilityAbuseSignalDataWrapper { + optional string accessibilityAbuseSignalData = 1; +} + +message DisplayListenerMetadataWrapper { + optional int32 isActiveDisplayPresent = 1; + optional Timestamp displayListenerInitialisationTimeDelta = 2; + optional Timestamp lastDisplayAddedTimeDelta = 3; + optional int32 displayListenerUsed = 4; +} + +message AppAccessRiskDetailsResponse { + optional InstalledAppsSignalDataWrapper installedAppsSignalDataWrapper = 1; + optional ScreenCaptureSignalDataWrapper screenCaptureSignalDataWrapper = 2; + optional ScreenOverlaySignalDataWrapper screenOverlaySignalDataWrapper = 3; + optional AccessibilityAbuseSignalDataWrapper accessibilityAbuseSignalDataWrapper = 4; + optional DisplayListenerMetadataWrapper displayListenerMetadataWrapper = 5; +} + +message IntegrityResponse { + optional IntegrityContentWrapper contentWrapper = 1; + optional IntegrityResponseError integrityResponseError = 2; +} + +message IntegrityResponseError { + optional string error = 2; +} + +message IntegrityContentWrapper { + optional IntegrityContent content = 186; +} + +message IntegrityContent { + optional string token = 1; +} + +message PlayCoreVersion { + optional int32 major = 1; + optional int32 minor = 2; + optional int32 patch = 3; +} + +message PlayProtectDetails { + optional PlayProtectState state = 1; +} + +enum PlayProtectState { + PLAY_PROTECT_STATE_UNKNOWN_PHA_STATE = 0; + PLAY_PROTECT_STATE_NONE = 1; + PLAY_PROTECT_STATE_NO_PROBLEMS = 2; + PLAY_PROTECT_STATE_WARNING = 3; + PLAY_PROTECT_STATE_DANGER = 4; + PLAY_PROTECT_STATE_OFF = 5; +} + +message MetricsUpdate { + optional string requestHash = 1; + optional int32 statesStored = 2; + optional int32 additionalStatesSet = 3; + optional int32 bytesStored = 4; +} + +message IntermediateIntegrityRequest { + optional bytes clientKeyExtendBytes = 1; + optional bytes deviceIntegrityToken = 2; + optional PlayCoreVersion playCoreVersion = 3; + optional PlayProtectDetails playProtectDetails = 4; + // optional bytes c = 5; + optional RequestMode requestMode = 6; + optional int64 sessionId = 7; + message CertificateChainWrapper { + repeated bytes certificateChains = 1; + } + optional CertificateChainWrapper certificateChainWrapper = 8; +} + +message IntermediateIntegrityResponseWrapperExtend { + optional IntermediateIntegrityResponseWrapper intermediateIntegrityResponseWrapper = 1; +} + +message IntermediateIntegrityResponseWrapper { + optional IntermediateIntegrityResponse intermediateIntegrityResponse = 221; +} + +message IntermediateIntegrityResponse { + optional bytes intermediateToken = 1; + optional Timestamp serverGenerated = 2; + optional bool appAccessRiskVerdictEnabled = 4; + optional ErrorResponse errorInfo = 5; +} + +message ExpressIntegrityResponse { + optional bytes deviceIntegrityToken = 1; + optional bytes sessionHashAes128 = 3; + optional bytes appAccessRiskDetailsResponse = 4; +} + +message ErrorResponse { + optional int32 errorCode = 1; + optional int32 status = 2; +} + +message IntermediateIntegritySession { + optional string requestHash = 1; + optional Timestamp creationTime = 2; + optional bytes sessionId = 3; + optional int32 timestampMillis = 4; +} + +message TokenTime { + optional int64 type = 2; + optional Timestamp timestamp = 3; +} + +message IntegrityTokenTime { + optional TokenTime warmUpTokenTime = 1; + optional TokenTime requestTokenTime = 2; +} + +message IntegrityTokenTimeMap { + map timeMap = 1; +} + +message ClientKey { + optional Timestamp generated = 2; + optional bytes keySetHandle = 3; +} + +message AuthTokenWrapper { + optional DeviceIntegrityWrapper deviceIntegrityWrapper = 1; + optional ClientKey clientKey = 2; + optional Timestamp lastManualSoftRefreshTime = 3; +} + +message DeviceIntegrityWrapper { + optional bytes deviceIntegrityToken = 1; + optional Timestamp creationTime = 2; + optional Timestamp serverGenerated = 3; + optional int32 errorCode = 5; +} + +message IntegrityRequestWrapper { + optional DeviceIntegrityWrapper deviceIntegrityWrapper = 1; + optional ClientKey callerKey = 2; + optional string packageName = 3; + optional string accountName = 4; + optional uint64 cloudProjectNumber = 5; + optional int32 webViewRequestMode = 7; +} + +message ExpressFilePB { + optional AuthTokenWrapper tokenWrapper = 2; + repeated IntegrityRequestWrapper integrityRequestWrapper = 3; + optional ClientKey clientKey = 4; + optional IntegrityTokenTimeMap integrityTokenTimeMap = 5; + optional ClientKey expiredDeviceKey = 6; +} + +message AccountNameWrapper { + optional string accountName = 1; +} + +enum INSTALLED_APPS_STATUS { + UNKNOWN_INSTALLED_APPS_SIGNAL = 0; + RECOGNIZED_APPS_INSTALLED = 1; + UNRECOGNIZED_APPS_INSTALLED = 2; +} + +message RequestMode { + optional int32 mode = 1; +} + +message ClientKeyExtend { + optional string optPackageName = 1; + optional int32 versionCode = 2; + repeated string certificateSha256Hashes = 3; + optional int64 cloudProjectNumber = 4; + optional bytes keySetHandle = 5; +} + +message TokenRequestWrapper { + repeated TokenRequest request = 1; +} + +message TokenRequest { + oneof body { + DroidGuardBody droidGuardBody = 5; + } +} + +message DroidGuardBody { + oneof content { + DeviceIntegrityTokenContent deviceBody = 1; + DroidGuardSessionTokenContent tokenBody = 2; + } +} + +message DeviceIntegrityTokenContent { + oneof session { + SessionWrapper sessionWrapper = 1; + } + optional string token = 2; + optional string flowName = 3; +} + +message SessionWrapper { + optional string type = 1; + optional Session session = 2; +} + +message Session { + optional string id = 1; +} + +message DroidGuardSessionTokenContent {} + +message TokenResponse { + optional TokenWrapper tokenWrapper = 1; +} + +message TokenWrapper { + optional TokenContent tokenContent = 183; +} + +message TokenContent { + repeated TokenType tokenType = 1; + repeated string c = 3; +} + +message TokenType { + oneof token { + TokenSessionWrapper tokenSessionWrapper = 2; + } + optional int64 type = 1; +} + +message TokenSessionWrapper { + optional SessionContentWrapper wrapper = 1; +} + +message SessionContentWrapper { + oneof content { + SessionContent sessionContent = 4; + } +} + +message SessionContent { + oneof content { + TokenV1Content tokenContent = 2; + } + optional Session session = 1; +} + +message TokenV1Content { + optional TokenV1Wrapper tokenWrapper = 1; +} + +message TokenV1Wrapper { + optional bytes token = 1; +} \ No newline at end of file From 107083e017f020bc06de7ea0926d510c762a5a54 Mon Sep 17 00:00:00 2001 From: DaVinci9196 <150454414+DaVinci9196@users.noreply.github.com> Date: Wed, 18 Dec 2024 17:38:06 +0800 Subject: [PATCH 132/132] Auth: Fix exceptions caused by folding screens (#2644) --- .../src/main/AndroidManifest.xml | 2 +- .../gms/auth/signin/AssistedSignInActivity.kt | 16 +++-- .../gms/auth/signin/AssistedSignInFragment.kt | 65 +++++++++++++++---- 3 files changed, 64 insertions(+), 19 deletions(-) diff --git a/play-services-core/src/main/AndroidManifest.xml b/play-services-core/src/main/AndroidManifest.xml index 498980221e..540ba14640 100644 --- a/play-services-core/src/main/AndroidManifest.xml +++ b/play-services-core/src/main/AndroidManifest.xml @@ -559,7 +559,7 @@ android:enabled="true" android:exported="true" android:process=":ui" - android:configChanges="keyboardHidden|keyboard|orientation|screenSize" + android:configChanges="keyboardHidden|keyboard|orientation|screenSize|smallestScreenSize|layoutDirection" android:launchMode="singleTask" android:excludeFromRecents="false"> diff --git a/play-services-core/src/main/kotlin/org/microg/gms/auth/signin/AssistedSignInActivity.kt b/play-services-core/src/main/kotlin/org/microg/gms/auth/signin/AssistedSignInActivity.kt index 86b28f286c..353c8be3c5 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/auth/signin/AssistedSignInActivity.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/auth/signin/AssistedSignInActivity.kt @@ -91,10 +91,14 @@ class AssistedSignInActivity : AppCompatActivity() { errorResult(Status(CommonStatusCodes.ERROR, "accounts is empty.")) return } - AssistedSignInFragment(googleSignInOptions!!, beginSignInRequest!!, accounts, clientPackageName!!, - { errorResult(it) }, - { loginResult(it) }) - .show(supportFragmentManager, AssistedSignInFragment.TAG) + val fragment = supportFragmentManager.findFragmentByTag(AssistedSignInFragment.TAG) + if (fragment != null) { + val assistedSignInFragment = fragment as AssistedSignInFragment + assistedSignInFragment.cancelLogin(true) + } else { + AssistedSignInFragment.newInstance(clientPackageName!!, googleSignInOptions!!, beginSignInRequest!!) + .show(supportFragmentManager, AssistedSignInFragment.TAG) + } return } @@ -114,7 +118,7 @@ class AssistedSignInActivity : AppCompatActivity() { startActivityForResult(intent, REQUEST_CODE_SIGN_IN) } - private fun errorResult(status: Status) { + fun errorResult(status: Status) { Log.d(TAG, "errorResult: $status") setResult(RESULT_CANCELED, Intent().apply { putExtra(AuthConstants.STATUS, SafeParcelableSerializer.serializeToBytes(status)) @@ -122,7 +126,7 @@ class AssistedSignInActivity : AppCompatActivity() { finish() } - private fun loginResult(googleSignInAccount: GoogleSignInAccount?) { + fun loginResult(googleSignInAccount: GoogleSignInAccount?) { if (googleSignInAccount == null) { errorResult(Status(CommonStatusCodes.CANCELED, "User cancelled.")) return diff --git a/play-services-core/src/main/kotlin/org/microg/gms/auth/signin/AssistedSignInFragment.kt b/play-services-core/src/main/kotlin/org/microg/gms/auth/signin/AssistedSignInFragment.kt index 673197229f..1d43328eaa 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/auth/signin/AssistedSignInFragment.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/auth/signin/AssistedSignInFragment.kt @@ -6,6 +6,7 @@ package org.microg.gms.auth.signin import android.accounts.Account +import android.accounts.AccountManager import android.app.Dialog import android.content.DialogInterface import android.os.Bundle @@ -19,6 +20,7 @@ import android.widget.ImageView import android.widget.LinearLayout import android.widget.ProgressBar import android.widget.TextView +import androidx.core.content.getSystemService import androidx.lifecycle.lifecycleScope import com.google.android.gms.R import com.google.android.gms.auth.api.identity.BeginSignInRequest @@ -35,22 +37,35 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import org.microg.gms.auth.AuthConstants import org.microg.gms.people.PeopleManager import org.microg.gms.utils.getApplicationLabel -class AssistedSignInFragment( - private val options: GoogleSignInOptions, - private val beginSignInRequest: BeginSignInRequest, - private val accounts: Array, - private val clientPackageName: String, - private val errorBlock: (Status) -> Unit, - private val loginBlock: (GoogleSignInAccount?) -> Unit -) : BottomSheetDialogFragment() { +class AssistedSignInFragment : BottomSheetDialogFragment() { companion object { const val TAG = "AssistedSignInFragment" + private const val KEY_PACKAGE_NAME = "clientPackageName" + private const val KEY_GOOGLE_SIGN_IN_OPTIONS = "googleSignInOptions" + private const val KEY_BEGIN_SIGN_IN_REQUEST = "beginSignInRequest" + + fun newInstance(clientPackageName: String, options: GoogleSignInOptions, request: BeginSignInRequest): AssistedSignInFragment { + val fragment = AssistedSignInFragment() + val args = Bundle().apply { + putString(KEY_PACKAGE_NAME, clientPackageName) + putParcelable(KEY_GOOGLE_SIGN_IN_OPTIONS, options) + putParcelable(KEY_BEGIN_SIGN_IN_REQUEST, request) + } + fragment.arguments = args + return fragment + } } + private lateinit var clientPackageName: String + private lateinit var options: GoogleSignInOptions + private lateinit var beginSignInRequest: BeginSignInRequest + private lateinit var accounts: Array + private var cancelBtn: ImageView? = null private var container: FrameLayout? = null private var loginJob: Job? = null @@ -59,6 +74,16 @@ class AssistedSignInFragment( private var lastChooseAccount: Account? = null private var lastChooseAccountPermitted = false + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + Log.d(TAG, "onCreate start") + clientPackageName = arguments?.getString(KEY_PACKAGE_NAME) ?: return errorResult() + options = arguments?.getParcelable(KEY_GOOGLE_SIGN_IN_OPTIONS) ?: return errorResult() + beginSignInRequest = arguments?.getParcelable(KEY_BEGIN_SIGN_IN_REQUEST) ?: return errorResult() + val accountManager = activity?.getSystemService() ?: return errorResult() + accounts = accountManager.getAccountsByType(AuthConstants.DEFAULT_ACCOUNT_TYPE) + } + override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) Log.d(TAG, "onActivityCreated start") @@ -91,7 +116,7 @@ class AssistedSignInFragment( } }.onFailure { Log.d(TAG, "filterAccountsLogin: error", it) - errorBlock(Status(CommonStatusCodes.INTERNAL_ERROR, "auth error")) + errorResult() return@launch } if (accounts.size == 1) { @@ -244,7 +269,7 @@ class AssistedSignInFragment( override fun onDismiss(dialog: DialogInterface) { val assistedSignInActivity = requireContext() as AssistedSignInActivity if (!assistedSignInActivity.isChangingConfigurations && !isSigningIn) { - errorBlock(Status(CommonStatusCodes.CANCELED, "User cancelled.")) + errorResult() } cancelLogin() super.onDismiss(dialog) @@ -270,10 +295,10 @@ class AssistedSignInFragment( prepareChooseLogin(account, showConsent = true, permitted = true) return@launch } - loginBlock(googleSignInAccount) + loginResult(googleSignInAccount) }.onFailure { Log.d(TAG, "startLogin: error", it) - errorBlock(Status(CommonStatusCodes.INTERNAL_ERROR, "signIn error")) + errorResult() } } } @@ -287,4 +312,20 @@ class AssistedSignInFragment( } } + private fun errorResult() { + if (activity != null && activity is AssistedSignInActivity) { + val assistedSignInActivity = activity as AssistedSignInActivity + assistedSignInActivity.errorResult(Status(CommonStatusCodes.INTERNAL_ERROR, "signIn error")) + } + activity?.finish() + } + + private fun loginResult(googleSignInAccount: GoogleSignInAccount?) { + if (activity != null && activity is AssistedSignInActivity) { + val assistedSignInActivity = activity as AssistedSignInActivity + assistedSignInActivity.loginResult(googleSignInAccount) + } + activity?.finish() + } + }