From b9b72785d3d13c4ecc0baaddb7bee08a909eeef9 Mon Sep 17 00:00:00 2001 From: Marvin W Date: Thu, 12 Dec 2024 11:48:01 +0100 Subject: [PATCH 1/6] 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 2/6] 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 3/6] 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 4/6] 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 5/6] 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 6/6] 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() + } + }