diff --git a/build.gradle b/build.gradle index 0f3b4e8e75..4023c8848d 100644 --- a/build.gradle +++ b/build.gradle @@ -37,6 +37,14 @@ buildscript { ext.androidTargetSdk = 29 ext.androidCompileSdk = 33 + ext.localProperties = new Properties() + + try { + ext.localProperties.load(rootProject.file('local.properties').newDataInputStream()) + } catch (ignored) { + // Ignore + } + repositories { mavenCentral() google() @@ -59,7 +67,7 @@ def execResult(...args) { return stdout.toString() } -def gmsVersion = "22.36.16" +def gmsVersion = "23.16.57" def gmsVersionCode = Integer.parseInt(gmsVersion.replaceAll('\\.', '')) def gitVersionBase = execResult('git', 'describe', '--tags', '--abbrev=0', '--match=v[0-9]*').trim().substring(1) def gitCommitCount = Integer.parseInt(execResult('git', 'rev-list', '--count', "v$gitVersionBase..HEAD").trim()) diff --git a/firebase-auth/core/src/main/kotlin/org/microg/gms/firebase/auth/FirebaseAuthService.kt b/firebase-auth/core/src/main/kotlin/org/microg/gms/firebase/auth/FirebaseAuthService.kt index dce3008791..d5af19cada 100644 --- a/firebase-auth/core/src/main/kotlin/org/microg/gms/firebase/auth/FirebaseAuthService.kt +++ b/firebase-auth/core/src/main/kotlin/org/microg/gms/firebase/auth/FirebaseAuthService.kt @@ -9,7 +9,7 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter -import android.os.Build +import android.os.Build.VERSION.SDK_INT import android.os.Handler import android.os.Parcel import android.provider.Telephony @@ -62,7 +62,7 @@ private val UserProfileChangeRequest.deleteAttributeList: List } private fun Intent.getSmsMessages(): Array { - return if (Build.VERSION.SDK_INT >= 19) { + return if (SDK_INT >= 19) { Telephony.Sms.Intents.getMessagesFromIntent(this) } else { (getSerializableExtra("pdus") as? Array)?.map { SmsMessage.createFromPdu(it) }.orEmpty().toTypedArray() @@ -83,7 +83,7 @@ class FirebaseAuthService : BaseService(TAG, GmsService.FIREBASE_AUTH) { } class FirebaseAuthServiceImpl(private val context: Context, private val lifecycle: Lifecycle, private val packageName: String, private val libraryVersion: String?, private val apiKey: String) : IFirebaseAuthService.Stub(), LifecycleOwner { - private val client = IdentityToolkitClient(context, apiKey) + private val client by lazy { IdentityToolkitClient(context, apiKey, packageName, PackageUtils.firstSignatureDigestBytes(context, packageName)) } private var authorizedDomain: String? = null private suspend fun getAuthorizedDomain(): String { diff --git a/firebase-auth/core/src/main/kotlin/org/microg/gms/firebase/auth/IdentityToolkitClient.kt b/firebase-auth/core/src/main/kotlin/org/microg/gms/firebase/auth/IdentityToolkitClient.kt index 459feb1f47..68021a7621 100644 --- a/firebase-auth/core/src/main/kotlin/org/microg/gms/firebase/auth/IdentityToolkitClient.kt +++ b/firebase-auth/core/src/main/kotlin/org/microg/gms/firebase/auth/IdentityToolkitClient.kt @@ -20,6 +20,7 @@ import com.android.volley.toolbox.Volley import org.json.JSONArray import org.json.JSONException import org.json.JSONObject +import org.microg.gms.utils.toHexString import java.io.UnsupportedEncodingException import java.lang.RuntimeException import java.nio.charset.Charset @@ -29,19 +30,26 @@ import kotlin.coroutines.suspendCoroutine private const val TAG = "GmsFirebaseAuthClient" -class IdentityToolkitClient(context: Context, private val apiKey: String) { +class IdentityToolkitClient(context: Context, private val apiKey: String, private val packageName: String? = null, private val certSha1Hash: ByteArray? = null) { private val queue = Volley.newRequestQueue(context) private fun buildRelyingPartyUrl(method: String) = "https://www.googleapis.com/identitytoolkit/v3/relyingparty/$method?key=$apiKey" private fun buildStsUrl(method: String) = "https://securetoken.googleapis.com/v1/$method?key=$apiKey" + private fun getRequestHeaders(): Map = hashMapOf().apply { + if (packageName != null) put("X-Android-Package", packageName) + if (certSha1Hash != null) put("X-Android-Cert", certSha1Hash.toHexString().uppercase()) + } + private suspend fun request(method: String, data: JSONObject): JSONObject = suspendCoroutine { continuation -> - queue.add(JsonObjectRequest(POST, buildRelyingPartyUrl(method), data, { + queue.add(object : JsonObjectRequest(POST, buildRelyingPartyUrl(method), data, { continuation.resume(it) }, { - Log.d(TAG, String(it.networkResponse.data)) + Log.d(TAG, "Error: ${it.networkResponse?.data?.decodeToString() ?: it.message}") continuation.resumeWithException(RuntimeException(it)) - })) + }) { + override fun getHeaders(): Map = getRequestHeaders() + }) } suspend fun createAuthUri(identifier: String? = null, tenantId: String? = null, continueUri: String? = "http://localhost"): JSONObject = @@ -117,23 +125,23 @@ class IdentityToolkitClient(context: Context, private val apiKey: String) { .put("sessionInfo", sessionInfo)) suspend fun getTokenByRefreshToken(refreshToken: String): JSONObject = suspendCoroutine { continuation -> - queue.add(StsRequest(POST, buildStsUrl("token"), "grant_type=refresh_token&refresh_token=$refreshToken", { continuation.resume(it) }, { continuation.resumeWithException(RuntimeException(it)) })) - } -} - -private class StsRequest(method: Int, url: String, request: String?, listener: (JSONObject) -> Unit, errorListener: (VolleyError) -> Unit) : JsonRequest(method, url, request, listener, errorListener) { - override fun parseNetworkResponse(response: NetworkResponse?): Response { - return try { - val jsonString = String(response!!.data, Charset.forName(HttpHeaderParser.parseCharset(response!!.headers, PROTOCOL_CHARSET))) - Response.success(JSONObject(jsonString), HttpHeaderParser.parseCacheHeaders(response)) - } catch (e: UnsupportedEncodingException) { - Response.error(ParseError(e)) - } catch (je: JSONException) { - Response.error(ParseError(je)) - } - } - - override fun getBodyContentType(): String { - return "application/x-www-form-urlencoded" + queue.add(object : JsonRequest(POST, buildStsUrl("token"), "grant_type=refresh_token&refresh_token=$refreshToken", { continuation.resume(it) }, { continuation.resumeWithException(RuntimeException(it)) }) { + override fun parseNetworkResponse(response: NetworkResponse?): Response { + return try { + val jsonString = String(response!!.data, Charset.forName(HttpHeaderParser.parseCharset(response!!.headers, PROTOCOL_CHARSET))) + Response.success(JSONObject(jsonString), HttpHeaderParser.parseCacheHeaders(response)) + } catch (e: UnsupportedEncodingException) { + Response.error(ParseError(e)) + } catch (je: JSONException) { + Response.error(ParseError(je)) + } + } + + override fun getBodyContentType(): String { + return "application/x-www-form-urlencoded" + } + + override fun getHeaders(): Map = getRequestHeaders() + }) } -} +} \ No newline at end of file diff --git a/play-services-ads-base/build.gradle b/play-services-ads-base/build.gradle new file mode 100644 index 0000000000..c270c2279f --- /dev/null +++ b/play-services-ads-base/build.gradle @@ -0,0 +1,32 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +apply plugin: 'com.android.library' +apply plugin: 'maven-publish' +apply plugin: 'signing' + +android { + compileSdkVersion androidCompileSdk + buildToolsVersion "$androidBuildVersionTools" + + defaultConfig { + versionName version + minSdkVersion androidMinSdk + targetSdkVersion androidTargetSdk + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } +} + +apply from: '../gradle/publish-android.gradle' + +description = 'microG implementation of play-services-ads-base' + +dependencies { + api project(':play-services-basement') +} diff --git a/play-services-ads-base/src/main/AndroidManifest.xml b/play-services-ads-base/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..dd01f9f11f --- /dev/null +++ b/play-services-ads-base/src/main/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/play-services-ads-identifier/build.gradle b/play-services-ads-identifier/build.gradle new file mode 100644 index 0000000000..83d509f7a8 --- /dev/null +++ b/play-services-ads-identifier/build.gradle @@ -0,0 +1,32 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +apply plugin: 'com.android.library' +apply plugin: 'maven-publish' +apply plugin: 'signing' + +android { + compileSdkVersion androidCompileSdk + buildToolsVersion "$androidBuildVersionTools" + + defaultConfig { + versionName version + minSdkVersion androidMinSdk + targetSdkVersion androidTargetSdk + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } +} + +apply from: '../gradle/publish-android.gradle' + +description = 'microG implementation of play-services-ads-identifier' + +dependencies { + api project(':play-services-basement') +} diff --git a/play-services-ads-identifier/core/build.gradle b/play-services-ads-identifier/core/build.gradle new file mode 100644 index 0000000000..264eaf6691 --- /dev/null +++ b/play-services-ads-identifier/core/build.gradle @@ -0,0 +1,32 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +dependencies { + api project(':play-services-ads-identifier') + implementation project(':play-services-base-core') +} + +android { + compileSdkVersion androidCompileSdk + buildToolsVersion "$androidBuildVersionTools" + + defaultConfig { + versionName version + minSdkVersion androidMinSdk + targetSdkVersion androidTargetSdk + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + compileOptions { + sourceCompatibility = 1.8 + targetCompatibility = 1.8 + } +} diff --git a/play-services-ads-identifier/core/src/main/AndroidManifest.xml b/play-services-ads-identifier/core/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..4f4c53e031 --- /dev/null +++ b/play-services-ads-identifier/core/src/main/AndroidManifest.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + diff --git a/play-services-ads-identifier/core/src/main/kotlin/org/microg/gms/ads/identifier/AdvertisingIdService.kt b/play-services-ads-identifier/core/src/main/kotlin/org/microg/gms/ads/identifier/AdvertisingIdService.kt new file mode 100644 index 0000000000..1705116d4a --- /dev/null +++ b/play-services-ads-identifier/core/src/main/kotlin/org/microg/gms/ads/identifier/AdvertisingIdService.kt @@ -0,0 +1,34 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ +package org.microg.gms.ads.identifier + +import android.app.Service +import android.content.Intent +import android.os.IBinder +import com.google.android.gms.ads.identifier.internal.IAdvertisingIdService + +class AdvertisingIdService : Service() { + override fun onBind(intent: Intent): IBinder? { + return AdvertisingIdServiceImpl().asBinder() + } +} + +class AdvertisingIdServiceImpl : IAdvertisingIdService.Stub() { + override fun getAdvertisingId(): String { + return "00000000-0000-0000-0000-000000000000" + } + + override fun isAdTrackingLimited(defaultHint: Boolean): Boolean { + return true + } + + override fun generateAdvertisingId(packageName: String): String { + return advertisingId // Ad tracking limited + } + + override fun setAdTrackingLimited(packageName: String, limited: Boolean) { + // Ignored, sorry :) + } +} diff --git a/play-services-ads-identifier/src/main/AndroidManifest.xml b/play-services-ads-identifier/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..266de1f1ec --- /dev/null +++ b/play-services-ads-identifier/src/main/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/play-services-api/src/main/aidl/com/google/android/gms/ads/identifier/internal/IAdvertisingIdService.aidl b/play-services-ads-identifier/src/main/aidl/com/google/android/gms/ads/identifier/internal/IAdvertisingIdService.aidl similarity index 100% rename from play-services-api/src/main/aidl/com/google/android/gms/ads/identifier/internal/IAdvertisingIdService.aidl rename to play-services-ads-identifier/src/main/aidl/com/google/android/gms/ads/identifier/internal/IAdvertisingIdService.aidl diff --git a/play-services-ads-identifier/src/main/java/com/google/android/gms/ads/identifier/AdvertisingIdClient.java b/play-services-ads-identifier/src/main/java/com/google/android/gms/ads/identifier/AdvertisingIdClient.java new file mode 100644 index 0000000000..657c4a54fa --- /dev/null +++ b/play-services-ads-identifier/src/main/java/com/google/android/gms/ads/identifier/AdvertisingIdClient.java @@ -0,0 +1,80 @@ +/* + * 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.ads.identifier; + +import android.app.Activity; +import android.content.Context; +import android.provider.Settings; +import com.google.android.gms.common.GooglePlayServicesNotAvailableException; +import com.google.android.gms.common.GooglePlayServicesRepairableException; + +import java.io.IOException; + +/** + * Helper library for retrieval of advertising ID and related information such as the limit ad tracking setting. + *

+ * It is intended that the advertising ID completely replace existing usage of other identifiers for ads purposes (such as use + * of {@code ANDROID_ID} in {@link Settings.Secure}) when Google Play Services is available. Cases where Google Play Services is + * unavailable are indicated by a {@link GooglePlayServicesNotAvailableException} being thrown by getAdvertisingIdInfo(). + */ +public class AdvertisingIdClient { + /** + * Retrieves the user's advertising ID and limit ad tracking preference. + *

+ * This method cannot be called in the main thread as it may block leading to ANRs. An {@code IllegalStateException} will be + * thrown if this is called on the main thread. + * + * @param context Current {@link Context} (such as the current {@link Activity}). + * @return AdvertisingIdClient.Info with user's advertising ID and limit ad tracking preference. + * @throws IOException signaling connection to Google Play Services failed. + * @throws IllegalStateException indicating this method was called on the main thread. + * @throws GooglePlayServicesNotAvailableException indicating that Google Play is not installed on this device. + * @throws GooglePlayServicesRepairableException indicating that there was a recoverable error connecting to Google Play Services. + */ + public static Info getAdvertisingIdInfo(Context context) { + // We don't actually implement this functionality, but always claim that ad tracking was limited by user preference + return new Info("00000000-0000-0000-0000-000000000000", true); + } + + /** + * Includes both the advertising ID as well as the limit ad tracking setting. + */ + public static class Info { + private final String advertisingId; + private final boolean limitAdTrackingEnabled; + + /** + * Constructs an {@code Info} Object with the specified advertising Id and limit ad tracking setting. + * + * @param advertisingId The advertising ID. + * @param limitAdTrackingEnabled The limit ad tracking setting. It is true if the user has limit ad tracking enabled. False, otherwise. + */ + public Info(String advertisingId, boolean limitAdTrackingEnabled) { + this.advertisingId = advertisingId; + this.limitAdTrackingEnabled = limitAdTrackingEnabled; + } + + /** + * Retrieves the advertising ID. + */ + public String getId() { + return advertisingId; + } + + /** + * Retrieves whether the user has limit ad tracking enabled or not. + *

+ * When the returned value is true, the returned value of {@link #getId()} will always be + * {@code 00000000-0000-0000-0000-000000000000} starting with Android 12. + */ + public boolean isLimitAdTrackingEnabled() { + return limitAdTrackingEnabled; + } + } +} diff --git a/play-services-ads-identifier/src/main/java/com/google/android/gms/ads/identifier/package-info.java b/play-services-ads-identifier/src/main/java/com/google/android/gms/ads/identifier/package-info.java new file mode 100644 index 0000000000..ff949da1a3 --- /dev/null +++ b/play-services-ads-identifier/src/main/java/com/google/android/gms/ads/identifier/package-info.java @@ -0,0 +1,11 @@ +/* + * SPDX-FileCopyrightText: 2022 microG Project Team + * SPDX-License-Identifier: CC-BY-4.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. + */ +/** + * Contains classes relating to the Android Advertising ID (AAID). + */ +package com.google.android.gms.ads.identifier; diff --git a/play-services-ads-lite/build.gradle b/play-services-ads-lite/build.gradle new file mode 100644 index 0000000000..f3bb5a485d --- /dev/null +++ b/play-services-ads-lite/build.gradle @@ -0,0 +1,36 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +apply plugin: 'com.android.library' +apply plugin: 'maven-publish' +apply plugin: 'signing' + +android { + compileSdkVersion androidCompileSdk + buildToolsVersion "$androidBuildVersionTools" + + defaultConfig { + versionName version + minSdkVersion androidMinSdk + targetSdkVersion androidTargetSdk + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } +} + +apply from: '../gradle/publish-android.gradle' + +description = 'microG implementation of play-services-ads-lite' + +dependencies { + api 'androidx.work:work-runtime:2.7.0' + api project(':play-services-ads-base') + api project(':play-services-basement') +// api project(':play-services-measurement-sdk-api') +// api project(':user-messaging-platform') +} diff --git a/play-services-ads-lite/core/build.gradle b/play-services-ads-lite/core/build.gradle new file mode 100644 index 0000000000..1725ce8baa --- /dev/null +++ b/play-services-ads-lite/core/build.gradle @@ -0,0 +1,32 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +dependencies { + api project(':play-services-ads-lite') + implementation project(':play-services-base-core') +} + +android { + compileSdkVersion androidCompileSdk + buildToolsVersion "$androidBuildVersionTools" + + defaultConfig { + versionName version + minSdkVersion androidMinSdk + targetSdkVersion androidTargetSdk + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + compileOptions { + sourceCompatibility = 1.8 + targetCompatibility = 1.8 + } +} diff --git a/play-services-ads-lite/core/src/main/AndroidManifest.xml b/play-services-ads-lite/core/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..8172918c78 --- /dev/null +++ b/play-services-ads-lite/core/src/main/AndroidManifest.xml @@ -0,0 +1,8 @@ + + + + diff --git a/play-services-ads-lite/core/src/main/kotlin/com/google/android/gms/ads/AdLoaderBuilderCreatorImpl.kt b/play-services-ads-lite/core/src/main/kotlin/com/google/android/gms/ads/AdLoaderBuilderCreatorImpl.kt new file mode 100644 index 0000000000..a4f930850d --- /dev/null +++ b/play-services-ads-lite/core/src/main/kotlin/com/google/android/gms/ads/AdLoaderBuilderCreatorImpl.kt @@ -0,0 +1,26 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ +package com.google.android.gms.ads + +import android.os.IBinder +import android.os.Parcel +import android.util.Log +import androidx.annotation.Keep +import com.google.android.gms.ads.internal.client.IAdLoaderBuilderCreator +import com.google.android.gms.ads.internal.meditation.client.IAdapterCreator +import com.google.android.gms.dynamic.IObjectWrapper +import org.microg.gms.utils.warnOnTransactionIssues + +private const val TAG = "AdLoaderBuilder" + +@Keep +class AdLoaderBuilderCreatorImpl : IAdLoaderBuilderCreator.Stub() { + override fun newAdLoaderBuilder(context: IObjectWrapper?, adUnitId: String, adapterCreator: IAdapterCreator?, clientVersion: Int): IBinder? { + Log.d(TAG, "newAdLoaderBuilder: adUnitId=$adUnitId clientVersion=$clientVersion") + return null + } + + override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean = warnOnTransactionIssues(code, reply, flags, TAG) { super.onTransact(code, data, reply, flags) } +} diff --git a/play-services-ads-lite/core/src/main/kotlin/com/google/android/gms/ads/AdManagerCreatorImpl.kt b/play-services-ads-lite/core/src/main/kotlin/com/google/android/gms/ads/AdManagerCreatorImpl.kt new file mode 100644 index 0000000000..398956f8c0 --- /dev/null +++ b/play-services-ads-lite/core/src/main/kotlin/com/google/android/gms/ads/AdManagerCreatorImpl.kt @@ -0,0 +1,17 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ +package com.google.android.gms.ads + +import android.os.Parcel +import androidx.annotation.Keep +import org.microg.gms.utils.warnOnTransactionIssues + +private const val TAG = "AdManager" + +@Keep +class AdManagerCreatorImpl : AdManagerCreator.Stub() { + override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean = + warnOnTransactionIssues(code, reply, flags, TAG) { super.onTransact(code, data, reply, flags) } +} diff --git a/play-services-ads-lite/core/src/main/kotlin/com/google/android/gms/ads/MobileAdsSettingManagerCreatorImpl.kt b/play-services-ads-lite/core/src/main/kotlin/com/google/android/gms/ads/MobileAdsSettingManagerCreatorImpl.kt new file mode 100644 index 0000000000..176a17d83c --- /dev/null +++ b/play-services-ads-lite/core/src/main/kotlin/com/google/android/gms/ads/MobileAdsSettingManagerCreatorImpl.kt @@ -0,0 +1,29 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ +package com.google.android.gms.ads + +import android.content.Context +import android.os.IBinder +import android.os.Parcel +import android.util.Log +import androidx.annotation.Keep +import com.google.android.gms.ads.internal.client.IMobileAdsSettingManagerCreator +import com.google.android.gms.dynamic.IObjectWrapper +import com.google.android.gms.dynamic.ObjectWrapper +import org.microg.gms.ads.MobileAdsSettingManagerImpl +import org.microg.gms.utils.warnOnTransactionIssues + +private const val TAG = "AdsSettingManager" + +@Keep +class MobileAdsSettingManagerCreatorImpl : IMobileAdsSettingManagerCreator.Stub() { + override fun getMobileAdsSettingManager(context: IObjectWrapper?, clientVersion: Int): IBinder { + Log.d(TAG, "getMobileAdsSettingManager($clientVersion)") + return MobileAdsSettingManagerImpl(ObjectWrapper.unwrap(context) as Context) + } + + override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean = warnOnTransactionIssues(code, reply, flags, TAG) { super.onTransact(code, data, reply, flags) } +} + diff --git a/play-services-ads-lite/core/src/main/kotlin/com/google/android/gms/ads/measurement/DynamiteMeasurementManager.kt b/play-services-ads-lite/core/src/main/kotlin/com/google/android/gms/ads/measurement/DynamiteMeasurementManager.kt new file mode 100644 index 0000000000..0ad53909ac --- /dev/null +++ b/play-services-ads-lite/core/src/main/kotlin/com/google/android/gms/ads/measurement/DynamiteMeasurementManager.kt @@ -0,0 +1,10 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ +package com.google.android.gms.ads.measurement + +import androidx.annotation.Keep + +@Keep +class DynamiteMeasurementManager : IMeasurementManager.Stub() diff --git a/play-services-ads-lite/core/src/main/kotlin/com/google/android/gms/ads/rewarded/ChimeraRewardedAdCreatorImpl.kt b/play-services-ads-lite/core/src/main/kotlin/com/google/android/gms/ads/rewarded/ChimeraRewardedAdCreatorImpl.kt new file mode 100644 index 0000000000..a2a1cb51e3 --- /dev/null +++ b/play-services-ads-lite/core/src/main/kotlin/com/google/android/gms/ads/rewarded/ChimeraRewardedAdCreatorImpl.kt @@ -0,0 +1,26 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ +package com.google.android.gms.ads.rewarded + +import android.content.Context +import android.os.IBinder +import android.util.Log +import androidx.annotation.Keep +import com.google.android.gms.ads.internal.meditation.client.IAdapterCreator +import com.google.android.gms.ads.internal.rewarded.client.IRewardedAdCreator +import com.google.android.gms.dynamic.IObjectWrapper +import com.google.android.gms.dynamic.ObjectWrapper +import org.microg.gms.ads.rewarded.RewardedAdImpl + +private const val TAG = "RewardedAd" + +@Keep +class ChimeraRewardedAdCreatorImpl : IRewardedAdCreator.Stub() { + override fun newRewardedAd(context: IObjectWrapper, str: String, adapterCreator: IAdapterCreator, clientVersion: Int): IBinder { + Log.d(TAG, "newRewardedAd($str, $clientVersion)") + return RewardedAdImpl(ObjectWrapper.unwrap(context) as Context?, str, adapterCreator, clientVersion) + } +} + diff --git a/play-services-ads-lite/core/src/main/kotlin/org/microg/gms/ads/MobileAdsSettingManagerImpl.kt b/play-services-ads-lite/core/src/main/kotlin/org/microg/gms/ads/MobileAdsSettingManagerImpl.kt new file mode 100644 index 0000000000..4620a4b1ad --- /dev/null +++ b/play-services-ads-lite/core/src/main/kotlin/org/microg/gms/ads/MobileAdsSettingManagerImpl.kt @@ -0,0 +1,97 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ +package org.microg.gms.ads + +import android.content.Context +import android.os.Handler +import android.os.Looper +import android.os.Parcel +import android.os.RemoteException +import android.util.Log +import com.google.android.gms.ads.internal.AdapterStatusParcel +import com.google.android.gms.ads.internal.RequestConfigurationParcel +import com.google.android.gms.ads.internal.client.IMobileAdsSettingManager +import com.google.android.gms.ads.internal.client.IOnAdInspectorClosedListener +import com.google.android.gms.ads.internal.initialization.IInitializationCallback +import com.google.android.gms.dynamic.IObjectWrapper +import org.microg.gms.utils.warnOnTransactionIssues + +private const val TAG = "AdsSettingManager" + +class MobileAdsSettingManagerImpl(private val context: Context?) : IMobileAdsSettingManager.Stub() { + override fun initialize() { + Log.d(TAG, "initialize") + } + + override fun setAppVolume(volume: Float) { + Log.d(TAG, "setAppVolume") + } + + override fun setAppMuted(muted: Boolean) { + Log.d(TAG, "setAppMuted") + } + + override fun openDebugMenu(context: IObjectWrapper?, adUnitId: String?) { + Log.d(TAG, "openDebugMenu($adUnitId)") + } + + override fun fetchAppSettings(appId: String?, runnable: IObjectWrapper?) { + Log.d(TAG, "fetchAppSettings($appId)") + } + + override fun getAdVolume(): Float { + return 0f + } + + override fun isAdMuted(): Boolean { + return true + } + + override fun getVersionString(): String { + return "" + } + + override fun registerRtbAdapter(className: String?) { + Log.d(TAG, "registerRtbAdapter($className)") + } + + override fun addInitializationCallback(callback: IInitializationCallback?) { + Log.d(TAG, "addInitializationCallback") + Handler(Looper.getMainLooper()).post(Runnable { + try { + callback?.onInitialized(adapterStatus) + } catch (e: RemoteException) { + Log.w(TAG, e) + } + }) + } + + override fun getAdapterStatus(): List { + Log.d(TAG, "getAdapterStatus") + return arrayListOf(AdapterStatusParcel("com.google.android.gms.ads.MobileAds", true, 0, "Dummy")) + } + + override fun setRequestConfiguration(configuration: RequestConfigurationParcel?) { + Log.d(TAG, "setRequestConfiguration") + } + + override fun disableMediationAdapterInitialization() { + Log.d(TAG, "disableMediationAdapterInitialization") + } + + override fun openAdInspector(listener: IOnAdInspectorClosedListener?) { + Log.d(TAG, "openAdInspector") + } + + override fun enableSameAppKey(enabled: Boolean) { + Log.d(TAG, "enableSameAppKey($enabled)") + } + + override fun setPlugin(plugin: String?) { + Log.d(TAG, "setPlugin($plugin)") + } + + 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-ads-lite/core/src/main/kotlin/org/microg/gms/ads/rewarded/ResponseInfoImpl.kt b/play-services-ads-lite/core/src/main/kotlin/org/microg/gms/ads/rewarded/ResponseInfoImpl.kt new file mode 100644 index 0000000000..7767301d09 --- /dev/null +++ b/play-services-ads-lite/core/src/main/kotlin/org/microg/gms/ads/rewarded/ResponseInfoImpl.kt @@ -0,0 +1,40 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.ads.rewarded + +import android.os.Bundle +import android.util.Log +import com.google.android.gms.ads.internal.AdapterResponseInfoParcel +import com.google.android.gms.ads.internal.client.IResponseInfo + +private const val TAG = "RewardedAdResponseInfo" + +class ResponseInfoImpl : IResponseInfo.Stub() { + override fun getMediationAdapterClassName(): String? { + Log.d(TAG, "getMediationAdapterClassName") + return null + } + + override fun getResponseId(): String? { + Log.d(TAG, "getResponseId") + return null + } + + override fun getAdapterResponseInfo(): List { + Log.d(TAG, "getAdapterResponseInfo") + return arrayListOf() + } + + override fun getLoadedAdapterResponse(): AdapterResponseInfoParcel? { + Log.d(TAG, "getLoadedAdapterResponse") + return null + } + + override fun getResponseExtras(): Bundle { + Log.d(TAG, "getResponseExtras") + return Bundle() + } +} \ No newline at end of file diff --git a/play-services-ads-lite/core/src/main/kotlin/org/microg/gms/ads/rewarded/RewardedAdImpl.kt b/play-services-ads-lite/core/src/main/kotlin/org/microg/gms/ads/rewarded/RewardedAdImpl.kt new file mode 100644 index 0000000000..e34cb38d5b --- /dev/null +++ b/play-services-ads-lite/core/src/main/kotlin/org/microg/gms/ads/rewarded/RewardedAdImpl.kt @@ -0,0 +1,108 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.ads.rewarded + +import android.content.Context +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.os.RemoteException +import android.util.Log +import com.google.android.gms.ads.internal.AdErrorParcel +import com.google.android.gms.ads.internal.AdRequestParcel +import com.google.android.gms.ads.internal.ServerSideVerificationOptionsParcel +import com.google.android.gms.ads.internal.client.IOnAdMetadataChangedListener +import com.google.android.gms.ads.internal.client.IOnPaidEventListener +import com.google.android.gms.ads.internal.client.IResponseInfo +import com.google.android.gms.ads.internal.meditation.client.IAdapterCreator +import com.google.android.gms.ads.internal.rewarded.client.* +import com.google.android.gms.common.api.CommonStatusCodes +import com.google.android.gms.dynamic.IObjectWrapper + +private const val TAG = "RewardedAd" + +class RewardedAdImpl(context: Context?, str: String?, adapterCreator: IAdapterCreator?, clientVersion: Int) : IRewardedAd.Stub() { + private var immersive: Boolean = false + + private fun load(request: AdRequestParcel, callback: IRewardedAdLoadCallback, interstitial: Boolean) { + Handler(Looper.getMainLooper()).post { + try { + callback.onAdLoadError(AdErrorParcel().apply { code = CommonStatusCodes.INTERNAL_ERROR; message = "Not supported" }) + } catch (e: RemoteException) { + Log.w(TAG, e) + } + } + } + + override fun load(request: AdRequestParcel, callback: IRewardedAdLoadCallback) { + Log.d(TAG, "load") + load(request, callback, false) + } + + override fun setCallback(callback: IRewardedAdCallback) { + Log.d(TAG, "setCallback") + } + + override fun canBeShown(): Boolean { + Log.d(TAG, "canBeShown") + return false + } + + override fun getMediationAdapterClassName(): String { + Log.d(TAG, "getMediationAdapterClassName") + return responseInfo.mediationAdapterClassName + } + + override fun show(activity: IObjectWrapper) { + Log.d(TAG, "show") + showWithImmersive(activity, immersive) + } + + override fun setRewardedAdSkuListener(listener: IRewardedAdSkuListener?) { + Log.d(TAG, "setRewardedAdSkuListener") + } + + override fun setServerSideVerificationOptions(options: ServerSideVerificationOptionsParcel) { + Log.d(TAG, "setServerSideVerificationOptions") + } + + override fun setOnAdMetadataChangedListener(listener: IOnAdMetadataChangedListener) { + Log.d(TAG, "setOnAdMetadataChangedListener") + } + + override fun getAdMetadata(): Bundle { + Log.d(TAG, "getAdMetadata") + return Bundle() + } + + override fun showWithImmersive(activity: IObjectWrapper?, immersive: Boolean) { + Log.d(TAG, "showWithBoolean") + } + + override fun getRewardItem(): IRewardItem? { + Log.d(TAG, "getRewardItem") + return null + } + + override fun getResponseInfo(): IResponseInfo { + Log.d(TAG, "getResponseInfo") + return ResponseInfoImpl() + } + + override fun setOnPaidEventListener(listener: IOnPaidEventListener) { + Log.d(TAG, "setOnPaidEventListener") + } + + override fun loadInterstitial(request: AdRequestParcel, callback: IRewardedAdLoadCallback) { + Log.d(TAG, "loadInterstitial") + load(request, callback, true) + } + + override fun setImmersiveMode(enabled: Boolean) { + Log.d(TAG, "setImmersiveMode($enabled)") + } +} + diff --git a/play-services-ads-lite/src/main/AndroidManifest.xml b/play-services-ads-lite/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..45aedac2dd --- /dev/null +++ b/play-services-ads-lite/src/main/AndroidManifest.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/play-services-api/src/main/aidl/com/google/android/gms/ads/AdManagerCreator.aidl b/play-services-ads-lite/src/main/aidl/com/google/android/gms/ads/AdManagerCreator.aidl similarity index 100% rename from play-services-api/src/main/aidl/com/google/android/gms/ads/AdManagerCreator.aidl rename to play-services-ads-lite/src/main/aidl/com/google/android/gms/ads/AdManagerCreator.aidl diff --git a/play-services-ads-lite/src/main/aidl/com/google/android/gms/ads/internal/AdErrorParcel.aidl b/play-services-ads-lite/src/main/aidl/com/google/android/gms/ads/internal/AdErrorParcel.aidl new file mode 100644 index 0000000000..54251c3470 --- /dev/null +++ b/play-services-ads-lite/src/main/aidl/com/google/android/gms/ads/internal/AdErrorParcel.aidl @@ -0,0 +1,3 @@ +package com.google.android.gms.ads.internal; + +parcelable AdErrorParcel; \ No newline at end of file diff --git a/play-services-ads-lite/src/main/aidl/com/google/android/gms/ads/internal/AdRequestParcel.aidl b/play-services-ads-lite/src/main/aidl/com/google/android/gms/ads/internal/AdRequestParcel.aidl new file mode 100644 index 0000000000..220425ea78 --- /dev/null +++ b/play-services-ads-lite/src/main/aidl/com/google/android/gms/ads/internal/AdRequestParcel.aidl @@ -0,0 +1,3 @@ +package com.google.android.gms.ads.internal; + +parcelable AdRequestParcel; \ No newline at end of file diff --git a/play-services-ads-lite/src/main/aidl/com/google/android/gms/ads/internal/AdapterResponseInfoParcel.aidl b/play-services-ads-lite/src/main/aidl/com/google/android/gms/ads/internal/AdapterResponseInfoParcel.aidl new file mode 100644 index 0000000000..822be57d43 --- /dev/null +++ b/play-services-ads-lite/src/main/aidl/com/google/android/gms/ads/internal/AdapterResponseInfoParcel.aidl @@ -0,0 +1,3 @@ +package com.google.android.gms.ads.internal; + +parcelable AdapterResponseInfoParcel; \ No newline at end of file diff --git a/play-services-ads-lite/src/main/aidl/com/google/android/gms/ads/internal/AdapterStatusParcel.aidl b/play-services-ads-lite/src/main/aidl/com/google/android/gms/ads/internal/AdapterStatusParcel.aidl new file mode 100644 index 0000000000..fc1bd40cfe --- /dev/null +++ b/play-services-ads-lite/src/main/aidl/com/google/android/gms/ads/internal/AdapterStatusParcel.aidl @@ -0,0 +1,3 @@ +package com.google.android.gms.ads.internal; + +parcelable AdapterStatusParcel; \ No newline at end of file diff --git a/play-services-ads-lite/src/main/aidl/com/google/android/gms/ads/internal/RequestConfigurationParcel.aidl b/play-services-ads-lite/src/main/aidl/com/google/android/gms/ads/internal/RequestConfigurationParcel.aidl new file mode 100644 index 0000000000..45e9ebeb2f --- /dev/null +++ b/play-services-ads-lite/src/main/aidl/com/google/android/gms/ads/internal/RequestConfigurationParcel.aidl @@ -0,0 +1,3 @@ +package com.google.android.gms.ads.internal; + +parcelable RequestConfigurationParcel; \ No newline at end of file diff --git a/play-services-ads-lite/src/main/aidl/com/google/android/gms/ads/internal/ServerSideVerificationOptionsParcel.aidl b/play-services-ads-lite/src/main/aidl/com/google/android/gms/ads/internal/ServerSideVerificationOptionsParcel.aidl new file mode 100644 index 0000000000..b0b93d492d --- /dev/null +++ b/play-services-ads-lite/src/main/aidl/com/google/android/gms/ads/internal/ServerSideVerificationOptionsParcel.aidl @@ -0,0 +1,3 @@ +package com.google.android.gms.ads.internal; + +parcelable ServerSideVerificationOptionsParcel; \ No newline at end of file diff --git a/play-services-ads-lite/src/main/aidl/com/google/android/gms/ads/internal/client/IAdLoaderBuilderCreator.aidl b/play-services-ads-lite/src/main/aidl/com/google/android/gms/ads/internal/client/IAdLoaderBuilderCreator.aidl new file mode 100644 index 0000000000..3feeb1da3e --- /dev/null +++ b/play-services-ads-lite/src/main/aidl/com/google/android/gms/ads/internal/client/IAdLoaderBuilderCreator.aidl @@ -0,0 +1,8 @@ +package com.google.android.gms.ads.internal.client; + +import com.google.android.gms.ads.internal.meditation.client.IAdapterCreator; +import com.google.android.gms.dynamic.IObjectWrapper; + +interface IAdLoaderBuilderCreator { + IBinder newAdLoaderBuilder(IObjectWrapper context, String adUnitId, IAdapterCreator adapterCreator, int clientVersion); +} \ No newline at end of file diff --git a/play-services-ads-lite/src/main/aidl/com/google/android/gms/ads/internal/client/IMobileAdsSettingManager.aidl b/play-services-ads-lite/src/main/aidl/com/google/android/gms/ads/internal/client/IMobileAdsSettingManager.aidl new file mode 100644 index 0000000000..aaf02a56f6 --- /dev/null +++ b/play-services-ads-lite/src/main/aidl/com/google/android/gms/ads/internal/client/IMobileAdsSettingManager.aidl @@ -0,0 +1,26 @@ +package com.google.android.gms.ads.internal.client; + +import com.google.android.gms.ads.internal.AdapterStatusParcel; +import com.google.android.gms.ads.internal.RequestConfigurationParcel; +import com.google.android.gms.ads.internal.client.IOnAdInspectorClosedListener; +import com.google.android.gms.ads.internal.initialization.IInitializationCallback; +import com.google.android.gms.dynamic.IObjectWrapper; + +interface IMobileAdsSettingManager { + void initialize() = 0; + void setAppVolume(float volume) = 1; + void setAppMuted(boolean muted) = 3; + void openDebugMenu(IObjectWrapper context, String adUnitId) = 4; + void fetchAppSettings(String appId, IObjectWrapper runnable) = 5; + float getAdVolume() = 6; + boolean isAdMuted() = 7; + String getVersionString() = 8; + void registerRtbAdapter(String className) = 9; + void addInitializationCallback(IInitializationCallback callback) = 11; + List getAdapterStatus() = 12; + void setRequestConfiguration(in RequestConfigurationParcel configuration) = 13; + void disableMediationAdapterInitialization() = 14; + void openAdInspector(IOnAdInspectorClosedListener listener) = 15; + void enableSameAppKey(boolean enabled) = 16; + void setPlugin(String plugin) = 17; +} \ No newline at end of file diff --git a/play-services-ads-lite/src/main/aidl/com/google/android/gms/ads/internal/client/IMobileAdsSettingManagerCreator.aidl b/play-services-ads-lite/src/main/aidl/com/google/android/gms/ads/internal/client/IMobileAdsSettingManagerCreator.aidl new file mode 100644 index 0000000000..f468200741 --- /dev/null +++ b/play-services-ads-lite/src/main/aidl/com/google/android/gms/ads/internal/client/IMobileAdsSettingManagerCreator.aidl @@ -0,0 +1,7 @@ +package com.google.android.gms.ads.internal.client; + +import com.google.android.gms.dynamic.IObjectWrapper; + +interface IMobileAdsSettingManagerCreator { + IBinder getMobileAdsSettingManager(IObjectWrapper context, int clientVersion); +} \ No newline at end of file diff --git a/play-services-ads-lite/src/main/aidl/com/google/android/gms/ads/internal/client/IOnAdInspectorClosedListener.aidl b/play-services-ads-lite/src/main/aidl/com/google/android/gms/ads/internal/client/IOnAdInspectorClosedListener.aidl new file mode 100644 index 0000000000..63f625c8d4 --- /dev/null +++ b/play-services-ads-lite/src/main/aidl/com/google/android/gms/ads/internal/client/IOnAdInspectorClosedListener.aidl @@ -0,0 +1,7 @@ +package com.google.android.gms.ads.internal.client; + +import com.google.android.gms.ads.internal.AdErrorParcel; + +interface IOnAdInspectorClosedListener { + void onAdInspectorClosed(in @nullable AdErrorParcel adErrorParcel); +} \ No newline at end of file diff --git a/play-services-ads-lite/src/main/aidl/com/google/android/gms/ads/internal/client/IOnAdMetadataChangedListener.aidl b/play-services-ads-lite/src/main/aidl/com/google/android/gms/ads/internal/client/IOnAdMetadataChangedListener.aidl new file mode 100644 index 0000000000..3a4ba6ef48 --- /dev/null +++ b/play-services-ads-lite/src/main/aidl/com/google/android/gms/ads/internal/client/IOnAdMetadataChangedListener.aidl @@ -0,0 +1,6 @@ +package com.google.android.gms.ads.internal.client; + +import com.google.android.gms.ads.internal.AdErrorParcel; + +interface IOnAdMetadataChangedListener { +} \ No newline at end of file diff --git a/play-services-ads-lite/src/main/aidl/com/google/android/gms/ads/internal/client/IOnPaidEventListener.aidl b/play-services-ads-lite/src/main/aidl/com/google/android/gms/ads/internal/client/IOnPaidEventListener.aidl new file mode 100644 index 0000000000..8f2d342450 --- /dev/null +++ b/play-services-ads-lite/src/main/aidl/com/google/android/gms/ads/internal/client/IOnPaidEventListener.aidl @@ -0,0 +1,6 @@ +package com.google.android.gms.ads.internal.client; + +import com.google.android.gms.ads.internal.AdErrorParcel; + +interface IOnPaidEventListener { +} \ No newline at end of file diff --git a/play-services-ads-lite/src/main/aidl/com/google/android/gms/ads/internal/client/IResponseInfo.aidl b/play-services-ads-lite/src/main/aidl/com/google/android/gms/ads/internal/client/IResponseInfo.aidl new file mode 100644 index 0000000000..3b6fcd7991 --- /dev/null +++ b/play-services-ads-lite/src/main/aidl/com/google/android/gms/ads/internal/client/IResponseInfo.aidl @@ -0,0 +1,11 @@ +package com.google.android.gms.ads.internal.client; + +import com.google.android.gms.ads.internal.AdapterResponseInfoParcel; + +interface IResponseInfo { + String getMediationAdapterClassName() = 0; + String getResponseId() = 1; + List getAdapterResponseInfo() = 2; + AdapterResponseInfoParcel getLoadedAdapterResponse() = 3; + Bundle getResponseExtras() = 4; +} \ No newline at end of file diff --git a/play-services-ads-lite/src/main/aidl/com/google/android/gms/ads/internal/initialization/IInitializationCallback.aidl b/play-services-ads-lite/src/main/aidl/com/google/android/gms/ads/internal/initialization/IInitializationCallback.aidl new file mode 100644 index 0000000000..259c86995a --- /dev/null +++ b/play-services-ads-lite/src/main/aidl/com/google/android/gms/ads/internal/initialization/IInitializationCallback.aidl @@ -0,0 +1,7 @@ +package com.google.android.gms.ads.internal.initialization; + +import com.google.android.gms.ads.internal.AdapterStatusParcel; + +interface IInitializationCallback { + void onInitialized(in List status); +} \ No newline at end of file diff --git a/play-services-ads-lite/src/main/aidl/com/google/android/gms/ads/internal/meditation/client/IAdapterCreator.aidl b/play-services-ads-lite/src/main/aidl/com/google/android/gms/ads/internal/meditation/client/IAdapterCreator.aidl new file mode 100644 index 0000000000..f97fcf5872 --- /dev/null +++ b/play-services-ads-lite/src/main/aidl/com/google/android/gms/ads/internal/meditation/client/IAdapterCreator.aidl @@ -0,0 +1,5 @@ +package com.google.android.gms.ads.internal.meditation.client; + +interface IAdapterCreator { + +} \ No newline at end of file diff --git a/play-services-ads-lite/src/main/aidl/com/google/android/gms/ads/internal/rewarded/client/IRewardItem.aidl b/play-services-ads-lite/src/main/aidl/com/google/android/gms/ads/internal/rewarded/client/IRewardItem.aidl new file mode 100644 index 0000000000..9d0cb537d6 --- /dev/null +++ b/play-services-ads-lite/src/main/aidl/com/google/android/gms/ads/internal/rewarded/client/IRewardItem.aidl @@ -0,0 +1,4 @@ +package com.google.android.gms.ads.internal.rewarded.client; + +interface IRewardItem { +} \ No newline at end of file diff --git a/play-services-ads-lite/src/main/aidl/com/google/android/gms/ads/internal/rewarded/client/IRewardedAd.aidl b/play-services-ads-lite/src/main/aidl/com/google/android/gms/ads/internal/rewarded/client/IRewardedAd.aidl new file mode 100644 index 0000000000..b3e1f5170a --- /dev/null +++ b/play-services-ads-lite/src/main/aidl/com/google/android/gms/ads/internal/rewarded/client/IRewardedAd.aidl @@ -0,0 +1,30 @@ +package com.google.android.gms.ads.internal.rewarded.client; + +import com.google.android.gms.ads.internal.AdRequestParcel; +import com.google.android.gms.ads.internal.ServerSideVerificationOptionsParcel; +import com.google.android.gms.ads.internal.client.IOnPaidEventListener; +import com.google.android.gms.ads.internal.client.IOnAdMetadataChangedListener; +import com.google.android.gms.ads.internal.client.IResponseInfo; +import com.google.android.gms.ads.internal.rewarded.client.IRewardedAdCallback; +import com.google.android.gms.ads.internal.rewarded.client.IRewardedAdLoadCallback; +import com.google.android.gms.ads.internal.rewarded.client.IRewardedAdSkuListener; +import com.google.android.gms.ads.internal.rewarded.client.IRewardItem; +import com.google.android.gms.dynamic.IObjectWrapper; + +interface IRewardedAd { + void load(in AdRequestParcel request, IRewardedAdLoadCallback callback) = 0; + void setCallback(IRewardedAdCallback callback) = 1; + boolean canBeShown() = 2; + String getMediationAdapterClassName() = 3; + void show(IObjectWrapper activity) = 4; + void setRewardedAdSkuListener(IRewardedAdSkuListener listener) = 5; + void setServerSideVerificationOptions(in ServerSideVerificationOptionsParcel options) = 6; + void setOnAdMetadataChangedListener(IOnAdMetadataChangedListener listener) = 7; + Bundle getAdMetadata() = 8; + void showWithImmersive(IObjectWrapper activity, boolean immersive) = 9; + IRewardItem getRewardItem() = 10; + IResponseInfo getResponseInfo() = 11; + void setOnPaidEventListener(IOnPaidEventListener listener) = 12; + void loadInterstitial(in AdRequestParcel request, IRewardedAdLoadCallback callback) = 13; + void setImmersiveMode(boolean enabled) = 14; +} \ No newline at end of file diff --git a/play-services-ads-lite/src/main/aidl/com/google/android/gms/ads/internal/rewarded/client/IRewardedAdCallback.aidl b/play-services-ads-lite/src/main/aidl/com/google/android/gms/ads/internal/rewarded/client/IRewardedAdCallback.aidl new file mode 100644 index 0000000000..22b7f59717 --- /dev/null +++ b/play-services-ads-lite/src/main/aidl/com/google/android/gms/ads/internal/rewarded/client/IRewardedAdCallback.aidl @@ -0,0 +1,4 @@ +package com.google.android.gms.ads.internal.rewarded.client; + +interface IRewardedAdCallback { +} \ No newline at end of file diff --git a/play-services-ads-lite/src/main/aidl/com/google/android/gms/ads/internal/rewarded/client/IRewardedAdCreator.aidl b/play-services-ads-lite/src/main/aidl/com/google/android/gms/ads/internal/rewarded/client/IRewardedAdCreator.aidl new file mode 100644 index 0000000000..8edf85acf5 --- /dev/null +++ b/play-services-ads-lite/src/main/aidl/com/google/android/gms/ads/internal/rewarded/client/IRewardedAdCreator.aidl @@ -0,0 +1,8 @@ +package com.google.android.gms.ads.internal.rewarded.client; + +import com.google.android.gms.ads.internal.meditation.client.IAdapterCreator; +import com.google.android.gms.dynamic.IObjectWrapper; + +interface IRewardedAdCreator { + IBinder newRewardedAd(IObjectWrapper context, String str, IAdapterCreator adapterCreator, int clientVersion); +} \ No newline at end of file diff --git a/play-services-ads-lite/src/main/aidl/com/google/android/gms/ads/internal/rewarded/client/IRewardedAdLoadCallback.aidl b/play-services-ads-lite/src/main/aidl/com/google/android/gms/ads/internal/rewarded/client/IRewardedAdLoadCallback.aidl new file mode 100644 index 0000000000..2bfb179b96 --- /dev/null +++ b/play-services-ads-lite/src/main/aidl/com/google/android/gms/ads/internal/rewarded/client/IRewardedAdLoadCallback.aidl @@ -0,0 +1,9 @@ +package com.google.android.gms.ads.internal.rewarded.client; + +import com.google.android.gms.ads.internal.AdErrorParcel; + +interface IRewardedAdLoadCallback { + void onAdLoaded() = 0; + void onAdLoadErrorCode(int code) = 1; + void onAdLoadError(in AdErrorParcel error) = 2; +} \ No newline at end of file diff --git a/play-services-ads-lite/src/main/aidl/com/google/android/gms/ads/internal/rewarded/client/IRewardedAdSkuListener.aidl b/play-services-ads-lite/src/main/aidl/com/google/android/gms/ads/internal/rewarded/client/IRewardedAdSkuListener.aidl new file mode 100644 index 0000000000..e41f52ea51 --- /dev/null +++ b/play-services-ads-lite/src/main/aidl/com/google/android/gms/ads/internal/rewarded/client/IRewardedAdSkuListener.aidl @@ -0,0 +1,6 @@ +package com.google.android.gms.ads.internal.rewarded.client; + +import com.google.android.gms.ads.internal.AdErrorParcel; + +interface IRewardedAdSkuListener { +} \ No newline at end of file diff --git a/play-services-ads-lite/src/main/aidl/com/google/android/gms/ads/measurement/IMeasurementManager.aidl b/play-services-ads-lite/src/main/aidl/com/google/android/gms/ads/measurement/IMeasurementManager.aidl new file mode 100644 index 0000000000..e82159df4f --- /dev/null +++ b/play-services-ads-lite/src/main/aidl/com/google/android/gms/ads/measurement/IMeasurementManager.aidl @@ -0,0 +1,5 @@ +package com.google.android.gms.ads.measurement; + +interface IMeasurementManager { + +} \ No newline at end of file diff --git a/play-services-ads-lite/src/main/java/com/google/android/gms/ads/admanager/package-info.java b/play-services-ads-lite/src/main/java/com/google/android/gms/ads/admanager/package-info.java new file mode 100644 index 0000000000..a08a3ba9c3 --- /dev/null +++ b/play-services-ads-lite/src/main/java/com/google/android/gms/ads/admanager/package-info.java @@ -0,0 +1,11 @@ +/* + * SPDX-FileCopyrightText: 2022 microG Project Team + * SPDX-License-Identifier: CC-BY-4.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. + */ +/** + * Contains classes for Google Ad Manager. + */ +package com.google.android.gms.ads.admanager; diff --git a/play-services-ads-lite/src/main/java/com/google/android/gms/ads/h5/package-info.java b/play-services-ads-lite/src/main/java/com/google/android/gms/ads/h5/package-info.java new file mode 100644 index 0000000000..c40c2689f3 --- /dev/null +++ b/play-services-ads-lite/src/main/java/com/google/android/gms/ads/h5/package-info.java @@ -0,0 +1,11 @@ +/* + * SPDX-FileCopyrightText: 2022 microG Project Team + * SPDX-License-Identifier: CC-BY-4.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. + */ +/** + * Contains classes for H5 ads. + */ +package com.google.android.gms.ads.h5; diff --git a/play-services-ads-lite/src/main/java/com/google/android/gms/ads/initialization/package-info.java b/play-services-ads-lite/src/main/java/com/google/android/gms/ads/initialization/package-info.java new file mode 100644 index 0000000000..f06f3c0f74 --- /dev/null +++ b/play-services-ads-lite/src/main/java/com/google/android/gms/ads/initialization/package-info.java @@ -0,0 +1,11 @@ +/* + * SPDX-FileCopyrightText: 2022 microG Project Team + * SPDX-License-Identifier: CC-BY-4.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. + */ +/** + * Contains classes related to SDK initialization. + */ +package com.google.android.gms.ads.initialization; diff --git a/play-services-ads-lite/src/main/java/com/google/android/gms/ads/internal/AdDataParcel.java b/play-services-ads-lite/src/main/java/com/google/android/gms/ads/internal/AdDataParcel.java new file mode 100644 index 0000000000..89f1f30719 --- /dev/null +++ b/play-services-ads-lite/src/main/java/com/google/android/gms/ads/internal/AdDataParcel.java @@ -0,0 +1,12 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.ads.internal; + +import org.microg.safeparcel.AutoSafeParcelable; + +public class AdDataParcel extends AutoSafeParcelable { + public static final Creator CREATOR = new AutoCreator<>(AdDataParcel.class); +} diff --git a/play-services-ads-lite/src/main/java/com/google/android/gms/ads/internal/AdErrorParcel.java b/play-services-ads-lite/src/main/java/com/google/android/gms/ads/internal/AdErrorParcel.java new file mode 100644 index 0000000000..9757b47453 --- /dev/null +++ b/play-services-ads-lite/src/main/java/com/google/android/gms/ads/internal/AdErrorParcel.java @@ -0,0 +1,23 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.ads.internal; + +import com.google.android.gms.ads.internal.client.IResponseInfo; +import org.microg.safeparcel.AutoSafeParcelable; + +public class AdErrorParcel extends AutoSafeParcelable { + @Field(1) + public int code; + @Field(2) + public String message; + @Field(3) + public String domain; + @Field(4) + public AdErrorParcel cause; + @Field(5) + public IResponseInfo responseInfo; + public static final Creator CREATOR = new AutoCreator<>(AdErrorParcel.class); +} diff --git a/play-services-ads-lite/src/main/java/com/google/android/gms/ads/internal/AdRequestParcel.java b/play-services-ads-lite/src/main/java/com/google/android/gms/ads/internal/AdRequestParcel.java new file mode 100644 index 0000000000..cb96053db3 --- /dev/null +++ b/play-services-ads-lite/src/main/java/com/google/android/gms/ads/internal/AdRequestParcel.java @@ -0,0 +1,62 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.ads.internal; + +import android.location.Location; +import android.os.Bundle; +import org.microg.safeparcel.AutoSafeParcelable; + +import java.util.ArrayList; +import java.util.List; + +public class AdRequestParcel extends AutoSafeParcelable { + @Field(1) + private int versionCode = 8; + @Field(2) + public long birthday; + @Field(3) + public Bundle adMobNetworkExtras = new Bundle(); + @Field(4) + public int gender; + @Field(5) + public ArrayList keywords; + @Field(6) + public boolean isTestDevice; + @Field(7) + public int taggedForChildDirectedTreatment; + @Field(9) + public String publisherProvidedId; + @Field(10) + public SearchAdRequestParcel searchAdRequest; + @Field(11) + public Location location; + @Field(12) + public String contentUrl; + @Field(13) + public Bundle networkExtrasBundles = new Bundle(); + @Field(14) + public Bundle customTargeting; + @Field(15) + public List categoryExclusion; + @Field(16) + public String requestAgent; + @Field(18) + public boolean designedForFamilies; + @Field(19) + public AdDataParcel adData; + @Field(20) + public int tagForUnderAgeOfConsent; + @Field(21) + public String maxAdContentRating; + @Field(22) + public List neighboringContentUrls; + @Field(23) + public int httpTimeoutMillis; + @Field(24) + public String adString; + + public static final Creator CREATOR = new AutoCreator<>(AdRequestParcel.class); +} diff --git a/play-services-ads-lite/src/main/java/com/google/android/gms/ads/internal/AdapterResponseInfoParcel.java b/play-services-ads-lite/src/main/java/com/google/android/gms/ads/internal/AdapterResponseInfoParcel.java new file mode 100644 index 0000000000..f1a452e33c --- /dev/null +++ b/play-services-ads-lite/src/main/java/com/google/android/gms/ads/internal/AdapterResponseInfoParcel.java @@ -0,0 +1,30 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.ads.internal; + +import android.os.Bundle; +import org.microg.safeparcel.AutoSafeParcelable; + +public class AdapterResponseInfoParcel extends AutoSafeParcelable { + @Field(1) + public String adapterClassName; + @Field(2) + public long latencyMillis; + @Field(3) + public AdErrorParcel error; + @Field(4) + public Bundle credentials; + @Field(5) + public String adSourceName; + @Field(6) + public String adSourceId; + @Field(7) + public String adSourceInstanceName; + @Field(8) + public String adSourceInstanceId; + + public static final Creator CREATOR = new AutoCreator<>(AdapterResponseInfoParcel.class); +} diff --git a/play-services-ads-lite/src/main/java/com/google/android/gms/ads/internal/AdapterStatusParcel.java b/play-services-ads-lite/src/main/java/com/google/android/gms/ads/internal/AdapterStatusParcel.java new file mode 100644 index 0000000000..cd04062a13 --- /dev/null +++ b/play-services-ads-lite/src/main/java/com/google/android/gms/ads/internal/AdapterStatusParcel.java @@ -0,0 +1,30 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.ads.internal; + +import org.microg.safeparcel.AutoSafeParcelable; + +public class AdapterStatusParcel extends AutoSafeParcelable { + @Field(1) + public String className; + @Field(2) + public boolean isReady; + @Field(3) + public int latency; + @Field(4) + public String description; + + public AdapterStatusParcel() {} + + public AdapterStatusParcel(String className, boolean isReady, int latency, String description) { + this.className = className; + this.isReady = isReady; + this.latency = latency; + this.description = description; + } + + public static final Creator CREATOR = new AutoCreator<>(AdapterStatusParcel.class); +} diff --git a/play-services-ads-lite/src/main/java/com/google/android/gms/ads/internal/RequestConfigurationParcel.java b/play-services-ads-lite/src/main/java/com/google/android/gms/ads/internal/RequestConfigurationParcel.java new file mode 100644 index 0000000000..f773f34868 --- /dev/null +++ b/play-services-ads-lite/src/main/java/com/google/android/gms/ads/internal/RequestConfigurationParcel.java @@ -0,0 +1,12 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.ads.internal; + +import org.microg.safeparcel.AutoSafeParcelable; + +public class RequestConfigurationParcel extends AutoSafeParcelable { + public static final Creator CREATOR = new AutoCreator<>(RequestConfigurationParcel.class); +} diff --git a/play-services-ads-lite/src/main/java/com/google/android/gms/ads/internal/SearchAdRequestParcel.java b/play-services-ads-lite/src/main/java/com/google/android/gms/ads/internal/SearchAdRequestParcel.java new file mode 100644 index 0000000000..d37b1c56b2 --- /dev/null +++ b/play-services-ads-lite/src/main/java/com/google/android/gms/ads/internal/SearchAdRequestParcel.java @@ -0,0 +1,14 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.ads.internal; + +import org.microg.safeparcel.AutoSafeParcelable; + +public class SearchAdRequestParcel extends AutoSafeParcelable { + @Field(15) + public String query; + public static final Creator CREATOR = new AutoCreator<>(SearchAdRequestParcel.class); +} diff --git a/play-services-ads-lite/src/main/java/com/google/android/gms/ads/internal/ServerSideVerificationOptionsParcel.java b/play-services-ads-lite/src/main/java/com/google/android/gms/ads/internal/ServerSideVerificationOptionsParcel.java new file mode 100644 index 0000000000..1b516a33e6 --- /dev/null +++ b/play-services-ads-lite/src/main/java/com/google/android/gms/ads/internal/ServerSideVerificationOptionsParcel.java @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.ads.internal; + +import org.microg.safeparcel.AutoSafeParcelable; + +public class ServerSideVerificationOptionsParcel extends AutoSafeParcelable { + @Field(1) + public String userId; + @Field(2) + public String customData; + public static final Creator CREATOR = new AutoCreator<>(ServerSideVerificationOptionsParcel.class); +} diff --git a/play-services-ads-lite/src/main/java/com/google/android/gms/ads/interstitial/package-info.java b/play-services-ads-lite/src/main/java/com/google/android/gms/ads/interstitial/package-info.java new file mode 100644 index 0000000000..dc0a9f2710 --- /dev/null +++ b/play-services-ads-lite/src/main/java/com/google/android/gms/ads/interstitial/package-info.java @@ -0,0 +1,11 @@ +/* + * SPDX-FileCopyrightText: 2022 microG Project Team + * SPDX-License-Identifier: CC-BY-4.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. + */ +/** + * Contains classes for Interstitial Ads. + */ +package com.google.android.gms.ads.interstitial; diff --git a/play-services-ads-lite/src/main/java/com/google/android/gms/ads/mediation/customevent/package-info.java b/play-services-ads-lite/src/main/java/com/google/android/gms/ads/mediation/customevent/package-info.java new file mode 100644 index 0000000000..06bb35b183 --- /dev/null +++ b/play-services-ads-lite/src/main/java/com/google/android/gms/ads/mediation/customevent/package-info.java @@ -0,0 +1,11 @@ +/* + * SPDX-FileCopyrightText: 2022 microG Project Team + * SPDX-License-Identifier: CC-BY-4.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. + */ +/** + * Contains classes for Google Mobile Ads mediation custom events. + */ +package com.google.android.gms.ads.mediation.customevent; diff --git a/play-services-ads-lite/src/main/java/com/google/android/gms/ads/mediation/package-info.java b/play-services-ads-lite/src/main/java/com/google/android/gms/ads/mediation/package-info.java new file mode 100644 index 0000000000..d1df53da9c --- /dev/null +++ b/play-services-ads-lite/src/main/java/com/google/android/gms/ads/mediation/package-info.java @@ -0,0 +1,11 @@ +/* + * SPDX-FileCopyrightText: 2022 microG Project Team + * SPDX-License-Identifier: CC-BY-4.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. + */ +/** + * Contains classes for Google Mobile Ads mediation adapters. + */ +package com.google.android.gms.ads.mediation; diff --git a/play-services-ads-lite/src/main/java/com/google/android/gms/ads/mediation/rtb/package-info.java b/play-services-ads-lite/src/main/java/com/google/android/gms/ads/mediation/rtb/package-info.java new file mode 100644 index 0000000000..af175ec4f4 --- /dev/null +++ b/play-services-ads-lite/src/main/java/com/google/android/gms/ads/mediation/rtb/package-info.java @@ -0,0 +1,11 @@ +/* + * SPDX-FileCopyrightText: 2022 microG Project Team + * SPDX-License-Identifier: CC-BY-4.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. + */ +/** + * Contains classes for Google Mobile Ads RTB mediation adapters. + */ +package com.google.android.gms.ads.mediation.rtb; diff --git a/play-services-ads-lite/src/main/java/com/google/android/gms/ads/nativead/package-info.java b/play-services-ads-lite/src/main/java/com/google/android/gms/ads/nativead/package-info.java new file mode 100644 index 0000000000..a1c245cc9c --- /dev/null +++ b/play-services-ads-lite/src/main/java/com/google/android/gms/ads/nativead/package-info.java @@ -0,0 +1,12 @@ +/* + * SPDX-FileCopyrightText: 2022 microG Project Team + * SPDX-License-Identifier: CC-BY-4.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. + */ +/** + * Contains classes for native ads functionality within Google Mobile + Ads. + */ +package com.google.android.gms.ads.nativead; diff --git a/play-services-ads-lite/src/main/java/com/google/android/gms/ads/package-info.java b/play-services-ads-lite/src/main/java/com/google/android/gms/ads/package-info.java new file mode 100644 index 0000000000..69f78b98cd --- /dev/null +++ b/play-services-ads-lite/src/main/java/com/google/android/gms/ads/package-info.java @@ -0,0 +1,11 @@ +/* + * SPDX-FileCopyrightText: 2022 microG Project Team + * SPDX-License-Identifier: CC-BY-4.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. + */ +/** + * Contains classes for Google Mobile Ads. + */ +package com.google.android.gms.ads; diff --git a/play-services-ads-lite/src/main/java/com/google/android/gms/ads/rewarded/package-info.java b/play-services-ads-lite/src/main/java/com/google/android/gms/ads/rewarded/package-info.java new file mode 100644 index 0000000000..685d4c4e68 --- /dev/null +++ b/play-services-ads-lite/src/main/java/com/google/android/gms/ads/rewarded/package-info.java @@ -0,0 +1,11 @@ +/* + * SPDX-FileCopyrightText: 2022 microG Project Team + * SPDX-License-Identifier: CC-BY-4.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. + */ +/** + * Contains classes for Rewarded Ads. + */ +package com.google.android.gms.ads.rewarded; diff --git a/play-services-ads-lite/src/main/java/com/google/android/gms/ads/rewardedinterstitial/package-info.java b/play-services-ads-lite/src/main/java/com/google/android/gms/ads/rewardedinterstitial/package-info.java new file mode 100644 index 0000000000..c37680e687 --- /dev/null +++ b/play-services-ads-lite/src/main/java/com/google/android/gms/ads/rewardedinterstitial/package-info.java @@ -0,0 +1,11 @@ +/* + * SPDX-FileCopyrightText: 2022 microG Project Team + * SPDX-License-Identifier: CC-BY-4.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. + */ +/** + * Contains classes for Rewarded Interstitial Ads. + */ +package com.google.android.gms.ads.rewardedinterstitial; diff --git a/play-services-ads-lite/src/main/java/com/google/android/gms/ads/search/package-info.java b/play-services-ads-lite/src/main/java/com/google/android/gms/ads/search/package-info.java new file mode 100644 index 0000000000..50d72e4d9e --- /dev/null +++ b/play-services-ads-lite/src/main/java/com/google/android/gms/ads/search/package-info.java @@ -0,0 +1,11 @@ +/* + * SPDX-FileCopyrightText: 2022 microG Project Team + * SPDX-License-Identifier: CC-BY-4.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. + */ +/** + * Contains classes for Search Ads for Apps. + */ +package com.google.android.gms.ads.search; diff --git a/play-services-ads/build.gradle b/play-services-ads/build.gradle new file mode 100644 index 0000000000..a51316f42b --- /dev/null +++ b/play-services-ads/build.gradle @@ -0,0 +1,40 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +apply plugin: 'com.android.library' +apply plugin: 'maven-publish' +apply plugin: 'signing' + +android { + compileSdkVersion androidCompileSdk + buildToolsVersion "$androidBuildVersionTools" + + defaultConfig { + versionName version + minSdkVersion androidMinSdk + targetSdkVersion androidTargetSdk + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } +} + +apply from: '../gradle/publish-android.gradle' + +description = 'microG implementation of play-services-ads' + +dependencies { + implementation 'androidx.browser:browser:1.4.0' + implementation 'androidx.collection:collection:1.0.0' + implementation 'androidx.core:core:1.0.0' + api project(':play-services-ads-base') + api project(':play-services-ads-identifier') + api project(':play-services-ads-lite') +// api project(':play-services-appset') + api project(':play-services-basement') + api project(':play-services-tasks') +} diff --git a/play-services-ads/core/build.gradle b/play-services-ads/core/build.gradle new file mode 100644 index 0000000000..c4293cad2d --- /dev/null +++ b/play-services-ads/core/build.gradle @@ -0,0 +1,34 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +dependencies { + api project(':play-services-ads') + implementation project(':play-services-base-core') + + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion" +} + +android { + compileSdkVersion androidCompileSdk + buildToolsVersion "$androidBuildVersionTools" + + defaultConfig { + versionName version + minSdkVersion androidMinSdk + targetSdkVersion androidTargetSdk + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + compileOptions { + sourceCompatibility = 1.8 + targetCompatibility = 1.8 + } +} diff --git a/play-services-ads/core/src/main/AndroidManifest.xml b/play-services-ads/core/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..3b9a89d17f --- /dev/null +++ b/play-services-ads/core/src/main/AndroidManifest.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + 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 new file mode 100644 index 0000000000..4583d77b57 --- /dev/null +++ b/play-services-ads/core/src/main/java/com/google/android/gms/dynamite/descriptors/com/google/android/gms/ads/dynamite/ModuleDescriptor.java @@ -0,0 +1,14 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.dynamite.descriptors.com.google.android.gms.ads.dynamite; + +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; +} diff --git a/play-services-ads/core/src/main/kotlin/org/microg/gms/ads/AdRequestService.kt b/play-services-ads/core/src/main/kotlin/org/microg/gms/ads/AdRequestService.kt new file mode 100644 index 0000000000..e451313286 --- /dev/null +++ b/play-services-ads/core/src/main/kotlin/org/microg/gms/ads/AdRequestService.kt @@ -0,0 +1,68 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.ads + +import android.os.Bundle +import android.os.Parcel +import android.util.Log +import com.google.android.gms.ads.internal.ExceptionParcel +import com.google.android.gms.ads.internal.NonagonRequestParcel +import com.google.android.gms.ads.internal.request.IAdRequestService +import com.google.android.gms.ads.internal.request.INonagonStreamingResponseListener +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 org.microg.gms.BaseService +import org.microg.gms.common.GmsService +import org.microg.gms.common.PackageUtils +import org.microg.gms.utils.warnOnTransactionIssues + +private const val TAG = "AdRequestService" + +class AdRequestService : BaseService(TAG, GmsService.ADREQUEST) { + override fun handleServiceRequest(callback: IGmsCallbacks, request: GetServiceRequest, service: GmsService) { + val packageName = PackageUtils.getAndCheckCallingPackage(this, request.packageName) + ?: throw IllegalArgumentException("Missing package name") + val binder = AdRequestServiceImpl().asBinder() + callback.onPostInitComplete(CommonStatusCodes.SUCCESS, binder, Bundle()) + } +} + +class AdRequestServiceImpl : IAdRequestService.Stub() { + override fun getAdRequest(request: NonagonRequestParcel, listener: INonagonStreamingResponseListener) { + Log.d(TAG, "getAdRequest") + listener.onException(ExceptionParcel().apply { + message = "Not supported" + code = CommonStatusCodes.INTERNAL_ERROR + }) + } + + override fun getSignals(request: NonagonRequestParcel, listener: INonagonStreamingResponseListener) { + Log.d(TAG, "getSignals") + listener.onException(ExceptionParcel().apply { + message = "Not supported" + code = CommonStatusCodes.INTERNAL_ERROR + }) + } + + override fun getUrlAndCacheKey(request: NonagonRequestParcel, listener: INonagonStreamingResponseListener) { + Log.d(TAG, "getUrlAndCacheKey") + listener.onException(ExceptionParcel().apply { + message = "Not supported" + code = CommonStatusCodes.INTERNAL_ERROR + }) + } + + override fun removeCacheUrl(key: String, listener: INonagonStreamingResponseListener) { + Log.d(TAG, "removeCacheUrl") + listener.onException(ExceptionParcel().apply { + message = "Not supported" + code = CommonStatusCodes.INTERNAL_ERROR + }) + } + + 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-ads/src/main/AndroidManifest.xml b/play-services-ads/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..fc70b763e1 --- /dev/null +++ b/play-services-ads/src/main/AndroidManifest.xml @@ -0,0 +1,10 @@ + + + + + + diff --git a/play-services-ads/src/main/aidl/com/google/android/gms/ads/internal/ExceptionParcel.aidl b/play-services-ads/src/main/aidl/com/google/android/gms/ads/internal/ExceptionParcel.aidl new file mode 100644 index 0000000000..41b877490c --- /dev/null +++ b/play-services-ads/src/main/aidl/com/google/android/gms/ads/internal/ExceptionParcel.aidl @@ -0,0 +1,3 @@ +package com.google.android.gms.ads.internal; + +parcelable ExceptionParcel; \ No newline at end of file diff --git a/play-services-ads/src/main/aidl/com/google/android/gms/ads/internal/NonagonRequestParcel.aidl b/play-services-ads/src/main/aidl/com/google/android/gms/ads/internal/NonagonRequestParcel.aidl new file mode 100644 index 0000000000..f952919d53 --- /dev/null +++ b/play-services-ads/src/main/aidl/com/google/android/gms/ads/internal/NonagonRequestParcel.aidl @@ -0,0 +1,3 @@ +package com.google.android.gms.ads.internal; + +parcelable NonagonRequestParcel; \ No newline at end of file diff --git a/play-services-ads/src/main/aidl/com/google/android/gms/ads/internal/request/IAdRequestService.aidl b/play-services-ads/src/main/aidl/com/google/android/gms/ads/internal/request/IAdRequestService.aidl new file mode 100644 index 0000000000..52ca62a0cd --- /dev/null +++ b/play-services-ads/src/main/aidl/com/google/android/gms/ads/internal/request/IAdRequestService.aidl @@ -0,0 +1,11 @@ +package com.google.android.gms.ads.internal.request; + +import com.google.android.gms.ads.internal.NonagonRequestParcel; +import com.google.android.gms.ads.internal.request.INonagonStreamingResponseListener; + +interface IAdRequestService { + void getAdRequest(in NonagonRequestParcel request, INonagonStreamingResponseListener listener) = 3; + void getSignals(in NonagonRequestParcel request, INonagonStreamingResponseListener listener) = 4; + void getUrlAndCacheKey(in NonagonRequestParcel request, INonagonStreamingResponseListener listener) = 5; + void removeCacheUrl(String key, INonagonStreamingResponseListener listener) = 6; +} \ No newline at end of file diff --git a/play-services-ads/src/main/aidl/com/google/android/gms/ads/internal/request/INonagonStreamingResponseListener.aidl b/play-services-ads/src/main/aidl/com/google/android/gms/ads/internal/request/INonagonStreamingResponseListener.aidl new file mode 100644 index 0000000000..78eae30d51 --- /dev/null +++ b/play-services-ads/src/main/aidl/com/google/android/gms/ads/internal/request/INonagonStreamingResponseListener.aidl @@ -0,0 +1,8 @@ +package com.google.android.gms.ads.internal.request; + +import com.google.android.gms.ads.internal.ExceptionParcel; + +interface INonagonStreamingResponseListener { + void onSuccess(in ParcelFileDescriptor fd); + void onException(in ExceptionParcel exception); +} \ No newline at end of file diff --git a/play-services-ads/src/main/java/com/google/android/gms/ads/internal/ExceptionParcel.java b/play-services-ads/src/main/java/com/google/android/gms/ads/internal/ExceptionParcel.java new file mode 100644 index 0000000000..55ddf346df --- /dev/null +++ b/play-services-ads/src/main/java/com/google/android/gms/ads/internal/ExceptionParcel.java @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.ads.internal; + +import org.microg.safeparcel.AutoSafeParcelable; + +public class ExceptionParcel extends AutoSafeParcelable { + @Field(1) + public String message; + @Field(2) + public int code; + public static final Creator CREATOR = new AutoCreator<>(ExceptionParcel.class); +} diff --git a/play-services-ads/src/main/java/com/google/android/gms/ads/internal/NonagonRequestParcel.java b/play-services-ads/src/main/java/com/google/android/gms/ads/internal/NonagonRequestParcel.java new file mode 100644 index 0000000000..866ef36b46 --- /dev/null +++ b/play-services-ads/src/main/java/com/google/android/gms/ads/internal/NonagonRequestParcel.java @@ -0,0 +1,12 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.ads.internal; + +import org.microg.safeparcel.AutoSafeParcelable; + +public class NonagonRequestParcel extends AutoSafeParcelable { + public static final Creator CREATOR = new AutoCreator<>(NonagonRequestParcel.class); +} diff --git a/play-services-api/src/main/aidl/com/google/android/gms/ads/omid/IOmid.aidl b/play-services-api/src/main/aidl/com/google/android/gms/ads/omid/IOmid.aidl new file mode 100644 index 0000000000..22a0cbc592 --- /dev/null +++ b/play-services-api/src/main/aidl/com/google/android/gms/ads/omid/IOmid.aidl @@ -0,0 +1,16 @@ +package com.google.android.gms.ads.omid; + +import com.google.android.gms.dynamic.IObjectWrapper; + +interface IOmid { + boolean initializeOmid(IObjectWrapper context) = 1; + IObjectWrapper createHtmlAdSession(String version, IObjectWrapper webView, String customReferenceData, String impressionOwner, String altImpressionOwner) = 2; + void startAdSession(IObjectWrapper adSession) = 3; + void registerAdView(IObjectWrapper adSession, IObjectWrapper view) = 4; + String getVersion() = 5; + void finishAdSession(IObjectWrapper adSession) = 6; + void addFriendlyObstruction(IObjectWrapper adSession, IObjectWrapper view) = 7; + IObjectWrapper createHtmlAdSessionWithPartnerName(String version, IObjectWrapper webView, String customReferenceData, String impressionOwner, String altImpressionOwner, String parterName) = 8; + IObjectWrapper createJavascriptAdSessionWithPartnerNameImpressionCreativeType(String version, IObjectWrapper webView, String customReferenceData, String impressionOwner, String altImpressionOwner, String parterName, String impressionType, String creativeType, String contentUrl) = 9; + IObjectWrapper createHtmlAdSessionWithPartnerNameImpressionCreativeType(String version, IObjectWrapper webView, String customReferenceData, String impressionOwner, String altImpressionOwner, String parterName, String impressionType, String creativeType, String contentUrl) = 10; +} \ No newline at end of file diff --git a/play-services-api/src/main/aidl/com/google/android/gms/credential/manager/common/IPendingIntentCallback.aidl b/play-services-api/src/main/aidl/com/google/android/gms/credential/manager/common/IPendingIntentCallback.aidl new file mode 100644 index 0000000000..7af7a04d5f --- /dev/null +++ b/play-services-api/src/main/aidl/com/google/android/gms/credential/manager/common/IPendingIntentCallback.aidl @@ -0,0 +1,12 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.credential.manager.common; + +import com.google.android.gms.common.api.Status; + +interface IPendingIntentCallback { + void onPendingIntent(in Status status, in PendingIntent pendingIntent); +} \ No newline at end of file diff --git a/play-services-api/src/main/aidl/com/google/android/gms/credential/manager/common/ISettingsCallback.aidl b/play-services-api/src/main/aidl/com/google/android/gms/credential/manager/common/ISettingsCallback.aidl new file mode 100644 index 0000000000..ef9ef410a9 --- /dev/null +++ b/play-services-api/src/main/aidl/com/google/android/gms/credential/manager/common/ISettingsCallback.aidl @@ -0,0 +1,12 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.credential.manager.common; + +import com.google.android.gms.common.api.Status; + +interface ISettingsCallback { + void onSetting(in Status status, in byte[] value); +} \ No newline at end of file diff --git a/play-services-api/src/main/aidl/com/google/android/gms/credential/manager/firstparty/internal/ICredentialManagerService.aidl b/play-services-api/src/main/aidl/com/google/android/gms/credential/manager/firstparty/internal/ICredentialManagerService.aidl new file mode 100644 index 0000000000..39ab381d2a --- /dev/null +++ b/play-services-api/src/main/aidl/com/google/android/gms/credential/manager/firstparty/internal/ICredentialManagerService.aidl @@ -0,0 +1,17 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.credential.manager.firstparty.internal; + +import com.google.android.gms.common.api.internal.IStatusCallback; +import com.google.android.gms.credential.manager.common.IPendingIntentCallback; +import com.google.android.gms.credential.manager.common.ISettingsCallback; +import com.google.android.gms.credential.manager.invocationparams.CredentialManagerInvocationParams; + +interface ICredentialManagerService { + void getCredentialManagerIntent(IPendingIntentCallback callback, in CredentialManagerInvocationParams params) = 0; + void getSetting(ISettingsCallback callback, String key) = 1; + void setSetting(IStatusCallback callback, String key, in byte[] value) = 2; +} \ No newline at end of file diff --git a/play-services-api/src/main/aidl/com/google/android/gms/credential/manager/invocationparams/CredentialManagerInvocationParams.aidl b/play-services-api/src/main/aidl/com/google/android/gms/credential/manager/invocationparams/CredentialManagerInvocationParams.aidl new file mode 100644 index 0000000000..81d43014a6 --- /dev/null +++ b/play-services-api/src/main/aidl/com/google/android/gms/credential/manager/invocationparams/CredentialManagerInvocationParams.aidl @@ -0,0 +1,8 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.credential.manager.invocationparams; + +parcelable CredentialManagerInvocationParams; \ No newline at end of file diff --git a/play-services-api/src/main/aidl/com/google/android/gms/phenotype/internal/IPhenotypeService.aidl b/play-services-api/src/main/aidl/com/google/android/gms/phenotype/internal/IPhenotypeService.aidl index 77d315b741..4d53ab85d5 100644 --- a/play-services-api/src/main/aidl/com/google/android/gms/phenotype/internal/IPhenotypeService.aidl +++ b/play-services-api/src/main/aidl/com/google/android/gms/phenotype/internal/IPhenotypeService.aidl @@ -1,5 +1,6 @@ package com.google.android.gms.phenotype.internal; +import com.google.android.gms.common.api.internal.IStatusCallback; import com.google.android.gms.phenotype.internal.IPhenotypeCallbacks; import com.google.android.gms.phenotype.Flag; import com.google.android.gms.phenotype.RegistrationInfo; @@ -28,4 +29,7 @@ interface IPhenotypeService { oneway void getServingVersion(IPhenotypeCallbacks callbacks) = 21; // returns via callbacks.onServingVersion() oneway void getExperimentTokens2(IPhenotypeCallbacks callbacks, String p1, String p2, String p3, String p4) = 22; // returns via callbacks.onExperimentTokens() + oneway void syncAfterOperation2(IPhenotypeCallbacks callbacks, long p1) = 23; // returns via callbacks.onSyncFinished() + oneway void setRuntimeProperties(IStatusCallback callbacks, String p1, in byte[] p2) = 24; +// oneway void setExternalExperiments(IStatusCallback callbacks, String p1, in List p2) = 25; } diff --git a/play-services-api/src/main/java/com/google/android/gms/credential/manager/invocationparams/CallerInfo.java b/play-services-api/src/main/java/com/google/android/gms/credential/manager/invocationparams/CallerInfo.java new file mode 100644 index 0000000000..85cdac1754 --- /dev/null +++ b/play-services-api/src/main/java/com/google/android/gms/credential/manager/invocationparams/CallerInfo.java @@ -0,0 +1,28 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.credential.manager.invocationparams; + +import androidx.annotation.NonNull; +import org.microg.safeparcel.AutoSafeParcelable; + +public class CallerInfo extends AutoSafeParcelable { + @Field(1) + public String s1; + @Field(2) + public String s2; + @Field(3) + public String s3; + @Field(4) + public String s4; + + @NonNull + @Override + public String toString() { + return "CallerInfo(" + s1 + "," + s2 + "," + s3 + "," + s4 + ")"; + } + + public static final Creator CREATOR = new AutoCreator<>(CallerInfo.class); +} diff --git a/play-services-api/src/main/java/com/google/android/gms/credential/manager/invocationparams/CredentialManagerAccount.java b/play-services-api/src/main/java/com/google/android/gms/credential/manager/invocationparams/CredentialManagerAccount.java new file mode 100644 index 0000000000..f12db2f444 --- /dev/null +++ b/play-services-api/src/main/java/com/google/android/gms/credential/manager/invocationparams/CredentialManagerAccount.java @@ -0,0 +1,23 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.credential.manager.invocationparams; + +import androidx.annotation.NonNull; +import org.microg.safeparcel.AutoSafeParcelable; + +public class CredentialManagerAccount extends AutoSafeParcelable { + @Field(1) + public String name; + + @NonNull + @Override + public String toString() { + return name; + } + + public static final String NAME_LOCAL = "pwm.constant.LocalAccount"; + public static final Creator CREATOR = new AutoCreator<>(CredentialManagerAccount.class); +} diff --git a/play-services-api/src/main/java/com/google/android/gms/credential/manager/invocationparams/CredentialManagerInvocationParams.java b/play-services-api/src/main/java/com/google/android/gms/credential/manager/invocationparams/CredentialManagerInvocationParams.java new file mode 100644 index 0000000000..871754f44c --- /dev/null +++ b/play-services-api/src/main/java/com/google/android/gms/credential/manager/invocationparams/CredentialManagerInvocationParams.java @@ -0,0 +1,28 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.credential.manager.invocationparams; + +import androidx.annotation.NonNull; +import org.microg.gms.utils.ToStringHelper; +import org.microg.safeparcel.AutoSafeParcelable; + +public class CredentialManagerInvocationParams extends AutoSafeParcelable { + @Field(1) + public CredentialManagerAccount account; + @Field(2) + public CallerInfo caller; + + @NonNull + @Override + public String toString() { + return ToStringHelper.name("CredentialManagerInvocationParams") + .field("account", account) + .field("caller", caller) + .end(); + } + + public static final Creator CREATOR = new AutoCreator<>(CredentialManagerInvocationParams.class); +} diff --git a/play-services-api/src/main/java/com/google/android/gms/measurement/internal/AppMetadata.java b/play-services-api/src/main/java/com/google/android/gms/measurement/internal/AppMetadata.java index 9c7c3671ba..84ac6a587d 100644 --- a/play-services-api/src/main/java/com/google/android/gms/measurement/internal/AppMetadata.java +++ b/play-services-api/src/main/java/com/google/android/gms/measurement/internal/AppMetadata.java @@ -19,27 +19,27 @@ public class AppMetadata extends AutoSafeParcelable { @Field(5) public String installerPackageName; @Field(6) - private long field6; + private long googleVersion; @Field(7) - private long field7; + private long devCertHash; @Field(8) - private String field8; + private String healthMonitor; @Field(9) - private boolean field9 = true; + private boolean measurementEnabled = true; @Field(10) - private boolean field10; + private boolean firstOpen; @Field(11) public long versionCode = Integer.MIN_VALUE; @Field(12) - private String field12; + private String firebaseInstanceId; @Field(13) - private long field13; + private long androidId; @Field(14) - private long field14; + private long instantiationTime; @Field(15) public int appType; @Field(16) - private boolean field16; + private boolean adIdReportingEnabled; @Field(17) public boolean ssaidCollectionEnabled = true; @Field(18) @@ -49,13 +49,21 @@ public class AppMetadata extends AutoSafeParcelable { @Field(21) public Boolean allowAdPersonalization; @Field(22) - private long field22; + private long dynamiteVersion; @Field(23) public List safelistedEvents; @Field(24) public String gaAppId; @Field(25) - private String field25; + private String consentSettings; + @Field(26) + private String ephemeralAppInstanceId; + @Field(27) + private String sessionStitchingToken; + @Field(28) + private boolean sgtmUploadEnabled; + @Field(29) + private long targetOsVersion; public String toString() { return "AppMetadata[" + packageName + "]"; diff --git a/play-services-api/src/main/java/com/google/android/gms/phenotype/Configuration.java b/play-services-api/src/main/java/com/google/android/gms/phenotype/Configuration.java index 2433df2277..70b4d286e1 100644 --- a/play-services-api/src/main/java/com/google/android/gms/phenotype/Configuration.java +++ b/play-services-api/src/main/java/com/google/android/gms/phenotype/Configuration.java @@ -9,10 +9,10 @@ public class Configuration extends AutoSafeParcelable { @Field(2) - public int flagType; + public int id; @Field(3) public Flag[] flags; @Field(4) - public String[] names; + public String[] removeNames; public static final Creator CREATOR = new AutoCreator<>(Configuration.class); } diff --git a/play-services-api/src/main/java/com/google/android/gms/phenotype/Configurations.java b/play-services-api/src/main/java/com/google/android/gms/phenotype/Configurations.java index 910b70089b..9d91f78452 100644 --- a/play-services-api/src/main/java/com/google/android/gms/phenotype/Configurations.java +++ b/play-services-api/src/main/java/com/google/android/gms/phenotype/Configurations.java @@ -9,9 +9,9 @@ public class Configurations extends AutoSafeParcelable { @Field(2) - public String field2; + public String snapshotToken; @Field(3) - public String field3; + public String serverToken; @Field(4) public Configuration[] field4; @Field(5) @@ -19,7 +19,7 @@ public class Configurations extends AutoSafeParcelable { @Field(6) public byte[] field6; @Field(7) - public long field7; + public long version; public static final Creator CREATOR = new AutoCreator<>(Configurations.class); } diff --git a/play-services-appinvite/core/build.gradle b/play-services-appinvite/core/build.gradle new file mode 100644 index 0000000000..e62ae85533 --- /dev/null +++ b/play-services-appinvite/core/build.gradle @@ -0,0 +1,34 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +dependencies { + api project(':play-services-appinvite') + implementation project(':play-services-base-core') + + implementation "androidx.appcompat:appcompat:$appcompatVersion" +} + +android { + compileSdkVersion androidCompileSdk + buildToolsVersion "$androidBuildVersionTools" + + defaultConfig { + versionName version + minSdkVersion androidMinSdk + targetSdkVersion androidTargetSdk + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + compileOptions { + sourceCompatibility = 1.8 + targetCompatibility = 1.8 + } +} diff --git a/play-services-appinvite/core/src/main/AndroidManifest.xml b/play-services-appinvite/core/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..c1cfe3f864 --- /dev/null +++ b/play-services-appinvite/core/src/main/AndroidManifest.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/play-services-appinvite/core/src/main/kotlin/org/microg/gms/appinivite/AppInviteActivity.kt b/play-services-appinvite/core/src/main/kotlin/org/microg/gms/appinivite/AppInviteActivity.kt new file mode 100644 index 0000000000..9eab03d889 --- /dev/null +++ b/play-services-appinvite/core/src/main/kotlin/org/microg/gms/appinivite/AppInviteActivity.kt @@ -0,0 +1,25 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.appinivite + +import android.app.Activity +import android.os.Bundle +import android.util.Log + +private const val TAG = "AppInviteActivity" + +class AppInviteActivity : Activity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val uri = intent?.data + if (uri == null) { + finish() + return + } + Log.d(TAG, "uri: $uri") + // TODO datamixer-pa.googleapis.com/ + } +} \ No newline at end of file diff --git a/play-services-appinvite/core/src/main/kotlin/org/microg/gms/appinivite/AppInviteService.kt b/play-services-appinvite/core/src/main/kotlin/org/microg/gms/appinivite/AppInviteService.kt new file mode 100644 index 0000000000..3cbaff0e0d --- /dev/null +++ b/play-services-appinvite/core/src/main/kotlin/org/microg/gms/appinivite/AppInviteService.kt @@ -0,0 +1,49 @@ +/* + * SPDX-FileCopyrightText: 2019 e Foundation + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ +package org.microg.gms.appinivite + +import android.app.Activity +import android.content.Context +import android.os.Bundle +import android.os.Parcel +import android.os.RemoteException +import android.util.Log +import com.google.android.gms.appinvite.internal.IAppInviteCallbacks +import com.google.android.gms.appinvite.internal.IAppInviteService +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 org.microg.gms.BaseService +import org.microg.gms.common.GmsService +import org.microg.gms.common.PackageUtils +import org.microg.gms.utils.warnOnTransactionIssues + +private const val TAG = "AppInviteService" + +class AppInviteService : BaseService(TAG, GmsService.APP_INVITE) { + override fun handleServiceRequest(callback: IGmsCallbacks, request: GetServiceRequest, service: GmsService) { + PackageUtils.getAndCheckCallingPackage(this, request.packageName) + Log.d(TAG, "callb: $callback ; req: $request ; serv: $service") + callback.onPostInitComplete(0, AppInviteServiceImpl(this, request.packageName, request.extras), null) + } +} + + +class AppInviteServiceImpl(context: Context?, packageName: String?, extras: Bundle?) : IAppInviteService.Stub() { + override fun updateInvitationOnInstall(callback: IAppInviteCallbacks, invitationId: String) { + callback.onStatus(Status.SUCCESS) + } + + override fun convertInvitation(callback: IAppInviteCallbacks, invitationId: String) { + callback.onStatus(Status.SUCCESS) + } + + override fun getInvitation(callback: IAppInviteCallbacks) { + callback.onStatusIntent(Status(Activity.RESULT_CANCELED), null) + } + + 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-auth-api-phone/build.gradle b/play-services-auth-api-phone/build.gradle new file mode 100644 index 0000000000..9da5db841c --- /dev/null +++ b/play-services-auth-api-phone/build.gradle @@ -0,0 +1,35 @@ +/* + * SPDX-FileCopyrightText: 2022 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +apply plugin: 'com.android.library' +apply plugin: 'maven-publish' +apply plugin: 'signing' + +android { + compileSdkVersion androidCompileSdk + buildToolsVersion "$androidBuildVersionTools" + + defaultConfig { + versionName version + minSdkVersion androidMinSdk + targetSdkVersion androidTargetSdk + } + + compileOptions { + sourceCompatibility = 1.8 + targetCompatibility = 1.8 + } +} + +apply from: '../gradle/publish-android.gradle' + +description = 'microG implementation of play-services-auth-api-phone' + +dependencies { + // Dependencies from play-services-auth-api-phone:18.0.1 + api project(':play-services-base') + api project(':play-services-basement') + api project(':play-services-tasks') +} diff --git a/play-services-auth-api-phone/src/main/AndroidManifest.xml b/play-services-auth-api-phone/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..c8ff378e80 --- /dev/null +++ b/play-services-auth-api-phone/src/main/AndroidManifest.xml @@ -0,0 +1,6 @@ + + + \ No newline at end of file diff --git a/play-services-auth-api-phone/src/main/aidl/com/google/android/gms/auth/api/phone/internal/IAutofillPermissionStateCallback.aidl b/play-services-auth-api-phone/src/main/aidl/com/google/android/gms/auth/api/phone/internal/IAutofillPermissionStateCallback.aidl new file mode 100644 index 0000000000..111abc7cbf --- /dev/null +++ b/play-services-auth-api-phone/src/main/aidl/com/google/android/gms/auth/api/phone/internal/IAutofillPermissionStateCallback.aidl @@ -0,0 +1,7 @@ +package com.google.android.gms.auth.api.phone.internal; + +import com.google.android.gms.common.api.Status; + +interface IAutofillPermissionStateCallback { + void onCheckPermissionStateResult(in Status status, int result) = 0; +} diff --git a/play-services-auth-api-phone/src/main/aidl/com/google/android/gms/auth/api/phone/internal/IOngoingSmsRequestCallback.aidl b/play-services-auth-api-phone/src/main/aidl/com/google/android/gms/auth/api/phone/internal/IOngoingSmsRequestCallback.aidl new file mode 100644 index 0000000000..fbf5ef531b --- /dev/null +++ b/play-services-auth-api-phone/src/main/aidl/com/google/android/gms/auth/api/phone/internal/IOngoingSmsRequestCallback.aidl @@ -0,0 +1,7 @@ +package com.google.android.gms.auth.api.phone.internal; + +import com.google.android.gms.common.api.Status; + +interface IOngoingSmsRequestCallback { + void onHasOngoingSmsRequestResult(in Status status, boolean hasOngoingSmsRequest) = 0; +} diff --git a/play-services-auth-api-phone/src/main/aidl/com/google/android/gms/auth/api/phone/internal/ISmsRetrieverApiService.aidl b/play-services-auth-api-phone/src/main/aidl/com/google/android/gms/auth/api/phone/internal/ISmsRetrieverApiService.aidl new file mode 100644 index 0000000000..36457dcfb7 --- /dev/null +++ b/play-services-auth-api-phone/src/main/aidl/com/google/android/gms/auth/api/phone/internal/ISmsRetrieverApiService.aidl @@ -0,0 +1,18 @@ +package com.google.android.gms.auth.api.phone.internal; + +import com.google.android.gms.auth.api.phone.internal.IAutofillPermissionStateCallback; +import com.google.android.gms.auth.api.phone.internal.IOngoingSmsRequestCallback; +import com.google.android.gms.auth.api.phone.internal.ISmsRetrieverResultCallback; +import com.google.android.gms.common.api.internal.IStatusCallback; +import com.google.android.gms.common.api.Status; + +import java.lang.String; + +interface ISmsRetrieverApiService { + void startSmsRetriever(ISmsRetrieverResultCallback callback) = 0; + void startWithConsentPrompt(String senderPhoneNumber, ISmsRetrieverResultCallback callback) = 1; + void startSmsCodeAutofill(IStatusCallback callback) = 2; + void checkAutofillPermissionState(IAutofillPermissionStateCallback callback) = 3; + void checkOngoingSmsRequest(String packageName, IOngoingSmsRequestCallback callback) = 4; + void startSmsCodeBrowser(IStatusCallback callback) = 5; +} \ No newline at end of file diff --git a/play-services-auth-api-phone/src/main/aidl/com/google/android/gms/auth/api/phone/internal/ISmsRetrieverResultCallback.aidl b/play-services-auth-api-phone/src/main/aidl/com/google/android/gms/auth/api/phone/internal/ISmsRetrieverResultCallback.aidl new file mode 100644 index 0000000000..1563ddedf8 --- /dev/null +++ b/play-services-auth-api-phone/src/main/aidl/com/google/android/gms/auth/api/phone/internal/ISmsRetrieverResultCallback.aidl @@ -0,0 +1,7 @@ +package com.google.android.gms.auth.api.phone.internal; + +import com.google.android.gms.common.api.Status; + +interface ISmsRetrieverResultCallback { + void onResult(in Status status) = 0; +} diff --git a/play-services-auth-api-phone/src/main/java/com/google/android/gms/auth/api/phone/SmsCodeAutofillClient.java b/play-services-auth-api-phone/src/main/java/com/google/android/gms/auth/api/phone/SmsCodeAutofillClient.java new file mode 100644 index 0000000000..d61267d732 --- /dev/null +++ b/play-services-auth-api-phone/src/main/java/com/google/android/gms/auth/api/phone/SmsCodeAutofillClient.java @@ -0,0 +1,93 @@ +/* + * 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.auth.api.phone; + +import android.app.Activity; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.IntentFilter; +import android.os.Handler; +import androidx.annotation.IntDef; +import com.google.android.gms.common.api.*; +import com.google.android.gms.tasks.Task; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * The interface for interacting with the SMS Code Autofill API. These methods are only supported on devices running Android P and later. + * For devices that run versions earlier than Android P, all method calls return {@link SmsRetrieverStatusCodes#PLATFORM_NOT_SUPPORTED}. + *

+ * Note: This interface works only for the current user-designated autofill service. + * Any calls from non user-designated autofill services or other applications will fail with {@link SmsRetrieverStatusCodes#API_NOT_AVAILABLE}. + */ +public interface SmsCodeAutofillClient extends HasApiKey { + /** + * Returns the {@link SmsCodeAutofillClient.PermissionState} of the current user-designated autofill service. + * The result could be {@code NONE}, {@code GRANTED}, or {@code DENIED}. + *

+ * The autofill service should check its permission state prior to showing the suggestion prompt for retrieving an SMS + * verification code, because it will definitely fail on calling {@link #startSmsCodeRetriever()} in permission denied state. + */ + Task<@PermissionState Integer> checkPermissionState(); + + /** + * Returns {@code true} if there are requests from {@link SmsRetriever} in progress for the given package name. + *

+ * The autofill service can check this method to avoid showing a suggestion prompt for retrieving an SMS verification code, + * in case that a user app may already be retrieving the SMS verification code through {@link SmsRetriever}. + *

+ * Note: This result does not include those requests from {@code SmsCodeAutofillClient}. + */ + Task hasOngoingSmsRequest(String packageName); + + /** + * Starts {@code SmsCodeRetriever}, which looks for an SMS verification code from messages recently received (up to 1 minute + * prior). If there is no SMS verification code found from the SMS inbox, it waits for new incoming SMS messages until it + * finds an SMS verification code or reaches the timeout (about 5 minutes). + *

+ * The SMS verification code will be sent via a Broadcast Intent with {@link SmsCodeRetriever#SMS_CODE_RETRIEVED_ACTION}. This Intent contains + * Extras with keys {@link SmsCodeRetriever#EXTRA_SMS_CODE} for the retrieved verification code as a {@code String}, and {@link SmsCodeRetriever#EXTRA_STATUS} for {@link Status} to + * indicate {@code RESULT_SUCCESS}, {@code RESULT_TIMEOUT} or {@link SmsRetrieverStatusCodes}. + *

+ * Note: Add {@link SmsRetriever#SEND_PERMISSION} in {@link Context#registerReceiver(BroadcastReceiver, IntentFilter, String, Handler)} while + * registering the receiver to detect that the broadcast intent is from the SMS Retriever. + */ + Task startSmsCodeRetriever(); + + /** + * Permission states for the current user-designated autofill service. The initial state is {@code NONE} upon the first time using the + * SMS Code Autofill API. This permission can be granted or denied through a consent dialog requested by the current + * autofill service, or an explicit change by users within the SMS verification codes settings. + */ + @Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE}) + @Retention(RetentionPolicy.SOURCE) + @IntDef({PermissionState.NONE, PermissionState.GRANTED, PermissionState.DENIED}) + @interface PermissionState { + /** + * Indicates that the current autofill service has not been granted or denied permission by the user. Calling + * {@link #startSmsCodeRetriever()} will fail with {@link CommonStatusCodes#RESOLUTION_REQUIRED}. The caller can use + * {@link ResolvableApiException#startResolutionForResult(Activity, int)} to show a consent dialog for requesting permission from the user. + */ + int NONE = 0; + /** + * Indicates that the current autofill service has been granted permission by the user. The user consent is not required for + * calling {@link #startSmsCodeRetriever()} in this state. + */ + int GRANTED = 1; + /** + * Indicates that the current autofill service has been denied permission by the user. Calling {@link #startSmsCodeRetriever()} + * will fail with {@link SmsRetrieverStatusCodes#USER_PERMISSION_REQUIRED}. It can only be resolved by the user explicitly turning on the permission + * in settings. + */ + int DENIED = 2; + } +} diff --git a/play-services-auth-api-phone/src/main/java/com/google/android/gms/auth/api/phone/SmsCodeBrowserClient.java b/play-services-auth-api-phone/src/main/java/com/google/android/gms/auth/api/phone/SmsCodeBrowserClient.java new file mode 100644 index 0000000000..663c691834 --- /dev/null +++ b/play-services-auth-api-phone/src/main/java/com/google/android/gms/auth/api/phone/SmsCodeBrowserClient.java @@ -0,0 +1,58 @@ +/* + * 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.auth.api.phone; + +import android.app.Activity; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Handler; +import com.google.android.gms.common.api.Api; +import com.google.android.gms.common.api.HasApiKey; +import com.google.android.gms.common.api.ResolvableApiException; +import com.google.android.gms.tasks.Task; + +/** + * The interface for interacting with the SMS Code Browser API. By using {@link #startSmsCodeRetriever()}, you can retrieve the + * origin-bound one-time code from SMS messages. + *

+ * The SMS message format should follow the origin-bound one-time code specification: + *

    + *
  • Can optionally begin with human-readable explanatory text. This consists of all but the last line of the message.
  • + *
  • The last line of the message contains both a host and a code, each prefixed with a sigil: U+0040 (@) before the host, and U+0023 (#) before the code.
  • + *
+ *

+ * Note: This interface works only for the default browser app set by the current user. Any other calls will fail with {@link SmsRetrieverStatusCodes#API_NOT_AVAILABLE}. + */ +public interface SmsCodeBrowserClient extends HasApiKey { + /** + * Starts {@code SmsCodeRetriever}, which looks for an origin-bound one-time code from SMS messages recently received (up to + * 1 minute prior). If there is no matching message found from the SMS inbox, it waits for new incoming SMS messages + * until it finds a matching message or reaches the timeout (about 5 minutes). Calling this method multiple times only + * returns one result, but it can extend the timeout period to the last call. Once the result is returned or it reaches + * the timeout, SmsCodeRetriever will stop automatically. + *

+ * The SMS verification code will be sent via a Broadcast Intent with {@link SmsCodeRetriever#SMS_CODE_RETRIEVED_ACTION}. + * This Intent contains Extras with keys: + *

    + *
  • {@link SmsCodeRetriever#EXTRA_SMS_CODE_LINE} for the retrieved line that contains the origin-bound one-time code and the metadata, or + * {@code null} in failed cases.
  • + *
  • {@link SmsCodeRetriever#EXTRA_STATUS} for the Status to indicate {@code RESULT_SUCCESS}, {@code RESULT_TIMEOUT} or other {@link SmsRetrieverStatusCodes}.
  • + *
+ * If the caller has not been granted or denied permission by the user, it will fail with a {@link ResolvableApiException}. The + * caller can use {@link ResolvableApiException#startResolutionForResult(Activity, int)} to show a consent dialog for requesting permission from + * the user. The dialog result is returned via {@link Activity#onActivityResult(int, int, Intent)}. If the user grants the permission, + * the activity result returns with {@code RESULT_OK}. Then you can start the retriever again to retrieve the verification code. + *

+ * Note: Add {@link SmsRetriever#SEND_PERMISSION} in {@link Context#registerReceiver(BroadcastReceiver, IntentFilter, String, Handler)} while + * registering the receiver to detect that the broadcast intent is from the SMS Retriever. + */ + Task startSmsCodeRetriever(); +} diff --git a/play-services-auth-api-phone/src/main/java/com/google/android/gms/auth/api/phone/SmsCodeRetriever.java b/play-services-auth-api-phone/src/main/java/com/google/android/gms/auth/api/phone/SmsCodeRetriever.java new file mode 100644 index 0000000000..67cd5dbbf7 --- /dev/null +++ b/play-services-auth-api-phone/src/main/java/com/google/android/gms/auth/api/phone/SmsCodeRetriever.java @@ -0,0 +1,89 @@ +/* + * 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.auth.api.phone; + +import android.app.Activity; +import android.content.Context; +import androidx.annotation.NonNull; +import com.google.android.gms.common.api.Status; +import org.microg.gms.auth.api.phone.SmsCodeAutofillClientImpl; +import org.microg.gms.auth.api.phone.SmsCodeBrowserClientImpl; + +/** + * {@code SmsCodeRetriever} is a variant of {@link SmsRetriever}, and it provides access to Google services that help you retrieve SMS + * verification codes sent to the user's device, without having to ask for {@code android.permission.READ_SMS} or {@code android.permission.RECEIVE_SMS}. + *

+ * To use {@code SmsCodeRetriever} in the Android autofill service, obtain an instance of {@link SmsCodeAutofillClient} using + * {@link #getAutofillClient(Context)} or {@link #getAutofillClient(Activity)}, and start SMS Code Retriever service by calling + * {@link SmsCodeAutofillClient#startSmsCodeRetriever()}. To use it in the browser app, you obtain an instance of {@link SmsCodeBrowserClient} using + * {@link #getBrowserClient(Context)} or {@link #getBrowserClient(Activity)} instead. + *

+ * The service first looks for an SMS verification code from messages recently received (up to 1 minute prior). If there is no + * SMS verification code found from the SMS inbox, it waits for new incoming SMS messages until it finds an SMS + * verification code or reaches the timeout (about 5 minutes). + */ +public class SmsCodeRetriever { + /** + * Intent extra key of the retrieved SMS verification code by the {@link SmsCodeAutofillClient}. + */ + @NonNull + public static final String EXTRA_SMS_CODE = "com.google.android.gms.auth.api.phone.EXTRA_SMS_CODE"; + /** + * Intent extra key of the retrieved SMS verification code line by the {@link SmsCodeBrowserClient}. + */ + @NonNull + public static final String EXTRA_SMS_CODE_LINE = "com.google.android.gms.auth.api.phone.EXTRA_SMS_CODE_LINE"; + /** + * Intent extra key of {@link Status}, which indicates {@code RESULT_SUCCESS}, {@code RESULT_TIMEOUT} or {@link SmsRetrieverStatusCodes}. + */ + @NonNull + public static final String EXTRA_STATUS = "com.google.android.gms.auth.api.phone.EXTRA_STATUS"; + /** + * Intent action when an SMS verification code is retrieved. + */ + @NonNull + public static final String SMS_CODE_RETRIEVED_ACTION = "com.google.android.gms.auth.api.phone.SMS_CODE_RETRIEVED"; + + /** + * Creates a new instance of {@link SmsCodeAutofillClient} for use in an {@link Activity}. + * This {@link SmsCodeAutofillClient} is intended to be used by the current user-designated autofill service only. + */ + @NonNull + public static SmsCodeAutofillClient getAutofillClient(Activity activity) { + return new SmsCodeAutofillClientImpl(activity); + } + + /** + * Creates a new instance of {@link SmsCodeAutofillClient} for use in a {@link Context}. + * This {@link SmsCodeAutofillClient} is intended to be used by the current user-designated autofill service only. + */ + @NonNull + public static SmsCodeAutofillClient getAutofillClient(Context context) { + return new SmsCodeAutofillClientImpl(context); + } + + /** + * Creates a new instance of {@link SmsCodeBrowserClient} for use in an {@link Activity}. + * This {@link SmsCodeBrowserClient} is intended to be used by the default browser app only. + */ + @NonNull + public static SmsCodeBrowserClient getBrowserClient(Activity activity) { + return new SmsCodeBrowserClientImpl(activity); + } + + /** + * Creates a new instance of {@link SmsCodeBrowserClient} for use in a {@link Context}. + * This {@link SmsCodeBrowserClient} is intended to be used by the default browser app only. + */ + @NonNull + public static SmsCodeBrowserClient getBrowserClient(Context context) { + return new SmsCodeBrowserClientImpl(context); + } + +} diff --git a/play-services-auth-api-phone/src/main/java/com/google/android/gms/auth/api/phone/SmsRetriever.java b/play-services-auth-api-phone/src/main/java/com/google/android/gms/auth/api/phone/SmsRetriever.java new file mode 100644 index 0000000000..63602c3287 --- /dev/null +++ b/play-services-auth-api-phone/src/main/java/com/google/android/gms/auth/api/phone/SmsRetriever.java @@ -0,0 +1,72 @@ +/* + * 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.auth.api.phone; + +import android.app.Activity; +import android.content.Context; +import androidx.annotation.NonNull; +import com.google.android.gms.common.api.Status; +import org.microg.gms.auth.api.phone.SmsRetrieverClientImpl; + +/** + * {@code SmsRetriever} provides access to Google services that help you retrieve SMS messages sent to your app without + * having to ask for {@code android.permission.READ_SMS} or {@code android.permission.RECEIVE_SMS}. + *

+ * To use {@code SmsRetriever}, obtain an instance of {@link SmsRetrieverClient} using {@link #getClient(Context)} or + * {@link #getClient(Activity)}, then start the SMS retriever service by calling {@link SmsRetrieverClient#startSmsRetriever()} or + * {@link SmsRetrieverClient#startSmsUserConsent(String)}. The service waits for a matching SMS message until timeout (5 minutes). + */ +public class SmsRetriever { + /** + * Intent extra key of the consent intent to be launched from client app. + */ + @NonNull + public static final String EXTRA_CONSENT_INTENT = "com.google.android.gms.auth.api.phone.EXTRA_CONSENT_INTENT"; + /** + * [Optional] Intent extra key of the retrieved Sim card subscription Id if any, as an {@code int}. + */ + @NonNull + public static final String EXTRA_SIM_SUBSCRIPTION_ID = "com.google.android.gms.auth.api.phone.EXTRA_SIM_SUBSCRIPTION_ID"; + /** + * Intent extra key of the retrieved SMS message as a {@code String}. + */ + @NonNull + public static final String EXTRA_SMS_MESSAGE = "com.google.android.gms.auth.api.phone.EXTRA_SMS_MESSAGE"; + /** + * Intent extra key of {@link Status}, which indicates SUCCESS or TIMEOUT. + */ + @NonNull + public static final String EXTRA_STATUS = "com.google.android.gms.auth.api.phone.EXTRA_STATUS"; + /** + * Permission that's used to register the receiver to detect that the broadcaster is the SMS Retriever. + */ + @NonNull + public static final String SEND_PERMISSION = "com.google.android.gms.auth.api.phone.permission.SEND"; + /** + * Intent action when SMS message is retrieved. + */ + @NonNull + public static final String SMS_RETRIEVED_ACTION = "com.google.android.gms.auth.api.phone.SMS_RETRIEVED"; + + /** + * Create a new instance of {@link SmsRetrieverClient} for use in an {@link Activity}. + */ + @NonNull + public static SmsRetrieverClient getClient(Activity activity) { + return new SmsRetrieverClientImpl(activity); + } + + /** + * Create a new instance of {@link SmsRetrieverClient} for use in a {@link Context}. + */ + @NonNull + public static SmsRetrieverClient getClient(Context context) { + return new SmsRetrieverClientImpl(context); + } +} diff --git a/play-services-auth-api-phone/src/main/java/com/google/android/gms/auth/api/phone/SmsRetrieverApi.java b/play-services-auth-api-phone/src/main/java/com/google/android/gms/auth/api/phone/SmsRetrieverApi.java new file mode 100644 index 0000000000..dc0bd796e2 --- /dev/null +++ b/play-services-auth-api-phone/src/main/java/com/google/android/gms/auth/api/phone/SmsRetrieverApi.java @@ -0,0 +1,58 @@ +/* + * 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.auth.api.phone; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.android.gms.common.api.Status; +import com.google.android.gms.tasks.OnCompleteListener; +import com.google.android.gms.tasks.Task; + +/** + * API interface for SmsRetriever. + */ +public interface SmsRetrieverApi { + /** + * Starts {@code SmsRetriever}, which waits for a matching SMS message until timeout (5 minutes). The matching SMS message + * will be sent via a Broadcast Intent with action {@link SmsRetriever#SMS_RETRIEVED_ACTION}. The Intent contains Extras with keys + * {@link SmsRetriever#EXTRA_SMS_MESSAGE} for the retrieved SMS message as a String, and {@link SmsRetriever#EXTRA_STATUS} for {@link Status} to indicate + * {@code SUCCESS}, {@code DEVELOPER_ERROR}, {@code ERROR}, or {@code TIMEOUT}. + *

+ * Note: Add {@link SmsRetriever#SEND_PERMISSION} while registering the receiver to detect that the broadcast intent is from the SMS Retriever. + *

+ * The possible causes for errors are: + *

    + *
  • DEVELOPER_ERROR: the caller app has incorrect number of certificates. Only one certificate is allowed.
  • + *
  • ERROR: the AppCode collides with other installed apps.
  • + *
+ * + * @return a Task for the call. Attach an {@link OnCompleteListener} and then check {@link Task#isSuccessful()} to determine if it was successful. + */ + @NonNull + Task startSmsRetriever(); + + /** + * Starts {@code SmsUserConsent}, which waits for an OTP-containing SMS message until timeout (5 minutes). OTP-containing + * SMS message can be retrieved with two steps. + *

+ * Note: Add {@link SmsRetriever#SEND_PERMISSION} while registering the receiver to detect that the broadcast intent is from the SMS Retriever. + *

    + *
  1. [Get consent Intent] While OTP-containing SMS message comes, a consent Intent will be sent via a Broadcast + * Intent with action {@link SmsRetriever#SMS_RETRIEVED_ACTION}. The Intent contains Extras with keys {@link SmsRetriever#EXTRA_CONSENT_INTENT} for the + * consent Intent and {@link SmsRetriever#EXTRA_STATUS} for {@link Status} to indicate {@code SUCCESS} or {@code TIMEOUT}.
  2. + *
  3. [Get OTP-containing SMS message] Calls {@code startActivityForResult} with consent Intent to launch a consent + * dialog to get user's approval, then the OTP-containing SMS message can be retrieved from the activity result.
  4. + *
+ * + * @param senderAddress address of desired SMS sender, or {@code null} to retrieve any sender + * @return a Task for the call. Attach an {@link OnCompleteListener} and then check {@link Task#isSuccessful()} to determine if it was successful. + */ + @NonNull + Task startSmsUserConsent(@Nullable String senderAddress); +} diff --git a/play-services-auth-api-phone/src/main/java/com/google/android/gms/auth/api/phone/SmsRetrieverClient.java b/play-services-auth-api-phone/src/main/java/com/google/android/gms/auth/api/phone/SmsRetrieverClient.java new file mode 100644 index 0000000000..88397abec0 --- /dev/null +++ b/play-services-auth-api-phone/src/main/java/com/google/android/gms/auth/api/phone/SmsRetrieverClient.java @@ -0,0 +1,27 @@ +/* + * 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.auth.api.phone; + +import android.content.Context; +import com.google.android.gms.common.api.Api; +import com.google.android.gms.common.api.GoogleApi; +import com.google.android.gms.common.api.GoogleApiClient; +import org.microg.gms.auth.api.phone.SmsRetrieverApiClient; + +/** + * The main entry point for interacting with SmsRetriever. + *

+ * This does not require a {@link GoogleApiClient}. See {@link GoogleApi} for more information. + */ +public abstract class SmsRetrieverClient extends GoogleApi implements SmsRetrieverApi { + + protected SmsRetrieverClient(Context context) { + super(context, SmsRetrieverApiClient.API); + } +} diff --git a/play-services-auth-api-phone/src/main/java/com/google/android/gms/auth/api/phone/SmsRetrieverStatusCodes.java b/play-services-auth-api-phone/src/main/java/com/google/android/gms/auth/api/phone/SmsRetrieverStatusCodes.java new file mode 100644 index 0000000000..e08998ddd6 --- /dev/null +++ b/play-services-auth-api-phone/src/main/java/com/google/android/gms/auth/api/phone/SmsRetrieverStatusCodes.java @@ -0,0 +1,51 @@ +/* + * 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.auth.api.phone; + +import androidx.annotation.NonNull; +import com.google.android.gms.common.api.CommonStatusCodes; +import com.google.android.gms.common.api.Status; + +/** + * SMS Retriever specific status codes, for use in {@link Status#getStatusCode()}. + */ +public class SmsRetrieverStatusCodes extends CommonStatusCodes { + /** + * The current Android platform does not support this particular API. + */ + public static final int PLATFORM_NOT_SUPPORTED = 36500; + /** + * The calling application is not eligible to use this particular API. + *

+ * Note: For {@link SmsCodeAutofillClient}, this status indicates that the calling application is not the current user-designated + * autofill service. For {@link SmsCodeBrowserClient}, it indicates that the caller is not the system default browser app. + */ + public static final int API_NOT_AVAILABLE = 36501; + /** + * The user has not granted the calling application permission to use this particular API. + */ + public static final int USER_PERMISSION_REQUIRED = 36502; + + /** + * Returns an untranslated debug string based on the given status code. + */ + @NonNull + public static String getStatusCodeString(int statusCode) { + switch (statusCode) { + case PLATFORM_NOT_SUPPORTED: + return "PLATFORM_NOT_SUPPORTED"; + case API_NOT_AVAILABLE: + return "API_NOT_AVAILABLE"; + case USER_PERMISSION_REQUIRED: + return "USER_PERMISSION_REQUIRED"; + default: + return CommonStatusCodes.getStatusCodeString(statusCode); + } + } +} diff --git a/play-services-auth-api-phone/src/main/java/com/google/android/gms/auth/api/phone/package-info.java b/play-services-auth-api-phone/src/main/java/com/google/android/gms/auth/api/phone/package-info.java new file mode 100644 index 0000000000..16fcf4293d --- /dev/null +++ b/play-services-auth-api-phone/src/main/java/com/google/android/gms/auth/api/phone/package-info.java @@ -0,0 +1,36 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: CC-BY-4.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. + */ +/** + * {@code SmsRetriever} contains two APIs, the SMS Retriever API and the SMS User Consent API, that provide access to Google + * services that help you retrieve SMS messages directed to your app, without having to ask for + * {@code android.permission.READ_SMS} or {@code android.permission.RECEIVE_SMS}. The {@code SmsCodeRetriever} is for autofill + * services and browser apps to retrieve SMS-based verification codes. + *

+ * Many apps use phone numbers to verify the identity of a user. The app sends an SMS message containing an OTP (One + * Time Passcode) to the user, who then enters the OTP from the received SMS message to prove ownership of the phone number. + *

+ * In Android, to provide a streamlined UX, an app may request the SMS read permission, and retrieve the OTP + * automatically. This is problematic since this permission allows the app to read other SMS messages which may contain + * the user's private information. Also, the latest Play Store policy changes restrict access to SMS messages. + *

+ * The SMS Retriever API solves this problem by providing app developers a way to automatically retrieve only the SMS + * directed to the app without asking for the SMS read permission or gaining the ability to read any other SMS messages on the device. + *

+ * The SMS User Consent API complements the SMS Retriever API by allowing an app to prompt the user to grant access to + * the content of the next SMS message that contains an OTP. When a user gives consent, the app will then have access to + * the entire message body to automatically complete SMS verification. + *

+ * The SMS Retriever API completely automates the SMS-based OTP verification process for the user. However, there are + * situations where you don’t control the format of the SMS message and as a result cannot use the SMS Retriever API. + * In these situations, you can use the SMS User Consent API to streamline the process. + *

+ * With the SMS Code Autofill API, a user-designated autofill service can retrieve the SMS verification codes from the SMS + * inbox or new incoming SMS messages, then fill in this code for a user to complete any SMS verification requests in a + * user app. For browser apps, you can achieve this by using the SMS Code Browser API. + */ +package com.google.android.gms.auth.api.phone; diff --git a/play-services-auth-api-phone/src/main/java/org/microg/gms/auth/api/phone/SmsCodeAutofillClientImpl.java b/play-services-auth-api-phone/src/main/java/org/microg/gms/auth/api/phone/SmsCodeAutofillClientImpl.java new file mode 100644 index 0000000000..c3a851944c --- /dev/null +++ b/play-services-auth-api-phone/src/main/java/org/microg/gms/auth/api/phone/SmsCodeAutofillClientImpl.java @@ -0,0 +1,56 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.auth.api.phone; + +import android.content.Context; +import com.google.android.gms.auth.api.phone.SmsCodeAutofillClient; +import com.google.android.gms.auth.api.phone.internal.IAutofillPermissionStateCallback; +import com.google.android.gms.auth.api.phone.internal.IOngoingSmsRequestCallback; +import com.google.android.gms.common.api.Api; +import com.google.android.gms.common.api.ApiException; +import com.google.android.gms.common.api.GoogleApi; +import com.google.android.gms.common.api.Status; +import com.google.android.gms.tasks.Task; +import org.microg.gms.common.api.PendingGoogleApiCall; + +public class SmsCodeAutofillClientImpl extends GoogleApi implements SmsCodeAutofillClient { + public SmsCodeAutofillClientImpl(Context context) { + super(context, SmsRetrieverApiClient.API); + } + + @Override + public Task<@PermissionState Integer> checkPermissionState() { + return scheduleTask((PendingGoogleApiCall) (client, completionSource) -> client.checkAutofillPermissionState(new IAutofillPermissionStateCallback.Stub() { + @Override + public void onCheckPermissionStateResult(Status status, int result) { + if (status.isSuccess()) { + completionSource.trySetResult(result); + } else { + completionSource.trySetException(new ApiException(status)); + } + } + })); + } + + @Override + public Task hasOngoingSmsRequest(String packageName) { + return scheduleTask((PendingGoogleApiCall) (client, completionSource) -> client.checkOngoingSmsRequest(packageName, new IOngoingSmsRequestCallback.Stub() { + @Override + public void onHasOngoingSmsRequestResult(Status status, boolean result) { + if (status.isSuccess()) { + completionSource.trySetResult(result); + } else { + completionSource.trySetException(new ApiException(status)); + } + } + })); + } + + @Override + public Task startSmsCodeRetriever() { + return scheduleTask((PendingGoogleApiCall) (client, completionSource) -> client.startSmsCodeAutofill(new StatusCallbackImpl(completionSource))); + } +} diff --git a/play-services-auth-api-phone/src/main/java/org/microg/gms/auth/api/phone/SmsCodeBrowserClientImpl.java b/play-services-auth-api-phone/src/main/java/org/microg/gms/auth/api/phone/SmsCodeBrowserClientImpl.java new file mode 100644 index 0000000000..6279329ab3 --- /dev/null +++ b/play-services-auth-api-phone/src/main/java/org/microg/gms/auth/api/phone/SmsCodeBrowserClientImpl.java @@ -0,0 +1,24 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.auth.api.phone; + +import android.content.Context; +import com.google.android.gms.auth.api.phone.SmsCodeBrowserClient; +import com.google.android.gms.common.api.Api; +import com.google.android.gms.common.api.GoogleApi; +import com.google.android.gms.tasks.Task; +import org.microg.gms.common.api.PendingGoogleApiCall; + +public class SmsCodeBrowserClientImpl extends GoogleApi implements SmsCodeBrowserClient { + public SmsCodeBrowserClientImpl(Context context) { + super(context, SmsRetrieverApiClient.API); + } + + @Override + public Task startSmsCodeRetriever() { + return scheduleTask((PendingGoogleApiCall) (client, completionSource) -> client.startSmsCodeBrowser(new StatusCallbackImpl(completionSource))); + } +} diff --git a/play-services-auth-api-phone/src/main/java/org/microg/gms/auth/api/phone/SmsRetrieverApiClient.java b/play-services-auth-api-phone/src/main/java/org/microg/gms/auth/api/phone/SmsRetrieverApiClient.java new file mode 100644 index 0000000000..4186154239 --- /dev/null +++ b/play-services-auth-api-phone/src/main/java/org/microg/gms/auth/api/phone/SmsRetrieverApiClient.java @@ -0,0 +1,102 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.auth.api.phone; + +import android.content.Context; +import android.os.IBinder; +import android.os.RemoteException; +import androidx.annotation.Nullable; +import com.google.android.gms.auth.api.phone.internal.IAutofillPermissionStateCallback; +import com.google.android.gms.auth.api.phone.internal.IOngoingSmsRequestCallback; +import com.google.android.gms.auth.api.phone.internal.ISmsRetrieverApiService; +import com.google.android.gms.auth.api.phone.internal.ISmsRetrieverResultCallback; +import com.google.android.gms.common.api.Api; +import com.google.android.gms.common.api.Status; +import com.google.android.gms.common.api.internal.IStatusCallback; +import org.microg.gms.common.GmsClient; +import org.microg.gms.common.GmsService; +import org.microg.gms.common.api.ConnectionCallbacks; +import org.microg.gms.common.api.OnConnectionFailedListener; + +public class SmsRetrieverApiClient extends GmsClient { + public static final Api API = new Api<>((options, context, looper, clientSettings, callbacks, connectionFailedListener) -> new SmsRetrieverApiClient(context, callbacks, connectionFailedListener)); + + public SmsRetrieverApiClient(Context context, ConnectionCallbacks callbacks, OnConnectionFailedListener connectionFailedListener) { + super(context, callbacks, connectionFailedListener, GmsService.SMS_RETRIEVER.ACTION); + serviceId = GmsService.SMS_RETRIEVER.SERVICE_ID; + } + + @Override + protected ISmsRetrieverApiService interfaceFromBinder(IBinder binder) { + return ISmsRetrieverApiService.Stub.asInterface(binder); + } + + public void startSmsRetriever(ISmsRetrieverResultCallback callback) { + try { + getServiceInterface().startSmsRetriever(callback); + } catch (RemoteException e) { + try { + callback.onResult(Status.INTERNAL_ERROR); + } catch (RemoteException ignored) { + } + } + } + + public void startWithConsentPrompt(@Nullable String senderAddress, ISmsRetrieverResultCallback callback) { + try { + getServiceInterface().startWithConsentPrompt(senderAddress, callback); + } catch (RemoteException e) { + try { + callback.onResult(Status.INTERNAL_ERROR); + } catch (RemoteException ignored) { + } + } + } + + public void startSmsCodeAutofill(IStatusCallback callback) { + try { + getServiceInterface().startSmsCodeAutofill(callback); + } catch (RemoteException e) { + try { + callback.onResult(Status.INTERNAL_ERROR); + } catch (RemoteException ignored) { + } + } + } + + public void checkAutofillPermissionState(IAutofillPermissionStateCallback callback) { + try { + getServiceInterface().checkAutofillPermissionState(callback); + } catch (RemoteException e) { + try { + callback.onCheckPermissionStateResult(Status.INTERNAL_ERROR, -1); + } catch (RemoteException ignored) { + } + } + } + + public void checkOngoingSmsRequest(String packageName, IOngoingSmsRequestCallback callback) { + try { + getServiceInterface().checkOngoingSmsRequest(packageName, callback); + } catch (RemoteException e) { + try { + callback.onHasOngoingSmsRequestResult(Status.INTERNAL_ERROR, false); + } catch (RemoteException ignored) { + } + } + } + + public void startSmsCodeBrowser(IStatusCallback callback) { + try { + getServiceInterface().startSmsCodeBrowser(callback); + } catch (RemoteException e) { + try { + callback.onResult(Status.INTERNAL_ERROR); + } catch (RemoteException ignored) { + } + } + } +} diff --git a/play-services-auth-api-phone/src/main/java/org/microg/gms/auth/api/phone/SmsRetrieverClientImpl.java b/play-services-auth-api-phone/src/main/java/org/microg/gms/auth/api/phone/SmsRetrieverClientImpl.java new file mode 100644 index 0000000000..63a0a3076e --- /dev/null +++ b/play-services-auth-api-phone/src/main/java/org/microg/gms/auth/api/phone/SmsRetrieverClientImpl.java @@ -0,0 +1,32 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.auth.api.phone; + +import android.content.Context; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.android.gms.auth.api.phone.SmsRetrieverClient; +import com.google.android.gms.tasks.Task; +import org.microg.gms.common.api.PendingGoogleApiCall; + +public class SmsRetrieverClientImpl extends SmsRetrieverClient { + public SmsRetrieverClientImpl(Context context) { + super(context); + } + + @NonNull + @Override + public Task startSmsRetriever() { + return scheduleTask((PendingGoogleApiCall) (client, completionSource) -> client.startSmsRetriever(new SmsRetrieverResultCallbackImpl(completionSource))); + } + + @NonNull + @Override + public Task startSmsUserConsent(@Nullable String senderAddress) { + return scheduleTask((PendingGoogleApiCall) (client, completionSource) -> client.startWithConsentPrompt(senderAddress, new SmsRetrieverResultCallbackImpl(completionSource))); + } + +} diff --git a/play-services-auth-api-phone/src/main/java/org/microg/gms/auth/api/phone/SmsRetrieverResultCallbackImpl.java b/play-services-auth-api-phone/src/main/java/org/microg/gms/auth/api/phone/SmsRetrieverResultCallbackImpl.java new file mode 100644 index 0000000000..fe5900880b --- /dev/null +++ b/play-services-auth-api-phone/src/main/java/org/microg/gms/auth/api/phone/SmsRetrieverResultCallbackImpl.java @@ -0,0 +1,28 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.auth.api.phone; + +import com.google.android.gms.auth.api.phone.internal.ISmsRetrieverResultCallback; +import com.google.android.gms.common.api.ApiException; +import com.google.android.gms.common.api.Status; +import com.google.android.gms.tasks.TaskCompletionSource; + +class SmsRetrieverResultCallbackImpl extends ISmsRetrieverResultCallback.Stub { + private final TaskCompletionSource completionSource; + + public SmsRetrieverResultCallbackImpl(TaskCompletionSource completionSource) { + this.completionSource = completionSource; + } + + @Override + public void onResult(Status status) { + if (status.isSuccess()) { + completionSource.trySetResult(null); + } else { + completionSource.trySetException(new ApiException(status)); + } + } +} diff --git a/play-services-auth-api-phone/src/main/java/org/microg/gms/auth/api/phone/StatusCallbackImpl.java b/play-services-auth-api-phone/src/main/java/org/microg/gms/auth/api/phone/StatusCallbackImpl.java new file mode 100644 index 0000000000..6ff61203fb --- /dev/null +++ b/play-services-auth-api-phone/src/main/java/org/microg/gms/auth/api/phone/StatusCallbackImpl.java @@ -0,0 +1,28 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.auth.api.phone; + +import com.google.android.gms.common.api.ApiException; +import com.google.android.gms.common.api.Status; +import com.google.android.gms.common.api.internal.IStatusCallback; +import com.google.android.gms.tasks.TaskCompletionSource; + +class StatusCallbackImpl extends IStatusCallback.Stub { + private final TaskCompletionSource completionSource; + + public StatusCallbackImpl(TaskCompletionSource completionSource) { + this.completionSource = completionSource; + } + + @Override + public void onResult(Status status) { + if (status.isSuccess()) { + completionSource.trySetResult(null); + } else { + completionSource.trySetException(new ApiException(status)); + } + } +} diff --git a/play-services-auth/src/main/java/com/google/android/gms/auth/package-info.java b/play-services-auth-base/src/main/java/com/google/android/gms/auth/package-info.java similarity index 100% rename from play-services-auth/src/main/java/com/google/android/gms/auth/package-info.java rename to play-services-auth-base/src/main/java/com/google/android/gms/auth/package-info.java diff --git a/play-services-auth/build.gradle b/play-services-auth/build.gradle index 78efc1bd84..b828427049 100644 --- a/play-services-auth/build.gradle +++ b/play-services-auth/build.gradle @@ -25,13 +25,13 @@ android { apply from: '../gradle/publish-android.gradle' -description = 'microG implementation of play-services-auth-base' +description = 'microG implementation of play-services-auth' dependencies { // Dependencies from play-services-auth:20.4.0 api "androidx.fragment:fragment:1.0.0" api "androidx.loader:loader:1.0.0" -// api project(':play-services-auth-api-phone') + api project(':play-services-auth-api-phone') api project(':play-services-auth-base') api project(':play-services-base') api project(':play-services-basement') diff --git a/play-services-auth/src/main/aidl/com/google/android/gms/auth/api/signin/internal/ISignInCallbacks.aidl b/play-services-auth/src/main/aidl/com/google/android/gms/auth/api/signin/internal/ISignInCallbacks.aidl new file mode 100644 index 0000000000..9884a73afe --- /dev/null +++ b/play-services-auth/src/main/aidl/com/google/android/gms/auth/api/signin/internal/ISignInCallbacks.aidl @@ -0,0 +1,10 @@ +package com.google.android.gms.auth.api.signin.internal; + +import com.google.android.gms.auth.api.signin.GoogleSignInAccount; +import com.google.android.gms.common.api.Status; + +interface ISignInCallbacks { + void onSignIn(in GoogleSignInAccount callbacks, in Status status) = 100; + void onSignOut(in Status status) = 101; + void onRevokeAccess(in Status status) = 102; +} \ No newline at end of file diff --git a/play-services-auth/src/main/aidl/com/google/android/gms/auth/api/signin/internal/ISignInService.aidl b/play-services-auth/src/main/aidl/com/google/android/gms/auth/api/signin/internal/ISignInService.aidl new file mode 100644 index 0000000000..6cf4332f46 --- /dev/null +++ b/play-services-auth/src/main/aidl/com/google/android/gms/auth/api/signin/internal/ISignInService.aidl @@ -0,0 +1,10 @@ +package com.google.android.gms.auth.api.signin.internal; + +import com.google.android.gms.auth.api.signin.internal.ISignInCallbacks; +import com.google.android.gms.auth.api.signin.GoogleSignInOptions; + +interface ISignInService { + void silentSignIn(ISignInCallbacks callbacks, in GoogleSignInOptions options) = 100; + void signOut(ISignInCallbacks callbacks, in GoogleSignInOptions options) = 101; + void revokeAccess(ISignInCallbacks callbacks, in GoogleSignInOptions options) = 102; +} \ No newline at end of file diff --git a/play-services-auth/src/main/java/com/google/android/gms/auth/api/credentials/package-info.java b/play-services-auth/src/main/java/com/google/android/gms/auth/api/credentials/package-info.java new file mode 100644 index 0000000000..09f7a2a75c --- /dev/null +++ b/play-services-auth/src/main/java/com/google/android/gms/auth/api/credentials/package-info.java @@ -0,0 +1,11 @@ +/* + * SPDX-FileCopyrightText: 2022 microG Project Team + * SPDX-License-Identifier: CC-BY-4.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. + */ +/** + * Provides facilities to retrieve and save app login credentials. + */ +package com.google.android.gms.auth.api.credentials; diff --git a/play-services-base/core/src/main/java/org/microg/gms/DummyService.java b/play-services-base/core/src/main/java/org/microg/gms/DummyService.java index 3fb2749a8f..3f178f5ead 100644 --- a/play-services-base/core/src/main/java/org/microg/gms/DummyService.java +++ b/play-services-base/core/src/main/java/org/microg/gms/DummyService.java @@ -18,6 +18,7 @@ import android.os.RemoteException; +import com.google.android.gms.common.ConnectionResult; import com.google.android.gms.common.api.CommonStatusCodes; import com.google.android.gms.common.internal.GetServiceRequest; import com.google.android.gms.common.internal.IGmsCallbacks; @@ -31,6 +32,6 @@ public DummyService() { @Override public void handleServiceRequest(IGmsCallbacks callback, GetServiceRequest request, GmsService service) throws RemoteException { - callback.onPostInitComplete(CommonStatusCodes.ERROR, null, null); + callback.onPostInitComplete(ConnectionResult.API_DISABLED, null, null); } } diff --git a/play-services-base/core/src/main/java/org/microg/gms/common/DeviceConfiguration.java b/play-services-base/core/src/main/java/org/microg/gms/common/DeviceConfiguration.java index 411de8aa84..98e334aad4 100644 --- a/play-services-base/core/src/main/java/org/microg/gms/common/DeviceConfiguration.java +++ b/play-services-base/core/src/main/java/org/microg/gms/common/DeviceConfiguration.java @@ -23,8 +23,9 @@ import android.content.pm.PackageManager; import android.content.res.Configuration; import android.opengl.GLES10; -import android.os.Build; import android.util.DisplayMetrics; +import org.microg.gms.profile.Build; +import org.microg.gms.profile.ProfileManager; import java.util.ArrayList; import java.util.Arrays; @@ -56,6 +57,7 @@ public class DeviceConfiguration { public int widthPixels; public DeviceConfiguration(Context context) { + ProfileManager.ensureInitialized(context); ConfigurationInfo configurationInfo = ((ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE)).getDeviceConfigurationInfo(); touchScreen = configurationInfo.reqTouchScreen; keyboardType = configurationInfo.reqKeyboardType; @@ -101,7 +103,7 @@ public DeviceConfiguration(Context context) { @SuppressWarnings({"deprecation", "InlinedApi"}) private static List getNativePlatforms() { List nativePlatforms; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + if (Build.VERSION.SDK_INT >= 21) { return Arrays.asList(Build.SUPPORTED_ABIS); } else { nativePlatforms = new ArrayList(); diff --git a/play-services-base/core/src/main/java/org/microg/gms/common/ForegroundServiceContext.java b/play-services-base/core/src/main/java/org/microg/gms/common/ForegroundServiceContext.java index 3e17ca29ce..dc00dc702b 100644 --- a/play-services-base/core/src/main/java/org/microg/gms/common/ForegroundServiceContext.java +++ b/play-services-base/core/src/main/java/org/microg/gms/common/ForegroundServiceContext.java @@ -8,7 +8,6 @@ import android.content.Context; import android.content.ContextWrapper; import android.content.Intent; -import android.os.Build; import android.os.PowerManager; import android.util.Log; @@ -16,6 +15,8 @@ import org.microg.gms.base.core.R; +import static android.os.Build.VERSION.SDK_INT; + public class ForegroundServiceContext extends ContextWrapper { private static final String TAG = "ForegroundService"; public static final String EXTRA_FOREGROUND = "foreground"; @@ -26,7 +27,7 @@ public ForegroundServiceContext(Context base) { @Override public ComponentName startService(Intent service) { - if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && !isIgnoringBatteryOptimizations()) { + if (SDK_INT >= 26 && !isIgnoringBatteryOptimizations()) { Log.d(TAG, "Starting in foreground mode."); service.putExtra(EXTRA_FOREGROUND, true); return super.startForegroundService(service); @@ -34,7 +35,7 @@ public ComponentName startService(Intent service) { return super.startService(service); } - @RequiresApi(api = Build.VERSION_CODES.M) + @RequiresApi(23) private boolean isIgnoringBatteryOptimizations() { PowerManager powerManager = (PowerManager) getSystemService(POWER_SERVICE); return powerManager.isIgnoringBatteryOptimizations(getPackageName()); @@ -64,7 +65,7 @@ private static String getServiceName(Service service) { } public static void completeForegroundService(Service service, Intent intent, String tag) { - if (intent != null && intent.getBooleanExtra(EXTRA_FOREGROUND, false) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + if (intent != null && intent.getBooleanExtra(EXTRA_FOREGROUND, false) && SDK_INT >= 26) { String serviceName = getServiceName(service); Log.d(tag, "Started " + serviceName + " in foreground mode."); try { @@ -77,7 +78,7 @@ public static void completeForegroundService(Service service, Intent intent, Str } } - @RequiresApi(api = Build.VERSION_CODES.O) + @RequiresApi(26) private static Notification buildForegroundNotification(Context context, String serviceName) { NotificationChannel channel = new NotificationChannel("foreground-service", "Foreground Service", NotificationManager.IMPORTANCE_NONE); channel.setLockscreenVisibility(Notification.VISIBILITY_SECRET); 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 cf611404c8..f30fdf3aa8 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 @@ -74,6 +74,8 @@ public class PackageUtils { KNOWN_GOOGLE_PACKAGES.put("com.google.stadia.android", "133aad3b3d3b580e286573c37f20549f9d3d1cce"); KNOWN_GOOGLE_PACKAGES.put("com.google.android.apps.kids.familylink", "88652b8464743e5ce80da0d4b890d13f9b1873df"); KNOWN_GOOGLE_PACKAGES.put("com.google.android.apps.walletnfcrel", "82759e2db43f9ccbafce313bc674f35748fabd7a"); + KNOWN_GOOGLE_PACKAGES.put("com.google.android.apps.recorder", "394d84cd2cf89d3453702c663f98ec6554afc3cd"); + KNOWN_GOOGLE_PACKAGES.put("com.google.android.apps.messaging", "0980a12be993528c19107bc21ad811478c63cefc"); } public static boolean isGooglePackage(Context context, String packageName) { @@ -211,7 +213,7 @@ public static String getAndCheckCallingPackage(Context context, String suggested if (suggestedCallerPid > 0 && suggestedCallerPid != callingPid) { throw new SecurityException("suggested PID [" + suggestedCallerPid + "] and real calling PID [" + callingPid + "] mismatch!"); } - return getAndCheckPackage(context, suggestedPackageName, callingUid, Binder.getCallingPid()); + return getAndCheckPackage(context, suggestedPackageName, callingUid, callingPid); } @Nullable @@ -269,7 +271,7 @@ public static String firstPackageFromUserId(Context context, int uid) { @SuppressWarnings("deprecation") public static String packageFromPendingIntent(PendingIntent pi) { if (pi == null) return null; - if (SDK_INT < android.os.Build.VERSION_CODES.JELLY_BEAN_MR1) { + if (SDK_INT < 17) { return pi.getTargetPackage(); } else { return pi.getCreatorPackage(); diff --git a/play-services-base/core/src/main/kotlin/org/microg/gms/profile/Build.kt b/play-services-base/core/src/main/kotlin/org/microg/gms/profile/Build.kt index a52059a911..a40e1792be 100644 --- a/play-services-base/core/src/main/kotlin/org/microg/gms/profile/Build.kt +++ b/play-services-base/core/src/main/kotlin/org/microg/gms/profile/Build.kt @@ -90,6 +90,9 @@ object Build { @JvmField var SECURITY_PATCH: String? = null + + @JvmField + var DEVICE_INITIAL_SDK_INT: Int = 0 } fun generateWebViewUserAgentString(original: String): String { diff --git a/play-services-base/core/src/main/kotlin/org/microg/gms/profile/ProfileManager.kt b/play-services-base/core/src/main/kotlin/org/microg/gms/profile/ProfileManager.kt index 56b313e5d6..65eb5684f9 100644 --- a/play-services-base/core/src/main/kotlin/org/microg/gms/profile/ProfileManager.kt +++ b/play-services-base/core/src/main/kotlin/org/microg/gms/profile/ProfileManager.kt @@ -202,6 +202,7 @@ object ProfileManager { return serial } + @SuppressLint("BlockedPrivateApi") private fun getRealData(): Map = mutableMapOf( "Build.BOARD" to android.os.Build.BOARD, "Build.BOOTLOADER" to android.os.Build.BOOTLOADER, @@ -235,6 +236,12 @@ object ProfileManager { if (android.os.Build.VERSION.SDK_INT >= 23) { put("Build.VERSION.SECURITY_PATCH", android.os.Build.VERSION.SECURITY_PATCH) } + try { + val field = android.os.Build.VERSION::class.java.getDeclaredField("DEVICE_INITIAL_SDK_INT") + field.isAccessible = true + put("Build.VERSION.DEVICE_INITIAL_SDK_INT", field.getInt(null).toString()) + } catch (ignored: Exception) { + } } private fun applyProfileData(profileData: Map) { @@ -267,6 +274,7 @@ object ProfileManager { applyStringField("Build.VERSION.RELEASE") { Build.VERSION.RELEASE = it } applyStringField("Build.VERSION.SDK") { Build.VERSION.SDK = it } applyIntField("Build.VERSION.SDK_INT") { Build.VERSION.SDK_INT = it } + applyIntField("Build.VERSION.DEVICE_INITIAL_SDK_INT") { Build.VERSION.DEVICE_INITIAL_SDK_INT = it } if (android.os.Build.VERSION.SDK_INT >= 21) { Build.SUPPORTED_ABIS = profileData["Build.SUPPORTED_ABIS"]?.split(",")?.toTypedArray() ?: emptyArray() } else { diff --git a/play-services-base/core/src/main/kotlin/org/microg/gms/settings/SettingsContract.kt b/play-services-base/core/src/main/kotlin/org/microg/gms/settings/SettingsContract.kt index 42b8b7cb98..9a1a50c8ee 100644 --- a/play-services-base/core/src/main/kotlin/org/microg/gms/settings/SettingsContract.kt +++ b/play-services-base/core/src/main/kotlin/org/microg/gms/settings/SettingsContract.kt @@ -123,11 +123,13 @@ object SettingsContract { const val ENABLED = "droidguard_enabled" const val MODE = "droidguard_mode" const val NETWORK_SERVER_URL = "droidguard_network_server_url" + const val FORCE_LOCAL_DISABLED = "droidguard_force_local_disabled" val PROJECTION = arrayOf( ENABLED, MODE, - NETWORK_SERVER_URL + NETWORK_SERVER_URL, + FORCE_LOCAL_DISABLED, ) } @@ -145,6 +147,28 @@ object SettingsContract { ) } + object Location { + private const val id = "location" + fun getContentUri(context: Context) = Uri.withAppendedPath(getAuthorityUri(context), id) + fun getContentType(context: Context) = "vnd.android.cursor.item/vnd.${getAuthority(context)}.$id" + + const val WIFI_MLS = "location_wifi_mls" + const val WIFI_MOVING = "location_wifi_moving" + const val WIFI_LEARNING = "location_wifi_learning" + const val CELL_MLS = "location_cell_mls" + const val CELL_LEARNING = "location_cell_learning" + const val GEOCODER_NOMINATIM = "location_geocoder_nominatim" + + val PROJECTION = arrayOf( + WIFI_MLS, + WIFI_MOVING, + WIFI_LEARNING, + CELL_MLS, + CELL_LEARNING, + GEOCODER_NOMINATIM, + ) + } + private fun withoutCallingIdentity(f: () -> T): T { val identity = Binder.clearCallingIdentity() try { diff --git a/play-services-base/core/src/main/kotlin/org/microg/gms/settings/SettingsProvider.kt b/play-services-base/core/src/main/kotlin/org/microg/gms/settings/SettingsProvider.kt index 667d480db0..0ff85092b9 100644 --- a/play-services-base/core/src/main/kotlin/org/microg/gms/settings/SettingsProvider.kt +++ b/play-services-base/core/src/main/kotlin/org/microg/gms/settings/SettingsProvider.kt @@ -13,6 +13,7 @@ import android.content.SharedPreferences import android.database.Cursor import android.database.MatrixCursor import android.net.Uri +import android.os.Build.VERSION.SDK_INT import android.preference.PreferenceManager import org.microg.gms.common.PackageUtils.warnIfNotMainProcess import org.microg.gms.settings.SettingsContract.Auth @@ -20,6 +21,7 @@ import org.microg.gms.settings.SettingsContract.CheckIn import org.microg.gms.settings.SettingsContract.DroidGuard import org.microg.gms.settings.SettingsContract.Exposure import org.microg.gms.settings.SettingsContract.Gcm +import org.microg.gms.settings.SettingsContract.Location import org.microg.gms.settings.SettingsContract.Profile import org.microg.gms.settings.SettingsContract.SafetyNet import org.microg.gms.settings.SettingsContract.getAuthority @@ -37,6 +39,9 @@ class SettingsProvider : ContentProvider() { private val checkInPrefs by lazy { context!!.getSharedPreferences(CheckIn.PREFERENCES_NAME, MODE_PRIVATE) } + private val unifiedNlpPreferences by lazy { + context!!.getSharedPreferences("unified_nlp", MODE_PRIVATE) + } private val systemDefaultPreferences: SharedPreferences? by lazy { try { Context::class.java.getDeclaredMethod( @@ -67,6 +72,7 @@ class SettingsProvider : ContentProvider() { SafetyNet.getContentUri(context!!) -> querySafetyNet(projection ?: SafetyNet.PROJECTION) DroidGuard.getContentUri(context!!) -> queryDroidGuard(projection ?: DroidGuard.PROJECTION) Profile.getContentUri(context!!) -> queryProfile(projection ?: Profile.PROJECTION) + Location.getContentUri(context!!) -> queryLocation(projection ?: Location.PROJECTION) else -> null } @@ -86,6 +92,7 @@ class SettingsProvider : ContentProvider() { SafetyNet.getContentUri(context!!) -> updateSafetyNet(values) DroidGuard.getContentUri(context!!) -> updateDroidGuard(values) Profile.getContentUri(context!!) -> updateProfile(values) + Location.getContentUri(context!!) -> updateLocation(values) else -> return 0 } return 1 @@ -249,6 +256,7 @@ class SettingsProvider : ContentProvider() { DroidGuard.ENABLED -> getSettingsBoolean(key, false) DroidGuard.MODE -> getSettingsString(key) DroidGuard.NETWORK_SERVER_URL -> getSettingsString(key) + DroidGuard.FORCE_LOCAL_DISABLED -> systemDefaultPreferences?.getBoolean(key, false) ?: false else -> throw IllegalArgumentException("Unknown key: $key") } } @@ -288,6 +296,38 @@ class SettingsProvider : ContentProvider() { editor.apply() } + private fun queryLocation(p: Array): Cursor = MatrixCursor(p).addRow(p) { key -> + when (key) { + Location.WIFI_MLS -> getSettingsBoolean(key, hasUnifiedNlpLocationBackend("org.microg.nlp.backend.ichnaea")) + Location.WIFI_MOVING -> getSettingsBoolean(key, hasUnifiedNlpLocationBackend("de.sorunome.unifiednlp.trains")) + Location.WIFI_LEARNING -> getSettingsBoolean(key, hasUnifiedNlpLocationBackend("helium314.localbackend", "org.fitchfamily.android.dejavu")) + Location.CELL_MLS -> getSettingsBoolean(key, hasUnifiedNlpLocationBackend("org.microg.nlp.backend.ichnaea")) + Location.CELL_LEARNING -> getSettingsBoolean(key, hasUnifiedNlpLocationBackend("helium314.localbackend", "org.fitchfamily.android.dejavu")) + Location.GEOCODER_NOMINATIM -> getSettingsBoolean(key, hasUnifiedNlpGeocoderBackend("org.microg.nlp.backend.nominatim") ) + else -> throw IllegalArgumentException("Unknown key: $key") + } + } + private fun hasUnifiedNlpPrefixInStringSet(key: String, vararg prefixes: String) = getUnifiedNlpSettingsStringSetCompat(key, emptySet()).any { entry -> prefixes.any { prefix -> entry.startsWith(prefix)}} + private fun hasUnifiedNlpLocationBackend(vararg packageNames: String) = hasUnifiedNlpPrefixInStringSet("location_backends", *packageNames.map { "$it/" }.toTypedArray()) + private fun hasUnifiedNlpGeocoderBackend(vararg packageNames: String) = hasUnifiedNlpPrefixInStringSet("geocoder_backends", *packageNames.map { "$it/" }.toTypedArray()) + + private fun updateLocation(values: ContentValues) { + if (values.size() == 0) return + val editor = preferences.edit() + values.valueSet().forEach { (key, value) -> + when (key) { + Location.WIFI_MLS -> editor.putBoolean(key, value as Boolean) + Location.WIFI_MOVING -> editor.putBoolean(key, value as Boolean) + Location.WIFI_LEARNING -> editor.putBoolean(key, value as Boolean) + Location.CELL_MLS -> editor.putBoolean(key, value as Boolean) + Location.CELL_LEARNING -> editor.putBoolean(key, value as Boolean) + Location.GEOCODER_NOMINATIM -> editor.putBoolean(key, value as Boolean) + else -> throw IllegalArgumentException("Unknown key: $key") + } + } + editor.apply() + } + private fun MatrixCursor.addRow( p: Array, valueGetter: (String) -> Any? @@ -321,7 +361,27 @@ class SettingsProvider : ContentProvider() { private fun getSettingsString(key: String, def: String? = null): String? = listOf(preferences, systemDefaultPreferences).getString(key, def) private fun getSettingsInt(key: String, def: Int): Int = listOf(preferences, systemDefaultPreferences).getInt(key, def) private fun getSettingsLong(key: String, def: Long): Long = listOf(preferences, systemDefaultPreferences).getLong(key, def) + private fun getUnifiedNlpSettingsStringSetCompat(key: String, def: Set): Set = listOf(unifiedNlpPreferences, preferences, systemDefaultPreferences).getStringSetCompat(key, def) + + private fun SharedPreferences.getStringSetCompat(key: String, def: Set): Set { + if (SDK_INT >= 11) { + try { + val res = getStringSet(key, null) + if (res != null) return res.filter { it.isNotEmpty() }.toSet() + } catch (ignored: Exception) { + // Ignore + } + } + try { + val str = getString(key, null) + if (str != null) return str.split("\\|".toRegex()).filter { it.isNotEmpty() }.toSet() + } catch (ignored: Exception) { + // Ignore + } + return def + } + private fun List.getStringSetCompat(key: String, def: Set): Set = foldRight(def) { preferences, defValue -> preferences?.getStringSetCompat(key, defValue) ?: defValue } private fun List.getString(key: String, def: String?): String? = foldRight(def) { preferences, defValue -> preferences?.getString(key, defValue) ?: defValue } private fun List.getInt(key: String, def: Int): Int = foldRight(def) { preferences, defValue -> preferences?.getInt(key, defValue) ?: defValue } private fun List.getLong(key: String, def: Long): Long = foldRight(def) { preferences, defValue -> preferences?.getLong(key, defValue) ?: defValue } diff --git a/play-services-base/core/src/main/kotlin/org/microg/gms/ui/AppHeadingPreference.kt b/play-services-base/core/src/main/kotlin/org/microg/gms/ui/AppHeadingPreference.kt new file mode 100644 index 0000000000..320d0dfa4d --- /dev/null +++ b/play-services-base/core/src/main/kotlin/org/microg/gms/ui/AppHeadingPreference.kt @@ -0,0 +1,49 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.ui + +import android.content.Context +import android.content.Intent +import android.content.pm.ApplicationInfo +import android.net.Uri +import android.provider.Settings +import android.util.AttributeSet +import android.util.Log +import android.view.View +import android.widget.ImageView +import androidx.appcompat.content.res.AppCompatResources +import androidx.preference.Preference +import androidx.preference.PreferenceViewHolder +import org.microg.gms.base.core.R + +class AppHeadingPreference : AppPreference, Preference.OnPreferenceClickListener { + constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) + constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) + constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) + constructor(context: Context) : super(context) + + init { + layoutResource = R.layout.preference_app_heading + onPreferenceClickListener = this + } + + override fun onPreferenceClick(preference: Preference): Boolean { + if (packageName != null) { + val intent = Intent() + intent.action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS + val uri: Uri = Uri.fromParts("package", packageName, null) + intent.data = uri + try { + context.startActivity(intent) + } catch (e: Exception) { + Log.w(TAG, "Failed to launch app", e) + } + return true + } else { + return false + } + } +} \ No newline at end of file diff --git a/play-services-base/core/src/main/kotlin/org/microg/gms/ui/AppIconPreference.kt b/play-services-base/core/src/main/kotlin/org/microg/gms/ui/AppIconPreference.kt index de5020c018..0f74913821 100644 --- a/play-services-base/core/src/main/kotlin/org/microg/gms/ui/AppIconPreference.kt +++ b/play-services-base/core/src/main/kotlin/org/microg/gms/ui/AppIconPreference.kt @@ -6,13 +6,15 @@ package org.microg.gms.ui import android.content.Context +import android.content.pm.ApplicationInfo import android.util.AttributeSet import android.util.DisplayMetrics import android.widget.ImageView +import androidx.appcompat.content.res.AppCompatResources import androidx.preference.Preference import androidx.preference.PreferenceViewHolder -class AppIconPreference : Preference { +class AppIconPreference : AppPreference { constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) diff --git a/play-services-base/core/src/main/kotlin/org/microg/gms/ui/AppPreference.kt b/play-services-base/core/src/main/kotlin/org/microg/gms/ui/AppPreference.kt new file mode 100644 index 0000000000..066b4d9a71 --- /dev/null +++ b/play-services-base/core/src/main/kotlin/org/microg/gms/ui/AppPreference.kt @@ -0,0 +1,54 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.ui + +import android.content.Context +import android.content.pm.ApplicationInfo +import android.util.AttributeSet +import androidx.appcompat.content.res.AppCompatResources +import androidx.preference.Preference + +abstract class AppPreference : Preference { + constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) + constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) + constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) + constructor(context: Context) : super(context) + + init { + isPersistent = false + } + + private var packageNameField: String? = null + + var applicationInfo: ApplicationInfo? + get() = context.packageManager.getApplicationInfoIfExists(packageNameField) + set(value) { + if (value == null && packageNameField != null) { + title = null + icon = null + } else if (value != null) { + val pm = context.packageManager + title = value.loadLabel(pm) ?: value.packageName + icon = value.loadIcon(pm) ?: AppCompatResources.getDrawable(context, android.R.mipmap.sym_def_app_icon) + } + packageNameField = value?.packageName + } + + var packageName: String? + get() = packageNameField + set(value) { + if (value == null && packageNameField != null) { + title = null + icon = null + } else if (value != null) { + val pm = context.packageManager + val applicationInfo = pm.getApplicationInfoIfExists(value) + title = applicationInfo?.loadLabel(pm)?.toString() ?: value + icon = applicationInfo?.loadIcon(pm) ?: AppCompatResources.getDrawable(context, android.R.mipmap.sym_def_app_icon) + } + packageNameField = value + } +} \ No newline at end of file diff --git a/play-services-base/core/src/main/kotlin/org/microg/gms/ui/PreferenceSwitchBar.kt b/play-services-base/core/src/main/kotlin/org/microg/gms/ui/PreferenceSwitchBar.kt deleted file mode 100644 index b66f11193c..0000000000 --- a/play-services-base/core/src/main/kotlin/org/microg/gms/ui/PreferenceSwitchBar.kt +++ /dev/null @@ -1,10 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2020, microG Project Team - * SPDX-License-Identifier: Apache-2.0 - */ - -package org.microg.gms.ui - -interface PreferenceSwitchBarCallback { - fun onChecked(newStatus: Boolean) -} diff --git a/play-services-base/core/src/main/kotlin/org/microg/gms/ui/SwitchBarPreference.kt b/play-services-base/core/src/main/kotlin/org/microg/gms/ui/SwitchBarPreference.kt new file mode 100644 index 0000000000..b2332e9ce9 --- /dev/null +++ b/play-services-base/core/src/main/kotlin/org/microg/gms/ui/SwitchBarPreference.kt @@ -0,0 +1,51 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.ui + +import android.content.Context +import android.util.AttributeSet +import androidx.appcompat.widget.SwitchCompat +import androidx.preference.PreferenceViewHolder +import androidx.preference.TwoStatePreference +import org.microg.gms.base.core.R + +// TODO +class SwitchBarPreference : TwoStatePreference { + constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) + constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) + constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) + constructor(context: Context) : super(context) + + init { + layoutResource = R.layout.preference_switch_bar + } + + override fun onBindViewHolder(holder: PreferenceViewHolder) { + super.onBindViewHolder(holder) + holder.isDividerAllowedBelow = false + holder.isDividerAllowedAbove = false + val switch = holder.findViewById(R.id.switch_widget) as SwitchCompat + switch.setOnCheckedChangeListener(null) + switch.isChecked = isChecked + switch.setOnCheckedChangeListener { view, isChecked -> + if (!callChangeListener(isChecked)) { + view.isChecked = !isChecked + return@setOnCheckedChangeListener + } + this.isChecked = isChecked + } + holder.itemView.setBackgroundColorAttribute(when { + isChecked -> androidx.appcompat.R.attr.colorControlActivated + isEnabled -> androidx.appcompat.R.attr.colorButtonNormal + else -> androidx.appcompat.R.attr.colorControlHighlight + }) + } +} + +@Deprecated("Get rid") +interface PreferenceSwitchBarCallback { + fun onChecked(newStatus: Boolean) +} \ No newline at end of file diff --git a/play-services-base/core/src/main/kotlin/org/microg/gms/ui/Utils.kt b/play-services-base/core/src/main/kotlin/org/microg/gms/ui/Utils.kt index 5df78d3d76..2994a039bb 100644 --- a/play-services-base/core/src/main/kotlin/org/microg/gms/ui/Utils.kt +++ b/play-services-base/core/src/main/kotlin/org/microg/gms/ui/Utils.kt @@ -8,7 +8,7 @@ package org.microg.gms.ui import android.content.Context import android.content.pm.ApplicationInfo import android.content.pm.PackageManager -import android.os.Build +import android.os.Build.VERSION.SDK_INT import android.os.Bundle import android.provider.Settings import android.util.Log @@ -47,7 +47,7 @@ val Context.systemAnimationsEnabled: Boolean get() { val duration: Float val transition: Float - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + if (SDK_INT >= 17) { duration = Settings.Global.getFloat(contentResolver, Settings.Global.ANIMATOR_DURATION_SCALE, 1f) transition = Settings.Global.getFloat(contentResolver, Settings.Global.TRANSITION_ANIMATION_SCALE, 1f) } else { diff --git a/play-services-base/core/src/main/kotlin/org/microg/gms/utils/BinderUtils.kt b/play-services-base/core/src/main/kotlin/org/microg/gms/utils/BinderUtils.kt index 79e8ff8f4b..226bb43132 100644 --- a/play-services-base/core/src/main/kotlin/org/microg/gms/utils/BinderUtils.kt +++ b/play-services-base/core/src/main/kotlin/org/microg/gms/utils/BinderUtils.kt @@ -12,13 +12,13 @@ import android.util.Log private const val TAG = "BinderUtils" -fun IBinder.warnOnTransactionIssues(code: Int, reply: Parcel?, flags: Int, base: () -> Boolean): Boolean { +fun IBinder.warnOnTransactionIssues(code: Int, reply: Parcel?, flags: Int, tag: String = TAG, base: () -> Boolean): Boolean { if (base.invoke()) { if ((flags and Binder.FLAG_ONEWAY) > 0 && (reply?.dataSize() ?: 0) > 0) { - Log.w(TAG, "Method $code in $interfaceDescriptor is oneway, but returned data") + Log.w(tag, "Method $code in $interfaceDescriptor is oneway, but returned data") } return true } - Log.w(TAG, "Unknown method $code in $interfaceDescriptor, skipping") + Log.w(tag, "Unknown method $code in $interfaceDescriptor, skipping") return (flags and Binder.FLAG_ONEWAY) > 0 // Don't return false on oneway transaction to suppress warning } diff --git a/play-services-base/core/src/main/kotlin/org/microg/gms/utils/IntentCacheManager.kt b/play-services-base/core/src/main/kotlin/org/microg/gms/utils/IntentCacheManager.kt new file mode 100644 index 0000000000..fc57ced6f4 --- /dev/null +++ b/play-services-base/core/src/main/kotlin/org/microg/gms/utils/IntentCacheManager.kt @@ -0,0 +1,150 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.utils + +import android.app.AlarmManager +import android.app.AlarmManager.ELAPSED_REALTIME_WAKEUP +import android.app.PendingIntent +import android.app.PendingIntent.FLAG_NO_CREATE +import android.app.PendingIntent.FLAG_MUTABLE +import android.app.PendingIntent.FLAG_UPDATE_CURRENT +import android.app.Service +import android.content.Context +import android.content.Intent +import android.os.Build.VERSION.SDK_INT +import android.os.Parcelable +import android.os.SystemClock +import android.util.Log +import androidx.core.content.getSystemService +import java.util.UUID + +class IntentCacheManager(private val context: Context, private val clazz: Class, private val type: Int) { + private val lock = Any() + private lateinit var content: ArrayList + private lateinit var id: String + private var isReady: Boolean = false + private val pendingActions: MutableList<() -> Unit> = arrayListOf() + + init { + val pendingIntent = PendingIntent.getService(context, type, getIntent(), if (SDK_INT >= 31) FLAG_MUTABLE else 0) + val alarmManager = context.getSystemService() + if (SDK_INT >= 19) { + alarmManager?.setWindow(ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime() + TEN_YEARS, -1, pendingIntent) + } else { + alarmManager?.set(ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime() + TEN_YEARS, pendingIntent) + } + pendingIntent.send() + } + + private fun getIntent() = Intent(context, clazz).apply { + action = ACTION + putExtra(EXTRA_IS_CACHE, true) + putExtra(EXTRA_CACHE_TYPE, this@IntentCacheManager.type) + } + + fun add(entry: T, check: (T) -> Boolean = { false }) = runIfReady { + val iterator = content.iterator() + while (iterator.hasNext()) { + if (check(iterator.next())) { + iterator.remove() + } + } + content.add(entry) + updateIntent() + } + + fun remove(entry: T) = runIfReady { + if (content.remove(entry)) updateIntent() + } + + fun removeIf(check: (T) -> Boolean) = runIfReady { + var removed = false + val iterator = content.iterator() + while (iterator.hasNext()) { + if (check(iterator.next())) { + iterator.remove() + removed = true + } + } + if (removed) updateIntent() + } + + fun clear() = runIfReady { + content.clear() + updateIntent() + } + + fun getId(): String? = if (this::id.isInitialized) id else null + + fun getEntries(): List = if (this::content.isInitialized) content else emptyList() + + fun processIntent(intent: Intent) { + if (isCache(intent) && getType(intent) == type) { + synchronized(lock) { + content = intent.getParcelableArrayListExtra(EXTRA_DATA) ?: arrayListOf() + id = intent.getStringExtra(EXTRA_ID) ?: UUID.randomUUID().toString() + if (!intent.hasExtra(EXTRA_ID)) { + Log.d(TAG, "Created new intent cache with id $id") + } else if (intent.hasExtra(EXTRA_DATA)) { + Log.d(TAG, "Recovered data from intent cache with id $id") + } + pendingActions.forEach { it() } + pendingActions.clear() + isReady = true + updateIntent() + } + } + } + + private fun runIfReady(action: () -> Unit) { + synchronized(lock) { + if (isReady) { + action() + } else { + pendingActions.add(action) + } + } + } + + private fun updateIntent() { + synchronized(lock) { + if (isReady) { + val intent = getIntent().apply { + putExtra(EXTRA_ID, id) + putParcelableArrayListExtra(EXTRA_DATA, content) + } + val pendingIntent = PendingIntent.getService(context, type, intent, FLAG_NO_CREATE or FLAG_UPDATE_CURRENT or if (SDK_INT >= 31) FLAG_MUTABLE else 0) + if (pendingIntent == null) { + Log.w(TAG, "Failed to update existing pending intent, will likely have a loss of information") + } + } + } + } + + companion object { + private const val TAG = "IntentCacheManager" + private const val TEN_YEARS = 315360000000L + private const val ACTION = "org.microg.gms.ACTION_INTENT_CACHE_MANAGER" + private const val EXTRA_IS_CACHE = "org.microg.gms.IntentCacheManager.is_cache" + private const val EXTRA_CACHE_TYPE = "org.microg.gms.IntentCacheManager.cache_type" + private const val EXTRA_ID = "org.microg.gms.IntentCacheManager.id" + private const val EXTRA_DATA = "org.microg.gms.IntentCacheManager.data" + + inline fun create(context: Context, type: Int) = IntentCacheManager(context, S::class.java, type) + + fun isCache(intent: Intent): Boolean = try { + intent.getBooleanExtra(EXTRA_IS_CACHE, false) + } catch (e: Exception) { + false + } + + fun getType(intent: Intent): Int { + val ret = intent.getIntExtra(EXTRA_CACHE_TYPE, -1) + if (ret == -1) throw IllegalArgumentException() + return ret + } + } +} \ No newline at end of file diff --git a/play-services-base/core/src/main/res/layout/preference_app_heading.xml b/play-services-base/core/src/main/res/layout/preference_app_heading.xml new file mode 100644 index 0000000000..42f8f760e7 --- /dev/null +++ b/play-services-base/core/src/main/res/layout/preference_app_heading.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/play-services-core/src/main/res/layout/preference_progress_bar.xml b/play-services-base/core/src/main/res/layout/preference_progress_bar.xml similarity index 100% rename from play-services-core/src/main/res/layout/preference_progress_bar.xml rename to play-services-base/core/src/main/res/layout/preference_progress_bar.xml diff --git a/play-services-base/core/src/main/res/layout/preference_switch_bar.xml b/play-services-base/core/src/main/res/layout/preference_switch_bar.xml index 08ad65300b..9818305b23 100644 --- a/play-services-base/core/src/main/res/layout/preference_switch_bar.xml +++ b/play-services-base/core/src/main/res/layout/preference_switch_bar.xml @@ -1,73 +1,42 @@ - - - - - - - - - - - - - - - - - - - - - - + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="?attr/actionBarSize" + android:gravity="center" + android:orientation="horizontal" + android:paddingStart="?attr/listPreferredItemPaddingStart" + android:paddingLeft="?attr/listPreferredItemPaddingLeft" + android:paddingEnd="?attr/listPreferredItemPaddingEnd" + android:paddingRight="?attr/listPreferredItemPaddingRight" + tools:background="?attr/colorControlActivated"> + + + + + \ No newline at end of file diff --git a/play-services-base/src/main/aidl/com/google/android/gms/auth/api/signin/GoogleSignInAccount.aidl b/play-services-base/src/main/aidl/com/google/android/gms/auth/api/signin/GoogleSignInAccount.aidl new file mode 100644 index 0000000000..38df8e14f3 --- /dev/null +++ b/play-services-base/src/main/aidl/com/google/android/gms/auth/api/signin/GoogleSignInAccount.aidl @@ -0,0 +1,3 @@ +package com.google.android.gms.auth.api.signin; + +parcelable GoogleSignInAccount; \ No newline at end of file diff --git a/play-services-base/src/main/aidl/com/google/android/gms/auth/api/signin/GoogleSignInOptions.aidl b/play-services-base/src/main/aidl/com/google/android/gms/auth/api/signin/GoogleSignInOptions.aidl new file mode 100644 index 0000000000..eb24d69d7a --- /dev/null +++ b/play-services-base/src/main/aidl/com/google/android/gms/auth/api/signin/GoogleSignInOptions.aidl @@ -0,0 +1,3 @@ +package com.google.android.gms.auth.api.signin; + +parcelable GoogleSignInOptions; \ No newline at end of file diff --git a/play-services-base/src/main/aidl/com/google/android/gms/common/internal/AuthAccountRequest.aidl b/play-services-base/src/main/aidl/com/google/android/gms/common/internal/AuthAccountRequest.aidl new file mode 100644 index 0000000000..b212248e21 --- /dev/null +++ b/play-services-base/src/main/aidl/com/google/android/gms/common/internal/AuthAccountRequest.aidl @@ -0,0 +1,3 @@ +package com.google.android.gms.common.internal; + +parcelable AuthAccountRequest; diff --git a/play-services-base/src/main/aidl/com/google/android/gms/common/internal/IResolveAccountCallbacks.aidl b/play-services-base/src/main/aidl/com/google/android/gms/common/internal/IResolveAccountCallbacks.aidl new file mode 100644 index 0000000000..e2ac6778dd --- /dev/null +++ b/play-services-base/src/main/aidl/com/google/android/gms/common/internal/IResolveAccountCallbacks.aidl @@ -0,0 +1,5 @@ +package com.google.android.gms.common.internal; + +interface IResolveAccountCallbacks { + +} diff --git a/play-services-base/src/main/aidl/com/google/android/gms/common/internal/ResolveAccountRequest.aidl b/play-services-base/src/main/aidl/com/google/android/gms/common/internal/ResolveAccountRequest.aidl new file mode 100644 index 0000000000..a53ff86eea --- /dev/null +++ b/play-services-base/src/main/aidl/com/google/android/gms/common/internal/ResolveAccountRequest.aidl @@ -0,0 +1,3 @@ +package com.google.android.gms.common.internal; + +parcelable ResolveAccountRequest; diff --git a/play-services-base/src/main/aidl/com/google/android/gms/signin/internal/CheckServerAuthResult.aidl b/play-services-base/src/main/aidl/com/google/android/gms/signin/internal/CheckServerAuthResult.aidl new file mode 100644 index 0000000000..1a56473134 --- /dev/null +++ b/play-services-base/src/main/aidl/com/google/android/gms/signin/internal/CheckServerAuthResult.aidl @@ -0,0 +1,3 @@ +package com.google.android.gms.signin.internal; + +parcelable CheckServerAuthResult; \ No newline at end of file diff --git a/play-services-base/src/main/aidl/com/google/android/gms/signin/internal/ISignInCallbacks.aidl b/play-services-base/src/main/aidl/com/google/android/gms/signin/internal/ISignInCallbacks.aidl new file mode 100644 index 0000000000..16d2471f1d --- /dev/null +++ b/play-services-base/src/main/aidl/com/google/android/gms/signin/internal/ISignInCallbacks.aidl @@ -0,0 +1,7 @@ +package com.google.android.gms.signin.internal; + +import com.google.android.gms.signin.internal.SignInResponse; + +interface ISignInCallbacks { + void onSignIn(in SignInResponse response) = 7; +} \ No newline at end of file diff --git a/play-services-base/src/main/aidl/com/google/android/gms/signin/internal/ISignInService.aidl b/play-services-base/src/main/aidl/com/google/android/gms/signin/internal/ISignInService.aidl new file mode 100644 index 0000000000..326c415113 --- /dev/null +++ b/play-services-base/src/main/aidl/com/google/android/gms/signin/internal/ISignInService.aidl @@ -0,0 +1,27 @@ +package com.google.android.gms.signin.internal; + +import com.google.android.gms.common.internal.AuthAccountRequest; +import com.google.android.gms.common.internal.ResolveAccountRequest; +import com.google.android.gms.common.internal.IAccountAccessor; +import com.google.android.gms.common.internal.IResolveAccountCallbacks; +import com.google.android.gms.signin.internal.ISignInCallbacks; +import com.google.android.gms.signin.internal.CheckServerAuthResult; +import com.google.android.gms.signin.internal.RecordConsentRequest; +import com.google.android.gms.signin.internal.RecordConsentByConsentResultRequest; +import com.google.android.gms.signin.internal.SignInRequest; + +interface ISignInService { + void authAccount(in AuthAccountRequest request, ISignInCallbacks callbacks) = 1; + void onCheckServerAuthorization(in CheckServerAuthResult result) = 2; + void onUploadServerAuthCode(int sessionId) = 3; + void resolveAccount(in ResolveAccountRequest request, IResolveAccountCallbacks callbacks) = 4; + + void clearAccountFromSessionStore(int sessionId) = 6; + void putAccount(int sessionId, in Account account, ISignInCallbacks callbacks) = 7; + void saveDefaultAccount(IAccountAccessor accountAccessor, int sessionId, boolean crossClient) = 8; + void saveConsent(in RecordConsentRequest request, ISignInCallbacks callbacks) = 9; + void getCurrentAccount(ISignInCallbacks callbacks) = 10; + void signIn(in SignInRequest request, ISignInCallbacks callbacks) = 11; + void setGamesHasBeenGreeted(boolean hasGreeted) = 12; + void recordConsentByConsentResult(in RecordConsentByConsentResultRequest request, ISignInCallbacks callbacks) = 13; +} \ No newline at end of file diff --git a/play-services-base/src/main/aidl/com/google/android/gms/signin/internal/RecordConsentByConsentResultRequest.aidl b/play-services-base/src/main/aidl/com/google/android/gms/signin/internal/RecordConsentByConsentResultRequest.aidl new file mode 100644 index 0000000000..ebe30c2bef --- /dev/null +++ b/play-services-base/src/main/aidl/com/google/android/gms/signin/internal/RecordConsentByConsentResultRequest.aidl @@ -0,0 +1,3 @@ +package com.google.android.gms.signin.internal; + +parcelable RecordConsentByConsentResultRequest; \ No newline at end of file diff --git a/play-services-base/src/main/aidl/com/google/android/gms/signin/internal/RecordConsentRequest.aidl b/play-services-base/src/main/aidl/com/google/android/gms/signin/internal/RecordConsentRequest.aidl new file mode 100644 index 0000000000..160f49696a --- /dev/null +++ b/play-services-base/src/main/aidl/com/google/android/gms/signin/internal/RecordConsentRequest.aidl @@ -0,0 +1,3 @@ +package com.google.android.gms.signin.internal; + +parcelable RecordConsentRequest; \ No newline at end of file diff --git a/play-services-base/src/main/aidl/com/google/android/gms/signin/internal/SignInRequest.aidl b/play-services-base/src/main/aidl/com/google/android/gms/signin/internal/SignInRequest.aidl new file mode 100644 index 0000000000..df5f236849 --- /dev/null +++ b/play-services-base/src/main/aidl/com/google/android/gms/signin/internal/SignInRequest.aidl @@ -0,0 +1,3 @@ +package com.google.android.gms.signin.internal; + +parcelable SignInRequest; \ No newline at end of file diff --git a/play-services-base/src/main/aidl/com/google/android/gms/signin/internal/SignInResponse.aidl b/play-services-base/src/main/aidl/com/google/android/gms/signin/internal/SignInResponse.aidl new file mode 100644 index 0000000000..45908b2ba9 --- /dev/null +++ b/play-services-base/src/main/aidl/com/google/android/gms/signin/internal/SignInResponse.aidl @@ -0,0 +1,3 @@ +package com.google.android.gms.signin.internal; + +parcelable SignInResponse; \ No newline at end of file diff --git a/play-services-base/src/main/java/com/google/android/gms/auth/api/signin/GoogleSignInAccount.java b/play-services-base/src/main/java/com/google/android/gms/auth/api/signin/GoogleSignInAccount.java index 802db9a361..fb3a505ea7 100644 --- a/play-services-base/src/main/java/com/google/android/gms/auth/api/signin/GoogleSignInAccount.java +++ b/play-services-base/src/main/java/com/google/android/gms/auth/api/signin/GoogleSignInAccount.java @@ -1,12 +1,188 @@ /* - * SPDX-FileCopyrightText: 2020, microG Project Team + * 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.auth.api.signin; +import android.accounts.Account; +import android.net.Uri; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.android.gms.common.api.Scope; +import org.microg.gms.auth.AuthConstants; +import org.microg.gms.common.Hide; import org.microg.safeparcel.AutoSafeParcelable; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Set; + +/** + * Class that holds the basic account information of the signed in Google user. + */ public class GoogleSignInAccount extends AutoSafeParcelable { - public static final Creator CREATOR = new AutoCreator(GoogleSignInAccount.class); + @Field(1) + private int versionCode = 3; + @Field(2) + @Nullable + private String id; + @Field(3) + @Nullable + private String tokenId; + @Field(4) + @Nullable + private String email; + @Field(5) + @Nullable + private String displayName; + @Field(6) + @Nullable + private Uri photoUrl; + @Field(7) + private String serverAuthCode; + @Field(8) + private long expirationTime; + @Field(9) + private String obfuscatedIdentifier; + @Field(10) + private ArrayList grantedScopes; + @Field(11) + @Nullable + private String givenName; + @Field(12) + @Nullable + private String familyName; + + private GoogleSignInAccount() { + } + + @Hide + public GoogleSignInAccount(Account account, Set grantedScopes) { + this.email = account.name; + this.obfuscatedIdentifier = account.name; + this.expirationTime = 0; + this.grantedScopes = new ArrayList<>(grantedScopes); + } + + /** + * A convenient wrapper for {@link #getEmail()} which returns an android.accounts.Account object. See {@link #getEmail()} doc for details. + */ + public @Nullable Account getAccount() { + if (email == null) return null; + return new Account(email, AuthConstants.DEFAULT_ACCOUNT_TYPE); + } + + /** + * Returns the display name of the signed in user if you built your configuration starting from + * {@code new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)} or with {@link GoogleSignInOptions.Builder#requestProfile()} configured; + * {@code null} otherwise. Not guaranteed to be present for all users, even when configured. + */ + public @Nullable String getDisplayName() { + return displayName; + } + + /** + * Returns the email address of the signed in user if {@link GoogleSignInOptions.Builder#requestEmail()} was configured; {@code null} otherwise. + *

+ * Applications should not key users by email address since a Google account's email address can change. Use {@link #getId()} as a key instead. + *

+ * Important: Do not use this returned email address to communicate the currently signed in user to your backend server. Instead, send an ID token + * ({@link GoogleSignInOptions.Builder#requestIdToken(String)}), which can be securely validated on the server; or send server auth code + * ({@link GoogleSignInOptions.Builder#requestServerAuthCode(String)}) which can be in turn exchanged for id token. + * + * @return + */ + public @Nullable String getEmail() { + return email; + } + + /** + * Returns the family name of the signed in user if you built your configuration starting from + * {@code new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)} or with {@link GoogleSignInOptions.Builder#requestProfile()} configured; + * {@code null} otherwise. Not guaranteed to be present for all users, even when configured. + */ + public @Nullable String getFamilyName() { + return familyName; + } + + /** + * Returns the given name of the signed in user if you built your configuration starting from + * {@code new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)} or with {@link GoogleSignInOptions.Builder#requestProfile()} configured; + * {@code null} otherwise. Not guaranteed to be present for all users, even when configured. + */ + public @Nullable String getGivenName() { + return givenName; + } + + /** + * Returns all scopes that have been authorized to your application. + *

+ * This can be a larger set than what you have requested via {@link GoogleSignInOptions}. We recommend apps requesting minimum scopes at user sign in time + * and later requesting additional scopes incrementally when user is using a certain feature. For those apps following this incremental auth practice, + * they can use the returned scope set to determine all authorized scopes (across platforms and app re-installs) to turn on bonus features accordingly. + * The returned set can also be larger due to other scope handling logic. + */ + public @NonNull Set getGrantedScopes() { + return new HashSet<>(grantedScopes); + } + + /** + * Returns the unique ID for the Google account if you built your configuration starting from + * {@code new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)} or with {@link GoogleSignInOptions.Builder#requestId()} configured; + * {@code null} otherwise. + *

+ * This is the preferred unique key to use for a user record. + *

+ * Important: Do not use this returned Google ID to communicate the currently signed in user to your backend server. Instead, send an ID token + * ({@link GoogleSignInOptions.Builder#requestIdToken(String)}), which can be securely validated on the server; or send a server auth code + * ({@link GoogleSignInOptions.Builder#requestServerAuthCode(String)}) which can be in turn exchanged for id token. + */ + public @Nullable String getId() { + return id; + } + + /** + * Returns an ID token that you can send to your server if {@link GoogleSignInOptions.Builder#requestIdToken(String)} was configured; {@code null} otherwise. + *

+ * ID token is a JSON Web Token signed by Google that can be used to identify a user to a backend. + */ + public @Nullable String getIdToken() { + return tokenId; + } + + /** + * Returns the photo url of the signed in user if the user has a profile picture and you built your configuration either starting from + * {@code new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)} or with {@link GoogleSignInOptions.Builder#requestProfile()} configured; + * {@code null} otherwise. Not guaranteed to be present for all users, even when configured. + */ + public @Nullable Uri getPhotoUrl() { + return photoUrl; + } + + /** + * Returns a one-time server auth code to send to your web server which can be exchanged for access token and sometimes refresh token if + * {@link GoogleSignInOptions.Builder#requestServerAuthCode(String)} is configured; {@code null} otherwise. + */ + public @Nullable String getServerAuthCode() { + return serverAuthCode; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (obj == null) return false; + if (obj == this) return true; + if (!(obj instanceof GoogleSignInAccount)) return false; + return ((GoogleSignInAccount) obj).obfuscatedIdentifier.equals(obfuscatedIdentifier) && ((GoogleSignInAccount) obj).getGrantedScopes().equals(getGrantedScopes()); + } + + @Override + public int hashCode() { + return (obfuscatedIdentifier.hashCode() + 527) * 31 + getGrantedScopes().hashCode(); + } + + public static final Creator CREATOR = new AutoCreator<>(GoogleSignInAccount.class); } diff --git a/play-services-base/src/main/java/com/google/android/gms/auth/api/signin/GoogleSignInOptions.java b/play-services-base/src/main/java/com/google/android/gms/auth/api/signin/GoogleSignInOptions.java new file mode 100644 index 0000000000..a16f141234 --- /dev/null +++ b/play-services-base/src/main/java/com/google/android/gms/auth/api/signin/GoogleSignInOptions.java @@ -0,0 +1,255 @@ +/* + * 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.auth.api.signin; + +import android.accounts.Account; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.android.gms.common.Scopes; +import com.google.android.gms.common.api.Scope; +import org.microg.gms.auth.AuthConstants; +import org.microg.safeparcel.AutoSafeParcelable; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +/** + * {@code GoogleSignInOptions} contains options used to configure the {@link Auth#GOOGLE_SIGN_IN_API}. + */ +public class GoogleSignInOptions extends AutoSafeParcelable { + /** + * Default and recommended configuration for Games Sign In. + *

    + *
  • If your app has a server, you can build a configuration via {@code new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_GAMES_SIGN_IN)} and + * further configure {@link GoogleSignInOptions.Builder#requestServerAuthCode(String)}.
  • + *
  • If you want to customize Games sign-in options, you can build a configuration via {@code new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_GAMES_SIGN_IN)} + * and further configure {@link Games.GamesOptions} via {@link GoogleSignInOptions.Builder#addExtension(GoogleSignInOptionsExtension)}.
  • + *
+ * To maximize chance of auto-sign-in, do NOT use {@link GoogleSignInOptions.Builder#requestScopes(Scope, Scope...)} to request additional scopes and do + * NOT use {@link GoogleSignInOptions.Builder#requestIdToken(String)} to request user's real Google identity assertion. + */ + @NonNull + public static final GoogleSignInOptions DEFAULT_GAMES_SIGN_IN = null; + + /** + * Default configuration for Google Sign In. You can get a stable user ID and basic profile info back via {@link GoogleSignInAccount#getId()} after you + * trigger sign in from either {@link GoogleSignInApi#silentSignIn} or {@link GoogleSignInApi#getSignInIntent}. If you require more information for the + * sign in result, please build a configuration via {@code new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)}. + */ + @NonNull + public static final GoogleSignInOptions DEFAULT_SIGN_IN = null; + + @Field(1) + private int versionCode = 3; + @Field(2) + private ArrayList scopes; + @Field(3) + private Account account; + @Field(4) + private boolean idTokenRequested; + @Field(5) + private boolean serverAuthCodeRequested; + @Field(6) + private boolean forceCodeForRefreshToken; + @Field(7) + private String serverClientId; + @Field(8) + private String hostedDomain; + // @Field(9) +// private ArrayList extensions; + @Field(10) + private String logSessionId; + + private GoogleSignInOptions() { + } + + /** + * Gets an array of all the requested scopes. If you use DEFAULT_SIGN_IN, this array will also include those scopes set by default in DEFAULT_SIGN_IN. + *

+ * A usage of this method could be set the scopes for the contextual SignInButton. E.g., {@code signInButton.setScopes(googleSignInOptions.getScopeArray())} + */ + @NonNull + public Scope[] getScopeArray() { + return null; + } + + /** + * Builder for {@link GoogleSignInOptions}. + */ + public static final class Builder { + private final Set scopes; + private boolean requestIdToken; + private boolean requestServerAuthCode; + private boolean forceCodeForRefreshToken; + @Nullable + private String serverClientId; + @Nullable + private Account account; + @Nullable + private String hostedDomain; + + public Builder() { + this.scopes = new HashSet<>(); + } + + public Builder(GoogleSignInOptions options) { + this.scopes = new HashSet<>(options.scopes); + this.requestIdToken = options.idTokenRequested; + this.requestServerAuthCode = options.serverAuthCodeRequested; + this.forceCodeForRefreshToken = options.forceCodeForRefreshToken; + this.serverClientId = options.serverClientId; + this.account = options.account; + this.hostedDomain = options.hostedDomain; + } + + /** + * Specifies additional sign-in options via the given extension. + * + * @param extension A sign-in extension used to further configure API specific sign-in options. Supported values include: {@link Games.GamesOptions}. + */ + @NonNull + public Builder addExtension(GoogleSignInOptionsExtension extension) { + return this; + } + + /** + * Specifies that email info is requested by your application. Note that we don't recommend keying user by email address since email address might + * change. Keying user by ID is the preferable approach. + */ + @NonNull + public Builder requestEmail() { + this.scopes.add(new Scope(Scopes.EMAIL)); + return this; + } + + /** + * Specifies that user ID is requested by your application. + */ + @NonNull + public Builder requestId() { + this.scopes.add(new Scope(Scopes.OPENID)); + return this; + } + + /** + * Specifies that an ID token for authenticated users is requested. Requesting an ID token requires that the server client ID be specified. + * + * @param serverClientId The client ID of the server that will verify the integrity of the token. + */ + @NonNull + public Builder requestIdToken(@NonNull String serverClientId) { + this.requestIdToken = true; + this.serverClientId = serverClientId; + return this; + } + + /** + * Specifies that user's profile info is requested by your application. + */ + @NonNull + public Builder requestProfile() { + this.scopes.add(new Scope(Scopes.PROFILE)); + return this; + } + + /** + * Specifies OAuth 2.0 scopes your application requests. See {@link Scopes} for more information. + * + * @param scope An OAuth 2.0 scope requested by your app. + * @param scopes More OAuth 2.0 scopes requested by your app. + */ + @NonNull + public Builder requestScopes(@NonNull Scope scope, @NonNull Scope... scopes) { + this.scopes.add(scope); + this.scopes.addAll(Arrays.asList(scopes)); + return this; + } + + /** + * Specifies that offline access is requested. Requesting offline access requires that the server client ID be specified. + *

+ * You don't need to use {@link #requestIdToken(String)} when you use this option. When your server exchanges the code for tokens, an ID token will be + * returned together (as long as you either use {@link #requestEmail()} or {@link #requestProfile()} along with your configuration). + *

+ * The first time you retrieve a code, a refresh_token will be granted automatically. Subsequent requests will only return codes that can be exchanged for access token. + * + * @param serverClientId The client ID of the server that will need the auth code. + */ + public Builder requestServerAuthCode(String serverClientId) { + return requestServerAuthCode(serverClientId, false); + } + + /** + * Specifies that offline access is requested. Requesting offline access requires that the server client ID be specified. + *

+ * You don't need to use {@link #requestIdToken(String)} when you use this option. When your server exchanges the code for tokens, an ID token will be + * returned together (as long as you either use {@link #requestEmail()} or {@link #requestProfile()} along with this configuration). + * + * @param serverClientId The client ID of the server that will need the auth code. + * @param forceCodeForRefreshToken If true, the granted code can be exchanged for an access token and a refresh token. The first time you retrieve a + * code, a refresh_token will be granted automatically. Subsequent requests will require additional user consent. Use + * false by default; only use true if your server has suffered some failure and lost the user's refresh token. + */ + public Builder requestServerAuthCode(String serverClientId, boolean forceCodeForRefreshToken) { + this.requestServerAuthCode = true; + this.forceCodeForRefreshToken = true; + this.serverClientId = serverClientId; + return this; + + } + + /** + * Specifies an account name on the device that should be used. If this is never called, the client will use the current default account for this application. + * + * @param accountName The account name on the device that should be used to sign in. + */ + public GoogleSignInOptions.Builder setAccountName(String accountName) { + this.account = new Account(accountName, AuthConstants.DEFAULT_ACCOUNT_TYPE); + return this; + } + + /** + * Specifies a hosted domain restriction. By setting this, sign in will be restricted to accounts of the user in the specified domain. + * + * @param hostedDomain domain of the user to restrict (for example, "mycollege.edu") + */ + public GoogleSignInOptions.Builder setHostedDomain(String hostedDomain) { + this.hostedDomain = hostedDomain; + return this; + } + + /** + * Builds the {@link GoogleSignInOptions} object. + * + * @return a {@link GoogleSignInOptions} instance. + */ + @NonNull + public GoogleSignInOptions build() { + GoogleSignInOptions options = new GoogleSignInOptions(); + if (scopes.contains(new Scope(Scopes.GAMES))) { + scopes.remove(new Scope(Scopes.GAMES_LITE)); + } + if (requestIdToken && (account == null || !scopes.isEmpty())) { + scopes.add(new Scope(Scopes.OPENID)); + } + options.scopes = new ArrayList<>(scopes); + options.idTokenRequested = requestIdToken; + options.serverAuthCodeRequested = requestServerAuthCode; + options.forceCodeForRefreshToken = forceCodeForRefreshToken; + options.serverClientId = serverClientId; + options.account = account; + options.hostedDomain = hostedDomain; + return options; + } + } + + public static final Creator CREATOR = new AutoCreator<>(GoogleSignInOptions.class); +} diff --git a/play-services-base/src/main/java/com/google/android/gms/auth/api/signin/GoogleSignInOptionsExtension.java b/play-services-base/src/main/java/com/google/android/gms/auth/api/signin/GoogleSignInOptionsExtension.java new file mode 100644 index 0000000000..82a648ba35 --- /dev/null +++ b/play-services-base/src/main/java/com/google/android/gms/auth/api/signin/GoogleSignInOptionsExtension.java @@ -0,0 +1,17 @@ +/* + * 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.auth.api.signin; + +/** + * An interface for API specific extension for {@link GoogleSignInOptions}. + * + * @see GoogleSignInOptions.Builder#addExtension(GoogleSignInOptionsExtension). + */ +public interface GoogleSignInOptionsExtension { +} diff --git a/play-services-base/src/main/java/com/google/android/gms/common/api/GoogleApi.java b/play-services-base/src/main/java/com/google/android/gms/common/api/GoogleApi.java index 455ba014bf..2cb5e78341 100644 --- a/play-services-base/src/main/java/com/google/android/gms/common/api/GoogleApi.java +++ b/play-services-base/src/main/java/com/google/android/gms/common/api/GoogleApi.java @@ -11,6 +11,7 @@ import com.google.android.gms.tasks.Task; import com.google.android.gms.tasks.TaskCompletionSource; +import org.microg.gms.common.Hide; import org.microg.gms.common.PublicApi; import org.microg.gms.common.api.ApiClient; import org.microg.gms.common.api.GoogleApiManager; @@ -19,8 +20,10 @@ @PublicApi public abstract class GoogleApi implements HasApiKey { private GoogleApiManager manager; - @PublicApi(exclude = true) + @Hide public Api api; + @Hide + public O options; @PublicApi(exclude = true) protected GoogleApi(Context context, Api api) { @@ -28,6 +31,13 @@ protected GoogleApi(Context context, Api api) { this.manager = GoogleApiManager.getInstance(context); } + @PublicApi(exclude = true) + protected GoogleApi(Context context, Api api, O options) { + this.api = api; + this.manager = GoogleApiManager.getInstance(context); + this.options = options; + } + @PublicApi(exclude = true) protected Task scheduleTask(PendingGoogleApiCall apiCall) { TaskCompletionSource completionSource = new TaskCompletionSource<>(); @@ -43,6 +53,6 @@ public ApiKey getApiKey() { @PublicApi(exclude = true) public O getOptions() { - return null; + return options; } } diff --git a/play-services-base/src/main/java/com/google/android/gms/common/data/DataHolder.java b/play-services-base/src/main/java/com/google/android/gms/common/data/DataHolder.java index 2867e4139d..79902d34c1 100644 --- a/play-services-base/src/main/java/com/google/android/gms/common/data/DataHolder.java +++ b/play-services-base/src/main/java/com/google/android/gms/common/data/DataHolder.java @@ -23,7 +23,6 @@ import android.database.Cursor; import android.database.CursorWindow; import android.net.Uri; -import android.os.Build; import android.os.Bundle; import android.os.Parcel; @@ -38,6 +37,8 @@ import java.util.List; import java.util.Map; +import static android.os.Build.VERSION.SDK_INT; + /** * Class for accessing collections of data, organized into columns. This provides the backing * support for DataBuffer. Much like a cursor, the holder supports the notion of a current @@ -155,7 +156,7 @@ public static DataHolder empty(int statusCode) { @SuppressWarnings("deprecation") @SuppressLint({"NewApi", "ObsoleteSdkInt"}) static int getCursorType(Cursor cursor, int i) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { + if (SDK_INT >= 11) { return cursor.getType(i); } if (cursor instanceof AbstractWindowedCursor) { diff --git a/play-services-base/src/main/java/com/google/android/gms/common/internal/AuthAccountRequest.java b/play-services-base/src/main/java/com/google/android/gms/common/internal/AuthAccountRequest.java new file mode 100644 index 0000000000..64a67b2c18 --- /dev/null +++ b/play-services-base/src/main/java/com/google/android/gms/common/internal/AuthAccountRequest.java @@ -0,0 +1,12 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.common.internal; + +import org.microg.safeparcel.AutoSafeParcelable; + +public class AuthAccountRequest extends AutoSafeParcelable { + public static final Creator CREATOR = new AutoCreator<>(AuthAccountRequest.class); +} diff --git a/play-services-base/src/main/java/com/google/android/gms/common/internal/ResolveAccountRequest.java b/play-services-base/src/main/java/com/google/android/gms/common/internal/ResolveAccountRequest.java new file mode 100644 index 0000000000..4b4d14d93e --- /dev/null +++ b/play-services-base/src/main/java/com/google/android/gms/common/internal/ResolveAccountRequest.java @@ -0,0 +1,39 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.common.internal; + +import android.accounts.Account; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.android.gms.auth.api.signin.GoogleSignInAccount; +import org.microg.gms.common.Hide; +import org.microg.gms.utils.ToStringHelper; +import org.microg.safeparcel.AutoSafeParcelable; + +@Hide +public class ResolveAccountRequest extends AutoSafeParcelable { + @Field(1) + private int versionCode = 2; + @Field(2) + public Account account; + @Field(3) + public int sessionId; + @Field(4) + @Nullable + public GoogleSignInAccount signInAccountHint; + + @NonNull + @Override + public String toString() { + return ToStringHelper.name("ResolveAccountRequest") + .field("account", account) + .field("sessionId", sessionId) + .field("signInAccountHint", signInAccountHint) + .end(); + } + + public static final Creator CREATOR = new AutoCreator<>(ResolveAccountRequest.class); +} diff --git a/play-services-base/src/main/java/com/google/android/gms/common/internal/ResolveAccountResponse.java b/play-services-base/src/main/java/com/google/android/gms/common/internal/ResolveAccountResponse.java new file mode 100644 index 0000000000..4ea42db2db --- /dev/null +++ b/play-services-base/src/main/java/com/google/android/gms/common/internal/ResolveAccountResponse.java @@ -0,0 +1,39 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.common.internal; + +import android.os.IBinder; +import androidx.annotation.NonNull; +import com.google.android.gms.common.ConnectionResult; +import org.microg.gms.common.Hide; +import org.microg.gms.utils.ToStringHelper; +import org.microg.safeparcel.AutoSafeParcelable; + +@Hide +public class ResolveAccountResponse extends AutoSafeParcelable { + @Field(1) + private int versionCode = 2; + @Field(2) + public IBinder accountAccessor; + @Field(3) + public ConnectionResult connectionResult; + @Field(4) + public boolean saveDefaultAccount; + @Field(5) + public boolean fromCrossClientAuth; + + @NonNull + @Override + public String toString() { + return ToStringHelper.name("ResolveAccountResponse") + .field("connectionResult", connectionResult) + .field("saveDefaultAccount", saveDefaultAccount) + .field("fromCrossClientAuth", fromCrossClientAuth) + .end(); + } + + public static final Creator CREATOR = new AutoCreator<>(ResolveAccountResponse.class); +} diff --git a/play-services-base/src/main/java/com/google/android/gms/dynamic/DeferredLifecycleHelper.java b/play-services-base/src/main/java/com/google/android/gms/dynamic/DeferredLifecycleHelper.java new file mode 100644 index 0000000000..644ec593e1 --- /dev/null +++ b/play-services-base/src/main/java/com/google/android/gms/dynamic/DeferredLifecycleHelper.java @@ -0,0 +1,166 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.dynamic; + +import android.app.Activity; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.LinkedList; + +public abstract class DeferredLifecycleHelper { + private T delegate; + private Bundle savedInstanceState; + private final LinkedList> pendingStateOperations = new LinkedList<>(); + private final OnDelegateCreatedListener listener = (delegate) -> { + DeferredLifecycleHelper.this.delegate = delegate; + for (PendingStateOperation op : pendingStateOperations) { + op.apply(delegate); + } + pendingStateOperations.clear(); + savedInstanceState = null; + }; + + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup parent, @Nullable Bundle savedInstanceState) { + FrameLayout rootLayout = new FrameLayout(inflater.getContext()); + startStateOperation(savedInstanceState, new PendingStateOperation(State.VIEW_CREATED) { + @Override + public void apply(T delegate) { + rootLayout.removeAllViews(); + rootLayout.addView(delegate.onCreateView(inflater, parent, savedInstanceState)); + } + }); + return rootLayout; + } + + public T getDelegate() { + return delegate; + } + + protected abstract void createDelegate(@NonNull OnDelegateCreatedListener listener); + + public void onCreate(@Nullable Bundle savedInstanceState) { + startStateOperation(savedInstanceState, new PendingStateOperation(State.CREATED) { + @Override + public void apply(T delegate) { + delegate.onCreate(savedInstanceState); + } + }); + } + + public void onDestroy() { + if (delegate != null) { + delegate.onDestroy(); + } else { + removePendingStateOperations(State.CREATED); + } + } + + public void onDestroyView() { + if (delegate != null) { + delegate.onDestroyView(); + } else { + removePendingStateOperations(State.VIEW_CREATED); + } + } + + public void onInflate(@NonNull Activity activity, @NonNull Bundle attrs, @Nullable Bundle savedInstanceState) { + startStateOperation(savedInstanceState, new PendingStateOperation(State.NONE) { + @Override + public void apply(T delegate) { + delegate.onInflate(activity, attrs, savedInstanceState); + } + }); + } + + public void onLowMemory() { + if (delegate != null) delegate.onLowMemory(); + } + + public void onPause() { + if (delegate != null) { + delegate.onPause(); + } else { + removePendingStateOperations(State.RESUMED); + } + } + + public void onResume() { + startStateOperation(savedInstanceState, new PendingStateOperation(State.RESUMED) { + @Override + public void apply(T delegate) { + delegate.onResume(); + } + }); + } + + public void onSaveInstanceState(@NonNull Bundle outState) { + if (delegate != null) { + delegate.onSaveInstanceState(outState); + } else if (savedInstanceState != null) { + outState.putAll(savedInstanceState); + } + } + + public void onStart() { + startStateOperation(savedInstanceState, new PendingStateOperation(State.STARTED) { + @Override + public void apply(T delegate) { + delegate.onStart(); + } + }); + } + + public void onStop() { + if (delegate != null) { + delegate.onStop(); + } else { + removePendingStateOperations(State.STARTED); + } + } + + private void removePendingStateOperations(State state) { + while (!pendingStateOperations.isEmpty() && pendingStateOperations.getLast().state.isAtLeast(state)) { + pendingStateOperations.removeLast(); + } + } + + private void startStateOperation(@Nullable Bundle savedInstanceState, PendingStateOperation op) { + if (delegate != null) { + op.apply(delegate); + } else { + pendingStateOperations.add(op); + if (savedInstanceState != null) { + if (this.savedInstanceState == null) this.savedInstanceState = new Bundle(); + this.savedInstanceState.putAll(savedInstanceState); + } + createDelegate(listener); + } + } + + private static abstract class PendingStateOperation { + public final State state; + + public PendingStateOperation(State state) { + this.state = state; + } + + public abstract void apply(T delegate); + } + + private enum State { + NONE, CREATED, VIEW_CREATED, STARTED, RESUMED; + + public boolean isAtLeast(@NonNull State state) { + return compareTo(state) >= 0; + } + } +} diff --git a/play-services-base/src/main/java/com/google/android/gms/signin/internal/CheckServerAuthResult.java b/play-services-base/src/main/java/com/google/android/gms/signin/internal/CheckServerAuthResult.java new file mode 100644 index 0000000000..39b4b762de --- /dev/null +++ b/play-services-base/src/main/java/com/google/android/gms/signin/internal/CheckServerAuthResult.java @@ -0,0 +1,12 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.signin.internal; + +import org.microg.safeparcel.AutoSafeParcelable; + +public class CheckServerAuthResult extends AutoSafeParcelable { + public static final Creator CREATOR = new AutoCreator<>(CheckServerAuthResult.class); +} diff --git a/play-services-base/src/main/java/com/google/android/gms/signin/internal/RecordConsentByConsentResultRequest.java b/play-services-base/src/main/java/com/google/android/gms/signin/internal/RecordConsentByConsentResultRequest.java new file mode 100644 index 0000000000..6bbd2b458c --- /dev/null +++ b/play-services-base/src/main/java/com/google/android/gms/signin/internal/RecordConsentByConsentResultRequest.java @@ -0,0 +1,12 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.signin.internal; + +import org.microg.safeparcel.AutoSafeParcelable; + +public class RecordConsentByConsentResultRequest extends AutoSafeParcelable { + public static final Creator CREATOR = new AutoCreator<>(RecordConsentByConsentResultRequest.class); +} diff --git a/play-services-base/src/main/java/com/google/android/gms/signin/internal/RecordConsentRequest.java b/play-services-base/src/main/java/com/google/android/gms/signin/internal/RecordConsentRequest.java new file mode 100644 index 0000000000..39d650927b --- /dev/null +++ b/play-services-base/src/main/java/com/google/android/gms/signin/internal/RecordConsentRequest.java @@ -0,0 +1,12 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.signin.internal; + +import org.microg.safeparcel.AutoSafeParcelable; + +public class RecordConsentRequest extends AutoSafeParcelable { + public static final Creator CREATOR = new AutoCreator<>(RecordConsentRequest.class); +} diff --git a/play-services-base/src/main/java/com/google/android/gms/signin/internal/SignInRequest.java b/play-services-base/src/main/java/com/google/android/gms/signin/internal/SignInRequest.java new file mode 100644 index 0000000000..1b091a5eff --- /dev/null +++ b/play-services-base/src/main/java/com/google/android/gms/signin/internal/SignInRequest.java @@ -0,0 +1,30 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.signin.internal; + +import androidx.annotation.NonNull; +import com.google.android.gms.common.internal.ResolveAccountRequest; +import org.microg.gms.common.Hide; +import org.microg.gms.utils.ToStringHelper; +import org.microg.safeparcel.AutoSafeParcelable; + +@Hide +public class SignInRequest extends AutoSafeParcelable { + @Field(1) + private final int versionCode = 1; + @Field(2) + public ResolveAccountRequest request; + + @NonNull + @Override + public String toString() { + return ToStringHelper.name("SignInRequest") + .field("request", request) + .end(); + } + + public static final Creator CREATOR = new AutoCreator<>(SignInRequest.class); +} diff --git a/play-services-base/src/main/java/com/google/android/gms/signin/internal/SignInResponse.java b/play-services-base/src/main/java/com/google/android/gms/signin/internal/SignInResponse.java new file mode 100644 index 0000000000..7cfac8ff20 --- /dev/null +++ b/play-services-base/src/main/java/com/google/android/gms/signin/internal/SignInResponse.java @@ -0,0 +1,34 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.signin.internal; + +import androidx.annotation.NonNull; +import com.google.android.gms.common.ConnectionResult; +import com.google.android.gms.common.internal.ResolveAccountResponse; +import org.microg.gms.common.Hide; +import org.microg.gms.utils.ToStringHelper; +import org.microg.safeparcel.AutoSafeParcelable; + +@Hide +public class SignInResponse extends AutoSafeParcelable { + @Field(1) + private final int versionCode = 1; + @Field(2) + public ConnectionResult connectionResult; + @Field(3) + public ResolveAccountResponse response; + + @NonNull + @Override + public String toString() { + return ToStringHelper.name("SignInResponse") + .field("connectionResult", connectionResult) + .field("response", response) + .end(); + } + + public static final Creator CREATOR = new AutoCreator<>(SignInResponse.class); +} diff --git a/play-services-base/src/main/java/org/microg/gms/common/GmsClient.java b/play-services-base/src/main/java/org/microg/gms/common/GmsClient.java index 19873e8040..25ebe6cd33 100644 --- a/play-services-base/src/main/java/org/microg/gms/common/GmsClient.java +++ b/play-services-base/src/main/java/org/microg/gms/common/GmsClient.java @@ -47,22 +47,21 @@ public abstract class GmsClient implements ApiClient { private ServiceConnection serviceConnection; private I serviceInterface; private final String actionString; - private final boolean requireMicrog; + + protected boolean requireMicrog; + protected String packageName; protected int serviceId = -1; protected Account account = null; protected Bundle extras = new Bundle(); public GmsClient(Context context, ConnectionCallbacks callbacks, OnConnectionFailedListener connectionFailedListener, String actionString) { - this(context, callbacks, connectionFailedListener, actionString, false); - } - - public GmsClient(Context context, ConnectionCallbacks callbacks, OnConnectionFailedListener connectionFailedListener, String actionString, boolean requireMicrog) { this.context = context; this.callbacks = callbacks; this.connectionFailedListener = connectionFailedListener; this.actionString = actionString; - this.requireMicrog = requireMicrog; + this.requireMicrog = false; + this.packageName = context.getPackageName(); } protected void onConnectedToBroker(IGmsServiceBroker broker, GmsCallbacks callbacks) throws RemoteException { diff --git a/play-services-base/src/main/java/org/microg/gms/common/MultiConnectionKeeper.java b/play-services-base/src/main/java/org/microg/gms/common/MultiConnectionKeeper.java index 68cd6d426d..30c1d61c5f 100644 --- a/play-services-base/src/main/java/org/microg/gms/common/MultiConnectionKeeper.java +++ b/play-services-base/src/main/java/org/microg/gms/common/MultiConnectionKeeper.java @@ -51,7 +51,7 @@ public MultiConnectionKeeper(Context context) { public synchronized static MultiConnectionKeeper getInstance(Context context) { if (INSTANCE == null) - INSTANCE = new MultiConnectionKeeper(context); + INSTANCE = new MultiConnectionKeeper(context.getApplicationContext()); return INSTANCE; } @@ -60,8 +60,8 @@ public synchronized boolean bind(String action, ServiceConnection connection) { } public synchronized boolean bind(String action, ServiceConnection connection, boolean requireMicrog) { - Log.d(TAG, "bind(" + action + ", " + connection + ", " + requireMicrog + ")"); Connection con = connections.get(action); + Log.d(TAG, "bind(" + action + ", " + connection + ", " + requireMicrog + ") has=" + (con != null)); if (con != null) { if (!con.forwardsConnection(connection)) { con.addConnectionForward(connection); @@ -74,6 +74,7 @@ public synchronized boolean bind(String action, ServiceConnection connection, bo con.bind(); connections.put(action, con); } + Log.d(TAG, "bind() : bound=" + con.isBound()); return con.isBound(); } @@ -82,9 +83,13 @@ public synchronized void unbind(String action, ServiceConnection connection) { Connection con = connections.get(action); if (con != null) { con.removeConnectionForward(connection); - if (!con.hasForwards() && con.isBound()) { - con.unbind(); - connections.remove(action); + if (con.isBound()) { + if (!con.hasForwards()) { + con.unbind(); + connections.remove(action); + } else { + Log.d(TAG, "Not unbinding for " + connection + ": has pending other bindings on action " + action); + } } } } @@ -159,11 +164,12 @@ public void bind() { } else { intent = gmsIntent; } - int flags = Context.BIND_AUTO_CREATE; + int flags = Context.BIND_AUTO_CREATE | Context.BIND_DEBUG_UNBIND; if (SDK_INT >= ICE_CREAM_SANDWICH) { flags |= Context.BIND_ADJUST_WITH_ACTIVITY; } bound = context.bindService(intent, serviceConnection, flags); + Log.d(TAG, "Connection(" + actionString + ") : bind() : bindService=" + bound); if (!bound) { context.unbindService(serviceConnection); } diff --git a/play-services-basement/build.gradle b/play-services-basement/build.gradle index 1c1d7cb08b..261478b288 100644 --- a/play-services-basement/build.gradle +++ b/play-services-basement/build.gradle @@ -33,6 +33,7 @@ android { buildToolsVersion "$androidBuildVersionTools" aidlPackageWhiteList "com/google/android/gms/common/api/Status.aidl" + aidlPackageWhiteList "com/google/android/gms/common/internal/IAccountAccessor.aidl" aidlPackageWhiteList "com/google/android/gms/common/internal/ICancelToken.aidl" aidlPackageWhiteList "com/google/android/gms/common/server/FavaDiagnosticsEntity.aidl" aidlPackageWhiteList "com/google/android/gms/dynamic/IObjectWrapper.aidl" diff --git a/play-services-basement/src/main/aidl/com/google/android/gms/common/internal/IAccountAccessor.aidl b/play-services-basement/src/main/aidl/com/google/android/gms/common/internal/IAccountAccessor.aidl new file mode 100644 index 0000000000..28aebf7c6c --- /dev/null +++ b/play-services-basement/src/main/aidl/com/google/android/gms/common/internal/IAccountAccessor.aidl @@ -0,0 +1,5 @@ +package com.google.android.gms.common.internal; + +interface IAccountAccessor { + Account getAccount() = 1; +} \ No newline at end of file diff --git a/play-services-basement/src/main/aidl/com/google/android/gms/common/internal/IGoogleCertificatesApi.aidl b/play-services-basement/src/main/aidl/com/google/android/gms/common/internal/IGoogleCertificatesApi.aidl index da539f329e..c75982bb6e 100644 --- a/play-services-basement/src/main/aidl/com/google/android/gms/common/internal/IGoogleCertificatesApi.aidl +++ b/play-services-basement/src/main/aidl/com/google/android/gms/common/internal/IGoogleCertificatesApi.aidl @@ -4,7 +4,7 @@ import com.google.android.gms.common.internal.GoogleCertificatesQuery; import com.google.android.gms.dynamic.IObjectWrapper; interface IGoogleCertificatesApi { - IObjectWrapper getGoogleCertficates(); + IObjectWrapper getGoogleCertificates(); IObjectWrapper getGoogleReleaseCertificates(); boolean isGoogleReleaseSigned(String packageName, IObjectWrapper certData); boolean isGoogleSigned(String packageName, IObjectWrapper certData); diff --git a/play-services-basement/src/main/aidl/com/google/android/gms/dynamite/IDynamiteLoader.aidl b/play-services-basement/src/main/aidl/com/google/android/gms/dynamite/IDynamiteLoader.aidl index 017a2dc276..c6cc56e9c1 100644 --- a/play-services-basement/src/main/aidl/com/google/android/gms/dynamite/IDynamiteLoader.aidl +++ b/play-services-basement/src/main/aidl/com/google/android/gms/dynamite/IDynamiteLoader.aidl @@ -5,10 +5,12 @@ import com.google.android.gms.dynamic.IObjectWrapper; interface IDynamiteLoader { int getModuleVersion(IObjectWrapper wrappedContext, String moduleId) = 0; int getModuleVersion2(IObjectWrapper wrappedContext, String moduleId, boolean updateConfigIfRequired) = 2; - int getModuleVersion2NoCrashUtils(IObjectWrapper wrappedContext, String moduleId, boolean updateConfigIfRequired) = 4; + int getModuleVersionV2(IObjectWrapper wrappedContext, String moduleId, boolean updateConfigIfRequired) = 4; + IObjectWrapper getModuleVersionV3(IObjectWrapper wrappedContext, String moduleId, boolean updateConfigIfRequired, long requestStartTime) = 6; IObjectWrapper createModuleContext(IObjectWrapper wrappedContext, String moduleId, int minVersion) = 1; - IObjectWrapper createModuleContextNoCrashUtils(IObjectWrapper wrappedContext, String moduleId, int minVersion) = 3; + IObjectWrapper createModuleContextV2(IObjectWrapper wrappedContext, String moduleId, int minVersion) = 3; + IObjectWrapper createModuleContextV3(IObjectWrapper wrappedContext, String moduleId, int minVersion, IObjectWrapper cursorWrapped) = 7; int getIDynamiteLoaderVersion() = 5; } diff --git a/play-services-basement/src/main/java/com/google/android/gms/common/ConnectionResult.java b/play-services-basement/src/main/java/com/google/android/gms/common/ConnectionResult.java index f3272de847..5177385b5c 100644 --- a/play-services-basement/src/main/java/com/google/android/gms/common/ConnectionResult.java +++ b/play-services-basement/src/main/java/com/google/android/gms/common/ConnectionResult.java @@ -1,17 +1,9 @@ /* - * Copyright (C) 2013-2017 microG Project Team - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * SPDX-FileCopyrightText: 2016 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.common; @@ -21,6 +13,7 @@ import android.content.Intent; import android.content.IntentSender; import android.text.TextUtils; +import org.microg.safeparcel.AutoSafeParcelable; import java.util.Arrays; @@ -28,7 +21,7 @@ * Contains all possible error codes for when a client fails to connect to Google Play services. * These error codes are used by {@link GoogleApiClient.OnConnectionFailedListener}. */ -public class ConnectionResult { +public class ConnectionResult extends AutoSafeParcelable { /** * The connection was successful. */ @@ -115,11 +108,41 @@ public class ConnectionResult { * Using the API on the device should be avoided. */ public static final int API_UNAVAILABLE = 16; - + /** + * The client attempted to connect to the service but the user is not signed in. An error may have occurred when signing in the user and the error can not + * be recovered with any user interaction. Alternately, the API may have been requested with {@link GoogleApiClient.Builder#addApiIfAvailable(Api, Scope...)} + * and it may be the case that no required APIs needed authentication, so authentication did not occur. + *

+ * When seeing this error code, there is no resolution for the sign-in failure. + */ + public static final int SIGN_IN_FAILED = 17; + /** + * Google Play service is currently being updated on this device. + */ + public static final int SERVICE_UPDATING = 18; + /** * Service doesn't have one or more required permissions. */ public static final int SERVICE_MISSING_PERMISSION = 19; + /** + * The current user profile is restricted and cannot use authenticated features. (Jelly Bean MR2+ Restricted Profiles for Android tablets) + */ + public static final int RESTRICTED_PROFILE = 20; + /** + * There was a user-resolvable issue connecting to Google Play services, but when attempting to start the resolution, the activity was not found. + *

+ * This can occur when attempting to resolve issues connecting to Google Play services on emulators with Google APIs but not Google Play Store. + */ + public static final int RESOLUTION_ACTIVITY_NOT_FOUND = 22; + /** + * The API being requested is disabled on this device for this application. Trying again at a later time may succeed. + */ + public static final int API_DISABLED = 23; + /** + * The API being requested is disabled for this connection attempt, but may work for other connections. + */ + public static final int API_DISABLED_FOR_CONNECTION = 24; /** * The Drive API requires external storage (such as an SD card), but no external storage is @@ -134,9 +157,17 @@ public class ConnectionResult { @Deprecated public static final int DRIVE_EXTERNAL_STORAGE_REQUIRED = 1500; - private final int statusCode; - private final PendingIntent pendingIntent; - private final String message; + @Field(1) + private final int versionCode = 1; + @Field(2) + private int statusCode; + @Field(3) + private PendingIntent resolution; + @Field(4) + private String message; + + private ConnectionResult() { + } /** * Creates a connection result. @@ -150,23 +181,23 @@ public ConnectionResult(int statusCode) { /** * Creates a connection result. * - * @param statusCode The status code. - * @param pendingIntent A pending intent that will resolve the issue when started, or null. + * @param statusCode The status code. + * @param resolution A pending intent that will resolve the issue when started, or null. */ - public ConnectionResult(int statusCode, PendingIntent pendingIntent) { - this(statusCode, pendingIntent, getStatusString(statusCode)); + public ConnectionResult(int statusCode, PendingIntent resolution) { + this(statusCode, resolution, getStatusString(statusCode)); } /** * Creates a connection result. * - * @param statusCode The status code. - * @param pendingIntent A pending intent that will resolve the issue when started, or null. - * @param message An additional error message for the connection result, or null. + * @param statusCode The status code. + * @param resolution A pending intent that will resolve the issue when started, or null. + * @param message An additional error message for the connection result, or null. */ - public ConnectionResult(int statusCode, PendingIntent pendingIntent, String message) { + public ConnectionResult(int statusCode, PendingIntent resolution, String message) { this.statusCode = statusCode; - this.pendingIntent = pendingIntent; + this.resolution = resolution; this.message = message; } @@ -234,8 +265,8 @@ public boolean equals(Object o) { } else if (!(o instanceof ConnectionResult)) { return false; } else { - ConnectionResult r = (ConnectionResult)o; - return statusCode == r.statusCode && pendingIntent == null ? r.pendingIntent == null : pendingIntent.equals(r.pendingIntent) && TextUtils.equals(message, r.message); + ConnectionResult r = (ConnectionResult) o; + return statusCode == r.statusCode && resolution == null ? r.resolution == null : resolution.equals(r.resolution) && TextUtils.equals(message, r.message); } } @@ -265,12 +296,12 @@ public String getErrorMessage() { * @return The pending intent to resolve the connection failure. */ public PendingIntent getResolution() { - return pendingIntent; + return resolution; } @Override public int hashCode() { - return Arrays.hashCode(new Object[]{statusCode, pendingIntent, message}); + return Arrays.hashCode(new Object[]{statusCode, resolution, message}); } /** @@ -280,7 +311,7 @@ public int hashCode() { * @return {@code true} if there is a resolution that can be started. */ public boolean hasResolution() { - return statusCode != 0 && pendingIntent != null; + return statusCode != 0 && resolution != null; } /** @@ -307,7 +338,9 @@ public boolean isSuccess() { public void startResolutionForResult(Activity activity, int requestCode) throws IntentSender.SendIntentException { if (hasResolution()) { - activity.startIntentSenderForResult(pendingIntent.getIntentSender(), requestCode, null, 0, 0, 0); + activity.startIntentSenderForResult(resolution.getIntentSender(), requestCode, null, 0, 0, 0); } } + + public static final Creator CREATOR = new AutoCreator<>(ConnectionResult.class); } diff --git a/play-services-basement/src/main/java/com/google/android/gms/common/GooglePlayServicesNotAvailableException.java b/play-services-basement/src/main/java/com/google/android/gms/common/GooglePlayServicesNotAvailableException.java new file mode 100644 index 0000000000..3d4c3ca78d --- /dev/null +++ b/play-services-basement/src/main/java/com/google/android/gms/common/GooglePlayServicesNotAvailableException.java @@ -0,0 +1,23 @@ +/* + * 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.common; + +/** + * Indicates Google Play services is not available. + */ +public class GooglePlayServicesNotAvailableException extends Exception { + /** + * The error code returned by {@link GoogleApiAvailability#isGooglePlayServicesAvailable(Context)} call. + */ + public final int errorCode; + + public GooglePlayServicesNotAvailableException(int errorCode) { + this.errorCode = errorCode; + } +} diff --git a/play-services-basement/src/main/java/com/google/android/gms/common/GooglePlayServicesRepairableException.java b/play-services-basement/src/main/java/com/google/android/gms/common/GooglePlayServicesRepairableException.java new file mode 100644 index 0000000000..f10ffddd46 --- /dev/null +++ b/play-services-basement/src/main/java/com/google/android/gms/common/GooglePlayServicesRepairableException.java @@ -0,0 +1,44 @@ +/* + * 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.common; + +import android.app.Dialog; +import android.content.Intent; + +/** + * {@code GooglePlayServicesRepairableException}s are special instances of {@link UserRecoverableException}s which are + * thrown when Google Play services is not installed, up-to-date, or enabled. In these cases, client code can use + * {@link #getConnectionStatusCode()} in conjunction with {@link GoogleApiAvailability#getErrorDialog(android.app.Activity, int, int)} + * to provide users with a localized {@link Dialog} that will allow users to install, update, or otherwise enable Google Play services. + */ +public class GooglePlayServicesRepairableException extends UserRecoverableException { + private final int connectionStatusCode; + + /** + * Creates a {@link GooglePlayServicesRepairableException}. + * + * @param connectionStatusCode a code for the {@link ConnectionResult} {@code statusCode} of the exception + * @param message a string message for the exception + * @param intent an intent that may be started to resolve the connection issue with Google Play services + */ + public GooglePlayServicesRepairableException(int connectionStatusCode, String message, Intent intent) { + super(message, intent); + this.connectionStatusCode = connectionStatusCode; + } + + /** + * Returns the {@link ConnectionResult} {@code statusCode} of the exception. + *

+ * This value may be passed in to {@link GoogleApiAvailability#getErrorDialog(android.app.Activity, int, int)} to + * provide users with a localized {@link Dialog} that will allow users to install, update, or otherwise enable Google Play services. + */ + public int getConnectionStatusCode() { + return connectionStatusCode; + } +} diff --git a/play-services-basement/src/main/java/com/google/android/gms/common/Scopes.java b/play-services-basement/src/main/java/com/google/android/gms/common/Scopes.java index a3dfbf5f57..76a4cd2917 100644 --- a/play-services-basement/src/main/java/com/google/android/gms/common/Scopes.java +++ b/play-services-basement/src/main/java/com/google/android/gms/common/Scopes.java @@ -1,34 +1,91 @@ /* - * Copyright (C) 2013-2017 microG Project Team - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * SPDX-FileCopyrightText: 2015 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.common; +import org.microg.gms.common.Hide; + +/** + * OAuth 2.0 scopes for use with Google Play services. See the specific client methods for details on which scopes are required. + */ public class Scopes { + /** + * OAuth 2.0 scope for viewing a user's basic profile information. + */ public static final String PROFILE = "profile"; + @Hide + public static final String OPENID = "openid"; + /** + * OAuth 2.0 scope for accessing user's Google account email address. + */ + public static final String EMAIL = "email"; + /** + * OAuth 2.0 scope for accessing the user's name, basic profile info and Google+ profile info. + *

+ * When using this scope, your app will have access to: + *

    + *
  • the user's full name, profile picture, Google+ profile ID, age range, and language
  • + *
  • any other publicly available information on the user's Google+ profile
  • + *
+ * + * @deprecated We recommend switching to {@link #PROFILE} scope to get the one-tap sign-in experience. Your app will get much higher sign-in completion + * rate by switching to profile scopes because of the streamlined user experience. And your existing users with PLUS_LOGIN grant will not be asked to + * sign-in again. + * If you really need user's age range and locale information (which is the only additional information you can get from PLUS_LOGIN as of + * September 2016), use below scopes in addition to PROFILE:
    + *
  • www.googleapis.com/auth/profile.agerange.read
  • + *
  • www.googleapis.com/auth/profile.language.read
  • + *
+ */ + @Deprecated public static final String PLUS_LOGIN = "https://www.googleapis.com/auth/plus.login"; + /** + * This scope was previously named PLUS_PROFILE. + *

+ * When using this scope, it does the following: + *

    + *
  • It lets you know who the currently authenticated user is by letting you replace a Google+ user ID with "me", which represents the authenticated + * user, in any call to the Google+ API.
  • + *
+ */ public static final String PLUS_ME = "https://www.googleapis.com/auth/plus.me"; + /** + * Scope for accessing data from Google Play Games. + */ public static final String GAMES = "https://www.googleapis.com/auth/games"; + @Hide + public static final String GAMES_LITE = "https://www.googleapis.com/auth/games_lite"; + /** + * Scope for using the CloudSave service. + */ public static final String CLOUD_SAVE = "https://www.googleapis.com/auth/datastoremobile"; + /** + * Scope for using the App State service. + */ public static final String APP_STATE = "https://www.googleapis.com/auth/appstate"; + /** + * Scope for access user-authorized files from Google Drive. + */ public static final String DRIVE_FILE = "https://www.googleapis.com/auth/drive.file"; + /** + * Scope for accessing appfolder files from Google Drive. + */ public static final String DRIVE_APPFOLDER = "https://www.googleapis.com/auth/drive.appdata"; + @Hide public static final String FITNESS_ACTIVITY_READ = "https://www.googleapis.com/auth/fitness.activity.read"; + @Hide public static final String FITNESS_ACTIVITY_READ_WRITE = "https://www.googleapis.com/auth/fitness.activity.write"; + @Hide public static final String FITNESS_LOCATION_READ = "https://www.googleapis.com/auth/fitness.location.read"; + @Hide public static final String FITNESS_LOCATION_READ_WRITE = "https://www.googleapis.com/auth/fitness.location.write"; + @Hide public static final String FITNESS_BODY_READ = "https://www.googleapis.com/auth/fitness.body.read"; + @Hide public static final String FITNESS_BODY_READ_WRITE = "https://www.googleapis.com/auth/fitness.body.write"; } diff --git a/play-services-basement/src/main/java/com/google/android/gms/common/UserRecoverableException.java b/play-services-basement/src/main/java/com/google/android/gms/common/UserRecoverableException.java new file mode 100644 index 0000000000..6eaafad7da --- /dev/null +++ b/play-services-basement/src/main/java/com/google/android/gms/common/UserRecoverableException.java @@ -0,0 +1,32 @@ +/* + * 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.common; + +import android.app.Activity; +import android.content.Intent; + +/** + * UserRecoverableExceptions signal errors that can be recovered with user action, such as a user login. + */ +public class UserRecoverableException extends Exception { + private final Intent intent; + + public UserRecoverableException(String message, Intent intent) { + super(message); + this.intent = intent; + } + + /** + * Getter for an {@link Intent} that when supplied to {@link Activity#startActivityForResult(Intent, int)}, will allow user intervention. + * @return Intent representing the ameliorating user action. + */ + public Intent getIntent() { + return intent; + } +} diff --git a/play-services-basement/src/main/java/com/google/android/gms/dynamic/LifecycleDelegate.java b/play-services-basement/src/main/java/com/google/android/gms/dynamic/LifecycleDelegate.java new file mode 100644 index 0000000000..2246d6edd6 --- /dev/null +++ b/play-services-basement/src/main/java/com/google/android/gms/dynamic/LifecycleDelegate.java @@ -0,0 +1,38 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.dynamic; + +import android.app.Activity; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +public interface LifecycleDelegate { + View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup parent, @Nullable Bundle savedInstanceState); + + void onCreate(@Nullable Bundle savedInstanceState); + + void onDestroy(); + + void onDestroyView(); + + void onInflate(@NonNull Activity activity, @NonNull Bundle options, @Nullable Bundle onInflate); + + void onLowMemory(); + + void onPause(); + + void onResume(); + + void onSaveInstanceState(@NonNull Bundle outState); + + void onStart(); + + void onStop(); +} diff --git a/play-services-basement/src/main/java/com/google/android/gms/dynamic/OnDelegateCreatedListener.java b/play-services-basement/src/main/java/com/google/android/gms/dynamic/OnDelegateCreatedListener.java new file mode 100644 index 0000000000..4b792c9d16 --- /dev/null +++ b/play-services-basement/src/main/java/com/google/android/gms/dynamic/OnDelegateCreatedListener.java @@ -0,0 +1,10 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.dynamic; + +public interface OnDelegateCreatedListener { + void onDelegateCreated(T delegate); +} diff --git a/play-services-basement/src/main/java/com/google/android/gms/dynamite/DynamiteModule.java b/play-services-basement/src/main/java/com/google/android/gms/dynamite/DynamiteModule.java index 3d61f66409..70d45e5463 100644 --- a/play-services-basement/src/main/java/com/google/android/gms/dynamite/DynamiteModule.java +++ b/play-services-basement/src/main/java/com/google/android/gms/dynamite/DynamiteModule.java @@ -7,16 +7,77 @@ import android.content.Context; import android.os.IBinder; +import android.os.RemoteException; +import android.util.Log; import androidx.annotation.NonNull; +import java.lang.reflect.Field; +import java.util.Objects; + public class DynamiteModule { + private static final String TAG = "DynamiteModule"; + + public static final int NONE = 0; + public static final int LOCAL = -1; + public static final int REMOTE = 1; + @NonNull - public static final VersionPolicy PREFER_REMOTE = null; + public static final VersionPolicy PREFER_REMOTE = (context, moduleId, versions) -> { + VersionPolicy.SelectionResult result = new VersionPolicy.SelectionResult(); + result.remoteVersion = versions.getRemoteVersion(context, moduleId, true); + if (result.remoteVersion != 0) { + result.selection = REMOTE; + } else { + result.localVersion = versions.getLocalVersion(context, moduleId); + if (result.localVersion != 0) { + result.selection = LOCAL; + } + } + return result; + }; @NonNull - public static final VersionPolicy PREFER_LOCAL = null; + public static final VersionPolicy PREFER_LOCAL = (context, moduleId, versions) -> { + VersionPolicy.SelectionResult result = new VersionPolicy.SelectionResult(); + result.localVersion = versions.getLocalVersion(context, moduleId); + if (result.localVersion != 0) { + result.selection = LOCAL; + } else { + result.remoteVersion = versions.getRemoteVersion(context, moduleId, true); + if (result.remoteVersion != 0) { + result.selection = REMOTE; + } + } + return result; + }; public interface VersionPolicy { + interface IVersions { + /* renamed from: zza */ + int getLocalVersion(@NonNull Context context, @NonNull String moduleId); + + /* renamed from: zzb */ + int getRemoteVersion(@NonNull Context context, @NonNull String moduleId, boolean forceStaging) throws LoadingException; + IVersions Default = new IVersions() { + @Override + public int getLocalVersion(@NonNull Context context, @NonNull String moduleId) { + return DynamiteModule.getLocalVersion(context, moduleId); + } + + @Override + public int getRemoteVersion(@NonNull Context context, @NonNull String moduleId, boolean forceStaging) throws LoadingException { + return DynamiteModule.getRemoteVersion(context, moduleId, forceStaging); + } + }; + } + + class SelectionResult { + public int localVersion = 0; + public int remoteVersion = 0; + public int selection = NONE; + } + + SelectionResult selectModule(@NonNull Context context, @NonNull String moduleId, @NonNull IVersions versions) throws LoadingException; } public static class LoadingException extends Exception { @@ -29,21 +90,74 @@ public LoadingException(String message, Throwable cause) { } } - private Context remoteContext; + private Context moduleContext; - private DynamiteModule(Context remoteContext) { - this.remoteContext = remoteContext; + private DynamiteModule(Context moduleContext) { + this.moduleContext = moduleContext; + } + + public Context getModuleContext() { + return moduleContext; + } + + public static int getLocalVersion(@NonNull Context context, @NonNull String moduleId) { + try { + ClassLoader classLoader = context.getApplicationContext().getClassLoader(); + Class clazz = classLoader.loadClass("com.google.android.gms.dynamite.descriptors." + moduleId + ".ModuleDescriptor"); + Field moduleIdField = clazz.getDeclaredField("MODULE_ID"); + Field moduleVersionField = clazz.getDeclaredField("MODULE_VERSION"); + if (!Objects.equals(moduleIdField.get(null), moduleId)) { + Log.e(TAG, "Module descriptor id '" + moduleIdField.get(null) + "' didn't match expected id '" + moduleId + "'"); + return 0; + } + return moduleVersionField.getInt(null); + } catch (ClassNotFoundException e) { + Log.w(TAG, "Local module descriptor class for" + moduleId + " not found."); + return 0; + } catch (Exception e) { + Log.e(TAG, "Failed to load module descriptor class.", e); + return 0; + } + } + + public static int getRemoteVersion(@NonNull Context context, @NonNull String moduleId) { + return getRemoteVersion(context, moduleId, false); + } + + public static int getRemoteVersion(@NonNull Context context, @NonNull String moduleId, boolean forceStaging) { + Log.e(TAG, "Remote modules not yet supported"); + return 0; } @NonNull public static DynamiteModule load(@NonNull Context context, @NonNull VersionPolicy policy, @NonNull String moduleId) throws LoadingException { - throw new LoadingException("Not yet supported"); + Context applicationContext = context.getApplicationContext(); + if (applicationContext == null) throw new LoadingException("null application Context", null); + try { + VersionPolicy.SelectionResult result = policy.selectModule(context, moduleId, VersionPolicy.IVersions.Default); + Log.i(TAG, "Considering local module " + moduleId + ":" + result.localVersion + " and remote module " + moduleId + ":" + result.remoteVersion); + switch (result.selection) { + case NONE: + throw new LoadingException("No acceptable module " + moduleId + " found. Local version is " + result.localVersion + " and remote version is " + result.remoteVersion + "."); + case LOCAL: + Log.i(TAG, "Selected local version of " + moduleId); + return new DynamiteModule(context); + case REMOTE: + throw new UnsupportedOperationException(); + default: + throw new LoadingException("VersionPolicy returned invalid code:" + result.selection); + } + } catch (LoadingException loadingException) { + throw loadingException; + } catch (Throwable e) { + throw new LoadingException("Failed to load remote module.", e); + } } @NonNull public IBinder instantiate(@NonNull String className) throws LoadingException { try { - return (IBinder) this.remoteContext.getClassLoader().loadClass(className).newInstance(); + return (IBinder) this.moduleContext.getClassLoader().loadClass(className).newInstance(); } catch (ClassNotFoundException | IllegalAccessException | InstantiationException | RuntimeException e) { throw new LoadingException("Failed to instantiate module class: " + className, e); } diff --git a/play-services-basement/src/main/java/org/microg/gms/common/GmsService.java b/play-services-basement/src/main/java/org/microg/gms/common/GmsService.java index 91e54c1ba7..3f6541b3f4 100644 --- a/play-services-basement/src/main/java/org/microg/gms/common/GmsService.java +++ b/play-services-basement/src/main/java/org/microg/gms/common/GmsService.java @@ -106,6 +106,7 @@ public enum GmsService { INSTANT_APPS(121, "com.google.android.gms.instantapps.START"), CAST_FIRSTPATY(122, "com.google.android.gms.cast.firstparty.START"), AD_CACHE(123, "com.google.android.gms.ads.service.CACHE"), + SMS_RETRIEVER(126, "com.google.android.gms.auth.api.phone.service.SmsRetrieverApiService.START"), CRYPT_AUTH(129, "com.google.android.gms.auth.cryptauth.cryptauthservice.START"), DYNAMIC_LINKS(131, "com.google.firebase.dynamiclinks.service.START"), FONTS(132, "com.google.android.gms.fonts.service.START"), @@ -118,6 +119,7 @@ public enum GmsService { CONSTELLATION(155, "com.google.android.gms.constellation.service.START"), AUDIT(154, "com.google.android.gms.audit.service.START"), SYSTEM_UPDATE(157, "com.google.android.gms.update.START_API_SERVICE"), + MOBSTORE(160, "com.google.android.mobstore.service.START"), USER_LOCATION(163, "com.google.android.gms.userlocation.service.START"), AD_HTTP(166, "com.google.android.gms.ads.service.HTTP"), LANGUAGE_PROFILE(167, "com.google.android.gms.languageprofile.service.START"), @@ -133,6 +135,7 @@ public enum GmsService { APP_USAGE(193, "com.google.android.gms.appusage.service.START"), NEARBY_SHARING_2(194, "com.google.android.gms.nearby.sharing.START_SERVICE"), AD_CONSENT_LOOKUP(195, "com.google.android.gms.ads.service.CONSENT_LOOKUP"), + CREDENTIAL_MANAGER(196, "com.google.android.gms.credential.manager.service.firstparty.START"), PHONE_INTERNAL(197, "com.google.android.gms.auth.api.phone.service.InternalService.START"), PAY(198, "com.google.android.gms.pay.service.BIND"), ASTERISM(199, "com.google.android.gms.asterism.service.START"), @@ -142,6 +145,7 @@ public enum GmsService { CONTACT_SYNC(208, "com.google.android.gms.people.contactssync.service.START"), IDENTITY_SIGN_IN(212, "com.google.android.gms.auth.api.identity.service.signin.START"), CREDENTIAL_STORE(214, "com.google.android.gms.fido.credentialstore.internal_service.START"), + MDI_SYNC(215, "com.google.android.gms.mdisync.service.START"), EVENT_ATTESTATION(216, "com.google.android.gms.ads.identifier.service.EVENT_ATTESTATION"), SCHEDULER(218, "com.google.android.gms.scheduler.ACTION_PROXY_SCHEDULE"), AUTHORIZATION(219, "com.google.android.gms.auth.api.identity.service.authorization.START"), @@ -162,6 +166,7 @@ public enum GmsService { LOCATION_SHARING_REPORTER(277, "com.google.android.gms.locationsharingreporter.service.START"), OCR(279, "com.google.android.gms.ocr.service.START"), OCR_INTERNAL(281, "com.google.android.gms.ocr.service.internal.START"), + IN_APP_REACH(315, "com.google.android.gms.inappreach.service.START") ; public int SERVICE_ID; diff --git a/play-services-basement/src/main/java/org/microg/gms/utils/WorkSourceUtil.java b/play-services-basement/src/main/java/org/microg/gms/utils/WorkSourceUtil.java index 78689b5a5e..419800b92b 100644 --- a/play-services-basement/src/main/java/org/microg/gms/utils/WorkSourceUtil.java +++ b/play-services-basement/src/main/java/org/microg/gms/utils/WorkSourceUtil.java @@ -5,7 +5,6 @@ package org.microg.gms.utils; -import android.os.Build; import android.os.WorkSource; import android.util.Log; diff --git a/play-services-cast/core/src/main/java/org/microg/gms/cast/CastMediaRouteProvider.java b/play-services-cast/core/src/main/java/org/microg/gms/cast/CastMediaRouteProvider.java index 0b1e3a8600..52f48aeb0c 100644 --- a/play-services-cast/core/src/main/java/org/microg/gms/cast/CastMediaRouteProvider.java +++ b/play-services-cast/core/src/main/java/org/microg/gms/cast/CastMediaRouteProvider.java @@ -22,7 +22,6 @@ import android.net.Uri; import android.net.nsd.NsdManager; import android.net.nsd.NsdServiceInfo; -import android.os.Build; import android.os.Bundle; import android.os.AsyncTask; import android.os.Handler; diff --git a/play-services-chimera-core/src/main/kotlin/org/microg/gms/chimera/ServiceProxy.kt b/play-services-chimera-core/src/main/kotlin/org/microg/gms/chimera/ServiceProxy.kt index d61c242a6a..2fada1ded2 100644 --- a/play-services-chimera-core/src/main/kotlin/org/microg/gms/chimera/ServiceProxy.kt +++ b/play-services-chimera-core/src/main/kotlin/org/microg/gms/chimera/ServiceProxy.kt @@ -61,34 +61,34 @@ abstract class ServiceProxy(private val loader: ServiceLoader) : android.app.Ser } } - override fun onRebind(intent: Intent) { + override fun onRebind(intent: Intent?) { if (actualService != null) { - if (intent != null) intent.setExtrasClassLoader(actualService!!.classLoader) + intent?.setExtrasClassLoader(actualService!!.classLoader) actualService!!.onRebind(intent) } } - override fun onStart(intent: Intent, startId: Int) { + override fun onStart(intent: Intent?, startId: Int) { if (actualService != null) { - if (intent != null) intent.setExtrasClassLoader(actualService!!.classLoader) + intent?.setExtrasClassLoader(actualService!!.classLoader) actualService!!.onStart(intent, startId) } else { stopSelf(startId) } } - override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { return if (actualService != null) { - if (intent != null) intent.setExtrasClassLoader(actualService!!.classLoader) + intent?.setExtrasClassLoader(actualService!!.classLoader) actualService!!.onStartCommand(intent, flags, startId) } else { super.onStartCommand(intent, flags, startId) } } - override fun onTaskRemoved(rootIntent: Intent) { + override fun onTaskRemoved(rootIntent: Intent?) { if (actualService != null) { - if (rootIntent != null) rootIntent.setExtrasClassLoader(actualService!!.classLoader) + rootIntent?.setExtrasClassLoader(actualService!!.classLoader) actualService!!.onTaskRemoved(rootIntent) } } @@ -99,9 +99,9 @@ abstract class ServiceProxy(private val loader: ServiceLoader) : android.app.Ser } } - override fun onUnbind(intent: Intent): Boolean { + override fun onUnbind(intent: Intent?): Boolean { return if (actualService != null) { - if (intent != null) intent.setExtrasClassLoader(actualService!!.classLoader) + intent?.setExtrasClassLoader(actualService!!.classLoader) actualService!!.onUnbind(intent) } else { false diff --git a/play-services-clearcut/src/main/java/com/google/android/gms/clearcut/LogEventParcelable.java b/play-services-clearcut/src/main/java/com/google/android/gms/clearcut/LogEventParcelable.java index 0f193b9cf0..152d3b1d97 100644 --- a/play-services-clearcut/src/main/java/com/google/android/gms/clearcut/LogEventParcelable.java +++ b/play-services-clearcut/src/main/java/com/google/android/gms/clearcut/LogEventParcelable.java @@ -65,13 +65,19 @@ public class LogEventParcelable extends AutoSafeParcelable { @Field(11) public final LogVerifierResultParcelable logVerifierResult; + @Field(12) + private String[] mendelPackagesToFilter; + + @Field(13) + public int eventCode; + private LogEventParcelable() { context = null; bytes = null; testCodes = experimentIds = null; mendelPackages = null; experimentTokens = null; - addPhenotypeExperimentTokens = false; + addPhenotypeExperimentTokens = true; experimentTokenParcelables = null; genericDimensions = null; logVerifierResult = null; diff --git a/play-services-conscrypt-provider-core/src/main/java/com/google/android/gms/common/security/ProviderInstallerImpl.java b/play-services-conscrypt-provider-core/src/main/java/com/google/android/gms/common/security/ProviderInstallerImpl.java index 587d8158ce..67a65f8605 100644 --- a/play-services-conscrypt-provider-core/src/main/java/com/google/android/gms/common/security/ProviderInstallerImpl.java +++ b/play-services-conscrypt-provider-core/src/main/java/com/google/android/gms/common/security/ProviderInstallerImpl.java @@ -7,7 +7,6 @@ import android.content.Context; import android.content.pm.ApplicationInfo; -import android.os.Build; import android.os.Process; import android.util.Log; @@ -38,6 +37,7 @@ import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLContext; +import static android.os.Build.VERSION.SDK_INT; import static com.google.android.gms.security.ProviderInstaller.PROVIDER_NAME; @Keep @@ -58,7 +58,7 @@ private static String getRealSelfPackageName(Context context) { } catch (Exception e) { } - if (Build.VERSION.SDK_INT >= 29) { + if (SDK_INT >= 29) { return context.getOpPackageName(); } Context applicationContext = context.getApplicationContext(); diff --git a/play-services-core/build.gradle b/play-services-core/build.gradle index 1ed642bb10..f3aaae36df 100644 --- a/play-services-core/build.gradle +++ b/play-services-core/build.gradle @@ -20,6 +20,10 @@ dependencies { implementation project(':firebase-dynamic-links-api') implementation project(':firebase-auth-core') + implementation project(':play-services-ads-core') + implementation project(':play-services-ads-identifier-core') + implementation project(':play-services-ads-lite-core') + implementation project(':play-services-appinvite-core') implementation project(':play-services-base-core') implementation project(':play-services-cast-core') implementation project(':play-services-cast-framework-core') @@ -29,8 +33,11 @@ dependencies { implementation project(':play-services-fido-core') implementation project(':play-services-gmscompliance-core') implementation project(':play-services-location-core') + implementation project(':play-services-location-core-base') + implementation project(':play-services-location-core-provider') withNearbyImplementation project(':play-services-nearby-core') implementation project(':play-services-oss-licenses-core') + implementation project(':play-services-pay-core') implementation project(':play-services-recaptcha-core') implementation project(':play-services-safetynet-core') implementation project(':play-services-tapandpay-core') @@ -88,6 +95,13 @@ android { multiDexEnabled true multiDexKeepProguard file('multidex-keep.pro') + buildConfigField "String", "SAFETYNET_KEY", "\"${localProperties.get("safetynet.key", "")}\"" + buildConfigField "String", "RECAPTCHA_SITE_KEY", "\"${localProperties.get("recaptcha.siteKey", "")}\"" + buildConfigField "String", "RECAPTCHA_SECRET", "\"${localProperties.get("recaptcha.secret", "")}\"" + buildConfigField "String", "RECAPTCHA_ENTERPRISE_PROJECT_ID", "\"${localProperties.get("recaptchaEnterpreise.projectId", "")}\"" + buildConfigField "String", "RECAPTCHA_ENTERPRISE_SITE_KEY", "\"${localProperties.get("recaptchaEnterpreise.siteKey", "")}\"" + buildConfigField "String", "RECAPTCHA_ENTERPRISE_API_KEY", "\"${localProperties.get("recaptchaEnterpreise.apiKey", "")}\"" + ndk { abiFilters "armeabi-v7a", "arm64-v8a", "x86", "x86_64" } @@ -146,7 +160,7 @@ android { packagingOptions { exclude 'META-INF/ASL2.0' jniLibs { - useLegacyPackaging false + useLegacyPackaging true } } } diff --git a/play-services-core/microg-ui-tools/src/main/java/org/microg/tools/ui/SwitchBar.java b/play-services-core/microg-ui-tools/src/main/java/org/microg/tools/ui/SwitchBar.java index ab282dd50c..2d63313d16 100644 --- a/play-services-core/microg-ui-tools/src/main/java/org/microg/tools/ui/SwitchBar.java +++ b/play-services-core/microg-ui-tools/src/main/java/org/microg/tools/ui/SwitchBar.java @@ -18,7 +18,6 @@ package org.microg.tools.ui; import android.content.Context; -import android.os.Build; import android.os.Parcel; import android.os.Parcelable; import android.text.SpannableStringBuilder; @@ -70,7 +69,7 @@ public SwitchBar(Context context, AttributeSet attrs) { LayoutInflater.from(context).inflate(R.layout.switch_bar, this); mTextView = (TextView) findViewById(R.id.switch_text); - if (SDK_INT > Build.VERSION_CODES.JELLY_BEAN) { + if (SDK_INT > 16) { mTextView.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO); } mLabel = getResources().getString(R.string.abc_capital_off); @@ -81,7 +80,7 @@ public SwitchBar(Context context, AttributeSet attrs) { // Prevent onSaveInstanceState() to be called as we are managing the state of the Switch // on our own mSwitch.setSaveEnabled(false); - if (SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + if (SDK_INT >= 16) { mSwitch.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO); } diff --git a/play-services-core/src/main/AndroidManifest.xml b/play-services-core/src/main/AndroidManifest.xml index b5df7631a0..67c9bf79cb 100644 --- a/play-services-core/src/main/AndroidManifest.xml +++ b/play-services-core/src/main/AndroidManifest.xml @@ -117,6 +117,7 @@ android:icon="@mipmap/ic_core_service_app" android:label="@string/gms_app_name" android:multiArch="true" + android:networkSecurityConfig="@xml/network_security_config" android:theme="@style/Theme.AppCompat.DayNight"> @@ -422,6 +423,12 @@ + + + + + + @@ -632,12 +639,6 @@ - - - - - - @@ -653,12 +654,18 @@ - + + + + + + + @@ -672,12 +679,6 @@ - - - - - - @@ -704,7 +705,6 @@ - @@ -790,7 +790,6 @@ - diff --git a/play-services-core/src/main/java/com/google/android/gms/ads/AdActivity.java b/play-services-core/src/main/java/com/google/android/gms/ads/AdActivity.java deleted file mode 100644 index 3829065d46..0000000000 --- a/play-services-core/src/main/java/com/google/android/gms/ads/AdActivity.java +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright (C) 2013-2017 microG Project Team - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.android.gms.ads; - -import android.app.Activity; - -public class AdActivity extends Activity { -} 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 8484447b2b..cbc93d2777 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 @@ -9,7 +9,6 @@ import android.content.ContextWrapper; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; -import android.os.Build; import android.os.Process; import android.util.Log; @@ -21,6 +20,11 @@ import dalvik.system.PathClassLoader; +import static android.os.Build.CPU_ABI; +import static android.os.Build.SUPPORTED_32_BIT_ABIS; +import static android.os.Build.SUPPORTED_64_BIT_ABIS; +import static android.os.Build.VERSION.SDK_INT; + public class DynamiteContext extends ContextWrapper { private static final String TAG = "DynamiteContext"; private DynamiteModuleInfo moduleInfo; @@ -42,16 +46,16 @@ public DynamiteContext(DynamiteModuleInfo moduleInfo, Context base, Context gmsC public ClassLoader getClassLoader() { if (classLoader == null) { StringBuilder nativeLoaderDirs = new StringBuilder(gmsContext.getApplicationInfo().nativeLibraryDir); - if (Build.VERSION.SDK_INT >= 23 && Process.is64Bit()) { - for (String abi : Build.SUPPORTED_64_BIT_ABIS) { + if (SDK_INT >= 23 && Process.is64Bit()) { + for (String abi : SUPPORTED_64_BIT_ABIS) { nativeLoaderDirs.append(File.pathSeparator).append(gmsContext.getApplicationInfo().sourceDir).append("!/lib/").append(abi); } - } else if (Build.VERSION.SDK_INT >= 21) { - for (String abi : Build.SUPPORTED_32_BIT_ABIS) { + } else if (SDK_INT >= 21) { + for (String abi : SUPPORTED_32_BIT_ABIS) { nativeLoaderDirs.append(File.pathSeparator).append(gmsContext.getApplicationInfo().sourceDir).append("!/lib/").append(abi); } } else { - nativeLoaderDirs.append(File.pathSeparator).append(gmsContext.getApplicationInfo().sourceDir).append("!/lib/").append(Build.CPU_ABI); + nativeLoaderDirs.append(File.pathSeparator).append(gmsContext.getApplicationInfo().sourceDir).append("!/lib/").append(CPU_ABI); } classLoader = new PathClassLoader(gmsContext.getApplicationInfo().sourceDir, nativeLoaderDirs.toString(), new FilteredClassLoader(originalContext.getClassLoader(), moduleInfo.getMergedClasses(), moduleInfo.getMergedPackages())); } diff --git a/play-services-core/src/main/java/com/google/android/gms/chimera/container/DynamiteLoaderImpl.java b/play-services-core/src/main/java/com/google/android/gms/chimera/container/DynamiteLoaderImpl.java index e27537d386..b60bc263cb 100644 --- a/play-services-core/src/main/java/com/google/android/gms/chimera/container/DynamiteLoaderImpl.java +++ b/play-services-core/src/main/java/com/google/android/gms/chimera/container/DynamiteLoaderImpl.java @@ -36,16 +36,21 @@ public class DynamiteLoaderImpl extends IDynamiteLoader.Stub { @Override public IObjectWrapper createModuleContext(IObjectWrapper wrappedContext, String moduleId, int minVersion) throws RemoteException { // We don't have crash utils, so just forward - return createModuleContextNoCrashUtils(wrappedContext, moduleId, minVersion); + return createModuleContextV2(wrappedContext, moduleId, minVersion); } @Override - public IObjectWrapper createModuleContextNoCrashUtils(IObjectWrapper wrappedContext, String moduleId, int minVersion) throws RemoteException { + public IObjectWrapper createModuleContextV2(IObjectWrapper wrappedContext, String moduleId, int minVersion) throws RemoteException { Log.d(TAG, "createModuleContext for " + moduleId + " at version " + minVersion); final Context originalContext = (Context) ObjectWrapper.unwrap(wrappedContext); return ObjectWrapper.wrap(DynamiteContext.create(moduleId, originalContext)); } + @Override + public IObjectWrapper createModuleContextV3(IObjectWrapper wrappedContext, String moduleId, int minVersion, IObjectWrapper wrappedCursor) throws RemoteException { + throw new UnsupportedOperationException(); + } + @Override public int getIDynamiteLoaderVersion() throws RemoteException { return 2; @@ -59,11 +64,11 @@ public int getModuleVersion(IObjectWrapper wrappedContext, String moduleId) thro @Override public int getModuleVersion2(IObjectWrapper wrappedContext, String moduleId, boolean updateConfigIfRequired) throws RemoteException { // We don't have crash utils, so just forward - return getModuleVersion2NoCrashUtils(wrappedContext, moduleId, updateConfigIfRequired); + return getModuleVersionV2(wrappedContext, moduleId, updateConfigIfRequired); } @Override - public int getModuleVersion2NoCrashUtils(IObjectWrapper wrappedContext, String moduleId, boolean updateConfigIfRequired) throws RemoteException { + public int getModuleVersionV2(IObjectWrapper wrappedContext, String moduleId, boolean updateConfigIfRequired) throws RemoteException { final Context context = (Context) ObjectWrapper.unwrap(wrappedContext); if (context == null) { Log.w(TAG, "Invalid client context"); @@ -96,4 +101,9 @@ public int getModuleVersion2NoCrashUtils(IObjectWrapper wrappedContext, String m Log.d(TAG, "unimplemented Method: getModuleVersion for " + moduleId); return 0; } + + @Override + public IObjectWrapper getModuleVersionV3(IObjectWrapper wrappedContext, String moduleId, boolean updateConfigIfRequired, long requestStartTime) throws RemoteException { + throw new UnsupportedOperationException(); + } } diff --git a/play-services-core/src/main/java/com/google/android/gms/common/GoogleCertificatesImpl.java b/play-services-core/src/main/java/com/google/android/gms/common/GoogleCertificatesImpl.java index bbe72472ea..8eefeeeaf6 100644 --- a/play-services-core/src/main/java/com/google/android/gms/common/GoogleCertificatesImpl.java +++ b/play-services-core/src/main/java/com/google/android/gms/common/GoogleCertificatesImpl.java @@ -17,10 +17,12 @@ package com.google.android.gms.common; import android.content.pm.PackageManager; +import android.os.IBinder; import android.os.RemoteException; import androidx.annotation.Keep; import android.util.Log; +import com.google.android.gms.common.internal.CertData; import com.google.android.gms.common.internal.GoogleCertificatesQuery; import com.google.android.gms.common.internal.IGoogleCertificatesApi; import com.google.android.gms.dynamic.IObjectWrapper; @@ -28,20 +30,25 @@ import org.microg.gms.common.PackageUtils; +import java.util.Collections; +import java.util.Set; + @Keep public class GoogleCertificatesImpl extends IGoogleCertificatesApi.Stub { private static final String TAG = "GmsCertImpl"; + private Set googleCertificates = Collections.emptySet(); + private Set googleReleaseCertificates = Collections.emptySet(); @Override - public IObjectWrapper getGoogleCertficates() throws RemoteException { - Log.d(TAG, "unimplemented Method: getGoogleCertficates"); - return null; + public IObjectWrapper getGoogleCertificates() throws RemoteException { + Log.d(TAG, "unimplemented Method: getGoogleCertificates"); + return ObjectWrapper.wrap(googleCertificates.toArray(new IBinder[0])); } @Override public IObjectWrapper getGoogleReleaseCertificates() throws RemoteException { Log.d(TAG, "unimplemented Method: getGoogleReleaseCertificates"); - return null; + return ObjectWrapper.wrap(googleReleaseCertificates.toArray(new IBinder[0])); } @Override diff --git a/play-services-core/src/main/java/com/google/android/gms/dynamite/descriptors/com/google/android/gms/measurement/dynamite/ModuleDescriptor.java b/play-services-core/src/main/java/com/google/android/gms/dynamite/descriptors/com/google/android/gms/measurement/dynamite/ModuleDescriptor.java index 0c3f0bcd10..0f8dad376e 100644 --- a/play-services-core/src/main/java/com/google/android/gms/dynamite/descriptors/com/google/android/gms/measurement/dynamite/ModuleDescriptor.java +++ b/play-services-core/src/main/java/com/google/android/gms/dynamite/descriptors/com/google/android/gms/measurement/dynamite/ModuleDescriptor.java @@ -18,5 +18,5 @@ public class ModuleDescriptor { public static final String MODULE_ID = "com.google.android.gms.measurement.dynamite"; - public static final int MODULE_VERSION = 53; + public static final int MODULE_VERSION = 79; } diff --git a/play-services-core/src/main/java/org/microg/gms/ads/AdvertisingIdService.java b/play-services-core/src/main/java/org/microg/gms/ads/AdvertisingIdService.java deleted file mode 100644 index c3bc63d11f..0000000000 --- a/play-services-core/src/main/java/org/microg/gms/ads/AdvertisingIdService.java +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright (C) 2013-2017 microG Project Team - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.microg.gms.ads; - -import android.app.Service; -import android.content.Intent; -import android.os.IBinder; - -public class AdvertisingIdService extends Service { - @Override - public IBinder onBind(Intent intent) { - return new AdvertisingIdServiceImpl().asBinder(); - } -} diff --git a/play-services-core/src/main/java/org/microg/gms/ads/AdvertisingIdServiceImpl.java b/play-services-core/src/main/java/org/microg/gms/ads/AdvertisingIdServiceImpl.java deleted file mode 100644 index 30760da957..0000000000 --- a/play-services-core/src/main/java/org/microg/gms/ads/AdvertisingIdServiceImpl.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright (C) 2013-2017 microG Project Team - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.microg.gms.ads; - -import android.os.RemoteException; - -import com.google.android.gms.ads.identifier.internal.IAdvertisingIdService; - -import java.util.UUID; - -public class AdvertisingIdServiceImpl extends IAdvertisingIdService.Stub { - @Override - public String getAdvertisingId() throws RemoteException { - return generateAdvertisingId(null); - } - - @Override - public boolean isAdTrackingLimited(boolean defaultHint) throws RemoteException { - return true; - } - - @Override - public String generateAdvertisingId(String packageName) throws RemoteException { - return UUID.randomUUID().toString(); - } - - @Override - public void setAdTrackingLimited(String packageName, boolean limited) throws RemoteException { - // Ignored, sorry :) - } -} diff --git a/play-services-core/src/main/java/org/microg/gms/ads/GService.java b/play-services-core/src/main/java/org/microg/gms/ads/GService.java index c8239da32e..32a1ba52ac 100644 --- a/play-services-core/src/main/java/org/microg/gms/ads/GService.java +++ b/play-services-core/src/main/java/org/microg/gms/ads/GService.java @@ -16,6 +16,10 @@ package org.microg.gms.ads; +import android.os.Bundle; +import android.os.RemoteException; +import android.util.Log; +import com.google.android.gms.common.ConnectionResult; import com.google.android.gms.common.internal.GetServiceRequest; import com.google.android.gms.common.internal.IGmsCallbacks; @@ -29,7 +33,7 @@ public GService() { } @Override - public void handleServiceRequest(IGmsCallbacks callback, GetServiceRequest request, GmsService service) { - // TODO + public void handleServiceRequest(IGmsCallbacks callback, GetServiceRequest request, GmsService service) throws RemoteException { + callback.onPostInitComplete(ConnectionResult.API_DISABLED, null, null); } } diff --git a/play-services-core/src/main/java/org/microg/gms/appinvite/AppInviteService.java b/play-services-core/src/main/java/org/microg/gms/appinvite/AppInviteService.java deleted file mode 100644 index 8270f78ab4..0000000000 --- a/play-services-core/src/main/java/org/microg/gms/appinvite/AppInviteService.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright (C) 2019 e Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.microg.gms.appinvite; - -import android.app.Service; -import android.content.Intent; -import android.os.IBinder; -import android.util.Log; -import android.os.RemoteException; - -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 org.microg.gms.BaseService; -import org.microg.gms.common.GmsService; -import org.microg.gms.common.PackageUtils; - -import org.microg.gms.appinvite.AppInviteServiceImpl; - -public class AppInviteService extends BaseService { - private static final String TAG = "GmsAppInviteService"; - - public AppInviteService() { - super("GmsAppInviteSvc", GmsService.APP_INVITE); - } - - @Override - public void handleServiceRequest(IGmsCallbacks callback, GetServiceRequest request, GmsService service) throws RemoteException { - PackageUtils.getAndCheckCallingPackage(this, request.packageName); - Log.d(TAG, "callb: " + callback + " ; req: " + request + " ; serv: " + service); - - callback.onPostInitComplete(0, new AppInviteServiceImpl(this, request.packageName, request.extras), null); - } -} diff --git a/play-services-core/src/main/java/org/microg/gms/appinvite/AppInviteServiceImpl.java b/play-services-core/src/main/java/org/microg/gms/appinvite/AppInviteServiceImpl.java deleted file mode 100644 index e3bfe6d34c..0000000000 --- a/play-services-core/src/main/java/org/microg/gms/appinvite/AppInviteServiceImpl.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright (C) 2019 e Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.microg.gms.appinvite; - -import android.os.Parcel; -import android.os.RemoteException; -import android.os.Bundle; -import android.app.Activity; -import android.util.Log; -import android.content.Context; -import android.content.Intent; - -import com.google.android.gms.common.api.Status; - -import com.google.android.gms.dynamic.IObjectWrapper; -import com.google.android.gms.dynamic.ObjectWrapper; - -import com.google.android.gms.appinvite.internal.IAppInviteService; -import com.google.android.gms.appinvite.internal.IAppInviteCallbacks; - - -public class AppInviteServiceImpl extends IAppInviteService.Stub { - private static final String TAG = "GmsAppInviteServImpl"; - - public AppInviteServiceImpl(Context context, String packageName, Bundle extras) { - } - - - @Override - public void updateInvitationOnInstall(IAppInviteCallbacks callback, String invitationId) throws RemoteException { - callback.onStatus(Status.SUCCESS); - } - - @Override - public void convertInvitation(IAppInviteCallbacks callback, String invitationId) throws RemoteException { - callback.onStatus(Status.SUCCESS); - } - - @Override - public void getInvitation(IAppInviteCallbacks callback) throws RemoteException { - callback.onStatusIntent(new Status(Activity.RESULT_CANCELED), null); - } - - - @Override - public boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException { - if (super.onTransact(code, data, reply, flags)) { - return true; - } - - Log.d(TAG, "onTransact [unknown]: " + code + ", " + data + ", " + flags); - return false; - } -} diff --git a/play-services-core/src/main/java/org/microg/gms/auth/AccountContentProvider.java b/play-services-core/src/main/java/org/microg/gms/auth/AccountContentProvider.java index 57581a00e9..036bc8e581 100644 --- a/play-services-core/src/main/java/org/microg/gms/auth/AccountContentProvider.java +++ b/play-services-core/src/main/java/org/microg/gms/auth/AccountContentProvider.java @@ -27,7 +27,6 @@ import android.database.Cursor; import android.net.Uri; import android.os.Binder; -import android.os.Build; import android.os.Bundle; import android.util.Log; @@ -37,6 +36,7 @@ import java.util.Arrays; +import static android.os.Build.VERSION.SDK_INT; import static org.microg.gms.auth.AuthConstants.DEFAULT_ACCOUNT_TYPE; import static org.microg.gms.auth.AuthConstants.PROVIDER_EXTRA_ACCOUNTS; import static org.microg.gms.auth.AuthConstants.PROVIDER_EXTRA_CLEAR_PASSWORD; @@ -55,7 +55,7 @@ public boolean onCreate() { @Override public Bundle call(String method, String arg, Bundle extras) { String suggestedPackageName = null; - if (Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT) { + if (SDK_INT > 19) { suggestedPackageName = getCallingPackage(); } String packageName = PackageUtils.getAndCheckCallingPackage(getContext(), suggestedPackageName); @@ -73,13 +73,13 @@ public Bundle call(String method, String arg, Bundle extras) { Account[] accounts = null; if (arg != null && (arg.equals(DEFAULT_ACCOUNT_TYPE) || arg.startsWith(DEFAULT_ACCOUNT_TYPE + "."))) { AccountManager am = AccountManager.get(getContext()); - if (Build.VERSION.SDK_INT >= 18) { + if (SDK_INT >= 18) { accounts = am.getAccountsByTypeForPackage(arg, packageName); } if (accounts == null || accounts.length == 0) { accounts = am.getAccountsByType(arg); } - if (Build.VERSION.SDK_INT >= 26 && accounts != null && arg.equals(DEFAULT_ACCOUNT_TYPE)) { + if (SDK_INT >= 26 && accounts != null && arg.equals(DEFAULT_ACCOUNT_TYPE)) { for (Account account : accounts) { if (am.getAccountVisibility(account, packageName) == AccountManager.VISIBILITY_UNDEFINED) { Log.d(TAG, "Make account " + account + " visible to " + packageName); diff --git a/play-services-core/src/main/java/org/microg/gms/auth/AuthManager.java b/play-services-core/src/main/java/org/microg/gms/auth/AuthManager.java index 5b8282cf9c..8ba6c6a939 100644 --- a/play-services-core/src/main/java/org/microg/gms/auth/AuthManager.java +++ b/play-services-core/src/main/java/org/microg/gms/auth/AuthManager.java @@ -20,7 +20,6 @@ import android.accounts.AccountManager; import android.content.Context; import android.content.pm.PackageManager; -import android.os.Build; import android.util.Log; import org.microg.gms.common.PackageUtils; @@ -30,6 +29,7 @@ import static android.content.pm.ApplicationInfo.FLAG_SYSTEM; import static android.content.pm.ApplicationInfo.FLAG_UPDATED_SYSTEM_APP; +import static android.os.Build.VERSION.SDK_INT; import static org.microg.gms.auth.AuthPrefs.isTrustGooglePermitted; public class AuthManager { @@ -93,7 +93,7 @@ public String buildPermKey() { public void setPermitted(boolean value) { setUserData(buildPermKey(), value ? "1" : "0"); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && value && packageName != null) { + if (SDK_INT >= 26 && value && packageName != null) { // Make account persistently visible as we already granted access accountManager.setAccountVisibility(getAccount(), packageName, AccountManager.VISIBILITY_VISIBLE); } @@ -161,7 +161,7 @@ public void setAuthToken(String auth) { public void setAuthToken(String service, String auth) { getAccountManager().setAuthToken(getAccount(), buildTokenKey(service), auth); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && packageName != null && auth != null) { + if (SDK_INT >= 26 && packageName != null && auth != null) { // Make account persistently visible as we already granted access accountManager.setAccountVisibility(getAccount(), packageName, AccountManager.VISIBILITY_VISIBLE); } diff --git a/play-services-core/src/main/java/org/microg/gms/auth/AuthManagerServiceImpl.java b/play-services-core/src/main/java/org/microg/gms/auth/AuthManagerServiceImpl.java index 59422af29a..c3a4f110e9 100644 --- a/play-services-core/src/main/java/org/microg/gms/auth/AuthManagerServiceImpl.java +++ b/play-services-core/src/main/java/org/microg/gms/auth/AuthManagerServiceImpl.java @@ -52,11 +52,8 @@ import java.util.List; import java.util.concurrent.TimeUnit; -import static android.accounts.AccountManager.KEY_ACCOUNTS; -import static android.accounts.AccountManager.KEY_ACCOUNT_NAME; -import static android.accounts.AccountManager.KEY_ACCOUNT_TYPE; -import static android.accounts.AccountManager.KEY_AUTHTOKEN; -import static android.accounts.AccountManager.KEY_CALLER_PID; +import static android.accounts.AccountManager.*; +import static android.os.Build.VERSION.SDK_INT; import static org.microg.gms.auth.AskPermissionActivity.EXTRA_CONSENT_DATA; public class AuthManagerServiceImpl extends IAuthManagerService.Stub { @@ -205,7 +202,17 @@ public Bundle removeAccount(Account account) { @Override public Bundle requestGoogleAccountsAccess(String packageName) throws RemoteException { - Log.w(TAG, "Not implemented: requestGoogleAccountsAccess(" + packageName + ")"); + PackageUtils.assertExtendedAccess(context); + if (SDK_INT >= 26) { + for (Account account : get(context).getAccountsByType(AuthConstants.DEFAULT_ACCOUNT_TYPE)) { + AccountManager.get(context).setAccountVisibility(account, packageName, VISIBILITY_VISIBLE); + } + Bundle res = new Bundle(); + res.putString("Error", "Ok"); + return res; + } else { + Log.w(TAG, "Not implemented: requestGoogleAccountsAccess(" + packageName + ")"); + } return null; } diff --git a/play-services-core/src/main/java/org/microg/gms/auth/SignInService.java b/play-services-core/src/main/java/org/microg/gms/auth/SignInService.java deleted file mode 100644 index 8bbcbb8070..0000000000 --- a/play-services-core/src/main/java/org/microg/gms/auth/SignInService.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright (C) 2013-2017 microG Project Team - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.microg.gms.auth; - -import android.os.RemoteException; -import android.util.Log; - -import com.google.android.gms.common.internal.GetServiceRequest; -import com.google.android.gms.common.internal.IGmsCallbacks; - -import org.microg.gms.BaseService; -import org.microg.gms.common.GmsService; - -public class SignInService extends BaseService { - public SignInService() { - super("GmsSignInSvc", GmsService.SIGN_IN); - } - - @Override - public void handleServiceRequest(IGmsCallbacks callback, GetServiceRequest request, GmsService service) throws RemoteException { - Log.d(TAG, "unimplemented Method: handleServiceRequest"); - - } -} diff --git a/play-services-core/src/main/java/org/microg/gms/checkin/CheckinManager.java b/play-services-core/src/main/java/org/microg/gms/checkin/CheckinManager.java index 38c478b3e0..22211e160b 100644 --- a/play-services-core/src/main/java/org/microg/gms/checkin/CheckinManager.java +++ b/play-services-core/src/main/java/org/microg/gms/checkin/CheckinManager.java @@ -20,7 +20,6 @@ import android.accounts.AccountManager; import android.content.ContentResolver; import android.content.Context; -import android.preference.PreferenceManager; import org.microg.gms.auth.AuthConstants; import org.microg.gms.auth.AuthRequest; @@ -41,7 +40,7 @@ public static synchronized LastCheckinInfo checkin(Context context, boolean forc LastCheckinInfo info = LastCheckinInfo.read(context); if (!force && info.getLastCheckin() > System.currentTimeMillis() - MIN_CHECKIN_INTERVAL) return null; - if (!CheckinPrefs.isEnabled(context)) + if (!CheckinPreferences.isEnabled(context)) return null; List accounts = new ArrayList(); AccountManager accountManager = AccountManager.get(context); diff --git a/play-services-core/src/main/java/org/microg/gms/checkin/CheckinService.java b/play-services-core/src/main/java/org/microg/gms/checkin/CheckinService.java index deb520febb..08d337d997 100644 --- a/play-services-core/src/main/java/org/microg/gms/checkin/CheckinService.java +++ b/play-services-core/src/main/java/org/microg/gms/checkin/CheckinService.java @@ -80,7 +80,7 @@ public CheckinService() { protected void onHandleIntent(Intent intent) { try { ForegroundServiceContext.completeForegroundService(this, intent, TAG); - if (CheckinPrefs.isEnabled(this)) { + if (CheckinPreferences.isEnabled(this)) { LastCheckinInfo info = CheckinManager.checkin(this, intent.getBooleanExtra(EXTRA_FORCE_CHECKIN, false)); if (info != null) { Log.d(TAG, "Checked in as " + Long.toHexString(info.getAndroidId())); diff --git a/play-services-core/src/main/java/org/microg/gms/checkin/TriggerReceiver.java b/play-services-core/src/main/java/org/microg/gms/checkin/TriggerReceiver.java index 33805f0050..7af4d9e78d 100644 --- a/play-services-core/src/main/java/org/microg/gms/checkin/TriggerReceiver.java +++ b/play-services-core/src/main/java/org/microg/gms/checkin/TriggerReceiver.java @@ -43,7 +43,7 @@ public void onReceive(Context context, Intent intent) { try { boolean force = "android.provider.Telephony.SECRET_CODE".equals(intent.getAction()); - if (CheckinPrefs.isEnabled(context) || force) { + if (CheckinPreferences.isEnabled(context) || force) { if (LastCheckinInfo.read(context).getLastCheckin() > System.currentTimeMillis() - REGULAR_CHECKIN_INTERVAL && !force) { CheckinService.schedule(context); return; diff --git a/play-services-core/src/main/java/org/microg/gms/gcm/GcmDatabase.java b/play-services-core/src/main/java/org/microg/gms/gcm/GcmDatabase.java index ce32d8d3db..4762797206 100644 --- a/play-services-core/src/main/java/org/microg/gms/gcm/GcmDatabase.java +++ b/play-services-core/src/main/java/org/microg/gms/gcm/GcmDatabase.java @@ -10,13 +10,14 @@ import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; -import android.os.Build; import android.text.TextUtils; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import static android.os.Build.VERSION.SDK_INT; + public class GcmDatabase extends SQLiteOpenHelper { private static final String TAG = GcmDatabase.class.getSimpleName(); public static final String DB_NAME = "gcmstatus"; @@ -55,7 +56,7 @@ public class GcmDatabase extends SQLiteOpenHelper { public GcmDatabase(Context context) { super(context, DB_NAME, null, DB_VERSION); this.context = context; - if (Build.VERSION.SDK_INT >= 16) { + if (SDK_INT >= 16) { this.setWriteAheadLoggingEnabled(true); } } diff --git a/play-services-core/src/main/java/org/microg/gms/gcm/McsService.java b/play-services-core/src/main/java/org/microg/gms/gcm/McsService.java index 7afa64373f..c0ce947f10 100644 --- a/play-services-core/src/main/java/org/microg/gms/gcm/McsService.java +++ b/play-services-core/src/main/java/org/microg/gms/gcm/McsService.java @@ -28,7 +28,6 @@ import android.content.pm.ResolveInfo; import android.net.ConnectivityManager; import android.net.NetworkInfo; -import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; @@ -38,6 +37,7 @@ import android.os.PowerManager; import android.os.SystemClock; import android.os.UserHandle; +import android.util.Base64; import android.util.Log; import androidx.annotation.Nullable; @@ -136,10 +136,10 @@ public class McsService extends Service implements Handler.Callback { @Nullable private Method addPowerSaveTempWhitelistAppMethod; @Nullable - @RequiresApi(Build.VERSION_CODES.S) + @RequiresApi(31) private Object powerExemptionManager; @Nullable - @RequiresApi(Build.VERSION_CODES.S) + @RequiresApi(31) private Method addToTemporaryAllowListMethod; private class HandlerThread extends Thread { @@ -177,9 +177,9 @@ public void onCreate() { heartbeatIntent = PendingIntent.getService(this, 0, new Intent(ACTION_HEARTBEAT, null, this, McsService.class), 0); alarmManager = (AlarmManager) getSystemService(ALARM_SERVICE); powerManager = (PowerManager) getSystemService(POWER_SERVICE); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && checkSelfPermission("android.permission.CHANGE_DEVICE_IDLE_TEMP_WHITELIST") == PackageManager.PERMISSION_GRANTED) { + if (SDK_INT >= 23 && checkSelfPermission("android.permission.CHANGE_DEVICE_IDLE_TEMP_WHITELIST") == PackageManager.PERMISSION_GRANTED) { try { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + if (SDK_INT >= 31) { Class powerExemptionManagerClass = Class.forName("android.os.PowerExemptionManager"); powerExemptionManager = getSystemService(powerExemptionManagerClass); addToTemporaryAllowListMethod = @@ -266,7 +266,7 @@ public static void scheduleReconnect(Context context) { long delay = getCurrentDelay(); logd(context, "Scheduling reconnect in " + delay / 1000 + " seconds..."); PendingIntent pi = PendingIntent.getBroadcast(context, 1, new Intent(ACTION_RECONNECT, null, context, TriggerReceiver.class), 0); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + if (SDK_INT >= 23) { alarmManager.setExactAndAllowWhileIdle(ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime() + delay, pi); } else { alarmManager.set(ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime() + delay, pi); @@ -281,10 +281,10 @@ public void scheduleHeartbeat(Context context) { closeAll(); } logd(context, "Scheduling heartbeat in " + heartbeatMs / 1000 + " seconds..."); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + if (SDK_INT >= 23) { // This is supposed to work even when running in idle and without battery optimization disabled alarmManager.setExactAndAllowWhileIdle(ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime() + heartbeatMs, heartbeatIntent); - } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + } else if (SDK_INT >= 19) { // With KitKat, the alarms become inexact by default, but with the newly available setWindow we can get inexact alarms with guarantees. // Schedule the alarm to fire within the interval [heartbeatMs/3*4, heartbeatMs] alarmManager.setWindow(ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime() + heartbeatMs / 4 * 3, heartbeatMs / 4, @@ -549,15 +549,17 @@ private void handleAppMessage(DataMessageStanza msg) { intent.setAction(ACTION_C2DM_RECEIVE); intent.putExtra(EXTRA_FROM, msg.from); intent.putExtra(EXTRA_MESSAGE_ID, msg.id); - if (msg.persistent_id != null) { - intent.putExtra(EXTRA_MESSAGE_ID, msg.persistent_id); + if (msg.persistent_id != null) intent.putExtra(EXTRA_MESSAGE_ID, msg.persistent_id); + if (msg.token != null) intent.putExtra(EXTRA_COLLAPSE_KEY, msg.token); + if (msg.raw_data != null) { + intent.putExtra(EXTRA_RAWDATA_BASE64, Base64.encodeToString(msg.raw_data.toByteArray(), Base64.DEFAULT)); + intent.putExtra(EXTRA_RAWDATA, msg.raw_data.toByteArray()); } if (app.wakeForDelivery) { intent.addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES); } else { intent.addFlags(Intent.FLAG_EXCLUDE_STOPPED_PACKAGES); } - if (msg.token != null) intent.putExtra(EXTRA_COLLAPSE_KEY, msg.token); for (AppData appData : msg.app_data) { intent.putExtra(appData.key, appData.value_); } @@ -603,7 +605,7 @@ private void handleAppMessage(DataMessageStanza msg) { } private void addPowerSaveTempWhitelistApp(String packageName) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + if (SDK_INT >= 31) { try { if (addToTemporaryAllowListMethod != null && powerExemptionManager != null) { logd(this, "Adding app " + packageName + " to the temp allowlist"); @@ -612,7 +614,7 @@ private void addPowerSaveTempWhitelistApp(String packageName) { } catch (Exception e) { Log.e(TAG, "Error adding app" + packageName + " to the temp allowlist.", e); } - } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + } else if (SDK_INT >= 23) { try { if (getUserIdMethod != null && addPowerSaveTempWhitelistAppMethod != null && deviceIdleController != null) { int userId = (int) getUserIdMethod.invoke(null, getPackageManager().getApplicationInfo(packageName, 0).uid); diff --git a/play-services-core/src/main/java/org/microg/gms/gcm/PushRegisterManager.java b/play-services-core/src/main/java/org/microg/gms/gcm/PushRegisterManager.java index e17b86fd2b..dd9fcf7871 100644 --- a/play-services-core/src/main/java/org/microg/gms/gcm/PushRegisterManager.java +++ b/play-services-core/src/main/java/org/microg/gms/gcm/PushRegisterManager.java @@ -149,7 +149,7 @@ private static Bundle handleResponse(GcmDatabase database, RegisterRequest reque resultBundle.putString(EXTRA_REGISTRATION_ID, attachRequestId(response.token, requestId)); } } else { - if (!request.app.equals(response.deleted) && !request.app.equals(response.token)) { + if (!request.app.equals(response.deleted) && !request.app.equals(response.token) && !request.sender.equals(response.token)) { database.noteAppRegistrationError(request.app, response.responseText); resultBundle.putString(EXTRA_ERROR, attachRequestId(ERROR_SERVICE_NOT_AVAILABLE, requestId)); } else { diff --git a/play-services-core/src/main/java/org/microg/gms/gcm/TriggerReceiver.java b/play-services-core/src/main/java/org/microg/gms/gcm/TriggerReceiver.java index b3f84d9694..55522902b1 100644 --- a/play-services-core/src/main/java/org/microg/gms/gcm/TriggerReceiver.java +++ b/play-services-core/src/main/java/org/microg/gms/gcm/TriggerReceiver.java @@ -25,7 +25,7 @@ import androidx.legacy.content.WakefulBroadcastReceiver; -import org.microg.gms.checkin.CheckinPrefs; +import org.microg.gms.checkin.CheckinPreferences; import org.microg.gms.checkin.LastCheckinInfo; import org.microg.gms.common.ForegroundServiceContext; @@ -68,7 +68,7 @@ public void onReceive(Context context, Intent intent) { if (LastCheckinInfo.read(context).getAndroidId() == 0) { Log.d(TAG, "Ignoring " + intent + ": need to checkin first."); - if (CheckinPrefs.isEnabled(context)) { + if (CheckinPreferences.isEnabled(context)) { // Do a check-in if we are not actually checked in, // but should be, e.g. cleared app data Log.d(TAG, "Requesting check-in..."); diff --git a/play-services-core/src/main/java/org/microg/gms/gservices/GServicesProvider.java b/play-services-core/src/main/java/org/microg/gms/gservices/GServicesProvider.java index 0dc21d3016..b0cb1e049f 100644 --- a/play-services-core/src/main/java/org/microg/gms/gservices/GServicesProvider.java +++ b/play-services-core/src/main/java/org/microg/gms/gservices/GServicesProvider.java @@ -21,7 +21,6 @@ import android.database.Cursor; import android.database.MatrixCursor; import android.net.Uri; -import android.os.Build; import android.util.Log; import java.util.HashMap; @@ -30,6 +29,8 @@ import java.util.Map; import java.util.Set; +import static android.os.Build.VERSION.SDK_INT; + /** * Originally found in Google Services Framework (com.google.android.gsf), this provides a generic * key-value store, that is written by the checkin service and read from various Google Apps. @@ -56,7 +57,7 @@ public boolean onCreate() { } private String getCallingPackageName() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + if (SDK_INT >= 19) { return getCallingPackage(); } else { return "unknown"; diff --git a/play-services-core/src/main/java/org/microg/gms/ui/AccountSettingsActivity.java b/play-services-core/src/main/java/org/microg/gms/ui/AccountSettingsActivity.java index 491b720109..e1e1dd5a6b 100644 --- a/play-services-core/src/main/java/org/microg/gms/ui/AccountSettingsActivity.java +++ b/play-services-core/src/main/java/org/microg/gms/ui/AccountSettingsActivity.java @@ -18,7 +18,6 @@ import android.accounts.Account; import android.accounts.AccountManager; -import android.os.Build; import android.os.Bundle; import androidx.annotation.Nullable; @@ -34,6 +33,7 @@ import static android.accounts.AccountManager.PACKAGE_NAME_KEY_LEGACY_NOT_VISIBLE; import static android.accounts.AccountManager.VISIBILITY_NOT_VISIBLE; import static android.accounts.AccountManager.VISIBILITY_VISIBLE; +import static android.os.Build.VERSION.SDK_INT; import static org.microg.gms.auth.AuthManager.PREF_AUTH_VISIBLE; public class AccountSettingsActivity extends AbstractSettingsActivity { @@ -53,7 +53,7 @@ public void onCreatePreferences(@Nullable Bundle savedInstanceState, String root super.onCreatePreferences(savedInstanceState, rootKey); Preference pref = findPreference(PREF_AUTH_VISIBLE); if (pref != null) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + if (SDK_INT < 26) { pref.setVisible(false); } else { pref.setOnPreferenceChangeListener((preference, newValue) -> { diff --git a/play-services-core/src/main/java/org/microg/gms/ui/Conditions.java b/play-services-core/src/main/java/org/microg/gms/ui/Conditions.java index de517dc5c2..6e295cc333 100644 --- a/play-services-core/src/main/java/org/microg/gms/ui/Conditions.java +++ b/play-services-core/src/main/java/org/microg/gms/ui/Conditions.java @@ -20,7 +20,6 @@ import android.content.Context; import android.content.Intent; import android.net.Uri; -import android.os.Build; import android.os.PowerManager; import android.provider.Settings; import android.view.View; @@ -74,7 +73,7 @@ public void onClick(View v) { @Override public boolean isActive(Context context) { count = 0; - if (SDK_INT >= Build.VERSION_CODES.M) { + if (SDK_INT >= 23) { for (String permission : REQUIRED_PERMISSIONS) { if (ContextCompat.checkSelfPermission(context, permission) != PERMISSION_GRANTED) count++; diff --git a/play-services-core/src/main/java/org/microg/gms/ui/PlacePickerActivity.java b/play-services-core/src/main/java/org/microg/gms/ui/PlacePickerActivity.java index e9563348ca..a86104443a 100644 --- a/play-services-core/src/main/java/org/microg/gms/ui/PlacePickerActivity.java +++ b/play-services-core/src/main/java/org/microg/gms/ui/PlacePickerActivity.java @@ -21,7 +21,6 @@ import android.location.Geocoder; import android.location.Location; import android.location.LocationManager; -import android.os.Build; import android.os.Bundle; import android.text.Html; import android.text.TextUtils; @@ -60,6 +59,7 @@ import static android.Manifest.permission.ACCESS_COARSE_LOCATION; import static android.Manifest.permission.ACCESS_FINE_LOCATION; import static android.content.pm.PackageManager.PERMISSION_GRANTED; +import static android.os.Build.VERSION.SDK_INT; import static org.microg.gms.location.LocationConstants.EXTRA_PRIMARY_COLOR; import static org.microg.gms.location.LocationConstants.EXTRA_PRIMARY_COLOR_DARK; //import static org.microg.gms.maps.vtm.GmsMapsTypeHelper.fromLatLngBounds; @@ -90,7 +90,7 @@ protected void onCreate(Bundle savedInstanceState) { if (getIntent().hasExtra(EXTRA_PRIMARY_COLOR)) { toolbar.setBackgroundColor(getIntent().getIntExtra(EXTRA_PRIMARY_COLOR, 0)); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) + if (SDK_INT >= 21) getWindow().setStatusBarColor(getIntent().getIntExtra(EXTRA_PRIMARY_COLOR_DARK, 0)); ((TextView) findViewById(R.id.place_picker_title)).setTextColor(getIntent().getIntExtra(EXTRA_PRIMARY_COLOR_DARK, 0)); } diff --git a/play-services-core/src/main/java/org/microg/gms/ui/SelfCheckFragment.java b/play-services-core/src/main/java/org/microg/gms/ui/SelfCheckFragment.java index 77c8303da2..8ebbff9a62 100644 --- a/play-services-core/src/main/java/org/microg/gms/ui/SelfCheckFragment.java +++ b/play-services-core/src/main/java/org/microg/gms/ui/SelfCheckFragment.java @@ -22,7 +22,6 @@ import android.content.pm.PermissionGroupInfo; import android.content.pm.PermissionInfo; import android.net.Uri; -import android.os.Build; import android.provider.Settings; import android.util.Log; import android.view.LayoutInflater; @@ -96,7 +95,7 @@ public void doChecks(Context context, ResultCollector collector) { } }); } - if (SDK_INT >= Build.VERSION_CODES.M) { + if (SDK_INT >= 23) { checks.add(new SystemChecks()); } // checks.add(new NlpOsCompatChecks()); diff --git a/play-services-core/src/main/java/com/google/android/gms/ads/AdManagerCreatorImpl.java b/play-services-core/src/main/kotlin/com/google/android/gms/ads/AdActivity.kt similarity index 85% rename from play-services-core/src/main/java/com/google/android/gms/ads/AdManagerCreatorImpl.java rename to play-services-core/src/main/kotlin/com/google/android/gms/ads/AdActivity.kt index f0f6a9a544..6e3fdaf8e1 100644 --- a/play-services-core/src/main/java/com/google/android/gms/ads/AdManagerCreatorImpl.java +++ b/play-services-core/src/main/kotlin/com/google/android/gms/ads/AdActivity.kt @@ -13,8 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +package com.google.android.gms.ads -package com.google.android.gms.ads; +import android.app.Activity -public class AdManagerCreatorImpl extends AdManagerCreator.Stub { -} +class AdActivity : Activity() diff --git a/play-services-core/src/main/kotlin/com/google/android/gms/ads/omid/AdSession.kt b/play-services-core/src/main/kotlin/com/google/android/gms/ads/omid/AdSession.kt new file mode 100644 index 0000000000..e2af3e66ef --- /dev/null +++ b/play-services-core/src/main/kotlin/com/google/android/gms/ads/omid/AdSession.kt @@ -0,0 +1,7 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ +package com.google.android.gms.ads.omid + +class AdSession diff --git a/play-services-core/src/main/kotlin/com/google/android/gms/ads/omid/DynamiteOmid.kt b/play-services-core/src/main/kotlin/com/google/android/gms/ads/omid/DynamiteOmid.kt new file mode 100644 index 0000000000..6b31aa12a7 --- /dev/null +++ b/play-services-core/src/main/kotlin/com/google/android/gms/ads/omid/DynamiteOmid.kt @@ -0,0 +1,61 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ +package com.google.android.gms.ads.omid + +import android.os.RemoteException +import android.util.Log +import androidx.annotation.Keep +import com.google.android.gms.dynamic.IObjectWrapper +import com.google.android.gms.dynamic.ObjectWrapper + +private const val TAG = "Omid" + +@Keep +class DynamiteOmid : IOmid.Stub() { + override fun initializeOmid(context: IObjectWrapper?): Boolean { + Log.d(TAG, "initializeOmid") + return true + } + + override fun createHtmlAdSession(version: String, webView: IObjectWrapper?, customReferenceData: String, impressionOwner: String, altImpressionOwner: String): IObjectWrapper { + return createHtmlAdSessionWithPartnerName(version, webView, customReferenceData, impressionOwner, altImpressionOwner, "Google") + } + + override fun startAdSession(adSession: IObjectWrapper?) { + Log.d(TAG, "startAdSession") + } + + override fun registerAdView(adSession: IObjectWrapper?, view: IObjectWrapper?) { + Log.d(TAG, "registerAdView") + } + + override fun getVersion(): String { + Log.d(TAG, "getVersion") + return "1.5.0" + } + + override fun finishAdSession(adSession: IObjectWrapper?) { + Log.d(TAG, "finishAdSession") + } + + override fun addFriendlyObstruction(adSession: IObjectWrapper?, view: IObjectWrapper?) { + Log.d(TAG, "addFriendlyObstruction") + } + + override fun createHtmlAdSessionWithPartnerName(version: String, webView: IObjectWrapper?, customReferenceData: String, impressionOwner: String, altImpressionOwner: String, partnerName: String): IObjectWrapper { + Log.d(TAG, "createHtmlAdSessionWithPartnerName($version, $customReferenceData, $impressionOwner, $altImpressionOwner, $partnerName)") + return ObjectWrapper.wrap(AdSession()) + } + + override fun createJavascriptAdSessionWithPartnerNameImpressionCreativeType(version: String, webView: IObjectWrapper?, customReferenceData: String, impressionOwner: String, altImpressionOwner: String, partnerName: String, impressionType: String, creativeType: String, contentUrl: String): IObjectWrapper { + Log.d(TAG, "createJavascriptAdSessionWithPartnerNameImpressionCreativeType($version, $customReferenceData, $impressionOwner, $altImpressionOwner, $partnerName, $impressionType, $creativeType, $contentUrl)") + return ObjectWrapper.wrap(AdSession()) + } + + override fun createHtmlAdSessionWithPartnerNameImpressionCreativeType(version: String, webView: IObjectWrapper?, customReferenceData: String, impressionOwner: String, altImpressionOwner: String, partnerName: String, impressionType: String, creativeType: String, contentUrl: String): IObjectWrapper { + Log.d(TAG, "createHtmlAdSessionWithPartnerNameImpressionCreativeType($version, $customReferenceData, $impressionOwner, $altImpressionOwner, $partnerName, $impressionType, $creativeType, $contentUrl)") + return ObjectWrapper.wrap(AdSession()) + } +} diff --git a/play-services-core/src/main/kotlin/com/google/android/gms/measurement/internal/AppMeasurementDynamiteService.kt b/play-services-core/src/main/kotlin/com/google/android/gms/measurement/internal/AppMeasurementDynamiteService.kt index a0f3373cb4..3d649ffa23 100644 --- a/play-services-core/src/main/kotlin/com/google/android/gms/measurement/internal/AppMeasurementDynamiteService.kt +++ b/play-services-core/src/main/kotlin/com/google/android/gms/measurement/internal/AppMeasurementDynamiteService.kt @@ -5,13 +5,17 @@ package com.google.android.gms.measurement.internal import android.os.Bundle +import android.os.Parcel import android.os.Parcelable import android.util.Log +import androidx.annotation.Keep import com.google.android.gms.dynamic.IObjectWrapper import com.google.android.gms.measurement.api.internal.* +import org.microg.gms.utils.warnOnTransactionIssues private const val TAG = "AppMeasurementService" +@Keep class AppMeasurementDynamiteService : IAppMeasurementDynamiteService.Stub() { override fun initialize(context: IObjectWrapper?, params: InitializationParams?, timestamp: Long) { Log.d(TAG, "Not yet implemented: initialize") @@ -79,10 +83,12 @@ class AppMeasurementDynamiteService : IAppMeasurementDynamiteService.Stub() { override fun getCurrentScreenName(receiver: IBundleReceiver?) { Log.d(TAG, "Not yet implemented: getCurrentScreenName") + receiver?.onBundle(Bundle().apply { putString("r", null) }) } override fun getCurrentScreenClass(receiver: IBundleReceiver?) { Log.d(TAG, "Not yet implemented: getCurrentScreenClass") + receiver?.onBundle(Bundle().apply { putString("r", null) }) } override fun setInstanceIdProvider(provider: IStringProvider?) { @@ -91,18 +97,22 @@ class AppMeasurementDynamiteService : IAppMeasurementDynamiteService.Stub() { override fun getCachedAppInstanceId(receiver: IBundleReceiver?) { Log.d(TAG, "Not yet implemented: getCachedAppInstanceId") + receiver?.onBundle(Bundle().apply { putString("r", null) }) } override fun getAppInstanceId(receiver: IBundleReceiver?) { Log.d(TAG, "Not yet implemented: getAppInstanceId") + receiver?.onBundle(Bundle().apply { putString("r", null) }) } override fun getGmpAppId(receiver: IBundleReceiver?) { Log.d(TAG, "Not yet implemented: getGmpAppId") + receiver?.onBundle(Bundle().apply { putString("r", null) }) } override fun generateEventId(receiver: IBundleReceiver?) { Log.d(TAG, "Not yet implemented: generateEventId") + receiver?.onBundle(Bundle().apply { putLong("r", 1L) }) } override fun beginAdUnitExposure(str: String?, j: Long) { @@ -144,6 +154,7 @@ class AppMeasurementDynamiteService : IAppMeasurementDynamiteService.Stub() { override fun performAction(bundle: Bundle?, receiver: IBundleReceiver?, j: Long) { Log.d(TAG, "Not yet implemented: performAction") + receiver?.onBundle(null) } override fun logHealthData(i: Int, str: String?, obj: IObjectWrapper?, obj2: IObjectWrapper?, obj3: IObjectWrapper?) { @@ -168,6 +179,13 @@ class AppMeasurementDynamiteService : IAppMeasurementDynamiteService.Stub() { override fun getTestFlag(receiver: IBundleReceiver?, i: Int) { Log.d(TAG, "Not yet implemented: getTestFlag") + when(i) { + 0 -> receiver?.onBundle(Bundle().apply { putString("r", "---") }) + 1 -> receiver?.onBundle(Bundle().apply { putLong("r", -1L) }) + 2 -> receiver?.onBundle(Bundle().apply { putDouble("r", -3.0) }) + 3 -> receiver?.onBundle(Bundle().apply { putInt("r", -2) }) + 4 -> receiver?.onBundle(Bundle().apply { putBoolean("r", false) }) + } } override fun setDataCollectionEnabled(z: Boolean) { @@ -195,4 +213,5 @@ class AppMeasurementDynamiteService : IAppMeasurementDynamiteService.Stub() { Log.d(TAG, "Not yet implemented: clearMeasurementEnabled") } + 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-core/src/main/kotlin/org/microg/gms/auth/appcert/AppCertManager.kt b/play-services-core/src/main/kotlin/org/microg/gms/auth/appcert/AppCertManager.kt index adc178fb49..0453e5d738 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/auth/appcert/AppCertManager.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/auth/appcert/AppCertManager.kt @@ -110,7 +110,11 @@ class AppCertManager(private val context: Context) { } override fun deliverError(error: VolleyError) { - Log.d(TAG, "Error: ${Base64.encodeToString(error.networkResponse.data, 2)}") + if (error.networkResponse != null) { + Log.d(TAG, "Error: ${Base64.encodeToString(error.networkResponse.data, 2)}") + } else { + Log.d(TAG, "Error: ${error.message}") + } deviceKeyCacheTime = 0 deferredResponse.complete(null) } @@ -163,11 +167,12 @@ class AppCertManager(private val context: Context) { ) } else { Log.d(TAG, "Using fallback spatula header based on Android ID") - val androidId = getSettings(context, CheckIn.getContentUri(context), arrayOf(CheckIn.ANDROID_ID, CheckIn.SECURITY_TOKEN)) { cursor: Cursor -> cursor.getLong(0) } + val androidId = getSettings(context, CheckIn.getContentUri(context), arrayOf(CheckIn.ANDROID_ID)) { cursor: Cursor -> cursor.getLong(0) } SpatulaHeaderProto( packageInfo = SpatulaHeaderProto.PackageInfo(packageName, packageCertificateHash), deviceId = androidId ) + return null // TODO } Log.d(TAG, "Spatula Header: $proto") return Base64.encodeToString(proto.encode(), Base64.NO_WRAP) diff --git a/play-services-core/src/main/kotlin/org/microg/gms/auth/appcert/AppCertService.kt b/play-services-core/src/main/kotlin/org/microg/gms/auth/appcert/AppCertService.kt index e73017e21f..9b14af49b1 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/auth/appcert/AppCertService.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/auth/appcert/AppCertService.kt @@ -38,5 +38,5 @@ class AppCertServiceImpl(private val context: Context) : IAppCertService.Stub() return runBlocking { manager.getSpatulaHeader(packageName) } } - override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean = warnOnTransactionIssues(code, reply, flags) { super.onTransact(code, data, reply, flags) } + 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-core/src/main/kotlin/org/microg/gms/auth/credentials/CredentialsService.kt b/play-services-core/src/main/kotlin/org/microg/gms/auth/credentials/CredentialsService.kt index adc0370ba4..6ac52845e1 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/auth/credentials/CredentialsService.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/auth/credentials/CredentialsService.kt @@ -5,28 +5,44 @@ package org.microg.gms.auth.credentials -import android.os.Bundle import android.os.Parcel import android.util.Log import com.google.android.gms.auth.api.credentials.CredentialRequest import com.google.android.gms.auth.api.credentials.internal.* +import com.google.android.gms.common.Feature import com.google.android.gms.common.api.CommonStatusCodes import com.google.android.gms.common.api.Status +import com.google.android.gms.common.internal.ConnectionInfo import com.google.android.gms.common.internal.GetServiceRequest import com.google.android.gms.common.internal.IGmsCallbacks import org.microg.gms.BaseService import org.microg.gms.common.GmsService +import org.microg.gms.common.PackageUtils import org.microg.gms.utils.warnOnTransactionIssues private const val TAG = "CredentialService" +val FEATURES = arrayOf( + Feature("auth_api_credentials_begin_sign_in", 8), + Feature("auth_api_credentials_sign_out", 2), + Feature("auth_api_credentials_authorize", 1), + Feature("auth_api_credentials_revoke_access", 1), + Feature("auth_api_credentials_save_password", 4), + Feature("auth_api_credentials_get_sign_in_intent", 6), + Feature("auth_api_credentials_save_account_linking_token", 3), + Feature("auth_api_credentials_get_phone_number_hint_intent", 3) +) + class CredentialsService : BaseService(TAG, GmsService.CREDENTIALS) { override fun handleServiceRequest(callback: IGmsCallbacks, request: GetServiceRequest, service: GmsService) { - callback.onPostInitComplete(CommonStatusCodes.SUCCESS, CredentialsServiceImpl(), Bundle()) + val packageName = PackageUtils.getAndCheckCallingPackage(this, request.packageName) + ?: throw IllegalArgumentException("Missing package name") + val binder = CredentialsServiceImpl(packageName).asBinder() + callback.onPostInitCompleteWithConnectionInfo(CommonStatusCodes.SUCCESS, binder, ConnectionInfo().apply { features = FEATURES }) } } -class CredentialsServiceImpl : ICredentialsService.Stub() { +class CredentialsServiceImpl(private val packageName: String) : ICredentialsService.Stub() { override fun request(callbacks: ICredentialsCallbacks, request: CredentialRequest) { Log.d(TAG, "request($request)") callbacks.onStatus(Status.CANCELED) @@ -52,5 +68,6 @@ class CredentialsServiceImpl : ICredentialsService.Stub() { callbacks.onStatus(Status.SUCCESS) } - override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean = warnOnTransactionIssues(code, reply, flags) { super.onTransact(code, data, reply, flags) } + 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-core/src/main/kotlin/org/microg/gms/auth/login/FidoHandler.kt b/play-services-core/src/main/kotlin/org/microg/gms/auth/login/FidoHandler.kt index bcc07012e0..24a12116a0 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/auth/login/FidoHandler.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/auth/login/FidoHandler.kt @@ -5,7 +5,7 @@ package org.microg.gms.auth.login -import android.os.Build +import android.os.Build.VERSION.SDK_INT import android.os.Bundle import android.util.Base64 import android.util.Log @@ -34,8 +34,8 @@ class FidoHandler(private val activity: LoginActivity) : TransportHandlerCallbac setOfNotNull( BluetoothTransportHandler(activity, this), NfcTransportHandler(activity, this), - if (Build.VERSION.SDK_INT >= 21) UsbTransportHandler(activity, this) else null, - if (Build.VERSION.SDK_INT >= 23) ScreenLockTransportHandler(activity, this) else null + if (SDK_INT >= 21) UsbTransportHandler(activity, this) else null, + if (SDK_INT >= 23) ScreenLockTransportHandler(activity, this) else null ) } diff --git a/play-services-core/src/main/kotlin/org/microg/gms/auth/proxy/AuthProxyService.kt b/play-services-core/src/main/kotlin/org/microg/gms/auth/proxy/AuthProxyService.kt index e1cedd593d..ede33d2694 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/auth/proxy/AuthProxyService.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/auth/proxy/AuthProxyService.kt @@ -59,5 +59,5 @@ class AuthServiceImpl(private val context: Context, private val lifecycle: Lifec override fun getLifecycle(): Lifecycle = lifecycle - override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean = warnOnTransactionIssues(code, reply, flags) { super.onTransact(code, data, reply, flags) } + 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-core/src/main/kotlin/org/microg/gms/auth/signin/AuthSignInService.kt b/play-services-core/src/main/kotlin/org/microg/gms/auth/signin/AuthSignInService.kt new file mode 100644 index 0000000000..a11badabf8 --- /dev/null +++ b/play-services-core/src/main/kotlin/org/microg/gms/auth/signin/AuthSignInService.kt @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2013-2017 microG Project Team + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.microg.gms.auth.signin + +import android.os.Bundle +import android.os.Parcel +import android.util.Log +import com.google.android.gms.auth.api.signin.GoogleSignInOptions +import com.google.android.gms.auth.api.signin.internal.ISignInCallbacks +import com.google.android.gms.auth.api.signin.internal.ISignInService +import com.google.android.gms.common.api.CommonStatusCodes +import com.google.android.gms.common.api.Scope +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 org.microg.gms.BaseService +import org.microg.gms.common.GmsService +import org.microg.gms.common.PackageUtils +import org.microg.gms.utils.warnOnTransactionIssues + +private const val TAG = "AuthSignInService" + +class AuthSignInService : BaseService(TAG, GmsService.AUTH_SIGN_IN) { + override fun handleServiceRequest(callback: IGmsCallbacks, request: GetServiceRequest, service: GmsService) { + val packageName = PackageUtils.getAndCheckCallingPackage(this, request.packageName) + ?: throw IllegalArgumentException("Missing package name") + val binder = SignInServiceImpl(packageName, request.scopes.asList(), request.extras).asBinder() + callback.onPostInitComplete(CommonStatusCodes.SUCCESS, binder, Bundle()) + } +} + +class SignInServiceImpl(private val packageName: String, private val scopes: List, private val extras: Bundle) : ISignInService.Stub() { + override fun silentSignIn(callbacks: ISignInCallbacks?, options: GoogleSignInOptions?) { + Log.d(TAG, "Not yet implemented: signIn: $options") + callbacks?.onSignIn(null, Status.INTERNAL_ERROR) + } + + override fun signOut(callbacks: ISignInCallbacks?, options: GoogleSignInOptions?) { + Log.d(TAG, "Not yet implemented: signOut: $options") + callbacks?.onSignOut(Status.INTERNAL_ERROR) + } + + override fun revokeAccess(callbacks: ISignInCallbacks?, options: GoogleSignInOptions?) { + Log.d(TAG, "Not yet implemented: revokeAccess: $options") + callbacks?.onRevokeAccess(Status.INTERNAL_ERROR) + } + + 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/org/microg/gms/checkin/CheckinPrefs.kt b/play-services-core/src/main/kotlin/org/microg/gms/checkin/CheckinPreferences.kt similarity index 57% rename from play-services-core/src/main/kotlin/org/microg/gms/checkin/CheckinPrefs.kt rename to play-services-core/src/main/kotlin/org/microg/gms/checkin/CheckinPreferences.kt index 844252583a..0323466548 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/checkin/CheckinPrefs.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/checkin/CheckinPreferences.kt @@ -5,10 +5,11 @@ package org.microg.gms.checkin import android.content.Context +import android.content.Intent import org.microg.gms.settings.SettingsContract import org.microg.gms.settings.SettingsContract.CheckIn -object CheckinPrefs { +object CheckinPreferences { @JvmStatic fun isEnabled(context: Context): Boolean { @@ -18,4 +19,14 @@ object CheckinPrefs { } } + @JvmStatic + fun setEnabled(context: Context, enabled: Boolean) { + SettingsContract.setSettings(context, CheckIn.getContentUri(context)) { + put(CheckIn.ENABLED, enabled) + } + if (enabled) { + context.sendOrderedBroadcast(Intent(context, TriggerReceiver::class.java), null) + } + } + } diff --git a/play-services-core/src/main/kotlin/org/microg/gms/clearcut/ClearcutLoggerService.kt b/play-services-core/src/main/kotlin/org/microg/gms/clearcut/ClearcutLoggerService.kt index 1e0d9b82d3..496c5f59f4 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/clearcut/ClearcutLoggerService.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/clearcut/ClearcutLoggerService.kt @@ -34,32 +34,32 @@ class ClearcutLoggerServiceImpl(private val lifecycle: Lifecycle) : IClearcutLog override fun log(callbacks: IClearcutLoggerCallbacks, event: LogEventParcelable) { lifecycleScope.launchWhenStarted { - callbacks.onLogResult(Status.SUCCESS) + runCatching { callbacks.onLogResult(Status.SUCCESS) } } } override fun forceUpload(callbacks: IClearcutLoggerCallbacks) { lifecycleScope.launchWhenStarted { - callbacks.onLogResult(Status.SUCCESS) + runCatching { callbacks.onLogResult(Status.SUCCESS) } } } override fun startCollectForDebug(callbacks: IClearcutLoggerCallbacks) { lifecycleScope.launchWhenStarted { collectForDebugExpiryTime = System.currentTimeMillis() + COLLECT_FOR_DEBUG_DURATION - callbacks.onStartCollectForDebugResult(Status.SUCCESS, collectForDebugExpiryTime) + runCatching { callbacks.onStartCollectForDebugResult(Status.SUCCESS, collectForDebugExpiryTime) } } } override fun stopCollectForDebug(callbacks: IClearcutLoggerCallbacks) { lifecycleScope.launchWhenStarted { - callbacks.onStopCollectForDebugResult(Status.SUCCESS) + runCatching { callbacks.onStopCollectForDebugResult(Status.SUCCESS) } } } override fun getCollectForDebugExpiryTime(callbacks: IClearcutLoggerCallbacks) { lifecycleScope.launchWhenStarted { - callbacks.onCollectForDebugExpiryTime(Status.SUCCESS, collectForDebugExpiryTime) + runCatching { callbacks.onCollectForDebugExpiryTime(Status.SUCCESS, collectForDebugExpiryTime) } } } @@ -69,11 +69,11 @@ class ClearcutLoggerServiceImpl(private val lifecycle: Lifecycle) : IClearcutLog override fun getLogEventParcelables(callbacks: IClearcutLoggerCallbacks) { lifecycleScope.launchWhenStarted { - callbacks.onLogEventParcelables(DataHolder.empty(CommonStatusCodes.SUCCESS)) + runCatching { callbacks.onLogEventParcelables(DataHolder.empty(CommonStatusCodes.SUCCESS)) } } } override fun getLifecycle(): Lifecycle = lifecycle - override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean = warnOnTransactionIssues(code, reply, flags) { super.onTransact(code, data, reply, flags) } + 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-core/src/main/kotlin/org/microg/gms/credential/CredentialManagerService.kt b/play-services-core/src/main/kotlin/org/microg/gms/credential/CredentialManagerService.kt new file mode 100644 index 0000000000..447d5c2172 --- /dev/null +++ b/play-services-core/src/main/kotlin/org/microg/gms/credential/CredentialManagerService.kt @@ -0,0 +1,90 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.credential + +import android.content.Context +import android.os.Bundle +import android.os.Parcel +import android.util.Base64 +import android.util.Log +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.lifecycleScope +import com.google.android.gms.common.ConnectionResult +import com.google.android.gms.common.Feature +import com.google.android.gms.common.api.Status +import com.google.android.gms.common.api.internal.IStatusCallback +import com.google.android.gms.common.internal.ConnectionInfo +import com.google.android.gms.common.internal.GetServiceRequest +import com.google.android.gms.common.internal.IGmsCallbacks +import com.google.android.gms.credential.manager.common.IPendingIntentCallback +import com.google.android.gms.credential.manager.common.ISettingsCallback +import com.google.android.gms.credential.manager.firstparty.internal.ICredentialManagerService +import com.google.android.gms.credential.manager.invocationparams.CredentialManagerInvocationParams +import org.microg.gms.BaseService +import org.microg.gms.common.GmsService +import org.microg.gms.common.PackageUtils +import org.microg.gms.utils.toBase64 +import org.microg.gms.utils.warnOnTransactionIssues + +private const val TAG = "CredentialManager" + +class CredentialManagerService : BaseService(TAG, GmsService.CREDENTIAL_MANAGER) { + override fun handleServiceRequest(callback: IGmsCallbacks, request: GetServiceRequest, service: GmsService) { + if (!PackageUtils.callerHasExtendedAccess(this)) { + Log.d(TAG, "No access to ${request.packageName}, lacks permission.") + callback.onPostInitComplete(ConnectionResult.API_DISABLED_FOR_CONNECTION, null, null) + return + } + callback.onPostInitCompleteWithConnectionInfo(ConnectionResult.SUCCESS, CredentialManagerServiceImpl(this, lifecycle), ConnectionInfo().apply { + features = arrayOf( + Feature("credential_manager_first_party_api", 1), + Feature("password_checkup_first_party_api", 1), + Feature("user_service_security", 1), + ) + }) + } + +} + +private class CredentialManagerServiceImpl(private val context: Context, private val lifecycle: Lifecycle) : ICredentialManagerService.Stub(), LifecycleOwner { + override fun getLifecycle(): Lifecycle = lifecycle + + override fun getCredentialManagerIntent(callback: IPendingIntentCallback?, params: CredentialManagerInvocationParams?) { + Log.d(TAG, "Not yet implemented: getCredentialManagerIntent $params") + lifecycleScope.launchWhenStarted { + try { + callback?.onPendingIntent(Status.INTERNAL_ERROR, null) + } catch (e: Exception) { + Log.w(TAG, e) + } + } + } + + override fun getSetting(callback: ISettingsCallback?, key: String?) { + Log.d(TAG, "Not yet implemented: getSetting $key") + lifecycleScope.launchWhenStarted { + try { + callback?.onSetting(Status.INTERNAL_ERROR, null) + } catch (e: Exception) { + Log.w(TAG, e) + } + } + } + + override fun setSetting(callback: IStatusCallback?, key: String?, value: ByteArray?) { + Log.d(TAG, "Not yet implemented: setSetting $key ${value?.toBase64(Base64.NO_WRAP)}") + lifecycleScope.launchWhenStarted { + try { + callback?.onResult(Status.INTERNAL_ERROR) + } catch (e: Exception) { + Log.w(TAG, 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/org/microg/gms/gcm/GcmPrefs.kt b/play-services-core/src/main/kotlin/org/microg/gms/gcm/GcmPrefs.kt index 8982149e83..b9aaeacff8 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/gcm/GcmPrefs.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/gcm/GcmPrefs.kt @@ -39,7 +39,7 @@ data class GcmPrefs( const val PREF_NETWORK_OTHER = Gcm.NETWORK_OTHER private const val MIN_INTERVAL = 5 * 60 * 1000 // 5 minutes - private const val MAX_INTERVAL = 30 * 60 * 1000 // 30 minutes + private const val MAX_INTERVAL = 15 * 60 * 1000 // 15 minutes @JvmStatic fun get(context: Context): GcmPrefs { @@ -73,6 +73,14 @@ data class GcmPrefs( gcmPrefs.setEnabled(context, config.enabled) } + fun setEnabled(context: Context, enabled: Boolean) { + val prefs = get(context) + setSettings(context, Gcm.getContentUri(context)) { + put(Gcm.ENABLE_GCM, enabled) + } + prefs.setEnabled(context, enabled) + } + @JvmStatic fun clearLastPersistedId(context: Context) { setSettings(context, Gcm.getContentUri(context)) { diff --git a/play-services-core/src/main/kotlin/org/microg/gms/gcm/PushRegisterService.kt b/play-services-core/src/main/kotlin/org/microg/gms/gcm/PushRegisterService.kt index 864c9e8384..6f1237259b 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/gcm/PushRegisterService.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/gcm/PushRegisterService.kt @@ -15,12 +15,11 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleService import androidx.lifecycle.lifecycleScope -import org.microg.gms.checkin.CheckinPrefs +import org.microg.gms.checkin.CheckinPreferences import org.microg.gms.checkin.CheckinService import org.microg.gms.checkin.LastCheckinInfo import org.microg.gms.common.ForegroundServiceContext import org.microg.gms.common.PackageUtils -import org.microg.gms.common.Utils import org.microg.gms.gcm.GcmConstants.* import org.microg.gms.ui.AskPushPermission import java.util.concurrent.atomic.AtomicBoolean @@ -30,7 +29,7 @@ import kotlin.coroutines.suspendCoroutine private const val TAG = "GmsGcmRegister" private suspend fun ensureCheckinIsUpToDate(context: Context) { - if (!CheckinPrefs.isEnabled(context)) throw RuntimeException("Checkin disabled") + if (!CheckinPreferences.isEnabled(context)) throw RuntimeException("Checkin disabled") val lastCheckin = LastCheckinInfo.read(context).lastCheckin if (lastCheckin < System.currentTimeMillis() - CheckinService.MAX_VALID_CHECKIN_AGE) { val resultData: Bundle = suspendCoroutine { continuation -> diff --git a/play-services-core/src/main/kotlin/org/microg/gms/measurement/MeasurementService.kt b/play-services-core/src/main/kotlin/org/microg/gms/measurement/MeasurementService.kt index 7f0a0afe40..8581822718 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/measurement/MeasurementService.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/measurement/MeasurementService.kt @@ -54,5 +54,5 @@ class MeasurementServiceImpl : IMeasurementService.Stub() { Log.d(TAG, "setDefaultEventParameters($params) for $app") } - override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean = warnOnTransactionIssues(code, reply, flags) { super.onTransact(code, data, reply, flags) } + 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-core/src/main/kotlin/org/microg/gms/phenotype/PhenotypeService.kt b/play-services-core/src/main/kotlin/org/microg/gms/phenotype/PhenotypeService.kt index 332cae4093..2619f74588 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/phenotype/PhenotypeService.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/phenotype/PhenotypeService.kt @@ -8,6 +8,7 @@ package org.microg.gms.phenotype import android.os.Parcel import android.util.Log import com.google.android.gms.common.api.Status +import com.google.android.gms.common.api.internal.IStatusCallback import com.google.android.gms.common.internal.GetServiceRequest import com.google.android.gms.common.internal.IGmsCallbacks import com.google.android.gms.phenotype.* @@ -139,6 +140,14 @@ class PhenotypeServiceImpl : IPhenotypeService.Stub() { callbacks.onExperimentTokens(Status.SUCCESS, ExperimentTokens()) } + override fun syncAfterOperation2(callbacks: IPhenotypeCallbacks?, p1: Long) { + Log.d(TAG, "Not yet implemented: syncAfterOperation2") + } + + override fun setRuntimeProperties(callbacks: IStatusCallback?, p1: String?, p2: ByteArray?) { + Log.d(TAG, "Not yet implemented: setRuntimeProperties") + } + override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean = - warnOnTransactionIssues(code, reply, flags) { super.onTransact(code, data, reply, flags) } + warnOnTransactionIssues(code, reply, flags, TAG) { super.onTransact(code, data, reply, flags) } } diff --git a/play-services-core/src/main/kotlin/org/microg/gms/provision/ProvisionService.kt b/play-services-core/src/main/kotlin/org/microg/gms/provision/ProvisionService.kt index b7c7da7042..3720e312ac 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/provision/ProvisionService.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/provision/ProvisionService.kt @@ -17,6 +17,7 @@ import org.microg.gms.checkin.setCheckinServiceConfiguration import org.microg.gms.droidguard.core.DroidGuardPreferences import org.microg.gms.gcm.getGcmServiceInfo import org.microg.gms.gcm.setGcmServiceConfiguration +import org.microg.gms.location.LocationSettings import org.microg.gms.safetynet.SafetyNetPreferences class ProvisionService : LifecycleService() { @@ -38,6 +39,16 @@ class ProvisionService : LifecycleService() { SafetyNetPreferences.setEnabled(this@ProvisionService, it) DroidGuardPreferences.setEnabled(this@ProvisionService, it) } + LocationSettings(this@ProvisionService).apply { + intent?.extras?.getBooleanOrNull("wifi_mls")?.let { wifiMls = it } + intent?.extras?.getBooleanOrNull("cell_mls")?.let { cellMls = it } + intent?.extras?.getBooleanOrNull("wifi_learning")?.let { wifiLearning = it } + intent?.extras?.getBooleanOrNull("cell_learning")?.let { cellLearning = it } + intent?.extras?.getBooleanOrNull("wifi_moving")?.let { wifiMoving = it } + intent?.extras?.getBooleanOrNull("nominatim_enabled")?.let { + geocoderNominatim = it + } + } // What else? delay(2 * 1000) // Wait 2 seconds to give provisioning some extra time 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 new file mode 100644 index 0000000000..b8ecaf8a89 --- /dev/null +++ b/play-services-core/src/main/kotlin/org/microg/gms/signin/SignInService.kt @@ -0,0 +1,88 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.signin + +import android.accounts.Account +import android.os.Bundle +import android.os.Parcel +import android.util.Log +import com.google.android.gms.common.ConnectionResult +import com.google.android.gms.common.api.CommonStatusCodes +import com.google.android.gms.common.internal.* +import com.google.android.gms.signin.internal.* +import org.microg.gms.BaseService +import org.microg.gms.common.GmsService +import org.microg.gms.common.PackageUtils +import org.microg.gms.utils.warnOnTransactionIssues + +private const val TAG = "SignInService" + +class SignInService : BaseService(TAG, GmsService.SIGN_IN) { + override fun handleServiceRequest(callback: IGmsCallbacks, request: GetServiceRequest, service: GmsService) { + val packageName = PackageUtils.getAndCheckCallingPackage(this, request.packageName) + ?: throw IllegalArgumentException("Missing package name") + val binder = SignInServiceImpl().asBinder() + callback.onPostInitComplete(CommonStatusCodes.SUCCESS, binder, Bundle()) + } +} + +class SignInServiceImpl : ISignInService.Stub() { + override fun clearAccountFromSessionStore(sessionId: Int) { + Log.d(TAG, "Not yet implemented: clearAccountFromSessionStore $sessionId") + } + + override fun putAccount(sessionId: Int, account: Account?, callbacks: ISignInCallbacks?) { + Log.d(TAG, "Not yet implemented: putAccount") + } + + override fun saveDefaultAccount(accountAccessor: IAccountAccessor?, sessionId: Int, crossClient: Boolean) { + Log.d(TAG, "Not yet implemented: saveDefaultAccount $sessionId $crossClient") + } + + override fun saveConsent(request: RecordConsentRequest?, callbacks: ISignInCallbacks?) { + Log.d(TAG, "Not yet implemented: saveConsent") + } + + override fun getCurrentAccount(callbacks: ISignInCallbacks?) { + Log.d(TAG, "Not yet implemented: getCurrentAccount") + } + + override fun signIn(request: SignInRequest?, callbacks: ISignInCallbacks?) { + Log.d(TAG, "Not yet implemented: signIn $request") + callbacks?.onSignIn(SignInResponse().apply { + connectionResult = ConnectionResult(ConnectionResult.INTERNAL_ERROR) + response = ResolveAccountResponse().apply { + connectionResult = ConnectionResult(ConnectionResult.INTERNAL_ERROR) + } + }) + } + + override fun setGamesHasBeenGreeted(hasGreeted: Boolean) { + Log.d(TAG, "Not yet implemented: setGamesHasBeenGreeted") + } + + override fun recordConsentByConsentResult(request: RecordConsentByConsentResultRequest?, callbacks: ISignInCallbacks?) { + Log.d(TAG, "Not yet implemented: recordConsentByConsentResult") + } + + override fun authAccount(request: AuthAccountRequest?, callbacks: ISignInCallbacks?) { + Log.d(TAG, "Not yet implemented: authAccount") + } + + override fun onCheckServerAuthorization(result: CheckServerAuthResult?) { + Log.d(TAG, "Not yet implemented: onCheckServerAuthorization") + } + + override fun onUploadServerAuthCode(sessionId: Int) { + Log.d(TAG, "Not yet implemented: onUploadServerAuthCode") + } + + override fun resolveAccount(request: ResolveAccountRequest?, callbacks: IResolveAccountCallbacks?) { + Log.d(TAG, "Not yet implemented: resolveAccount") + } + + 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/org/microg/gms/udc/FacsCacheService.kt b/play-services-core/src/main/kotlin/org/microg/gms/udc/FacsCacheService.kt index c48e0e8151..149fece0c5 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/udc/FacsCacheService.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/udc/FacsCacheService.kt @@ -51,5 +51,5 @@ class FacsCacheServiceImpl : IFacsCacheService.Stub() { callbacks.onWriteDeviceLevelSettingsResult(Status.CANCELED) } - override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean = warnOnTransactionIssues(code, reply, flags) { super.onTransact(code, data, reply, flags) } + 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-core/src/main/kotlin/org/microg/gms/ui/DeviceRegistrationFragment.kt b/play-services-core/src/main/kotlin/org/microg/gms/ui/DeviceRegistrationFragment.kt index 88938f7b52..b7d7e9c8ad 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/ui/DeviceRegistrationFragment.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/ui/DeviceRegistrationFragment.kt @@ -5,50 +5,171 @@ package org.microg.gms.ui +import android.annotation.SuppressLint +import android.net.Uri import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.fragment.app.Fragment +import android.os.Handler +import android.text.format.DateUtils +import android.util.Log +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts import androidx.lifecycle.lifecycleScope +import androidx.preference.ListPreference +import androidx.preference.Preference +import androidx.preference.PreferenceCategory +import androidx.preference.PreferenceFragmentCompat import com.google.android.gms.R -import com.google.android.gms.databinding.DeviceRegistrationFragmentBinding -import org.microg.gms.checkin.ServiceInfo +import org.microg.gms.checkin.CheckinPreferences import org.microg.gms.checkin.getCheckinServiceInfo -import org.microg.gms.checkin.setCheckinServiceConfiguration +import org.microg.gms.profile.ProfileManager +import org.microg.gms.profile.ProfileManager.PROFILE_AUTO +import org.microg.gms.profile.ProfileManager.PROFILE_NATIVE +import org.microg.gms.profile.ProfileManager.PROFILE_REAL +import org.microg.gms.profile.ProfileManager.PROFILE_SYSTEM +import org.microg.gms.profile.ProfileManager.PROFILE_USER +import java.io.File +import java.io.FileOutputStream -class DeviceRegistrationFragment : Fragment(R.layout.device_registration_fragment) { - private lateinit var binding: DeviceRegistrationFragmentBinding +class DeviceRegistrationFragment : PreferenceFragmentCompat() { + private lateinit var switchBarPreference: SwitchBarPreference + private lateinit var deviceProfile: ListPreference + private lateinit var importProfile: Preference + private lateinit var serial: Preference + private lateinit var statusCategory: PreferenceCategory + private lateinit var status: Preference + private lateinit var androidId: Preference + private val handler = Handler() + private val updateRunnable = Runnable { updateStatus() } + private lateinit var profileFileImport: ActivityResultLauncher - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - binding = DeviceRegistrationFragmentBinding.inflate(inflater, container, false) - binding.switchBarCallback = object : PreferenceSwitchBarCallback { - override fun onChecked(newStatus: Boolean) { - setEnabled(newStatus) + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + profileFileImport = registerForActivityResult(ActivityResultContracts.GetContent(), this::onFileSelected) + } + + private fun onFileSelected(uri: Uri?) { + if (uri == null) return + try { + val context = requireContext() + val file = File.createTempFile("profile_", ".xml", context.cacheDir) + context.contentResolver.openInputStream(uri)?.use { inputStream -> + FileOutputStream(file).use { inputStream.copyTo(it) } + } + val success = ProfileManager.importUserProfile(context, file) + file.delete() + if (success && ProfileManager.isAutoProfile(context, PROFILE_USER)) { + ProfileManager.setProfile(context, PROFILE_USER) } + updateStatus() + } catch (e: Exception) { + Log.w(TAG, e) } - return binding.root } - fun setEnabled(newStatus: Boolean) { - val appContext = requireContext().applicationContext - lifecycleScope.launchWhenResumed { - val info = getCheckinServiceInfo(appContext) - val newConfiguration = info.configuration.copy(enabled = newStatus) - setCheckinServiceConfiguration(appContext, newConfiguration) - displayServiceInfo(info.copy(configuration = newConfiguration)) + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + addPreferencesFromResource(R.xml.preferences_device_registration) + } + + @SuppressLint("RestrictedApi") + override fun onBindPreferences() { + switchBarPreference = preferenceScreen.findPreference("pref_checkin_enabled") ?: switchBarPreference + deviceProfile = preferenceScreen.findPreference("pref_device_profile") ?: deviceProfile + importProfile = preferenceScreen.findPreference("pref_device_profile_import") ?: importProfile + serial = preferenceScreen.findPreference("pref_device_serial") ?: serial + statusCategory = preferenceScreen.findPreference("prefcat_device_registration_status") ?: statusCategory + status = preferenceScreen.findPreference("pref_device_registration_status") ?: status + androidId = preferenceScreen.findPreference("pref_device_registration_android_id") ?: androidId + + deviceProfile.setOnPreferenceChangeListener { _, newValue -> + ProfileManager.setProfile(requireContext(), newValue as String? ?: PROFILE_AUTO) + updateStatus() + true + } + importProfile.setOnPreferenceClickListener { + profileFileImport.launch("text/xml") + true + } + switchBarPreference.setOnPreferenceChangeListener { _, newValue -> + val newStatus = newValue as Boolean + CheckinPreferences.setEnabled(requireContext(), newStatus) + true } } - private fun displayServiceInfo(serviceInfo: ServiceInfo) { - binding.checkinEnabled = serviceInfo.configuration.enabled + private fun configureProfilePreference() { + val context = requireContext() + val configuredProfile = ProfileManager.getConfiguredProfile(context) + val autoProfile = ProfileManager.getAutoProfile(context) + val autoProfileName = when (autoProfile) { + PROFILE_NATIVE -> getString(R.string.profile_name_native) + PROFILE_REAL -> getString(R.string.profile_name_real) + else -> ProfileManager.getProfileName(context, autoProfile) + } + val profiles = + mutableListOf(PROFILE_AUTO, PROFILE_NATIVE, PROFILE_REAL) + val profileNames = mutableListOf(getString(R.string.profile_name_auto, autoProfileName), getString(R.string.profile_name_native), getString(R.string.profile_name_real)) + if (ProfileManager.hasProfile(context, PROFILE_SYSTEM)) { + profiles.add(PROFILE_SYSTEM) + profileNames.add(getString(R.string.profile_name_system, ProfileManager.getProfileName(context, PROFILE_SYSTEM))) + } + if (ProfileManager.hasProfile(context, PROFILE_USER)) { + profiles.add(PROFILE_USER) + profileNames.add(getString(R.string.profile_name_user, ProfileManager.getProfileName(context, PROFILE_USER))) + } + for (profile in R.xml::class.java.declaredFields.map { it.name } + .filter { it.startsWith("profile_") } + .map { it.substring(8) } + .sorted()) { + val profileName = ProfileManager.getProfileName(context, profile) + if (profileName != null) { + profiles.add(profile) + profileNames.add(profileName) + } + } + deviceProfile.entryValues = profiles.toTypedArray() + deviceProfile.entries = profileNames.toTypedArray() + deviceProfile.value = configuredProfile + deviceProfile.summary = + profiles.indexOf(configuredProfile).takeIf { it >= 0 }?.let { profileNames[it] } ?: "Unknown" } override fun onResume() { super.onResume() + + switchBarPreference.isChecked = CheckinPreferences.isEnabled(requireContext()) + + updateStatus() + } + + override fun onPause() { + super.onPause() + handler.removeCallbacks(updateRunnable) + } + + private fun updateStatus() { + handler.removeCallbacks(updateRunnable) + handler.postDelayed(updateRunnable, UPDATE_INTERVAL) val appContext = requireContext().applicationContext lifecycleScope.launchWhenResumed { - displayServiceInfo(getCheckinServiceInfo(appContext)) + configureProfilePreference() + serial.summary = ProfileManager.getSerial(appContext) + val serviceInfo = getCheckinServiceInfo(appContext) + statusCategory.isVisible = serviceInfo.configuration.enabled + if (serviceInfo.lastCheckin > 0) { + status.summary = getString( + R.string.checkin_last_registration, + DateUtils.getRelativeTimeSpanString(serviceInfo.lastCheckin, System.currentTimeMillis(), 0) + ) + androidId.isVisible = true + androidId.summary = serviceInfo.androidId.toString(16) + } else { + status.summary = getString(R.string.checkin_not_registered) + androidId.isVisible = false + } } } + + companion object { + private const val UPDATE_INTERVAL = 1000L + } } diff --git a/play-services-core/src/main/kotlin/org/microg/gms/ui/DeviceRegistrationPreferencesFragment.kt b/play-services-core/src/main/kotlin/org/microg/gms/ui/DeviceRegistrationPreferencesFragment.kt deleted file mode 100644 index 24e6d61bfc..0000000000 --- a/play-services-core/src/main/kotlin/org/microg/gms/ui/DeviceRegistrationPreferencesFragment.kt +++ /dev/null @@ -1,164 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2020, microG Project Team - * SPDX-License-Identifier: Apache-2.0 - */ - -package org.microg.gms.ui - -import android.annotation.SuppressLint -import android.net.Uri -import android.os.Bundle -import android.os.Handler -import android.text.format.DateUtils -import android.util.Log -import androidx.activity.result.ActivityResultLauncher -import androidx.activity.result.contract.ActivityResultContracts -import androidx.lifecycle.lifecycleScope -import androidx.preference.ListPreference -import androidx.preference.Preference -import androidx.preference.PreferenceCategory -import androidx.preference.PreferenceFragmentCompat -import com.google.android.gms.R -import org.microg.gms.checkin.getCheckinServiceInfo -import org.microg.gms.profile.ProfileManager -import org.microg.gms.profile.ProfileManager.PROFILE_AUTO -import org.microg.gms.profile.ProfileManager.PROFILE_NATIVE -import org.microg.gms.profile.ProfileManager.PROFILE_REAL -import org.microg.gms.profile.ProfileManager.PROFILE_SYSTEM -import org.microg.gms.profile.ProfileManager.PROFILE_USER -import java.io.File -import java.io.FileOutputStream - -class DeviceRegistrationPreferencesFragment : PreferenceFragmentCompat() { - private lateinit var deviceProfile: ListPreference - private lateinit var importProfile: Preference - private lateinit var serial: Preference - private lateinit var statusCategory: PreferenceCategory - private lateinit var status: Preference - private lateinit var androidId: Preference - private val handler = Handler() - private val updateRunnable = Runnable { updateStatus() } - private lateinit var profileFileImport: ActivityResultLauncher - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - profileFileImport = registerForActivityResult(ActivityResultContracts.GetContent(), this::onFileSelected) - } - - private fun onFileSelected(uri: Uri?) { - if (uri == null) return - try { - val context = requireContext() - val file = File.createTempFile("profile_", ".xml", context.cacheDir) - context.contentResolver.openInputStream(uri)?.use { inputStream -> - FileOutputStream(file).use { inputStream.copyTo(it) } - } - val success = ProfileManager.importUserProfile(context, file) - file.delete() - if (success && ProfileManager.isAutoProfile(context, PROFILE_USER)) { - ProfileManager.setProfile(context, PROFILE_USER) - } - updateStatus() - } catch (e: Exception) { - Log.w(TAG, e) - } - } - - override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { - addPreferencesFromResource(R.xml.preferences_device_registration) - } - - @SuppressLint("RestrictedApi") - override fun onBindPreferences() { - deviceProfile = preferenceScreen.findPreference("pref_device_profile") ?: deviceProfile - importProfile = preferenceScreen.findPreference("pref_device_profile_import") ?: importProfile - serial = preferenceScreen.findPreference("pref_device_serial") ?: serial - statusCategory = preferenceScreen.findPreference("prefcat_device_registration_status") ?: statusCategory - status = preferenceScreen.findPreference("pref_device_registration_status") ?: status - androidId = preferenceScreen.findPreference("pref_device_registration_android_id") ?: androidId - - deviceProfile.setOnPreferenceChangeListener { _, newValue -> - ProfileManager.setProfile(requireContext(), newValue as String? ?: PROFILE_AUTO) - updateStatus() - true - } - importProfile.setOnPreferenceClickListener { - profileFileImport.launch("text/xml") - true - } - } - - private fun configureProfilePreference() { - val context = requireContext() - val configuredProfile = ProfileManager.getConfiguredProfile(context) - val autoProfile = ProfileManager.getAutoProfile(context) - val autoProfileName = when (autoProfile) { - PROFILE_NATIVE -> getString(R.string.profile_name_native) - PROFILE_REAL -> getString(R.string.profile_name_real) - else -> ProfileManager.getProfileName(context, autoProfile) - } - val profiles = - mutableListOf(PROFILE_AUTO, PROFILE_NATIVE, PROFILE_REAL) - val profileNames = mutableListOf(getString(R.string.profile_name_auto, autoProfileName), getString(R.string.profile_name_native), getString(R.string.profile_name_real)) - if (ProfileManager.hasProfile(context, PROFILE_SYSTEM)) { - profiles.add(PROFILE_SYSTEM) - profileNames.add(getString(R.string.profile_name_system, ProfileManager.getProfileName(context, PROFILE_SYSTEM))) - } - if (ProfileManager.hasProfile(context, PROFILE_USER)) { - profiles.add(PROFILE_USER) - profileNames.add(getString(R.string.profile_name_user, ProfileManager.getProfileName(context, PROFILE_USER))) - } - for (profile in R.xml::class.java.declaredFields.map { it.name } - .filter { it.startsWith("profile_") } - .map { it.substring(8) } - .sorted()) { - val profileName = ProfileManager.getProfileName(context, profile) - if (profileName != null) { - profiles.add(profile) - profileNames.add(profileName) - } - } - deviceProfile.entryValues = profiles.toTypedArray() - deviceProfile.entries = profileNames.toTypedArray() - deviceProfile.value = configuredProfile - deviceProfile.summary = - profiles.indexOf(configuredProfile).takeIf { it >= 0 }?.let { profileNames[it] } ?: "Unknown" - } - - override fun onResume() { - super.onResume() - updateStatus() - } - - override fun onPause() { - super.onPause() - handler.removeCallbacks(updateRunnable) - } - - private fun updateStatus() { - handler.removeCallbacks(updateRunnable) - handler.postDelayed(updateRunnable, UPDATE_INTERVAL) - val appContext = requireContext().applicationContext - lifecycleScope.launchWhenResumed { - configureProfilePreference() - serial.summary = ProfileManager.getSerial(appContext) - val serviceInfo = getCheckinServiceInfo(appContext) - statusCategory.isVisible = serviceInfo.configuration.enabled - if (serviceInfo.lastCheckin > 0) { - status.summary = getString( - R.string.checkin_last_registration, - DateUtils.getRelativeTimeSpanString(serviceInfo.lastCheckin, System.currentTimeMillis(), 0) - ) - androidId.isVisible = true - androidId.summary = serviceInfo.androidId.toString(16) - } else { - status.summary = getString(R.string.checkin_not_registered) - androidId.isVisible = false - } - } - } - - companion object { - private const val UPDATE_INTERVAL = 1000L - } -} diff --git a/play-services-core/src/main/kotlin/org/microg/gms/ui/PushNotificationAllAppsFragment.kt b/play-services-core/src/main/kotlin/org/microg/gms/ui/PushNotificationAllAppsFragment.kt index 3be5ae4f21..010c7c599f 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/ui/PushNotificationAllAppsFragment.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/ui/PushNotificationAllAppsFragment.kt @@ -56,16 +56,12 @@ class PushNotificationAllAppsFragment : PreferenceFragmentCompat() { lifecycleScope.launchWhenResumed { val apps = withContext(Dispatchers.IO) { val res = database.appList.map { app -> - app to context.packageManager.getApplicationInfoIfExists(app.packageName) - }.map { (app, applicationInfo) -> val pref = AppIconPreference(context) - pref.title = applicationInfo?.loadLabel(context.packageManager) ?: app.packageName + pref.packageName = app.packageName pref.summary = when { app.lastMessageTimestamp > 0 -> getString(R.string.gcm_last_message_at, DateUtils.getRelativeTimeSpanString(app.lastMessageTimestamp)) else -> null } - pref.icon = applicationInfo?.loadIcon(context.packageManager) - ?: AppCompatResources.getDrawable(context, android.R.mipmap.sym_def_app_icon) pref.onPreferenceClickListener = Preference.OnPreferenceClickListener { findNavController().navigate(requireContext(), R.id.openGcmAppDetailsFromAll, bundleOf( "package" to app.packageName diff --git a/play-services-core/src/main/kotlin/org/microg/gms/ui/PushNotificationAppFragment.kt b/play-services-core/src/main/kotlin/org/microg/gms/ui/PushNotificationAppFragment.kt index 6c65434420..c4ef0f7488 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/ui/PushNotificationAppFragment.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/ui/PushNotificationAppFragment.kt @@ -5,58 +5,135 @@ package org.microg.gms.ui -import android.content.Intent -import android.net.Uri +import android.annotation.SuppressLint import android.os.Bundle -import android.provider.Settings -import android.util.Log -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.appcompat.content.res.AppCompatResources -import androidx.fragment.app.Fragment +import android.text.format.DateUtils +import androidx.appcompat.app.AlertDialog import androidx.lifecycle.lifecycleScope +import androidx.preference.Preference +import androidx.preference.PreferenceCategory +import androidx.preference.PreferenceFragmentCompat +import androidx.preference.TwoStatePreference import com.google.android.gms.R -import com.google.android.gms.databinding.PushNotificationAppFragmentBinding +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.microg.gms.gcm.GcmDatabase +import org.microg.gms.gcm.PushRegisterManager +class PushNotificationAppFragment : PreferenceFragmentCompat() { + private lateinit var appHeadingPreference: AppHeadingPreference + private lateinit var wakeForDelivery: TwoStatePreference + private lateinit var allowRegister: TwoStatePreference + private lateinit var status: Preference + private lateinit var unregister: Preference + private lateinit var unregisterCat: PreferenceCategory -class PushNotificationAppFragment : Fragment(R.layout.push_notification_fragment) { - lateinit var binding: PushNotificationAppFragmentBinding - val packageName: String? + private lateinit var database: GcmDatabase + private val packageName: String? get() = arguments?.getString("package") - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - binding = PushNotificationAppFragmentBinding.inflate(inflater, container, false) - binding.callbacks = object : PushNotificationAppFragmentCallbacks { - override fun onAppClicked() { - val intent = Intent() - intent.action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS - val uri: Uri = Uri.fromParts("package", packageName, null) - intent.data = uri - try { - requireContext().startActivity(intent) - } catch (e: Exception) { - Log.w(TAG, "Failed to launch app", e) + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + addPreferencesFromResource(R.xml.preferences_push_notifications_app) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + database = GcmDatabase(context) + } + + @SuppressLint("RestrictedApi") + override fun onBindPreferences() { + appHeadingPreference = preferenceScreen.findPreference("pref_push_app_heading") ?: appHeadingPreference + wakeForDelivery = preferenceScreen.findPreference("pref_push_app_wake_for_delivery") ?: wakeForDelivery + allowRegister = preferenceScreen.findPreference("pref_push_app_allow_register") ?: allowRegister + unregister = preferenceScreen.findPreference("pref_push_app_unregister") ?: unregister + unregisterCat = preferenceScreen.findPreference("prefcat_push_app_unregister") ?: unregisterCat + status = preferenceScreen.findPreference("pref_push_app_status") ?: status + wakeForDelivery.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> + database.setAppWakeForDelivery(packageName, newValue as Boolean) + database.close() + true + } + allowRegister.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> + val enabled = newValue as? Boolean ?: return@OnPreferenceChangeListener false + if (!enabled) { + val registrations = packageName?.let { database.getRegistrationsByApp(it) } ?: emptyList() + if (registrations.isNotEmpty()) { + showUnregisterConfirm(R.string.gcm_unregister_after_deny_message) } } + database.setAppAllowRegister(packageName, enabled) + database.close() + true + } + unregister.onPreferenceClickListener = Preference.OnPreferenceClickListener { + showUnregisterConfirm(R.string.gcm_unregister_confirm_message) + true + } + } + + + private fun showUnregisterConfirm(unregisterConfirmDesc: Int) { + val pm = requireContext().packageManager + val applicationInfo = pm.getApplicationInfoIfExists(packageName) + AlertDialog.Builder(requireContext()) + .setTitle(getString(R.string.gcm_unregister_confirm_title, applicationInfo?.loadLabel(pm) + ?: packageName)) + .setMessage(unregisterConfirmDesc) + .setPositiveButton(android.R.string.yes) { _, _ -> unregister() } + .setNegativeButton(android.R.string.no) { _, _ -> }.show() + } + + private fun unregister() { + lifecycleScope.launchWhenResumed { + withContext(Dispatchers.IO) { + for (registration in database.getRegistrationsByApp(packageName)) { + PushRegisterManager.unregister(context, registration.packageName, registration.signature, null, null) + } + } + updateDetails() } - childFragmentManager.findFragmentById(R.id.sub_preferences)?.arguments = arguments - return binding.root } override fun onResume() { super.onResume() - val context = requireContext() + updateDetails() + } + + private fun updateDetails() { lifecycleScope.launchWhenResumed { - val pm = context.packageManager - val applicationInfo = pm.getApplicationInfoIfExists(packageName) - binding.appName = applicationInfo?.loadLabel(pm)?.toString() ?: packageName - binding.appIcon = applicationInfo?.loadIcon(pm) - ?: AppCompatResources.getDrawable(context, android.R.mipmap.sym_def_app_icon) + appHeadingPreference.packageName = packageName + val app = packageName?.let { database.getApp(it) } + wakeForDelivery.isChecked = app?.wakeForDelivery ?: true + allowRegister.isChecked = app?.allowRegister ?: true + val registrations = packageName?.let { database.getRegistrationsByApp(it) } ?: emptyList() + unregisterCat.isVisible = registrations.isNotEmpty() + + val sb = StringBuilder() + if ((app?.totalMessageCount ?: 0L) == 0L) { + sb.append(getString(R.string.gcm_no_message_yet)) + } else { + sb.append(getString(R.string.gcm_messages_counter, app?.totalMessageCount, app?.totalMessageBytes)) + if (app?.lastMessageTimestamp != 0L) { + sb.append("\n").append(getString(R.string.gcm_last_message_at, DateUtils.getRelativeDateTimeString(context, app?.lastMessageTimestamp ?: 0L, DateUtils.MINUTE_IN_MILLIS, DateUtils.WEEK_IN_MILLIS, DateUtils.FORMAT_SHOW_TIME))) + } + } + for (registration in registrations) { + sb.append("\n") + if (registration.timestamp == 0L) { + sb.append(getString(R.string.gcm_registered)) + } else { + sb.append(getString(R.string.gcm_registered_since, DateUtils.getRelativeDateTimeString(context, registration.timestamp, DateUtils.MINUTE_IN_MILLIS, DateUtils.WEEK_IN_MILLIS, DateUtils.FORMAT_SHOW_TIME))) + } + } + status.summary = sb.toString() + + database.close() } } -} -interface PushNotificationAppFragmentCallbacks { - fun onAppClicked() + override fun onPause() { + super.onPause() + database.close() + } } diff --git a/play-services-core/src/main/kotlin/org/microg/gms/ui/PushNotificationAppPreferencesFragment.kt b/play-services-core/src/main/kotlin/org/microg/gms/ui/PushNotificationAppPreferencesFragment.kt deleted file mode 100644 index 71b12faebc..0000000000 --- a/play-services-core/src/main/kotlin/org/microg/gms/ui/PushNotificationAppPreferencesFragment.kt +++ /dev/null @@ -1,136 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2020, microG Project Team - * SPDX-License-Identifier: Apache-2.0 - */ - -package org.microg.gms.ui - -import android.annotation.SuppressLint -import android.os.Bundle -import android.text.format.DateUtils -import androidx.appcompat.app.AlertDialog -import androidx.lifecycle.lifecycleScope -import androidx.preference.Preference -import androidx.preference.PreferenceCategory -import androidx.preference.PreferenceFragmentCompat -import androidx.preference.TwoStatePreference -import com.google.android.gms.R -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import org.microg.gms.gcm.GcmDatabase -import org.microg.gms.gcm.PushRegisterManager - -class PushNotificationAppPreferencesFragment : PreferenceFragmentCompat() { - private lateinit var wakeForDelivery: TwoStatePreference - private lateinit var allowRegister: TwoStatePreference - private lateinit var status: Preference - private lateinit var unregister: Preference - private lateinit var unregisterCat: PreferenceCategory - - private lateinit var database: GcmDatabase - private val packageName: String? - get() = arguments?.getString("package") - - override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { - addPreferencesFromResource(R.xml.preferences_push_notifications_app) - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - database = GcmDatabase(context) - } - - @SuppressLint("RestrictedApi") - override fun onBindPreferences() { - wakeForDelivery = preferenceScreen.findPreference("pref_push_app_wake_for_delivery") ?: wakeForDelivery - allowRegister = preferenceScreen.findPreference("pref_push_app_allow_register") ?: allowRegister - unregister = preferenceScreen.findPreference("pref_push_app_unregister") ?: unregister - unregisterCat = preferenceScreen.findPreference("prefcat_push_app_unregister") ?: unregisterCat - status = preferenceScreen.findPreference("pref_push_app_status") ?: status - wakeForDelivery.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> - database.setAppWakeForDelivery(packageName, newValue as Boolean) - database.close() - true - } - allowRegister.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> - val enabled = newValue as? Boolean ?: return@OnPreferenceChangeListener false - if (!enabled) { - val registrations = packageName?.let { database.getRegistrationsByApp(it) } ?: emptyList() - if (registrations.isNotEmpty()) { - showUnregisterConfirm(R.string.gcm_unregister_after_deny_message) - } - } - database.setAppAllowRegister(packageName, enabled) - database.close() - true - } - unregister.onPreferenceClickListener = Preference.OnPreferenceClickListener { - showUnregisterConfirm(R.string.gcm_unregister_confirm_message) - true - } - } - - - private fun showUnregisterConfirm(unregisterConfirmDesc: Int) { - val pm = requireContext().packageManager - val applicationInfo = pm.getApplicationInfoIfExists(packageName) - AlertDialog.Builder(requireContext()) - .setTitle(getString(R.string.gcm_unregister_confirm_title, applicationInfo?.loadLabel(pm) - ?: packageName)) - .setMessage(unregisterConfirmDesc) - .setPositiveButton(android.R.string.yes) { _, _ -> unregister() } - .setNegativeButton(android.R.string.no) { _, _ -> }.show() - } - - private fun unregister() { - lifecycleScope.launchWhenResumed { - withContext(Dispatchers.IO) { - for (registration in database.getRegistrationsByApp(packageName)) { - PushRegisterManager.unregister(context, registration.packageName, registration.signature, null, null) - } - } - updateDetails() - } - } - - override fun onResume() { - super.onResume() - updateDetails() - } - - private fun updateDetails() { - lifecycleScope.launchWhenResumed { - val app = packageName?.let { database.getApp(it) } - wakeForDelivery.isChecked = app?.wakeForDelivery ?: true - allowRegister.isChecked = app?.allowRegister ?: true - val registrations = packageName?.let { database.getRegistrationsByApp(it) } ?: emptyList() - unregisterCat.isVisible = registrations.isNotEmpty() - - val sb = StringBuilder() - if ((app?.totalMessageCount ?: 0L) == 0L) { - sb.append(getString(R.string.gcm_no_message_yet)) - } else { - sb.append(getString(R.string.gcm_messages_counter, app?.totalMessageCount, app?.totalMessageBytes)) - if (app?.lastMessageTimestamp != 0L) { - sb.append("\n").append(getString(R.string.gcm_last_message_at, DateUtils.getRelativeDateTimeString(context, app?.lastMessageTimestamp ?: 0L, DateUtils.MINUTE_IN_MILLIS, DateUtils.WEEK_IN_MILLIS, DateUtils.FORMAT_SHOW_TIME))) - } - } - for (registration in registrations) { - sb.append("\n") - if (registration.timestamp == 0L) { - sb.append(getString(R.string.gcm_registered)) - } else { - sb.append(getString(R.string.gcm_registered_since, DateUtils.getRelativeDateTimeString(context, registration.timestamp, DateUtils.MINUTE_IN_MILLIS, DateUtils.WEEK_IN_MILLIS, DateUtils.FORMAT_SHOW_TIME))) - } - } - status.summary = sb.toString() - - database.close() - } - } - - override fun onPause() { - super.onPause() - database.close() - } -} diff --git a/play-services-core/src/main/kotlin/org/microg/gms/ui/PushNotificationFragment.kt b/play-services-core/src/main/kotlin/org/microg/gms/ui/PushNotificationFragment.kt index 5f8dc3743a..6ec5a3491c 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/ui/PushNotificationFragment.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/ui/PushNotificationFragment.kt @@ -2,60 +2,142 @@ * SPDX-FileCopyrightText: 2020, microG Project Team * SPDX-License-Identifier: Apache-2.0 */ + package org.microg.gms.ui +import android.annotation.SuppressLint import android.os.Bundle -import android.view.* -import androidx.fragment.app.Fragment +import android.os.Handler +import android.text.format.DateUtils +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import androidx.core.os.bundleOf import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController +import androidx.preference.Preference +import androidx.preference.PreferenceCategory +import androidx.preference.PreferenceFragmentCompat import com.google.android.gms.R -import com.google.android.gms.databinding.PushNotificationFragmentBinding -import org.microg.gms.checkin.getCheckinServiceInfo -import org.microg.gms.gcm.ServiceInfo +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.microg.gms.checkin.CheckinPreferences +import org.microg.gms.gcm.GcmDatabase +import org.microg.gms.gcm.GcmPrefs import org.microg.gms.gcm.getGcmServiceInfo -import org.microg.gms.gcm.setGcmServiceConfiguration -class PushNotificationFragment : Fragment(R.layout.push_notification_fragment) { - lateinit var binding: PushNotificationFragmentBinding +class PushNotificationFragment : PreferenceFragmentCompat() { + private lateinit var switchBarPreference: SwitchBarPreference + private lateinit var pushStatusCategory: PreferenceCategory + private lateinit var pushStatus: Preference + private lateinit var pushApps: PreferenceCategory + private lateinit var pushAppsAll: Preference + private lateinit var pushAppsNone: Preference + private lateinit var database: GcmDatabase + private val handler = Handler() + private val updateRunnable = Runnable { updateStatus() } - init { - setHasOptionsMenu(true) + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + database = GcmDatabase(context) } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - binding = PushNotificationFragmentBinding.inflate(inflater, container, false) - binding.switchBarCallback = object : PreferenceSwitchBarCallback { - override fun onChecked(newStatus: Boolean) { - setEnabled(newStatus) - } - } - return binding.root + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + addPreferencesFromResource(R.xml.preferences_push_notifications) } - fun setEnabled(newStatus: Boolean) { - val appContext = requireContext().applicationContext - lifecycleScope.launchWhenResumed { - val info = getGcmServiceInfo(appContext) - val newConfiguration = info.configuration.copy(enabled = newStatus) - setGcmServiceConfiguration(appContext, newConfiguration) - displayServiceInfo(info.copy(configuration = newConfiguration)) + @SuppressLint("RestrictedApi") + override fun onBindPreferences() { + switchBarPreference = preferenceScreen.findPreference("pref_push_enabled") ?: switchBarPreference + pushStatusCategory = preferenceScreen.findPreference("prefcat_push_status") ?: pushStatusCategory + pushStatus = preferenceScreen.findPreference("pref_push_status") ?: pushStatus + pushApps = preferenceScreen.findPreference("prefcat_push_apps") ?: pushApps + pushAppsAll = preferenceScreen.findPreference("pref_push_apps_all") ?: pushAppsAll + pushAppsNone = preferenceScreen.findPreference("pref_push_apps_none") ?: pushAppsNone + pushAppsAll.setOnPreferenceClickListener { + findNavController().navigate(requireContext(), R.id.openAllGcmApps) + true + } + switchBarPreference.setOnPreferenceChangeListener { _, newValue -> + val newStatus = newValue as Boolean + GcmPrefs.setEnabled(requireContext(), newStatus) + true } - } - - private fun displayServiceInfo(serviceInfo: ServiceInfo) { - binding.gcmEnabled = serviceInfo.configuration.enabled } override fun onResume() { super.onResume() + + switchBarPreference.isEnabled = CheckinPreferences.isEnabled(requireContext()) + switchBarPreference.isChecked = GcmPrefs.get(requireContext()).isEnabled + + updateStatus() + updateContent() + } + + override fun onPause() { + super.onPause() + database.close() + handler.removeCallbacks(updateRunnable) + } + + private fun updateStatus() { + handler.postDelayed(updateRunnable, UPDATE_INTERVAL) val appContext = requireContext().applicationContext + lifecycleScope.launchWhenStarted { + val statusInfo = getGcmServiceInfo(appContext) + switchBarPreference.isChecked = statusInfo.configuration.enabled + pushStatusCategory.isVisible = statusInfo != null && statusInfo.configuration.enabled + pushStatus.summary = if (statusInfo != null && statusInfo.connected) { + appContext.getString(R.string.gcm_network_state_connected, DateUtils.getRelativeTimeSpanString(statusInfo.startTimestamp, System.currentTimeMillis(), 0)) + } else { + appContext.getString(R.string.gcm_network_state_disconnected) + } + } + } + + private fun updateContent() { lifecycleScope.launchWhenResumed { - displayServiceInfo(getGcmServiceInfo(appContext)) - binding.checkinEnabled = getCheckinServiceInfo(appContext).configuration.enabled + val context = requireContext() + val (apps, showAll) = withContext(Dispatchers.IO) { + val apps = database.appList.sortedByDescending { it.lastMessageTimestamp } + val res = apps.map { app -> + app to context.packageManager.getApplicationInfoIfExists(app.packageName) + }.mapNotNull { (app, info) -> + if (info == null) null else app to info + }.take(3).mapIndexed { idx, (app, applicationInfo) -> + val pref = AppIconPreference(context) + pref.order = idx + pref.applicationInfo = applicationInfo + pref.onPreferenceClickListener = Preference.OnPreferenceClickListener { + findNavController().navigate(requireContext(), R.id.openGcmAppDetails, bundleOf( + "package" to app.packageName + )) + true + } + pref.key = "pref_push_app_" + app.packageName + pref + }.let { it to (it.size < apps.size) } + database.close() + res + } + pushAppsAll.isVisible = showAll + pushApps.removeAll() + for (app in apps) { + pushApps.addPreference(app) + } + if (showAll) { + pushApps.addPreference(pushAppsAll) + } else if (apps.isEmpty()) { + pushApps.addPreference(pushAppsNone) + } } } + init { + setHasOptionsMenu(true) + } + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { menu.add(0, MENU_ADVANCED, 0, R.string.menu_advanced) super.onCreateOptionsMenu(menu, inflater) @@ -72,7 +154,7 @@ class PushNotificationFragment : Fragment(R.layout.push_notification_fragment) { } companion object { + private const val UPDATE_INTERVAL = 1000L private const val MENU_ADVANCED = Menu.FIRST } } - diff --git a/play-services-core/src/main/kotlin/org/microg/gms/ui/PushNotificationPreferencesFragment.kt b/play-services-core/src/main/kotlin/org/microg/gms/ui/PushNotificationPreferencesFragment.kt deleted file mode 100644 index ee558e1662..0000000000 --- a/play-services-core/src/main/kotlin/org/microg/gms/ui/PushNotificationPreferencesFragment.kt +++ /dev/null @@ -1,124 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2020, microG Project Team - * SPDX-License-Identifier: Apache-2.0 - */ - -package org.microg.gms.ui - -import android.annotation.SuppressLint -import android.os.Bundle -import android.os.Handler -import android.text.format.DateUtils -import androidx.core.os.bundleOf -import androidx.lifecycle.lifecycleScope -import androidx.navigation.fragment.findNavController -import androidx.preference.Preference -import androidx.preference.PreferenceCategory -import androidx.preference.PreferenceFragmentCompat -import com.google.android.gms.R -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import org.microg.gms.gcm.GcmDatabase -import org.microg.gms.gcm.getGcmServiceInfo - -class PushNotificationPreferencesFragment : PreferenceFragmentCompat() { - private lateinit var pushStatusCategory: PreferenceCategory - private lateinit var pushStatus: Preference - private lateinit var pushApps: PreferenceCategory - private lateinit var pushAppsAll: Preference - private lateinit var pushAppsNone: Preference - private lateinit var database: GcmDatabase - private val handler = Handler() - private val updateRunnable = Runnable { updateStatus() } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - database = GcmDatabase(context) - } - - override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { - addPreferencesFromResource(R.xml.preferences_push_notifications) - } - - @SuppressLint("RestrictedApi") - override fun onBindPreferences() { - pushStatusCategory = preferenceScreen.findPreference("prefcat_push_status") ?: pushStatusCategory - pushStatus = preferenceScreen.findPreference("pref_push_status") ?: pushStatus - pushApps = preferenceScreen.findPreference("prefcat_push_apps") ?: pushApps - pushAppsAll = preferenceScreen.findPreference("pref_push_apps_all") ?: pushAppsAll - pushAppsNone = preferenceScreen.findPreference("pref_push_apps_none") ?: pushAppsNone - pushAppsAll.onPreferenceClickListener = Preference.OnPreferenceClickListener { - findNavController().navigate(requireContext(), R.id.openAllGcmApps) - true - } - } - - override fun onResume() { - super.onResume() - updateStatus() - updateContent() - } - - override fun onPause() { - super.onPause() - database.close() - handler.removeCallbacks(updateRunnable) - } - - private fun updateStatus() { - handler.postDelayed(updateRunnable, UPDATE_INTERVAL) - val appContext = requireContext().applicationContext - lifecycleScope.launchWhenStarted { - val statusInfo = getGcmServiceInfo(appContext) - pushStatusCategory.isVisible = statusInfo != null && statusInfo.configuration.enabled - pushStatus.summary = if (statusInfo != null && statusInfo.connected) { - appContext.getString(R.string.gcm_network_state_connected, DateUtils.getRelativeTimeSpanString(statusInfo.startTimestamp, System.currentTimeMillis(), 0)) - } else { - appContext.getString(R.string.gcm_network_state_disconnected) - } - } - } - - private fun updateContent() { - lifecycleScope.launchWhenResumed { - val context = requireContext() - val (apps, showAll) = withContext(Dispatchers.IO) { - val apps = database.appList.sortedByDescending { it.lastMessageTimestamp } - val res = apps.map { app -> - app to context.packageManager.getApplicationInfoIfExists(app.packageName) - }.mapNotNull { (app, info) -> - if (info == null) null else app to info - }.take(3).mapIndexed { idx, (app, applicationInfo) -> - val pref = AppIconPreference(context) - pref.order = idx - pref.title = applicationInfo.loadLabel(context.packageManager) - pref.icon = applicationInfo.loadIcon(context.packageManager) - pref.onPreferenceClickListener = Preference.OnPreferenceClickListener { - findNavController().navigate(requireContext(), R.id.openGcmAppDetails, bundleOf( - "package" to app.packageName - )) - true - } - pref.key = "pref_push_app_" + app.packageName - pref - }.let { it to (it.size < apps.size) } - database.close() - res - } - pushAppsAll.isVisible = showAll - pushApps.removeAll() - for (app in apps) { - pushApps.addPreference(app) - } - if (showAll) { - pushApps.addPreference(pushAppsAll) - } else if (apps.isEmpty()) { - pushApps.addPreference(pushAppsNone) - } - } - } - - companion object { - private const val UPDATE_INTERVAL = 1000L - } -} diff --git a/play-services-core/src/main/kotlin/org/microg/gms/ui/SafetyNetAllAppsFragment.kt b/play-services-core/src/main/kotlin/org/microg/gms/ui/SafetyNetAllAppsFragment.kt index 951242d35d..78a489d040 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/ui/SafetyNetAllAppsFragment.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/ui/SafetyNetAllAppsFragment.kt @@ -52,16 +52,12 @@ class SafetyNetAllAppsFragment : PreferenceFragmentCompat() { lifecycleScope.launchWhenResumed { val apps = withContext(Dispatchers.IO) { val res = database.recentApps.map { app -> - app to context.packageManager.getApplicationInfoIfExists(app.first) - }.map { (app, applicationInfo) -> val pref = AppIconPreference(context) - pref.title = applicationInfo?.loadLabel(context.packageManager) ?: app.first + pref.packageName = app.first pref.summary = when { app.second > 0 -> getString(R.string.safetynet_last_run_at, DateUtils.getRelativeTimeSpanString(app.second)) else -> null } - pref.icon = applicationInfo?.loadIcon(context.packageManager) - ?: AppCompatResources.getDrawable(context, android.R.mipmap.sym_def_app_icon) pref.onPreferenceClickListener = Preference.OnPreferenceClickListener { findNavController().navigate( requireContext(), R.id.openSafetyNetAppDetailsFromAll, bundleOf( diff --git a/play-services-core/src/main/kotlin/org/microg/gms/ui/SafetyNetAppFragment.kt b/play-services-core/src/main/kotlin/org/microg/gms/ui/SafetyNetAppFragment.kt index c07a0aed83..6e3cb295c1 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/ui/SafetyNetAppFragment.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/ui/SafetyNetAppFragment.kt @@ -5,59 +5,86 @@ package org.microg.gms.ui -import android.content.Intent -import android.net.Uri +import android.annotation.SuppressLint import android.os.Bundle -import android.provider.Settings -import android.util.Log -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.appcompat.content.res.AppCompatResources -import androidx.fragment.app.Fragment +import android.text.format.DateUtils import androidx.lifecycle.lifecycleScope +import androidx.preference.* import com.google.android.gms.R -import com.google.android.gms.databinding.PushNotificationAppFragmentBinding -import com.google.android.gms.databinding.SafetyNetAppFragmentBinding +import org.microg.gms.safetynet.SafetyNetDatabase +import org.microg.gms.safetynet.SafetyNetRequestType.* - -class SafetyNetAppFragment : Fragment(R.layout.safety_net_app_fragment) { - lateinit var binding: SafetyNetAppFragmentBinding - val packageName: String? +class SafetyNetAppFragment : PreferenceFragmentCompat() { + private lateinit var appHeadingPreference: AppHeadingPreference + private lateinit var recents: PreferenceCategory + private lateinit var recentsNone: Preference + private val packageName: String? get() = arguments?.getString("package") - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - binding = SafetyNetAppFragmentBinding.inflate(inflater, container, false) - binding.callbacks = object : SafetyNetAppFragmentCallbacks { - override fun onAppClicked() { - val intent = Intent() - intent.action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS - val uri: Uri = Uri.fromParts("package", packageName, null) - intent.data = uri - try { - requireContext().startActivity(intent) - } catch (e: Exception) { - Log.w(TAG, "Failed to launch app", e) - } - } - } - childFragmentManager.findFragmentById(R.id.sub_preferences)?.arguments = arguments - return binding.root + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + addPreferencesFromResource(R.xml.preferences_safetynet_app) + } + + @SuppressLint("RestrictedApi") + override fun onBindPreferences() { + appHeadingPreference = preferenceScreen.findPreference("pref_safetynet_app_heading") ?: appHeadingPreference + recents = preferenceScreen.findPreference("prefcat_safetynet_recent_list") ?: recents + recentsNone = preferenceScreen.findPreference("pref_safetynet_recent_none") ?: recentsNone } override fun onResume() { super.onResume() - val context = requireContext() + updateContent() + } + + fun updateContent() { lifecycleScope.launchWhenResumed { - val pm = context.packageManager - val applicationInfo = pm.getApplicationInfoIfExists(packageName) - binding.appName = applicationInfo?.loadLabel(pm)?.toString() ?: packageName - binding.appIcon = applicationInfo?.loadIcon(pm) - ?: AppCompatResources.getDrawable(context, android.R.mipmap.sym_def_app_icon) + appHeadingPreference.packageName = packageName + val context = requireContext() + val summaries = + packageName?.let { packageName -> + val db = SafetyNetDatabase(context) + try { + db.getRecentRequests(packageName) + } finally { + db.close() + } + }.orEmpty() + recents.removeAll() + recents.addPreference(recentsNone) + recentsNone.isVisible = summaries.isEmpty() + for (summary in summaries) { + val preference = Preference(requireContext()) + preference.onPreferenceClickListener = Preference.OnPreferenceClickListener { + SafetyNetRecentDialogFragment().apply { + arguments = Bundle().apply { putParcelable("summary", summary) } + }.show(requireFragmentManager(), null) + true + } + val date = DateUtils.getRelativeDateTimeString( + context, + summary.timestamp, + DateUtils.MINUTE_IN_MILLIS, + DateUtils.WEEK_IN_MILLIS, + DateUtils.FORMAT_SHOW_TIME + ) + preference.title = date + formatSummaryForSafetyNetResult( + context, + summary.responseData, + summary.responseStatus, + summary.requestType + ).let { (text, icon) -> + preference.summary = when (summary.requestType) { + ATTESTATION -> getString(R.string.pref_safetynet_recent_attestation_summary, text) + RECAPTCHA -> getString(R.string.pref_safetynet_recent_recaptcha_summary, text) + RECAPTCHA_ENTERPRISE -> getString(R.string.pref_safetynet_recent_recaptcha_enterprise_summary, text) + } + preference.icon = icon + } + recents.addPreference(preference) + } } - } -} -interface SafetyNetAppFragmentCallbacks { - fun onAppClicked() + } } diff --git a/play-services-core/src/main/kotlin/org/microg/gms/ui/SafetyNetAppPreferencesFragment.kt b/play-services-core/src/main/kotlin/org/microg/gms/ui/SafetyNetAppPreferencesFragment.kt deleted file mode 100644 index ccff7316ed..0000000000 --- a/play-services-core/src/main/kotlin/org/microg/gms/ui/SafetyNetAppPreferencesFragment.kt +++ /dev/null @@ -1,87 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2020, microG Project Team - * SPDX-License-Identifier: Apache-2.0 - */ - -package org.microg.gms.ui - -import android.annotation.SuppressLint -import android.os.Bundle -import android.text.format.DateUtils -import androidx.lifecycle.lifecycleScope -import androidx.preference.* -import com.google.android.gms.R -import org.microg.gms.safetynet.SafetyNetDatabase -import org.microg.gms.safetynet.SafetyNetRequestType.ATTESTATION -import org.microg.gms.safetynet.SafetyNetRequestType.RECAPTCHA - -class SafetyNetAppPreferencesFragment : PreferenceFragmentCompat() { - private lateinit var recents: PreferenceCategory - private lateinit var recentsNone: Preference - private val packageName: String? - get() = arguments?.getString("package") - - override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { - addPreferencesFromResource(R.xml.preferences_safetynet_app) - } - - @SuppressLint("RestrictedApi") - override fun onBindPreferences() { - recents = preferenceScreen.findPreference("prefcat_safetynet_recent_list") ?: recents - recentsNone = preferenceScreen.findPreference("pref_safetynet_recent_none") ?: recentsNone - } - - override fun onResume() { - super.onResume() - updateContent() - } - - fun updateContent() { - lifecycleScope.launchWhenResumed { - val context = requireContext() - val summaries = - packageName?.let { packageName -> - val db = SafetyNetDatabase(context) - try { - db.getRecentRequests(packageName) - } finally { - db.close() - } - }.orEmpty() - recents.removeAll() - recents.addPreference(recentsNone) - recentsNone.isVisible = summaries.isEmpty() - for (summary in summaries) { - val preference = Preference(requireContext()) - preference.onPreferenceClickListener = Preference.OnPreferenceClickListener { - SafetyNetRecentDialogFragment().apply { - arguments = Bundle().apply { putParcelable("summary", summary) } - }.show(requireFragmentManager(), null) - true - } - val date = DateUtils.getRelativeDateTimeString( - context, - summary.timestamp, - DateUtils.MINUTE_IN_MILLIS, - DateUtils.WEEK_IN_MILLIS, - DateUtils.FORMAT_SHOW_TIME - ) - preference.title = date - formatSummaryForSafetyNetResult( - context, - summary.responseData, - summary.responseStatus, - summary.requestType - ).let { (text, icon) -> - preference.summary = when (summary.requestType) { - ATTESTATION -> getString(R.string.pref_safetynet_recent_attestation_summary, text) - RECAPTCHA -> getString(R.string.pref_safetynet_recent_recaptcha_summary, text) - } - preference.icon = icon - } - recents.addPreference(preference) - } - } - - } -} diff --git a/play-services-core/src/main/kotlin/org/microg/gms/ui/SafetyNetFragment.kt b/play-services-core/src/main/kotlin/org/microg/gms/ui/SafetyNetFragment.kt index 314cc1a4bc..adcd0e506a 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/ui/SafetyNetFragment.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/ui/SafetyNetFragment.kt @@ -5,58 +5,275 @@ package org.microg.gms.ui +import android.annotation.SuppressLint import android.os.Bundle -import android.view.* -import androidx.fragment.app.Fragment +import android.util.Base64 +import android.util.Log +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import androidx.core.content.ContextCompat +import androidx.core.os.bundleOf import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController +import androidx.preference.Preference +import androidx.preference.PreferenceCategory +import androidx.preference.PreferenceFragmentCompat +import com.android.volley.Request +import com.android.volley.toolbox.JsonObjectRequest +import com.android.volley.toolbox.Volley +import com.google.android.gms.BuildConfig import com.google.android.gms.R -import com.google.android.gms.databinding.SafetyNetFragmentBinding -import org.microg.gms.checkin.CheckinPrefs +import com.google.android.gms.recaptcha.Recaptcha +import com.google.android.gms.recaptcha.RecaptchaAction +import com.google.android.gms.recaptcha.RecaptchaActionType +import com.google.android.gms.safetynet.SafetyNet +import com.google.android.gms.tasks.await +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.json.JSONObject +import org.microg.gms.checkin.CheckinPreferences import org.microg.gms.droidguard.core.DroidGuardPreferences import org.microg.gms.safetynet.SafetyNetDatabase import org.microg.gms.safetynet.SafetyNetPreferences +import org.microg.gms.safetynet.SafetyNetRequestType.* +import java.net.URLEncoder +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlin.coroutines.suspendCoroutine +import kotlin.random.Random -class SafetyNetFragment : Fragment(R.layout.safety_net_fragment) { +class SafetyNetFragment : PreferenceFragmentCompat() { + private lateinit var switchBarPreference: SwitchBarPreference + private lateinit var runAttest: Preference + private lateinit var runReCaptcha: Preference + private lateinit var runReCaptchaEnterprise: Preference + private lateinit var apps: PreferenceCategory + private lateinit var appsAll: Preference + private lateinit var appsNone: Preference + private lateinit var droidguardUnsupported: Preference - private lateinit var binding: SafetyNetFragmentBinding + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + addPreferencesFromResource(R.xml.preferences_safetynet) - init { - setHasOptionsMenu(true) + switchBarPreference = preferenceScreen.findPreference("pref_safetynet_enabled") ?: switchBarPreference + runAttest = preferenceScreen.findPreference("pref_safetynet_run_attest") ?: runAttest + runReCaptcha = preferenceScreen.findPreference("pref_recaptcha_run_test") ?: runReCaptcha + runReCaptchaEnterprise = preferenceScreen.findPreference("pref_recaptcha_enterprise_run_test") ?: runReCaptchaEnterprise + apps = preferenceScreen.findPreference("prefcat_safetynet_apps") ?: apps + appsAll = preferenceScreen.findPreference("pref_safetynet_apps_all") ?: appsAll + appsNone = preferenceScreen.findPreference("pref_safetynet_apps_none") ?: appsNone + droidguardUnsupported = preferenceScreen.findPreference("pref_droidguard_unsupported") ?: droidguardUnsupported + + runAttest.isVisible = SAFETYNET_API_KEY != null + runReCaptcha.isVisible = RECAPTCHA_SITE_KEY != null + runReCaptchaEnterprise.isVisible = RECAPTCHA_ENTERPRISE_SITE_KEY != null + + runAttest.setOnPreferenceClickListener { runSafetyNetAttest(); true } + runReCaptcha.setOnPreferenceClickListener { runReCaptchaAttest(); true } + runReCaptchaEnterprise.setOnPreferenceClickListener { runReCaptchaEnterpriseAttest();true } + appsAll.setOnPreferenceClickListener { findNavController().navigate(requireContext(), R.id.openAllSafetyNetApps);true } + switchBarPreference.setOnPreferenceChangeListener { _, newValue -> + val newStatus = newValue as Boolean + SafetyNetPreferences.setEnabled(requireContext(), newStatus) + DroidGuardPreferences.setEnabled(requireContext(), newStatus) + droidguardUnsupported.isVisible = switchBarPreference.isChecked && !DroidGuardPreferences.isAvailable(requireContext()) + true + } } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - binding = SafetyNetFragmentBinding.inflate(inflater, container, false) - binding.switchBarCallback = object : PreferenceSwitchBarCallback { - override fun onChecked(newStatus: Boolean) { - setEnabled(newStatus) + private fun runSafetyNetAttest() { + val context = context ?: return + runAttest.setIcon(R.drawable.ic_circle_pending) + runAttest.setSummary(R.string.pref_test_summary_running) + lifecycleScope.launchWhenResumed { + try { + val response = SafetyNet.getClient(requireActivity()) + .attest(Random.nextBytes(32), SAFETYNET_API_KEY).await() + val (_, payload, _) = try { + response.jwsResult.split(".") + } catch (e: Exception) { + listOf(null, null, null) + } + formatSummaryForSafetyNetResult( + context, + payload?.let { Base64.decode(it, Base64.URL_SAFE).decodeToString() }, + response.result.status, + ATTESTATION + ) + .let { (summary, icon) -> + runAttest.summary = summary + runAttest.icon = icon + } + } catch (e: Exception) { + runAttest.summary = getString(R.string.pref_test_summary_failed, e.message) + runAttest.icon = ContextCompat.getDrawable(context, R.drawable.ic_circle_warn) } + updateContent() } - return binding.root } - fun setEnabled(newStatus: Boolean) { - val appContext = requireContext().applicationContext + private fun runReCaptchaAttest() { + val context = context ?: return + runReCaptcha.setIcon(R.drawable.ic_circle_pending) + runReCaptcha.setSummary(R.string.pref_test_summary_running) lifecycleScope.launchWhenResumed { - SafetyNetPreferences.setEnabled(appContext, newStatus) - DroidGuardPreferences.setEnabled(appContext, newStatus) - displayServiceInfo() + try { + val response = SafetyNet.getClient(requireActivity()) + .verifyWithRecaptcha(RECAPTCHA_SITE_KEY).await() + val result = if (response.tokenResult != null) { + val queue = Volley.newRequestQueue(context) + val json = + if (RECAPTCHA_SECRET != null) { + suspendCoroutine { continuation -> + queue.add(object : JsonObjectRequest( + Method.POST, + "https://www.google.com/recaptcha/api/siteverify", + null, + { continuation.resume(it) }, + { continuation.resumeWithException(it) } + ) { + override fun getBodyContentType(): String = "application/x-www-form-urlencoded; charset=UTF-8" + override fun getBody(): ByteArray = + "secret=$RECAPTCHA_SECRET&response=${URLEncoder.encode(response.tokenResult, "UTF-8")}".encodeToByteArray() + }) + } + } else { + // Can't properly verify, everything becomes a success + JSONObject(mapOf("success" to true)) + } + Log.d(TAG, "Result: $json") + json.toString() + } else { + null + } + formatSummaryForSafetyNetResult(context, result, response.result.status, RECAPTCHA) + .let { (summary, icon) -> + runReCaptcha.summary = summary + runReCaptcha.icon = icon + } + } catch (e: Exception) { + runReCaptcha.summary = getString(R.string.pref_test_summary_failed, e.message) + runReCaptcha.icon = ContextCompat.getDrawable(context, R.drawable.ic_circle_warn) + } + updateContent() } } - fun displayServiceInfo() { - binding.safetynetEnabled = SafetyNetPreferences.isEnabled(requireContext()) && DroidGuardPreferences.isEnabled(requireContext()) + private fun runReCaptchaEnterpriseAttest() { + val context = context ?: return + runReCaptchaEnterprise.setIcon(R.drawable.ic_circle_pending) + runReCaptchaEnterprise.setSummary(R.string.pref_test_summary_running) + lifecycleScope.launchWhenResumed { + try { + val client = Recaptcha.getClient(requireActivity()) + val handle = client.init(RECAPTCHA_ENTERPRISE_SITE_KEY).await() + val actionType = RecaptchaActionType.SIGNUP + val response = client.execute(handle, RecaptchaAction(RecaptchaActionType(actionType))).await() + Log.d(TAG, "Recaptcha Token: " + response.tokenResult) + client.close(handle).await() + val result = if (response.tokenResult != null) { + val queue = Volley.newRequestQueue(context) + val json = if (RECAPTCHA_ENTERPRISE_API_KEY != null) { + suspendCoroutine { continuation -> + queue.add(JsonObjectRequest( + Request.Method.POST, + "https://recaptchaenterprise.googleapis.com/v1/projects/$RECAPTCHA_ENTERPRISE_PROJECT_ID/assessments?key=$RECAPTCHA_ENTERPRISE_API_KEY", + JSONObject( + mapOf( + "event" to JSONObject( + mapOf( + "token" to response.tokenResult, + "siteKey" to RECAPTCHA_ENTERPRISE_SITE_KEY, + "expectedAction" to actionType + ) + ) + ) + ), + { continuation.resume(it) }, + { continuation.resumeWithException(it) } + )) + } + } else { + // Can't properly verify, everything becomes a success + JSONObject(mapOf("tokenProperties" to JSONObject(mapOf("valid" to true)), "riskAnalysis" to JSONObject(mapOf("score" to "unknown")))) + } + Log.d(TAG, "Result: $json") + json.toString() + } else { + null + } + formatSummaryForSafetyNetResult(context, result, null, RECAPTCHA_ENTERPRISE) + .let { (summary, icon) -> + runReCaptchaEnterprise.summary = summary + runReCaptchaEnterprise.icon = icon + } + } catch (e: Exception) { + runReCaptchaEnterprise.summary = getString(R.string.pref_test_summary_failed, e.message) + runReCaptchaEnterprise.icon = ContextCompat.getDrawable(context, R.drawable.ic_circle_warn) + } + updateContent() + } } override fun onResume() { super.onResume() - val appContext = requireContext().applicationContext + + switchBarPreference.isEnabled = CheckinPreferences.isEnabled(requireContext()) + switchBarPreference.isChecked = SafetyNetPreferences.isEnabled(requireContext()) && DroidGuardPreferences.isEnabled(requireContext()) + droidguardUnsupported.isVisible = switchBarPreference.isChecked && !DroidGuardPreferences.isAvailable(requireContext()) + + updateContent() + } + + fun updateContent() { lifecycleScope.launchWhenResumed { - binding.checkinEnabled = CheckinPrefs.isEnabled(appContext) - displayServiceInfo() + val context = requireContext() + val (apps, showAll) = withContext(Dispatchers.IO) { + val db = SafetyNetDatabase(context) + val apps = try { + db.recentApps + } finally { + db.close() + } + apps.map { app -> + app to context.packageManager.getApplicationInfoIfExists(app.first) + }.mapNotNull { (app, info) -> + if (info == null) null else app to info + }.take(3).mapIndexed { idx, (app, applicationInfo) -> + val pref = AppIconPreference(context) + pref.order = idx + pref.applicationInfo = applicationInfo + pref.onPreferenceClickListener = Preference.OnPreferenceClickListener { + findNavController().navigate( + requireContext(), R.id.openSafetyNetAppDetails, bundleOf( + "package" to app.first + ) + ) + true + } + pref.key = "pref_safetynet_app_" + app.first + pref + }.let { it to (it.size < apps.size) } + } + appsAll.isVisible = showAll + this@SafetyNetFragment.apps.removeAll() + for (app in apps) { + this@SafetyNetFragment.apps.addPreference(app) + } + if (showAll) { + this@SafetyNetFragment.apps.addPreference(appsAll) + } else if (apps.isEmpty()) { + this@SafetyNetFragment.apps.addPreference(appsNone) + } + } } + init { + setHasOptionsMenu(true) + } + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { menu.add(0, MENU_ADVANCED, 0, R.string.menu_advanced) menu.add(0, MENU_CLEAR_REQUESTS, 0, R.string.menu_clear_recent_requests) @@ -73,7 +290,7 @@ class SafetyNetFragment : Fragment(R.layout.safety_net_fragment) { val db = SafetyNetDatabase(requireContext()) db.clearAllRequests() db.close() - (childFragmentManager.findFragmentById(R.id.sub_preferences) as? SafetyNetPreferencesFragment)?.updateContent() + updateContent() true } else -> super.onOptionsItemSelected(item) @@ -81,6 +298,12 @@ class SafetyNetFragment : Fragment(R.layout.safety_net_fragment) { } companion object { + private val SAFETYNET_API_KEY: String? = BuildConfig.SAFETYNET_KEY.takeIf { it.isNotBlank() } + private val RECAPTCHA_SITE_KEY: String? = BuildConfig.RECAPTCHA_SITE_KEY.takeIf { it.isNotBlank() } + private val RECAPTCHA_SECRET: String? = BuildConfig.RECAPTCHA_SECRET.takeIf { it.isNotBlank() } + private val RECAPTCHA_ENTERPRISE_PROJECT_ID: String? = BuildConfig.RECAPTCHA_ENTERPRISE_PROJECT_ID.takeIf { it.isNotBlank() } + private val RECAPTCHA_ENTERPRISE_SITE_KEY: String? = BuildConfig.RECAPTCHA_ENTERPRISE_SITE_KEY.takeIf { it.isNotBlank() } + private val RECAPTCHA_ENTERPRISE_API_KEY: String? = BuildConfig.RECAPTCHA_ENTERPRISE_API_KEY.takeIf { it.isNotBlank() } private const val MENU_ADVANCED = Menu.FIRST private const val MENU_CLEAR_REQUESTS = Menu.FIRST + 1 } diff --git a/play-services-core/src/main/kotlin/org/microg/gms/ui/SafetyNetPreferencesFragment.kt b/play-services-core/src/main/kotlin/org/microg/gms/ui/SafetyNetPreferencesFragment.kt deleted file mode 100644 index 07a5390630..0000000000 --- a/play-services-core/src/main/kotlin/org/microg/gms/ui/SafetyNetPreferencesFragment.kt +++ /dev/null @@ -1,227 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2020, microG Project Team - * SPDX-License-Identifier: Apache-2.0 - */ - -package org.microg.gms.ui - -import android.annotation.SuppressLint -import android.os.Bundle -import android.util.Base64 -import android.util.Log -import androidx.core.content.ContextCompat -import androidx.core.os.bundleOf -import androidx.lifecycle.lifecycleScope -import androidx.navigation.fragment.findNavController -import androidx.preference.Preference -import androidx.preference.PreferenceCategory -import androidx.preference.PreferenceFragmentCompat -import com.android.volley.toolbox.JsonObjectRequest -import com.android.volley.toolbox.Volley -import com.google.android.gms.R -import com.google.android.gms.recaptcha.Recaptcha -import com.google.android.gms.recaptcha.RecaptchaAction -import com.google.android.gms.recaptcha.RecaptchaActionType -import com.google.android.gms.safetynet.SafetyNet -import com.google.android.gms.tasks.await -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import org.json.JSONObject -import org.microg.gms.safetynet.SafetyNetDatabase -import org.microg.gms.safetynet.SafetyNetRequestType.ATTESTATION -import org.microg.gms.safetynet.SafetyNetRequestType.RECAPTCHA -import java.net.URLEncoder -import kotlin.coroutines.resume -import kotlin.coroutines.resumeWithException -import kotlin.coroutines.suspendCoroutine -import kotlin.random.Random - -class SafetyNetPreferencesFragment : PreferenceFragmentCompat() { - private lateinit var runAttest: Preference - private lateinit var runReCaptcha: Preference - private lateinit var runReCaptchaEnterprise: Preference - private lateinit var apps: PreferenceCategory - private lateinit var appsAll: Preference - private lateinit var appsNone: Preference - - override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { - addPreferencesFromResource(R.xml.preferences_safetynet) - } - - @SuppressLint("RestrictedApi") - override fun onBindPreferences() { - runAttest = preferenceScreen.findPreference("pref_safetynet_run_attest") ?: runAttest - runReCaptcha = preferenceScreen.findPreference("pref_recaptcha_run_test") ?: runReCaptcha - runReCaptchaEnterprise = - preferenceScreen.findPreference("pref_recaptcha_enterprise_run_test") ?: runReCaptchaEnterprise - apps = preferenceScreen.findPreference("prefcat_safetynet_apps") ?: apps - appsAll = preferenceScreen.findPreference("pref_safetynet_apps_all") ?: appsAll - appsNone = preferenceScreen.findPreference("pref_safetynet_apps_none") ?: appsNone - - runAttest.isVisible = SAFETYNET_API_KEY != null - runReCaptcha.isVisible = RECAPTCHA_SITE_KEY != null - runReCaptchaEnterprise.isVisible = RECAPTCHA_ENTERPRISE_SITE_KEY != null - - runAttest.setOnPreferenceClickListener { - val context = context ?: return@setOnPreferenceClickListener false - runAttest.setIcon(R.drawable.ic_circle_pending) - runAttest.setSummary(R.string.pref_test_summary_running) - lifecycleScope.launchWhenResumed { - try { - val response = SafetyNet.getClient(requireActivity()) - .attest(Random.nextBytes(32), SAFETYNET_API_KEY).await() - val (_, payload, _) = try { - response.jwsResult.split(".") - } catch (e: Exception) { - listOf(null, null, null) - } - formatSummaryForSafetyNetResult( - context, - payload?.let { Base64.decode(it, Base64.URL_SAFE).decodeToString() }, - response.result.status, - ATTESTATION - ) - .let { (summary, icon) -> - runAttest.summary = summary - runAttest.icon = icon - } - } catch (e: Exception) { - runAttest.summary = getString(R.string.pref_test_summary_failed, e.message) - runAttest.icon = ContextCompat.getDrawable(context, R.drawable.ic_circle_warn) - } - updateContent() - } - true - } - runReCaptcha.setOnPreferenceClickListener { - val context = context ?: return@setOnPreferenceClickListener false - runReCaptcha.setIcon(R.drawable.ic_circle_pending) - runReCaptcha.setSummary(R.string.pref_test_summary_running) - lifecycleScope.launchWhenResumed { - try { - val response = SafetyNet.getClient(requireActivity()) - .verifyWithRecaptcha(RECAPTCHA_SITE_KEY).await() - val result = if (response.tokenResult != null) { - val queue = Volley.newRequestQueue(context) - val json = - if (RECAPTCHA_SECRET != null) { - suspendCoroutine { continuation -> - queue.add(object : JsonObjectRequest( - Method.POST, - "https://www.google.com/recaptcha/api/siteverify", - null, - { continuation.resume(it) }, - { continuation.resumeWithException(it) } - ) { - override fun getBodyContentType(): String = "application/x-www-form-urlencoded; charset=UTF-8" - override fun getBody(): ByteArray = - "secret=$RECAPTCHA_SECRET&response=${URLEncoder.encode(response.tokenResult, "UTF-8")}".encodeToByteArray() - }) - } - } else { - // Can't properly verify, everything becomes a success - JSONObject(mapOf("success" to true)) - } - Log.d(TAG, "Result: $json") - json.toString() - } else { - null - } - formatSummaryForSafetyNetResult(context, result, response.result.status, RECAPTCHA) - .let { (summary, icon) -> - runReCaptcha.summary = summary - runReCaptcha.icon = icon - } - } catch (e: Exception) { - runReCaptcha.summary = getString(R.string.pref_test_summary_failed, e.message) - runReCaptcha.icon = ContextCompat.getDrawable(context, R.drawable.ic_circle_warn) - } - updateContent() - } - true - } - runReCaptchaEnterprise.setOnPreferenceClickListener { - val context = context ?: return@setOnPreferenceClickListener false - runReCaptchaEnterprise.setIcon(R.drawable.ic_circle_pending) - runReCaptchaEnterprise.setSummary(R.string.pref_test_summary_running) - lifecycleScope.launchWhenResumed { - try { - val client = Recaptcha.getClient(requireActivity()) - val handle = client.init(RECAPTCHA_ENTERPRISE_SITE_KEY).await() - val result = - client.execute(handle, RecaptchaAction(RecaptchaActionType(RecaptchaActionType.SIGNUP))).await() - Log.d(TAG, "Recaptcha Token: " + result.tokenResult) - client.close(handle).await() - runReCaptchaEnterprise.summary = getString(R.string.pref_test_summary_warn, "Incomplete Test") - runReCaptchaEnterprise.icon = ContextCompat.getDrawable(context, R.drawable.ic_circle_warn) - } catch (e: Exception) { - runReCaptchaEnterprise.summary = getString(R.string.pref_test_summary_failed, e.message) - runReCaptchaEnterprise.icon = ContextCompat.getDrawable(context, R.drawable.ic_circle_warn) - } - updateContent() - } - true - } - appsAll.setOnPreferenceClickListener { - findNavController().navigate(requireContext(), R.id.openAllSafetyNetApps) - true - } - } - - override fun onResume() { - super.onResume() - updateContent() - } - - fun updateContent() { - lifecycleScope.launchWhenResumed { - val context = requireContext() - val (apps, showAll) = withContext(Dispatchers.IO) { - val db = SafetyNetDatabase(context) - val apps = try { - db.recentApps - } finally { - db.close() - } - apps.map { app -> - app to context.packageManager.getApplicationInfoIfExists(app.first) - }.mapNotNull { (app, info) -> - if (info == null) null else app to info - }.take(3).mapIndexed { idx, (app, applicationInfo) -> - val pref = AppIconPreference(context) - pref.order = idx - pref.title = applicationInfo.loadLabel(context.packageManager) - pref.icon = applicationInfo.loadIcon(context.packageManager) - pref.onPreferenceClickListener = Preference.OnPreferenceClickListener { - findNavController().navigate( - requireContext(), R.id.openSafetyNetAppDetails, bundleOf( - "package" to app.first - ) - ) - true - } - pref.key = "pref_safetynet_app_" + app.first - pref - }.let { it to (it.size < apps.size) } - } - appsAll.isVisible = showAll - this@SafetyNetPreferencesFragment.apps.removeAll() - for (app in apps) { - this@SafetyNetPreferencesFragment.apps.addPreference(app) - } - if (showAll) { - this@SafetyNetPreferencesFragment.apps.addPreference(appsAll) - } else if (apps.isEmpty()) { - this@SafetyNetPreferencesFragment.apps.addPreference(appsNone) - } - - } - } - - companion object { - private val SAFETYNET_API_KEY: String? = "AIzaSyCcJO6IZiA5Or_AXw3LFdaTCmpnfL4pJ-Q" - private val RECAPTCHA_SITE_KEY: String? = "6Lc4TzgeAAAAAJnW7Jbo6UtQ0xGuTKjHAeyhINuq" - private val RECAPTCHA_SECRET: String? = "6Lc4TzgeAAA${"AAAjwSDqU-uG"}_Lcu2f23URMI8fq0I" - private val RECAPTCHA_ENTERPRISE_SITE_KEY: String? = null - } -} diff --git a/play-services-core/src/main/kotlin/org/microg/gms/ui/SafetyNetRecentDialogFragment.kt b/play-services-core/src/main/kotlin/org/microg/gms/ui/SafetyNetRecentDialogFragment.kt index c99e3d71ed..908f6eedc3 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/ui/SafetyNetRecentDialogFragment.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/ui/SafetyNetRecentDialogFragment.kt @@ -15,8 +15,7 @@ import androidx.fragment.app.commit import com.google.android.gms.R import com.google.android.gms.databinding.SafetyNetRecentFragmentBinding import org.microg.gms.safetynet.SafetyNetRequestType -import org.microg.gms.safetynet.SafetyNetRequestType.ATTESTATION -import org.microg.gms.safetynet.SafetyNetRequestType.RECAPTCHA +import org.microg.gms.safetynet.SafetyNetRequestType.* import org.microg.gms.safetynet.SafetyNetSummary class SafetyNetRecentDialogFragment : DialogFragment(R.layout.safety_net_recent_fragment) { @@ -43,6 +42,10 @@ class SafetyNetRecentDialogFragment : DialogFragment(R.layout.safety_net_recent_ R.id.actual_content, args = arguments ) + RECAPTCHA_ENTERPRISE -> add( + R.id.actual_content, + args = arguments + ) } } } diff --git a/play-services-core/src/main/kotlin/org/microg/gms/ui/SafetyNetRecentRecaptchaEnterprisePreferencesFragment.kt b/play-services-core/src/main/kotlin/org/microg/gms/ui/SafetyNetRecentRecaptchaEnterprisePreferencesFragment.kt new file mode 100644 index 0000000000..b480081948 --- /dev/null +++ b/play-services-core/src/main/kotlin/org/microg/gms/ui/SafetyNetRecentRecaptchaEnterprisePreferencesFragment.kt @@ -0,0 +1,42 @@ +package org.microg.gms.ui + +import android.annotation.SuppressLint +import android.os.Bundle +import android.text.format.DateUtils +import androidx.preference.Preference +import androidx.preference.PreferenceFragmentCompat +import com.google.android.gms.R +import org.microg.gms.safetynet.SafetyNetSummary + + +class SafetyNetRecentRecaptchaEnterprisePreferencesFragment : PreferenceFragmentCompat() { + + lateinit var snetSummary: SafetyNetSummary + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + addPreferencesFromResource(R.xml.preferences_snet_recent_recaptcha) + snetSummary = arguments?.get("summary") as SafetyNetSummary + } + + @SuppressLint("RestrictedApi") + override fun onBindPreferences() { + val requestType: Preference = preferenceScreen.findPreference("pref_request_type")!! + val time : Preference = preferenceScreen.findPreference("pref_time")!! + val status : Preference = preferenceScreen.findPreference("pref_status")!! + val token : Preference = preferenceScreen.findPreference("pref_token")!! + + requestType.summary = "RECAPTCHA_ENTERPRISE" + + time.summary = DateUtils.getRelativeDateTimeString(context, snetSummary.timestamp, DateUtils.MINUTE_IN_MILLIS, DateUtils.WEEK_IN_MILLIS, DateUtils.FORMAT_SHOW_TIME) + + + val snetResponseStatus = snetSummary.responseStatus + if (snetResponseStatus == null) { + status.summary = getString(R.string.pref_safetynet_test_not_completed) + } else { + status.summary = snetResponseStatus.statusMessage + token.summary = snetSummary.responseData + } + } + +} \ No newline at end of file diff --git a/play-services-core/src/main/kotlin/org/microg/gms/ui/SafetyNetUtils.kt b/play-services-core/src/main/kotlin/org/microg/gms/ui/SafetyNetUtils.kt index 98af9d423e..7af2dd609d 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/ui/SafetyNetUtils.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/ui/SafetyNetUtils.kt @@ -102,5 +102,38 @@ fun formatSummaryForSafetyNetResult(context: Context, result: String?, status: S } } } + SafetyNetRequestType.RECAPTCHA_ENTERPRISE -> { + if (result == null) { + return context.getString(R.string.pref_test_summary_failed, context.getString(R.string.pref_safetynet_test_no_result)) to + ContextCompat.getDrawable(context, R.drawable.ic_circle_warn) + } + val (valid, score, invalidReason) = try { + JSONObject(result).let { + Triple(it.optJSONObject("tokenProperties")?.optBoolean("valid", false) ?: false, + it.optJSONObject("riskAnalysis")?.optString("score", "unknown") ?: "unknown", + it.optJSONObject("tokenProperties")?.optString("invalidReason")) + } + } catch (e: Exception) { + Log.w(TAG, e) + Triple(true, "unknown", null) + } + return when { + valid && (score == "unknown" || score.toDoubleOrNull()?.let { it > 0.5 } == true) -> { + context.getString(R.string.pref_test_summary_passed) to ContextCompat.getDrawable(context, R.drawable.ic_circle_check) + } + valid && score.toDoubleOrNull()?.let { it > 0.1 } == true -> { + context.getString( + R.string.pref_test_summary_warn, + "score = $score" + ) to ContextCompat.getDrawable(context, R.drawable.ic_circle_warn) + } + else -> { + context.getString( + R.string.pref_test_summary_failed, + invalidReason ?: "score = $score" + ) to ContextCompat.getDrawable(context, R.drawable.ic_circle_error) + } + } + } } } diff --git a/play-services-core/src/main/kotlin/org/microg/gms/ui/SettingsFragment.kt b/play-services-core/src/main/kotlin/org/microg/gms/ui/SettingsFragment.kt index 63e47ca838..336ae1dc3d 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/ui/SettingsFragment.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/ui/SettingsFragment.kt @@ -11,8 +11,9 @@ import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController import androidx.preference.Preference import com.google.android.gms.R -import org.microg.gms.checkin.CheckinPrefs +import org.microg.gms.checkin.CheckinPreferences import org.microg.gms.gcm.GcmDatabase +import org.microg.gms.gcm.GcmPrefs import org.microg.gms.gcm.getGcmServiceInfo import org.microg.gms.safetynet.SafetyNetPreferences import org.microg.tools.ui.ResourceSettingsFragment @@ -34,7 +35,7 @@ class SettingsFragment : ResourceSettingsFragment() { true } findPreference(PREF_LOCATION)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { - // TODO + findNavController().navigate(requireContext(), R.id.openLocationSettings) true } findPreference(PREF_EXPOSURE)?.onPreferenceClickListener = Preference.OnPreferenceClickListener { @@ -46,19 +47,16 @@ class SettingsFragment : ResourceSettingsFragment() { true } findPreference(PREF_ABOUT)!!.summary = getString(R.string.about_version_str, AboutFragment.getSelfVersion(context)) + + findPreference(PREF_EXPOSURE)?.isVisible = NearbyPreferencesIntegration.isAvailable + findPreference(PREF_EXPOSURE)?.icon = NearbyPreferencesIntegration.getIcon(requireContext()) + findPreference(PREF_EXPOSURE)?.summary = NearbyPreferencesIntegration.getExposurePreferenceSummary(requireContext()) } override fun onResume() { super.onResume() val context = requireContext() - lifecycleScope.launchWhenResumed { - updateDetails(context) - } - } - - private suspend fun updateDetails(context: Context) { - val gcmServiceInfo = getGcmServiceInfo(context) - if (gcmServiceInfo.configuration.enabled) { + if (GcmPrefs.get(requireContext()).isEnabled) { val database = GcmDatabase(context) val regCount = database.registrationList.size database.close() @@ -67,12 +65,8 @@ class SettingsFragment : ResourceSettingsFragment() { findPreference(PREF_GCM)!!.setSummary(R.string.service_status_disabled_short) } - findPreference(PREF_CHECKIN)!!.setSummary(if (CheckinPrefs.isEnabled(context)) R.string.service_status_enabled_short else R.string.service_status_disabled_short) - findPreference(PREF_SNET)!!.setSummary(if (SafetyNetPreferences.isEnabled(context)) R.string.service_status_enabled_short else R.string.service_status_disabled_short) - - findPreference(PREF_EXPOSURE)?.isVisible = NearbyPreferencesIntegration.isAvailable - findPreference(PREF_EXPOSURE)?.icon = NearbyPreferencesIntegration.getIcon(context) - findPreference(PREF_EXPOSURE)?.summary = NearbyPreferencesIntegration.getExposurePreferenceSummary(context) + findPreference(PREF_CHECKIN)!!.setSummary(if (CheckinPreferences.isEnabled(requireContext())) R.string.service_status_enabled_short else R.string.service_status_disabled_short) + findPreference(PREF_SNET)!!.setSummary(if (SafetyNetPreferences.isEnabled(requireContext())) R.string.service_status_enabled_short else R.string.service_status_disabled_short) } companion object { diff --git a/play-services-core/src/main/kotlin/org/microg/gms/usagereporting/UsageReportingService.kt b/play-services-core/src/main/kotlin/org/microg/gms/usagereporting/UsageReportingService.kt index 74168b9219..334fe10478 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/usagereporting/UsageReportingService.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/usagereporting/UsageReportingService.kt @@ -56,5 +56,5 @@ class UsageReportingServiceImpl : IUsageReportingService.Stub() { callbacks.onOptInOptionsChangedListenerRemoved(Status.SUCCESS) } - override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean = warnOnTransactionIssues(code, reply, flags) { super.onTransact(code, data, reply, flags) } + 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-core/src/main/res/layout/device_registration_fragment.xml b/play-services-core/src/main/res/layout/device_registration_fragment.xml deleted file mode 100644 index 391ae74f35..0000000000 --- a/play-services-core/src/main/res/layout/device_registration_fragment.xml +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/play-services-core/src/main/res/layout/push_notification_app_fragment.xml b/play-services-core/src/main/res/layout/push_notification_app_fragment.xml deleted file mode 100644 index 4f63f96155..0000000000 --- a/play-services-core/src/main/res/layout/push_notification_app_fragment.xml +++ /dev/null @@ -1,82 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/play-services-core/src/main/res/layout/push_notification_fragment.xml b/play-services-core/src/main/res/layout/push_notification_fragment.xml deleted file mode 100644 index 75d7671fdd..0000000000 --- a/play-services-core/src/main/res/layout/push_notification_fragment.xml +++ /dev/null @@ -1,45 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/play-services-core/src/main/res/layout/safety_net_app_fragment.xml b/play-services-core/src/main/res/layout/safety_net_app_fragment.xml deleted file mode 100644 index 2ad27fd1e9..0000000000 --- a/play-services-core/src/main/res/layout/safety_net_app_fragment.xml +++ /dev/null @@ -1,83 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/play-services-core/src/main/res/layout/safety_net_fragment.xml b/play-services-core/src/main/res/layout/safety_net_fragment.xml deleted file mode 100644 index d4db5e24b2..0000000000 --- a/play-services-core/src/main/res/layout/safety_net_fragment.xml +++ /dev/null @@ -1,45 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/play-services-core/src/main/res/navigation/nav_settings.xml b/play-services-core/src/main/res/navigation/nav_settings.xml index 4ad2b9c499..61ae70f21e 100644 --- a/play-services-core/src/main/res/navigation/nav_settings.xml +++ b/play-services-core/src/main/res/navigation/nav_settings.xml @@ -29,6 +29,9 @@ + @@ -152,4 +155,5 @@ + diff --git a/play-services-core/src/main/res/values/strings.xml b/play-services-core/src/main/res/values/strings.xml index ea99502cd8..e82a1afb2f 100644 --- a/play-services-core/src/main/res/values/strings.xml +++ b/play-services-core/src/main/res/values/strings.xml @@ -181,6 +181,7 @@ This can take a couple of minutes." Warning: %s Running… Operation mode + DroidGuard execution is unsupported on this device. SafetyNet services may misbehave. Apps using SafetyNet Clear recent requests Last use: %1$s @@ -199,6 +200,7 @@ This can take a couple of minutes." Recent uses Attestation: %s ReCaptcha: %s + ReCaptcha Enterprise: %s Copy JSON JWS data Advice Evaluation type diff --git a/play-services-core/src/main/res/xml/network_security_config.xml b/play-services-core/src/main/res/xml/network_security_config.xml new file mode 100644 index 0000000000..6d264fb1ca --- /dev/null +++ b/play-services-core/src/main/res/xml/network_security_config.xml @@ -0,0 +1,11 @@ + + + + + + portal.mav.hu + + \ No newline at end of file diff --git a/play-services-core/src/main/res/xml/preferences_device_registration.xml b/play-services-core/src/main/res/xml/preferences_device_registration.xml index 09e5495fdc..680e02677e 100644 --- a/play-services-core/src/main/res/xml/preferences_device_registration.xml +++ b/play-services-core/src/main/res/xml/preferences_device_registration.xml @@ -5,7 +5,14 @@ --> + xmlns:tools="http://schemas.android.com/tools" + xmlns:app="http://schemas.android.com/apk/res-auto"> + + + diff --git a/play-services-core/src/main/res/xml/preferences_gcm_advanced.xml b/play-services-core/src/main/res/xml/preferences_gcm_advanced.xml index bc14d3dd60..f2695a3f76 100644 --- a/play-services-core/src/main/res/xml/preferences_gcm_advanced.xml +++ b/play-services-core/src/main/res/xml/preferences_gcm_advanced.xml @@ -19,6 +19,7 @@ @@ -29,6 +30,7 @@ - @@ -8,6 +7,12 @@ xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:title="Push notifications"> + + + diff --git a/play-services-core/src/main/res/xml/preferences_push_notifications_app.xml b/play-services-core/src/main/res/xml/preferences_push_notifications_app.xml index 907fd2111d..f7a92b9db8 100644 --- a/play-services-core/src/main/res/xml/preferences_push_notifications_app.xml +++ b/play-services-core/src/main/res/xml/preferences_push_notifications_app.xml @@ -1,11 +1,15 @@ - - + + diff --git a/play-services-core/src/main/res/xml/preferences_safetynet.xml b/play-services-core/src/main/res/xml/preferences_safetynet.xml index a82ae8eedc..9fd6b9eb09 100644 --- a/play-services-core/src/main/res/xml/preferences_safetynet.xml +++ b/play-services-core/src/main/res/xml/preferences_safetynet.xml @@ -1,24 +1,17 @@ - - - + + + @@ -33,7 +26,8 @@ android:title="@string/list_item_see_all" /> + android:layout="@layout/preference_category_no_label" + android:dependency="pref_safetynet_enabled"> + diff --git a/play-services-core/src/main/res/xml/preferences_safetynet_app.xml b/play-services-core/src/main/res/xml/preferences_safetynet_app.xml index 70116ae5e8..fabd1d8190 100644 --- a/play-services-core/src/main/res/xml/preferences_safetynet_app.xml +++ b/play-services-core/src/main/res/xml/preferences_safetynet_app.xml @@ -1,11 +1,16 @@ - - + + + diff --git a/play-services-core/src/main/res/xml/preferences_start.xml b/play-services-core/src/main/res/xml/preferences_start.xml index 2245ea339a..80cc686e76 100644 --- a/play-services-core/src/main/res/xml/preferences_start.xml +++ b/play-services-core/src/main/res/xml/preferences_start.xml @@ -61,9 +61,7 @@ + android:title="@string/service_name_location"/> @@ -60,7 +61,7 @@ public final String c() { public final void d(final Object mediaDrm, final byte[] sessionId) { Log.d(TAG, "d[closeMediaDrmSession](" + mediaDrm + ", " + sessionId + ")"); synchronized (MediaDrmLock.LOCK) { - if (Build.VERSION.SDK_INT >= 18) { + if (SDK_INT >= 18) { ((MediaDrm) mediaDrm).closeSession(sessionId); } } 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 d9487d1da1..5ce8116564 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 @@ -81,7 +81,7 @@ class DroidGuardHandleImpl(private val context: Context, private val packageName } override fun snapshot(map: MutableMap): ByteArray { - Log.d(TAG, "snapshot()") + Log.d(TAG, "snapshot($map)") condition.block() handleInitError?.let { return FallbackCreator.create(flow, context, map, it) } val handleProxy = this.handleProxy ?: return FallbackCreator.create(flow, context, map, IllegalStateException()) 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 f62aa8e2ae..cb0e5eff07 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 @@ -11,6 +11,7 @@ import android.database.Cursor import androidx.core.database.getStringOrNull import org.microg.gms.settings.SettingsContract import org.microg.gms.settings.SettingsContract.DroidGuard.ENABLED +import org.microg.gms.settings.SettingsContract.DroidGuard.FORCE_LOCAL_DISABLED import org.microg.gms.settings.SettingsContract.DroidGuard.MODE import org.microg.gms.settings.SettingsContract.DroidGuard.NETWORK_SERVER_URL @@ -27,9 +28,18 @@ object DroidGuardPreferences { private fun setSettings(context: Context, f: ContentValues.() -> Unit) = SettingsContract.setSettings(context, SettingsContract.DroidGuard.getContentUri(context), f) + @JvmStatic + fun isForcedLocalDisabled(context: Context): Boolean = getSettings(context, FORCE_LOCAL_DISABLED, false) { it.getInt(0) != 0 } + @JvmStatic fun isEnabled(context: Context): Boolean = getSettings(context, ENABLED, false) { it.getInt(0) != 0 } + @JvmStatic + fun isAvailable(context: Context): Boolean = isEnabled(context) && (!isForcedLocalDisabled(context) || getMode(context) != Mode.Embedded) + + @JvmStatic + fun isLocalAvailable(context: Context): Boolean = isEnabled(context) && !isForcedLocalDisabled(context) + @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 9c42992b1c..701050da0e 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 @@ -21,7 +21,7 @@ interface DroidGuardResultCreator { companion object { fun getInstance(context: Context): DroidGuardResultCreator = - if (DroidGuardPreferences.isEnabled(context)) { + if (DroidGuardPreferences.isAvailable(context)) { when (DroidGuardPreferences.getMode(context)) { DroidGuardPreferences.Mode.Embedded -> EmbeddedDroidGuardResultCreator(context) DroidGuardPreferences.Mode.Network -> NetworkDroidGuardResultCreator(context) 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/HandleProxyFactory.kt index dcd4d62896..2f35821531 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/HandleProxyFactory.kt @@ -32,16 +32,19 @@ class HandleProxyFactory(private val context: Context) { private val queue = Volley.newRequestQueue(context) fun createHandle(packageName: String, flow: String?, callback: GuardCallback, request: DroidGuardResultsRequest?): HandleProxy { + if (!DroidGuardPreferences.isLocalAvailable(context)) throw IllegalAccessException("DroidGuard should not be available locally") val (vmKey, byteCode, bytes) = readFromDatabase(flow) ?: fetchFromServer(flow, packageName) return createHandleProxy(flow, vmKey, byteCode, bytes, callback, request) } fun createPingHandle(packageName: String, flow: String, callback: GuardCallback, pingData: PingData?): HandleProxy { + if (!DroidGuardPreferences.isLocalAvailable(context)) throw IllegalAccessException("DroidGuard should not be available locally") val (vmKey, byteCode, bytes) = fetchFromServer(flow, createRequest(flow, packageName, pingData)) return createHandleProxy(flow, vmKey, byteCode, bytes, callback, DroidGuardResultsRequest().also { it.clientVersion = 0 }) } fun createLowLatencyHandle(flow: String?, callback: GuardCallback, request: DroidGuardResultsRequest?): HandleProxy { + if (!DroidGuardPreferences.isLocalAvailable(context)) throw IllegalAccessException("DroidGuard should not be available locally") val (vmKey, byteCode, bytes) = readFromDatabase("fast") ?: throw Exception("low latency (fast) flow not available") return createHandleProxy(flow, vmKey, byteCode, bytes, callback, request) } diff --git a/play-services-droidguard/core/src/main/kotlin/org/microg/gms/droidguard/core/ui/DroidGuardPreferencesFragment.kt b/play-services-droidguard/core/src/main/kotlin/org/microg/gms/droidguard/core/ui/DroidGuardPreferencesFragment.kt index 69bcfb6099..22ac9dbbad 100644 --- a/play-services-droidguard/core/src/main/kotlin/org/microg/gms/droidguard/core/ui/DroidGuardPreferencesFragment.kt +++ b/play-services-droidguard/core/src/main/kotlin/org/microg/gms/droidguard/core/ui/DroidGuardPreferencesFragment.kt @@ -42,6 +42,7 @@ class DroidGuardPreferencesFragment : PreferenceFragmentCompat() { modeNetwork.textChangedListener = { DroidGuardPreferences.setNetworkServerUrl(requireContext(), it) } + modeEmbedded.isEnabled = !DroidGuardPreferences.isForcedLocalDisabled(requireContext()) updateConfiguration() } diff --git a/play-services-droidguard/core/src/main/res/xml/preferences_droidguard.xml b/play-services-droidguard/core/src/main/res/xml/preferences_droidguard.xml index 995f0a1dbf..bfac8b5655 100644 --- a/play-services-droidguard/core/src/main/res/xml/preferences_droidguard.xml +++ b/play-services-droidguard/core/src/main/res/xml/preferences_droidguard.xml @@ -4,11 +4,13 @@ diff --git a/play-services-droidguard/src/main/java/com/google/android/gms/droidguard/DroidGuard.java b/play-services-droidguard/src/main/java/com/google/android/gms/droidguard/DroidGuard.java index 3646353ce8..d0391d9a7d 100644 --- a/play-services-droidguard/src/main/java/com/google/android/gms/droidguard/DroidGuard.java +++ b/play-services-droidguard/src/main/java/com/google/android/gms/droidguard/DroidGuard.java @@ -13,4 +13,7 @@ public class DroidGuard { public static DroidGuardClient getClient(Context context) { return new DroidGuardClientImpl(context); } + public static DroidGuardClient getClient(Context context, String packageName) { + return new DroidGuardClientImpl(context, packageName); + } } diff --git a/play-services-droidguard/src/main/java/com/google/android/gms/droidguard/internal/DroidGuardResultsRequest.java b/play-services-droidguard/src/main/java/com/google/android/gms/droidguard/internal/DroidGuardResultsRequest.java index 162a6bfc00..d2f0948b5e 100644 --- a/play-services-droidguard/src/main/java/com/google/android/gms/droidguard/internal/DroidGuardResultsRequest.java +++ b/play-services-droidguard/src/main/java/com/google/android/gms/droidguard/internal/DroidGuardResultsRequest.java @@ -6,13 +6,14 @@ package com.google.android.gms.droidguard.internal; import android.net.Network; -import android.os.Build; import android.os.Bundle; import android.os.ParcelFileDescriptor; +import androidx.annotation.NonNull; import androidx.annotation.RequiresApi; import org.microg.gms.common.Constants; +import org.microg.gms.utils.ToStringHelper; import org.microg.safeparcel.AutoSafeParcelable; public class DroidGuardResultsRequest extends AutoSafeParcelable { @@ -89,5 +90,15 @@ public DroidGuardResultsRequest setNetworkToUse(Network networkToUse) { return this; } + @NonNull + @Override + public String toString() { + ToStringHelper helper = ToStringHelper.name("DroidGuardResultsRequest"); + for (String key : bundle.keySet()) { + helper.field(key, bundle.get(key)); + } + return helper.end(); + } + public static final Creator CREATOR = new AutoCreator<>(DroidGuardResultsRequest.class); } 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 593137f2a2..5399c347fc 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 @@ -40,6 +40,10 @@ public DroidGuardApiClient(Context context, ConnectionCallbacks callbacks, OnCon handler = new Handler(thread.getLooper()); } + public void setPackageName(String packageName) { + this.packageName = packageName; + } + public DroidGuardHandle openHandle(String flow, DroidGuardResultsRequest request) { try { IDroidGuardHandle handle = getServiceInterface().getHandle(); @@ -66,6 +70,15 @@ public DroidGuardHandle openHandle(String flow, DroidGuardResultsRequest request } } + public void markHandleClosed() { + if (openHandles == 0) { + Log.w(TAG, "Can't mark handle closed if none is open"); + return; + } + openHandles--; + if (openHandles == 0) disconnect(); + } + public void runOnHandler(Runnable runnable) { if (Looper.myLooper() == handler.getLooper()) { runnable.run(); diff --git a/play-services-droidguard/src/main/java/org/microg/gms/droidguard/DroidGuardClientImpl.java b/play-services-droidguard/src/main/java/org/microg/gms/droidguard/DroidGuardClientImpl.java index f4e064d6b2..63b6b56ab5 100644 --- a/play-services-droidguard/src/main/java/org/microg/gms/droidguard/DroidGuardClientImpl.java +++ b/play-services-droidguard/src/main/java/org/microg/gms/droidguard/DroidGuardClientImpl.java @@ -18,12 +18,19 @@ import java.util.Map; -public class DroidGuardClientImpl extends GoogleApi implements DroidGuardClient { - private static final Api API = new Api<>((options, context, looper, clientSettings, callbacks, connectionFailedListener) -> new DroidGuardApiClient(context, callbacks, connectionFailedListener)); +public class DroidGuardClientImpl extends GoogleApi implements DroidGuardClient { + private static final Api API = new Api<>((options, context, looper, clientSettings, callbacks, connectionFailedListener) -> { + DroidGuardApiClient client = new DroidGuardApiClient(context, callbacks, connectionFailedListener); + if (options != null && options.packageName != null) client.setPackageName(options.packageName); + return client; + }); public DroidGuardClientImpl(Context context) { super(context, API); } + public DroidGuardClientImpl(Context context, String packageName) { + super(context, API, new Options(packageName)); + } @Override public Task init(String flow, DroidGuardResultsRequest request) { @@ -41,4 +48,12 @@ public Task getResults(String flow, Map data, DroidGuard return results; }); } + + public static class Options implements Api.ApiOptions.Optional { + public final String packageName; + + public Options(String packageName) { + this.packageName = packageName; + } + } } diff --git a/play-services-droidguard/src/main/java/org/microg/gms/droidguard/DroidGuardHandleImpl.java b/play-services-droidguard/src/main/java/org/microg/gms/droidguard/DroidGuardHandleImpl.java index e99be7a3d8..1a80794f87 100644 --- a/play-services-droidguard/src/main/java/org/microg/gms/droidguard/DroidGuardHandleImpl.java +++ b/play-services-droidguard/src/main/java/org/microg/gms/droidguard/DroidGuardHandleImpl.java @@ -81,6 +81,7 @@ public void close() { } catch (Exception e) { Log.w(TAG, "Error while closing handle."); } + apiClient.markHandleClosed(); handle = null; } }); diff --git a/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/privileged/Fido2PrivilegedService.kt b/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/privileged/Fido2PrivilegedService.kt index 41dab87760..c57c24a864 100644 --- a/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/privileged/Fido2PrivilegedService.kt +++ b/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/privileged/Fido2PrivilegedService.kt @@ -12,7 +12,7 @@ import android.app.PendingIntent.FLAG_UPDATE_CURRENT import android.content.Context import android.content.Context.KEYGUARD_SERVICE import android.content.Intent -import android.os.Build +import android.os.Build.VERSION.SDK_INT import android.os.Parcel import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner @@ -90,7 +90,7 @@ class Fido2PrivilegedServiceImpl(private val context: Context, private val lifec override fun isUserVerifyingPlatformAuthenticatorAvailable(callbacks: IBooleanCallback) { lifecycleScope.launchWhenStarted { - if (Build.VERSION.SDK_INT < 24) { + if (SDK_INT < 24) { callbacks.onBoolean(false) } else { val keyguardManager = context.getSystemService(KEYGUARD_SERVICE) as? KeyguardManager? @@ -102,5 +102,5 @@ class Fido2PrivilegedServiceImpl(private val context: Context, private val lifec override fun getLifecycle(): Lifecycle = lifecycle override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean = - warnOnTransactionIssues(code, reply, flags) { super.onTransact(code, data, reply, flags) } + warnOnTransactionIssues(code, reply, flags, TAG) { super.onTransact(code, data, reply, flags) } } diff --git a/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/transport/bluetooth/BluetoothTransportHandler.kt b/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/transport/bluetooth/BluetoothTransportHandler.kt index 050e5fe9b6..c6e00b0279 100644 --- a/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/transport/bluetooth/BluetoothTransportHandler.kt +++ b/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/transport/bluetooth/BluetoothTransportHandler.kt @@ -7,7 +7,7 @@ package org.microg.gms.fido.core.transport.bluetooth import android.bluetooth.BluetoothManager import android.content.Context -import android.os.Build +import android.os.Build.VERSION.SDK_INT import androidx.core.content.getSystemService import org.microg.gms.fido.core.transport.Transport import org.microg.gms.fido.core.transport.TransportHandler @@ -16,5 +16,5 @@ import org.microg.gms.fido.core.transport.TransportHandlerCallback class BluetoothTransportHandler(private val context: Context, callback: TransportHandlerCallback? = null) : TransportHandler(Transport.BLUETOOTH, callback) { override val isSupported: Boolean - get() = Build.VERSION.SDK_INT >= 18 && context.getSystemService()?.adapter != null + get() = SDK_INT >= 18 && context.getSystemService()?.adapter != null } diff --git a/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/transport/screenlock/ScreenLockCredentialStore.kt b/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/transport/screenlock/ScreenLockCredentialStore.kt index fa536be821..7a0f1cdb5d 100644 --- a/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/transport/screenlock/ScreenLockCredentialStore.kt +++ b/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/transport/screenlock/ScreenLockCredentialStore.kt @@ -8,7 +8,7 @@ package org.microg.gms.fido.core.transport.screenlock import android.content.Context import android.database.sqlite.SQLiteDatabase import android.database.sqlite.SQLiteOpenHelper -import android.os.Build +import android.os.Build.VERSION.SDK_INT import android.security.keystore.KeyGenParameterSpec import android.security.keystore.KeyPermanentlyInvalidatedException import android.security.keystore.KeyProperties @@ -40,7 +40,7 @@ class ScreenLockCredentialStore(val context: Context) { .setDigests(KeyProperties.DIGEST_SHA256) .setAlgorithmParameterSpec(ECGenParameterSpec("secp256r1")) .setUserAuthenticationRequired(true) - if (Build.VERSION.SDK_INT >= 24) builder.setAttestationChallenge(challenge) + if (SDK_INT >= 24) builder.setAttestationChallenge(challenge) generator.initialize(builder.build()) generator.generateKeyPair() return keyId diff --git a/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/transport/screenlock/ScreenLockTransportHandler.kt b/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/transport/screenlock/ScreenLockTransportHandler.kt index 9bfbfb8974..de518add46 100644 --- a/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/transport/screenlock/ScreenLockTransportHandler.kt +++ b/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/transport/screenlock/ScreenLockTransportHandler.kt @@ -6,7 +6,7 @@ package org.microg.gms.fido.core.transport.screenlock import android.app.KeyguardManager -import android.os.Build +import android.os.Build.VERSION.SDK_INT import android.util.Log import androidx.annotation.RequiresApi import androidx.biometric.BiometricPrompt @@ -134,7 +134,7 @@ class ScreenLockTransportHandler(private val activity: FragmentActivity, callbac NoneAttestationObject(authenticatorData) } else { try { - if (Build.VERSION.SDK_INT >= 24) { + if (SDK_INT >= 24) { createAndroidKeyAttestation(signature, authenticatorData, clientDataHash, options.rpId, keyId) } else { createSafetyNetAttestation(authenticatorData, clientDataHash) diff --git a/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/transport/usb/UsbDevicePermissionManager.kt b/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/transport/usb/UsbDevicePermissionManager.kt index 29d3b20ae9..1a9d1503c2 100644 --- a/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/transport/usb/UsbDevicePermissionManager.kt +++ b/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/transport/usb/UsbDevicePermissionManager.kt @@ -12,12 +12,13 @@ import android.content.Intent import android.content.IntentFilter import android.hardware.usb.UsbDevice import android.hardware.usb.UsbManager +import android.os.Build.VERSION.SDK_INT import kotlinx.coroutines.CompletableDeferred private val Context.usbPermissionCallbackAction get() = "$packageName.USB_PERMISSION_CALLBACK" -private val receiver = object : BroadcastReceiver() { +private object UsbDevicePermissionReceiver : BroadcastReceiver() { private var registered = false private val pendingRequests = hashMapOf>>() @@ -72,9 +73,9 @@ class UsbDevicePermissionManager(private val context: Context) { suspend fun awaitPermission(device: UsbDevice): Boolean { if (context.usbManager?.hasPermission(device) == true) return true val res = CompletableDeferred() - if (receiver.addDeferred(device, res)) { - receiver.register(context) - val intent = PendingIntent.getBroadcast(context, 0, Intent(context.usbPermissionCallbackAction), 0) + if (UsbDevicePermissionReceiver.addDeferred(device, res)) { + UsbDevicePermissionReceiver.register(context) + val intent = PendingIntent.getBroadcast(context, 0, Intent(context.usbPermissionCallbackAction).apply { `package` = context.packageName }, if (SDK_INT >= 31) PendingIntent.FLAG_MUTABLE else 0) context.usbManager?.requestPermission(device, intent) } return res.await() diff --git a/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/ui/AuthenticatorActivity.kt b/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/ui/AuthenticatorActivity.kt index e894e344f9..1f1b79b7d8 100644 --- a/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/ui/AuthenticatorActivity.kt +++ b/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/ui/AuthenticatorActivity.kt @@ -8,7 +8,7 @@ package org.microg.gms.fido.core.ui import android.content.Intent import android.graphics.Color import android.graphics.drawable.ColorDrawable -import android.os.Build +import android.os.Build.VERSION.SDK_INT import android.os.Bundle import android.util.Base64 import android.util.Log @@ -60,8 +60,8 @@ class AuthenticatorActivity : AppCompatActivity(), TransportHandlerCallback { setOfNotNull( BluetoothTransportHandler(this, this), NfcTransportHandler(this, this), - if (Build.VERSION.SDK_INT >= 21) UsbTransportHandler(this, this) else null, - if (Build.VERSION.SDK_INT >= 23) ScreenLockTransportHandler(this, this) else null + if (SDK_INT >= 21) UsbTransportHandler(this, this) else null, + if (SDK_INT >= 23) ScreenLockTransportHandler(this, this) else null ) } @@ -84,7 +84,7 @@ class AuthenticatorActivity : AppCompatActivity(), TransportHandlerCallback { if (!intent.extras?.keySet().orEmpty().containsAll(REQUIRED_EXTRAS)) { return finishWithError(UNKNOWN_ERR, "Extra missing from request") } - if (Build.VERSION.SDK_INT < 24) { + if (SDK_INT < 24) { return finishWithError(NOT_SUPPORTED_ERR, "FIDO2 API is not supported on devices below N") } val options = options ?: return finishWithError(DATA_ERR, "The request options are not valid") diff --git a/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/ui/NfcTransportFragment.kt b/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/ui/NfcTransportFragment.kt index 75b6b4c311..1640807899 100644 --- a/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/ui/NfcTransportFragment.kt +++ b/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/ui/NfcTransportFragment.kt @@ -8,7 +8,7 @@ package org.microg.gms.fido.core.ui import android.graphics.drawable.Animatable2 import android.graphics.drawable.AnimatedVectorDrawable import android.graphics.drawable.Drawable -import android.os.Build +import android.os.Build.VERSION.SDK_INT import android.os.Bundle import android.view.LayoutInflater import android.view.View @@ -38,7 +38,7 @@ class NfcTransportFragment : AuthenticatorActivityFragment(), TransportHandlerCa navOptions { popUpTo(R.id.usbFragment) { inclusive = true } }) } } - if (Build.VERSION.SDK_INT >= 23) { + if (SDK_INT >= 23) { (binding.fidoNfcWaitConnectAnimation.drawable as? AnimatedVectorDrawable)?.registerAnimationCallback(object : Animatable2.AnimationCallback() { override fun onAnimationEnd(drawable: Drawable?) { diff --git a/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/ui/UsbTransportFragment.kt b/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/ui/UsbTransportFragment.kt index 8ca1705479..42f8c9f3de 100644 --- a/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/ui/UsbTransportFragment.kt +++ b/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/ui/UsbTransportFragment.kt @@ -10,7 +10,7 @@ import android.graphics.drawable.AnimatedVectorDrawable import android.graphics.drawable.Drawable import android.hardware.usb.UsbDevice import android.hardware.usb.UsbManager -import android.os.Build +import android.os.Build.VERSION.SDK_INT import android.os.Bundle import android.util.Log import android.view.LayoutInflater @@ -42,7 +42,7 @@ class UsbTransportFragment : AuthenticatorActivityFragment(), TransportHandlerCa navOptions { popUpTo(R.id.usbFragment) { inclusive = true } }) } } - if (Build.VERSION.SDK_INT >= 23) { + if (SDK_INT >= 23) { for (imageView in listOfNotNull(binding.fidoUsbWaitConnectAnimation, binding.fidoUsbWaitConfirmAnimation)) { (imageView.drawable as? AnimatedVectorDrawable)?.registerAnimationCallback(object : Animatable2.AnimationCallback() { override fun onAnimationEnd(drawable: Drawable?) { @@ -63,7 +63,7 @@ class UsbTransportFragment : AuthenticatorActivityFragment(), TransportHandlerCa override fun onStatusChanged(transport: Transport, status: String, extras: Bundle?) { if (transport != Transport.USB) return binding.status = status - if (Build.VERSION.SDK_INT >= 21) { + if (SDK_INT >= 21) { binding.deviceName = extras?.getParcelable(UsbManager.EXTRA_DEVICE)?.productName ?: "your security key" } else { diff --git a/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/ui/WelcomeFragment.kt b/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/ui/WelcomeFragment.kt index af15b910f5..4d9071cde4 100644 --- a/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/ui/WelcomeFragment.kt +++ b/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/ui/WelcomeFragment.kt @@ -5,7 +5,6 @@ package org.microg.gms.fido.core.ui -import android.os.Build import android.os.Bundle import android.view.LayoutInflater import android.view.View diff --git a/play-services-gcm/src/main/java/com/google/android/gms/gcm/GcmReceiver.java b/play-services-gcm/src/main/java/com/google/android/gms/gcm/GcmReceiver.java index e9bba44b9e..1df70bccb0 100644 --- a/play-services-gcm/src/main/java/com/google/android/gms/gcm/GcmReceiver.java +++ b/play-services-gcm/src/main/java/com/google/android/gms/gcm/GcmReceiver.java @@ -23,11 +23,11 @@ import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.pm.ServiceInfo; -import android.os.Build; import android.util.Base64; import android.util.Log; import androidx.legacy.content.WakefulBroadcastReceiver; +import static android.os.Build.VERSION.SDK_INT; import static org.microg.gms.gcm.GcmConstants.ACTION_C2DM_REGISTRATION; import static org.microg.gms.gcm.GcmConstants.ACTION_INSTANCE_ID; import static org.microg.gms.gcm.GcmConstants.EXTRA_FROM; @@ -69,7 +69,7 @@ public void onReceive(Context context, Intent intent) { private void sanitizeIntent(Context context, Intent intent) { intent.setComponent(null); intent.setPackage(context.getPackageName()); - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { + if (SDK_INT < 19) { intent.removeCategory(context.getPackageName()); } String from = intent.getStringExtra(EXTRA_FROM); diff --git a/play-services-iid/src/main/java/com/google/android/gms/iid/MessengerCompat.java b/play-services-iid/src/main/java/com/google/android/gms/iid/MessengerCompat.java index 05bf4c8c0f..5e397e515d 100644 --- a/play-services-iid/src/main/java/com/google/android/gms/iid/MessengerCompat.java +++ b/play-services-iid/src/main/java/com/google/android/gms/iid/MessengerCompat.java @@ -26,14 +26,13 @@ import android.os.RemoteException; import static android.os.Build.VERSION.SDK_INT; -import static android.os.Build.VERSION_CODES.LOLLIPOP; public class MessengerCompat implements Parcelable { private Messenger messenger; private IMessengerCompat messengerCompat; public MessengerCompat(IBinder binder) { - if (SDK_INT >= LOLLIPOP) { + if (SDK_INT >= 21) { messenger = new Messenger(binder); } else { messengerCompat = IMessengerCompat.Stub.asInterface(binder); @@ -41,7 +40,7 @@ public MessengerCompat(IBinder binder) { } public MessengerCompat(Handler handler) { - if (SDK_INT >= LOLLIPOP) { + if (SDK_INT >= 21) { messenger = new Messenger(handler); } else { messengerCompat = new IMessengerCompatImpl(handler); diff --git a/play-services-iid/src/main/java/org/microg/gms/iid/InstanceIdRpc.java b/play-services-iid/src/main/java/org/microg/gms/iid/InstanceIdRpc.java index cce6616f4f..2f3f660301 100644 --- a/play-services-iid/src/main/java/org/microg/gms/iid/InstanceIdRpc.java +++ b/play-services-iid/src/main/java/org/microg/gms/iid/InstanceIdRpc.java @@ -22,7 +22,6 @@ import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; -import android.os.Build; import android.os.Bundle; import android.os.ConditionVariable; import android.os.Handler; @@ -51,6 +50,7 @@ import java.util.Random; import static android.content.pm.PackageManager.PERMISSION_GRANTED; +import static android.os.Build.VERSION.SDK_INT; import static com.google.android.gms.iid.InstanceID.ERROR_BACKOFF; import static com.google.android.gms.iid.InstanceID.ERROR_MISSING_INSTANCEID_SERVICE; import static com.google.android.gms.iid.InstanceID.ERROR_SERVICE_NOT_AVAILABLE; @@ -281,7 +281,7 @@ private void sendRegisterMessage(Bundle data, KeyPair keyPair, String requestId) Intent intent = new Intent(ACTION_C2DM_REGISTER); intent.setPackage(iidPackageName); data.putString(EXTRA_GMS_VERSION, Integer.toString(getGmsVersionCode(context))); - data.putString(EXTRA_OS_VERSION, Integer.toString(Build.VERSION.SDK_INT)); + data.putString(EXTRA_OS_VERSION, Integer.toString(SDK_INT)); data.putString(EXTRA_APP_VERSION_CODE, Integer.toString(getSelfVersionCode(context))); data.putString(EXTRA_APP_VERSION_NAME, getSelfVersionName(context)); data.putString(EXTRA_CLIENT_VERSION, "iid-" + GMS_VERSION_CODE); diff --git a/play-services-location/core/base/build.gradle b/play-services-location/core/base/build.gradle new file mode 100644 index 0000000000..8f6663fe62 --- /dev/null +++ b/play-services-location/core/base/build.gradle @@ -0,0 +1,32 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +dependencies { + api project(':play-services-location') + implementation project(':play-services-base-core') +} + +android { + compileSdkVersion androidCompileSdk + buildToolsVersion "$androidBuildVersionTools" + + defaultConfig { + versionName version + minSdkVersion androidMinSdk + targetSdkVersion androidTargetSdk + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + compileOptions { + sourceCompatibility = 1.8 + targetCompatibility = 1.8 + } +} diff --git a/play-services-location/core/base/src/main/AndroidManifest.xml b/play-services-location/core/base/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..671251d213 --- /dev/null +++ b/play-services-location/core/base/src/main/AndroidManifest.xml @@ -0,0 +1,6 @@ + + + diff --git a/play-services-location/core/base/src/main/kotlin/org/microg/gms/location/LocationSettings.kt b/play-services-location/core/base/src/main/kotlin/org/microg/gms/location/LocationSettings.kt new file mode 100644 index 0000000000..e8891897e7 --- /dev/null +++ b/play-services-location/core/base/src/main/kotlin/org/microg/gms/location/LocationSettings.kt @@ -0,0 +1,59 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.location + +import android.content.Context +import org.microg.gms.settings.SettingsContract + +class LocationSettings(private val context: Context) { + var wifiMls : Boolean + get() = SettingsContract.getSettings(context, SettingsContract.Location.getContentUri(context), arrayOf(SettingsContract.Location.WIFI_MLS)) { c -> + c.getInt(0) != 0 + } + set(value) { + SettingsContract.setSettings(context, SettingsContract.Location.getContentUri(context)) { put(SettingsContract.Location.WIFI_MLS, value)} + } + + var wifiMoving: Boolean + get() = SettingsContract.getSettings(context, SettingsContract.Location.getContentUri(context), arrayOf(SettingsContract.Location.WIFI_MOVING)) { c -> + c.getInt(0) != 0 + } + set(value) { + SettingsContract.setSettings(context, SettingsContract.Location.getContentUri(context)) { put(SettingsContract.Location.WIFI_MOVING, value)} + } + + var wifiLearning: Boolean + get() = SettingsContract.getSettings(context, SettingsContract.Location.getContentUri(context), arrayOf(SettingsContract.Location.WIFI_LEARNING)) { c -> + c.getInt(0) != 0 + } + set(value) { + SettingsContract.setSettings(context, SettingsContract.Location.getContentUri(context)) { put(SettingsContract.Location.WIFI_LEARNING, value)} + } + + var cellMls : Boolean + get() = SettingsContract.getSettings(context, SettingsContract.Location.getContentUri(context), arrayOf(SettingsContract.Location.CELL_MLS)) { c -> + c.getInt(0) != 0 + } + set(value) { + SettingsContract.setSettings(context, SettingsContract.Location.getContentUri(context)) { put(SettingsContract.Location.CELL_MLS, value)} + } + + var cellLearning: Boolean + get() = SettingsContract.getSettings(context, SettingsContract.Location.getContentUri(context), arrayOf(SettingsContract.Location.CELL_LEARNING)) { c -> + c.getInt(0) != 0 + } + set(value) { + SettingsContract.setSettings(context, SettingsContract.Location.getContentUri(context)) { put(SettingsContract.Location.CELL_LEARNING, value)} + } + + var geocoderNominatim: Boolean + get() = SettingsContract.getSettings(context, SettingsContract.Location.getContentUri(context), arrayOf(SettingsContract.Location.GEOCODER_NOMINATIM)) { c -> + c.getInt(0) != 0 + } + set(value) { + SettingsContract.setSettings(context, SettingsContract.Location.getContentUri(context)) { put(SettingsContract.Location.GEOCODER_NOMINATIM, value)} + } +} \ No newline at end of file diff --git a/play-services-location/core/base/src/main/kotlin/org/microg/gms/location/extensions.kt b/play-services-location/core/base/src/main/kotlin/org/microg/gms/location/extensions.kt new file mode 100644 index 0000000000..d5a5c79d23 --- /dev/null +++ b/play-services-location/core/base/src/main/kotlin/org/microg/gms/location/extensions.kt @@ -0,0 +1,95 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.location + +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.location.Location +import android.os.SystemClock +import android.text.format.DateUtils +import android.util.Log +import androidx.core.location.LocationCompat + +const val ACTION_NETWORK_LOCATION_SERVICE = "org.microg.gms.location.network.ACTION_NETWORK_LOCATION_SERVICE" +const val EXTRA_LOCATION = "location" +const val EXTRA_PENDING_INTENT = "pending_intent" +const val EXTRA_ENABLE = "enable" +const val EXTRA_INTERVAL_MILLIS = "interval" +const val EXTRA_FORCE_NOW = "force_now" +const val EXTRA_LOW_POWER = "low_power" +const val EXTRA_WORK_SOURCE = "work_source" +const val EXTRA_BYPASS = "bypass" + +val Location.elapsedMillis: Long + get() = LocationCompat.getElapsedRealtimeMillis(this) + +fun Long.formatRealtime(): CharSequence = if (this <= 0) "n/a" else DateUtils.getRelativeTimeSpanString((this - SystemClock.elapsedRealtime()) + System.currentTimeMillis(), System.currentTimeMillis(), 0) +fun Long.formatDuration(): CharSequence { + if (this == 0L) return "0ms" + if (this > 315360000000L /* ten years */) return "\u221e" + val interval = listOf(1000, 60, 60, 24, Long.MAX_VALUE) + val intervalName = listOf("ms", "s", "m", "h", "d") + var ret = "" + var rem = this + for (i in 0 until interval.size) { + val mod = rem % interval[i] + if (mod != 0L) { + ret = "$mod${intervalName[i]}$ret" + } + rem /= interval[i] + if (mod == 0L && rem == 1L) { + ret = "${interval[i]}${intervalName[i]}$ret" + break + } else if (rem == 0L) { + break + } + } + return ret +} + +private var hasMozillaLocationServiceSupportFlag: Boolean? = null +fun Context.hasMozillaLocationServiceSupport(): Boolean { + if (!hasNetworkLocationServiceBuiltIn()) return false + var flag = hasMozillaLocationServiceSupportFlag + if (flag == null) { + return try { + val clazz = Class.forName("org.microg.gms.location.network.mozilla.MozillaLocationServiceClient") + val apiKey = clazz.getDeclaredField("API_KEY").get(null) as? String? + flag = apiKey != null + hasMozillaLocationServiceSupportFlag = flag + flag + } catch (e: Exception) { + Log.w("Location", e) + hasMozillaLocationServiceSupportFlag = false + false + } + } else { + return flag + } +} + +private var hasNetworkLocationServiceBuiltInFlag: Boolean? = null +fun Context.hasNetworkLocationServiceBuiltIn(): Boolean { + var flag = hasNetworkLocationServiceBuiltInFlag + if (flag == null) { + try { + val serviceIntent = Intent().apply { + action = ACTION_NETWORK_LOCATION_SERVICE + setPackage(packageName) + } + val services = packageManager?.queryIntentServices(serviceIntent, PackageManager.MATCH_DEFAULT_ONLY) + flag = services?.isNotEmpty() ?: false + hasNetworkLocationServiceBuiltInFlag = flag + return flag + } catch (e: Exception) { + hasNetworkLocationServiceBuiltInFlag = false + return false + } + } else { + return flag + } +} \ No newline at end of file diff --git a/play-services-location/core/build.gradle b/play-services-location/core/build.gradle index e69b8ba6fb..d048274a0e 100644 --- a/play-services-location/core/build.gradle +++ b/play-services-location/core/build.gradle @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2020, microG Project Team + * SPDX-FileCopyrightText: 2023 microG Project Team * SPDX-License-Identifier: Apache-2.0 */ @@ -8,18 +8,22 @@ apply plugin: 'kotlin-android' dependencies { api project(':play-services-location') - compileOnly project(':play-services-location-system-api') implementation project(':play-services-base-core') + implementation project(':play-services-location-core-base') + + implementation "androidx.lifecycle:lifecycle-service:$lifecycleVersion" + implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion" + implementation "androidx.navigation:navigation-fragment-ktx:$navigationVersion" + implementation "androidx.navigation:navigation-ui-ktx:$navigationVersion" + implementation "androidx.preference:preference-ktx:$preferenceVersion" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutineVersion" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutineVersion" - implementation "androidx.lifecycle:lifecycle-service:$lifecycleVersion" - implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion" implementation "com.android.volley:volley:$volleyVersion" - implementation 'org.microg:address-formatter:0.3.1' + compileOnly project(':play-services-maps') } android { @@ -30,6 +34,7 @@ android { versionName version minSdkVersion androidMinSdk targetSdkVersion androidTargetSdk + buildConfigField "String", "ICHNAEA_KEY", "\"${localProperties.get("ichnaea.key", "")}\"" } sourceSets { diff --git a/play-services-location/core/provider/build.gradle b/play-services-location/core/provider/build.gradle new file mode 100644 index 0000000000..1aa2e2948d --- /dev/null +++ b/play-services-location/core/provider/build.gradle @@ -0,0 +1,46 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +dependencies { + api project(':play-services-location') + compileOnly project(':play-services-location-core-system-api') + implementation project(':play-services-base-core') + implementation project(':play-services-location-core-base') + + implementation "androidx.lifecycle:lifecycle-service:$lifecycleVersion" + implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion" + + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutineVersion" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutineVersion" + + implementation "com.android.volley:volley:$volleyVersion" + + implementation 'org.microg:address-formatter:0.3.1' +} + +android { + compileSdkVersion androidCompileSdk + buildToolsVersion "$androidBuildVersionTools" + + defaultConfig { + versionName version + minSdkVersion androidMinSdk + targetSdkVersion androidTargetSdk + buildConfigField "String", "ICHNAEA_KEY", "\"${localProperties.get("ichnaea.key", "")}\"" + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + compileOptions { + sourceCompatibility = 1.8 + targetCompatibility = 1.8 + } +} diff --git a/play-services-location/core/provider/src/main/AndroidManifest.xml b/play-services-location/core/provider/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..e75eb59df5 --- /dev/null +++ b/play-services-location/core/provider/src/main/AndroidManifest.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/LocationCacheDatabase.kt b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/LocationCacheDatabase.kt new file mode 100644 index 0000000000..33403f2f33 --- /dev/null +++ b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/LocationCacheDatabase.kt @@ -0,0 +1,368 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.location.network + +import android.content.ContentValues +import android.content.Context +import android.database.Cursor +import android.database.DatabaseUtils +import android.database.sqlite.SQLiteDatabase +import android.database.sqlite.SQLiteOpenHelper +import android.location.Location +import androidx.core.content.contentValuesOf +import androidx.core.os.bundleOf +import org.microg.gms.location.network.cell.CellDetails +import org.microg.gms.location.network.cell.isValid +import org.microg.gms.location.network.wifi.WifiDetails +import org.microg.gms.location.network.wifi.isRequestable +import org.microg.gms.location.network.wifi.macBytes +import org.microg.gms.utils.toHexString +import java.io.PrintWriter +import kotlin.math.max + +internal class LocationCacheDatabase(context: Context?) : SQLiteOpenHelper(context, "geocache.db", null, 3) { + fun getCellLocation(cell: CellDetails, allowLearned: Boolean = true): Location? { + val cellLocation = readableDatabase.query( + TABLE_CELLS, + arrayOf(FIELD_LATITUDE, FIELD_LONGITUDE, FIELD_ACCURACY, FIELD_TIME, FIELD_PRECISION), + CELLS_SELECTION, + getCellSelectionArgs(cell), + null, + null, + null + ).use { cursor -> + if (cursor.moveToNext()) { + cursor.getLocation(MAX_CACHE_AGE) + } else { + null + } + } + if (cellLocation?.precision?.let { it >= 1f } == true) return cellLocation + if (allowLearned) { + readableDatabase.query( + TABLE_CELLS_LEARN, + arrayOf(FIELD_LATITUDE_HIGH, FIELD_LATITUDE_LOW, FIELD_LONGITUDE_HIGH, FIELD_LONGITUDE_LOW, FIELD_TIME, FIELD_BAD_TIME), + CELLS_SELECTION, + getCellSelectionArgs(cell), + null, + null, + null + ).use { cursor -> + if (cursor.moveToNext()) { + val badTime = cursor.getLong(5) + val time = cursor.getLong(4) + if (badTime > time - LEARN_BAD_CUTOFF) return@use + cursor.getMidLocation(MAX_CACHE_AGE, LEARN_BASE_ACCURACY_CELL)?.let { return it } + } + } + } + if (cellLocation != null) return cellLocation + readableDatabase.query(TABLE_CELLS_PRE, arrayOf(FIELD_TIME), CELLS_PRE_SELECTION, getCellPreSelectionArgs(cell), null, null, null).use { cursor -> + if (cursor.moveToNext()) { + if (cursor.getLong(1) > System.currentTimeMillis() - MAX_CACHE_AGE) { + return NEGATIVE_CACHE_ENTRY + } + } + } + return null + } + + fun getWifiLocation(wifi: WifiDetails): Location? { + val wifiLocation = readableDatabase.query( + TABLE_WIFIS, + arrayOf(FIELD_LATITUDE, FIELD_LONGITUDE, FIELD_ACCURACY, FIELD_TIME, FIELD_PRECISION), + getWifiSelection(wifi), + null, + null, + null, + null + ).use { cursor -> + if (cursor.moveToNext()) { + cursor.getLocation(MAX_CACHE_AGE) + } else { + null + } + } + if (wifiLocation?.precision?.let { it >= 1f } == true) return wifiLocation + readableDatabase.query( + TABLE_WIFI_LEARN, + arrayOf(FIELD_LATITUDE_HIGH, FIELD_LATITUDE_LOW, FIELD_LONGITUDE_HIGH, FIELD_LONGITUDE_LOW, FIELD_TIME, FIELD_BAD_TIME), + getWifiSelection(wifi), + null, + null, + null, + null + ).use { cursor -> + if (cursor.moveToNext()) { + val badTime = cursor.getLong(5) + val time = cursor.getLong(4) + if (badTime > time - LEARN_BAD_CUTOFF) return@use + cursor.getMidLocation(MAX_CACHE_AGE, LEARN_BASE_ACCURACY_WIFI, 0.5)?.let { return it } + } + } + return wifiLocation + } + + fun putCellLocation(cell: CellDetails, location: Location) { + if (!cell.isValid) return + val cv = contentValuesOf( + FIELD_MCC to cell.mcc, + FIELD_MNC to cell.mnc, + FIELD_LAC_TAC to (cell.lac ?: cell.tac ?: 0), + FIELD_TYPE to cell.type.ordinal, + FIELD_CID to cell.cid, + FIELD_PSC to (cell.psc ?: 0) + ).apply { putLocation(location) } + writableDatabase.insertWithOnConflict(TABLE_CELLS, null, cv, SQLiteDatabase.CONFLICT_REPLACE) + } + + fun learnCellLocation(cell: CellDetails, location: Location) { + if (!cell.isValid) return + val (exists, isBad) = readableDatabase.query( + TABLE_CELLS_LEARN, + arrayOf(FIELD_LATITUDE_HIGH, FIELD_LATITUDE_LOW, FIELD_LONGITUDE_HIGH, FIELD_LONGITUDE_LOW, FIELD_TIME), + CELLS_SELECTION, + getCellSelectionArgs(cell), + null, + null, + null + ).use { cursor -> + if (cursor.moveToNext()) { + val midLocation = cursor.getMidLocation(Long.MAX_VALUE, 0f) + (midLocation != null) to (midLocation?.let { it.distanceTo(location) > LEARN_BAD_SIZE_CELL } == true) + } else { + false to false + } + } + if (exists && isBad) { + writableDatabase.update( + TABLE_CELLS_LEARN, contentValuesOf( + FIELD_LATITUDE_HIGH to location.latitude, + FIELD_LATITUDE_LOW to location.latitude, + FIELD_LONGITUDE_HIGH to location.longitude, + FIELD_LONGITUDE_LOW to location.longitude, + FIELD_TIME to location.time, + FIELD_BAD_TIME to location.time + ), CELLS_SELECTION, getCellSelectionArgs(cell) + ) + } else if (!exists) { + writableDatabase.insertWithOnConflict( + TABLE_CELLS_LEARN, null, contentValuesOf( + FIELD_MCC to cell.mcc, + FIELD_MNC to cell.mnc, + FIELD_LAC_TAC to (cell.lac ?: cell.tac ?: 0), + FIELD_TYPE to cell.type.ordinal, + FIELD_CID to cell.cid, + FIELD_PSC to (cell.psc ?: 0), + FIELD_LATITUDE_HIGH to location.latitude, + FIELD_LATITUDE_LOW to location.latitude, + FIELD_LONGITUDE_HIGH to location.longitude, + FIELD_LONGITUDE_LOW to location.longitude, + FIELD_TIME to location.time, + FIELD_BAD_TIME to 0 + ), SQLiteDatabase.CONFLICT_REPLACE) + } else { + writableDatabase.rawQuery("UPDATE $TABLE_CELLS_LEARN SET $FIELD_LATITUDE_HIGH = max($FIELD_LATITUDE_HIGH, ?), $FIELD_LATITUDE_LOW = min($FIELD_LATITUDE_LOW, ?), $FIELD_LONGITUDE_HIGH = max($FIELD_LONGITUDE_HIGH, ?), $FIELD_LONGITUDE_LOW = min($FIELD_LONGITUDE_LOW, ?), $FIELD_TIME = ? WHERE $CELLS_SELECTION", arrayOf(location.latitude.toString(), location.latitude.toString(), location.longitude.toString(), location.longitude.toString()) + getCellSelectionArgs(cell)).close() + } + } + + fun learnWifiLocation(wifi: WifiDetails, location: Location) { + if (!wifi.isRequestable) return + val (exists, isBad) = readableDatabase.query( + TABLE_WIFI_LEARN, + arrayOf(FIELD_LATITUDE_HIGH, FIELD_LATITUDE_LOW, FIELD_LONGITUDE_HIGH, FIELD_LONGITUDE_LOW, FIELD_TIME), + getWifiSelection(wifi), + null, + null, + null, + null + ).use { cursor -> + if (cursor.moveToNext()) { + val midLocation = cursor.getMidLocation(Long.MAX_VALUE, 0f) + (midLocation != null) to (midLocation?.let { it.distanceTo(location) > LEARN_BAD_SIZE_WIFI } == true) + } else { + false to false + } + } + if (exists && isBad) { + writableDatabase.update( + TABLE_WIFI_LEARN, contentValuesOf( + FIELD_LATITUDE_HIGH to location.latitude, + FIELD_LATITUDE_LOW to location.latitude, + FIELD_LONGITUDE_HIGH to location.longitude, + FIELD_LONGITUDE_LOW to location.longitude, + FIELD_TIME to location.time, + FIELD_BAD_TIME to location.time + ), getWifiSelection(wifi), null) + } else if (!exists) { + writableDatabase.insertWithOnConflict( + TABLE_WIFI_LEARN, null, contentValuesOf( + FIELD_MAC to wifi.macBytes, + FIELD_LATITUDE_HIGH to location.latitude, + FIELD_LATITUDE_LOW to location.latitude, + FIELD_LONGITUDE_HIGH to location.longitude, + FIELD_LONGITUDE_LOW to location.longitude, + FIELD_TIME to location.time, + FIELD_BAD_TIME to 0 + ), SQLiteDatabase.CONFLICT_REPLACE) + } else { + writableDatabase.rawQuery("UPDATE $TABLE_WIFI_LEARN SET $FIELD_LATITUDE_HIGH = max($FIELD_LATITUDE_HIGH, ?), $FIELD_LATITUDE_LOW = min($FIELD_LATITUDE_LOW, ?), $FIELD_LONGITUDE_HIGH = max($FIELD_LONGITUDE_HIGH, ?), $FIELD_LONGITUDE_LOW = min($FIELD_LONGITUDE_LOW, ?), $FIELD_TIME = ? WHERE ${getWifiSelection(wifi)}", arrayOf(location.latitude.toString(), location.latitude.toString(), location.longitude.toString(), location.longitude.toString())).close() + } + } + + override fun onCreate(db: SQLiteDatabase) { + db.execSQL("CREATE TABLE IF NOT EXISTS $TABLE_CELLS($FIELD_MCC INTEGER NOT NULL, $FIELD_MNC INTEGER NOT NULL, $FIELD_TYPE INTEGER NOT NULL, $FIELD_LAC_TAC INTEGER NOT NULL, $FIELD_CID INTEGER NOT NULL, $FIELD_PSC INTEGER NOT NULL, $FIELD_LATITUDE REAL NOT NULL, $FIELD_LONGITUDE REAL NOT NULL, $FIELD_ACCURACY REAL NOT NULL, $FIELD_TIME INTEGER NOT NULL, $FIELD_PRECISION REAL NOT NULL);") + db.execSQL("CREATE TABLE IF NOT EXISTS $TABLE_CELLS_PRE($FIELD_MCC INTEGER NOT NULL, $FIELD_MNC INTEGER NOT NULL, $FIELD_TIME INTEGER NOT NULL);") + db.execSQL("CREATE TABLE IF NOT EXISTS $TABLE_WIFIS($FIELD_MAC BLOB, $FIELD_LATITUDE REAL NOT NULL, $FIELD_LONGITUDE REAL NOT NULL, $FIELD_ACCURACY REAL NOT NULL, $FIELD_TIME INTEGER NOT NULL, $FIELD_PRECISION REAL NOT NULL);") + db.execSQL("CREATE TABLE IF NOT EXISTS $TABLE_CELLS_LEARN($FIELD_MCC INTEGER NOT NULL, $FIELD_MNC INTEGER NOT NULL, $FIELD_TYPE INTEGER NOT NULL, $FIELD_LAC_TAC INTEGER NOT NULL, $FIELD_CID INTEGER NOT NULL, $FIELD_PSC INTEGER NOT NULL, $FIELD_LATITUDE_HIGH REAL NOT NULL, $FIELD_LATITUDE_LOW REAL NOT NULL, $FIELD_LONGITUDE_HIGH REAL NOT NULL, $FIELD_LONGITUDE_LOW REAL NOT NULL, $FIELD_TIME INTEGER NOT NULL, $FIELD_BAD_TIME INTEGER);") + db.execSQL("CREATE TABLE IF NOT EXISTS $TABLE_WIFI_LEARN($FIELD_MAC BLOB, $FIELD_LATITUDE_HIGH REAL NOT NULL, $FIELD_LATITUDE_LOW REAL NOT NULL, $FIELD_LONGITUDE_HIGH REAL NOT NULL, $FIELD_LONGITUDE_LOW REAL NOT NULL, $FIELD_TIME INTEGER NOT NULL, $FIELD_BAD_TIME INTEGER);") + db.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS ${TABLE_CELLS}_index ON $TABLE_CELLS($FIELD_MCC, $FIELD_MNC, $FIELD_TYPE, $FIELD_LAC_TAC, $FIELD_CID, $FIELD_PSC);") + db.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS ${TABLE_CELLS_PRE}_index ON $TABLE_CELLS_PRE($FIELD_MCC, $FIELD_MNC);") + db.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS ${TABLE_WIFIS}_index ON $TABLE_WIFIS($FIELD_MAC);") + db.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS ${TABLE_CELLS_LEARN}_index ON $TABLE_CELLS_LEARN($FIELD_MCC, $FIELD_MNC, $FIELD_TYPE, $FIELD_LAC_TAC, $FIELD_CID, $FIELD_PSC);") + db.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS ${TABLE_WIFI_LEARN}_index ON $TABLE_WIFI_LEARN($FIELD_MAC);") + db.execSQL("CREATE INDEX IF NOT EXISTS ${TABLE_CELLS}_time_index ON $TABLE_CELLS($FIELD_TIME);") + db.execSQL("CREATE INDEX IF NOT EXISTS ${TABLE_CELLS_PRE}_time_index ON $TABLE_CELLS_PRE($FIELD_TIME);") + db.execSQL("CREATE INDEX IF NOT EXISTS ${TABLE_WIFIS}_time_index ON $TABLE_WIFIS($FIELD_TIME);") + db.execSQL("CREATE INDEX IF NOT EXISTS ${TABLE_CELLS_LEARN}_time_index ON $TABLE_CELLS_LEARN($FIELD_TIME);") + db.execSQL("CREATE INDEX IF NOT EXISTS ${TABLE_WIFI_LEARN}_time_index ON $TABLE_WIFI_LEARN($FIELD_TIME);") + db.execSQL("DROP TABLE IF EXISTS $TABLE_WIFI_SCANS;") + } + + fun cleanup(db: SQLiteDatabase) { + db.delete(TABLE_CELLS, "$FIELD_TIME < ?", arrayOf((System.currentTimeMillis() - MAX_CACHE_AGE).toString())) + db.delete(TABLE_CELLS_PRE, "$FIELD_TIME < ?", arrayOf((System.currentTimeMillis() - MAX_CACHE_AGE).toString())) + db.delete(TABLE_WIFIS, "$FIELD_TIME < ?", arrayOf((System.currentTimeMillis() - MAX_CACHE_AGE).toString())) + db.delete(TABLE_CELLS_LEARN, "$FIELD_TIME < ?", arrayOf((System.currentTimeMillis() - MAX_LEARN_AGE).toString())) + db.delete(TABLE_WIFI_LEARN, "$FIELD_TIME < ?", arrayOf((System.currentTimeMillis() - MAX_LEARN_AGE).toString())) + } + + override fun onOpen(db: SQLiteDatabase) { + super.onOpen(db) + if (!db.isReadOnly) { + cleanup(db) + } + } + + override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { + onCreate(db) + } + + fun dump(writer: PrintWriter) { + writer.println("Cache: cells(fetched)=${DatabaseUtils.queryNumEntries(readableDatabase, TABLE_CELLS)}, cells(learnt)=${DatabaseUtils.queryNumEntries(readableDatabase, TABLE_CELLS_LEARN)}, wifis(fetched)=${DatabaseUtils.queryNumEntries(readableDatabase, TABLE_WIFIS)}, wifis(learnt)=${DatabaseUtils.queryNumEntries(readableDatabase, TABLE_WIFI_LEARN)}") + } + + companion object { + const val PROVIDER_CACHE = "cache" + const val EXTRA_HIGH_LOCATION = "high" + const val EXTRA_LOW_LOCATION = "low" + const val DEBUG = false + val NEGATIVE_CACHE_ENTRY = Location(PROVIDER_CACHE) + private const val MAX_CACHE_AGE = 1000L * 60 * 60 * 24 * 14 // 14 days + private const val MAX_LEARN_AGE = 1000L * 60 * 60 * 24 * 365 // 1 year + private const val TABLE_CELLS = "cells" + private const val TABLE_CELLS_PRE = "cells_pre" + private const val TABLE_WIFIS = "wifis" + private const val TABLE_WIFI_SCANS = "wifi_scans" + private const val TABLE_CELLS_LEARN = "cells_learn" + private const val TABLE_WIFI_LEARN = "wifis_learn" + private const val FIELD_MCC = "mcc" + private const val FIELD_MNC = "mnc" + private const val FIELD_TYPE = "type" + private const val FIELD_LAC_TAC = "lac" + private const val FIELD_CID = "cid" + private const val FIELD_PSC = "psc" + private const val FIELD_LATITUDE = "lat" + private const val FIELD_LONGITUDE = "lon" + private const val FIELD_ACCURACY = "acc" + private const val FIELD_TIME = "time" + private const val FIELD_PRECISION = "prec" + private const val FIELD_MAC = "mac" + private const val FIELD_SCAN_HASH = "hash" + private const val FIELD_LATITUDE_HIGH = "lath" + private const val FIELD_LATITUDE_LOW = "latl" + private const val FIELD_LONGITUDE_HIGH = "lonh" + private const val FIELD_LONGITUDE_LOW = "lonl" + private const val FIELD_BAD_TIME = "btime" + private const val LEARN_BASE_ACCURACY_CELL = 5_000f + private const val LEARN_BASE_ACCURACY_WIFI = 100f + private const val LEARN_BAD_SIZE_CELL = 10_000 + private const val LEARN_BAD_SIZE_WIFI = 200 + private const val LEARN_BAD_CUTOFF = 1000L * 60 * 60 * 24 * 14 + private const val CELLS_SELECTION = "$FIELD_MCC = ? AND $FIELD_MNC = ? AND $FIELD_TYPE = ? AND $FIELD_LAC_TAC = ? AND $FIELD_CID = ? AND $FIELD_PSC = ?" + private const val CELLS_PRE_SELECTION = "$FIELD_MCC = ? AND $FIELD_MNC = ?" + private fun getCellSelectionArgs(cell: CellDetails): Array { + return arrayOf( + cell.mcc.toString(), + cell.mnc.toString(), + cell.type.ordinal.toString(), + (cell.lac ?: cell.tac ?: 0).toString(), + cell.cid.toString(), + (cell.psc ?: 0).toString(), + ) + } + + private fun getWifiSelection(wifi: WifiDetails): String { + return "$FIELD_MAC = x'${wifi.macBytes.toHexString()}'" + } + + private fun getCellPreSelectionArgs(cell: CellDetails): Array { + return arrayOf( + cell.mcc.toString(), + cell.mnc.toString() + ) + } + + private fun Cursor.getLocation(maxAge: Long): Location? { + if (getLong(3) > System.currentTimeMillis() - maxAge) { + if (getDouble(2) == 0.0) return NEGATIVE_CACHE_ENTRY + return Location(PROVIDER_CACHE).apply { + latitude = getDouble(0) + longitude = getDouble(1) + accuracy = getDouble(2).toFloat() + precision = getDouble(4) + } + } + return null + } + + private fun Cursor.getMidLocation(maxAge: Long, minAccuracy: Float, precision: Double = 1.0): Location? { + if (maxAge == Long.MAX_VALUE || getLong(4) > System.currentTimeMillis() - maxAge) { + val high = Location(PROVIDER_CACHE).apply { latitude = getDouble(0); longitude = getDouble(2) } + val low = Location(PROVIDER_CACHE).apply { latitude = getDouble(1); longitude = getDouble(3) } + return Location(PROVIDER_CACHE).apply { + latitude = (high.latitude + low.latitude) / 2.0 + longitude = (high.longitude + low.longitude) / 2.0 + accuracy = max(high.distanceTo(low), minAccuracy) + this.precision = precision + if (DEBUG) { + extras = bundleOf( + EXTRA_HIGH_LOCATION to high, + EXTRA_LOW_LOCATION to low + ) + } + } + } + return null + } + + private fun ContentValues.putLocation(location: Location) { + if (location != NEGATIVE_CACHE_ENTRY) { + put(FIELD_LATITUDE, location.latitude) + put(FIELD_LONGITUDE, location.longitude) + put(FIELD_ACCURACY, location.accuracy) + put(FIELD_TIME, location.time) + put(FIELD_PRECISION, location.precision) + } else { + put(FIELD_LATITUDE, 0.0) + put(FIELD_LONGITUDE, 0.0) + put(FIELD_ACCURACY, 0.0) + put(FIELD_TIME, System.currentTimeMillis()) + put(FIELD_PRECISION, 0.0) + } + } + } +} \ No newline at end of file diff --git a/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/NetworkLocationRequest.kt b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/NetworkLocationRequest.kt similarity index 90% rename from play-services-location/core/src/main/kotlin/org/microg/gms/location/network/NetworkLocationRequest.kt rename to play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/NetworkLocationRequest.kt index 1bf4e2faf9..6c10cf72a1 100644 --- a/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/NetworkLocationRequest.kt +++ b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/NetworkLocationRequest.kt @@ -11,6 +11,7 @@ import android.content.Intent import android.location.Location import android.os.SystemClock import android.os.WorkSource +import org.microg.gms.location.EXTRA_LOCATION class NetworkLocationRequest( var pendingIntent: PendingIntent, @@ -23,6 +24,6 @@ class NetworkLocationRequest( fun send(context: Context, location: Location) { lastRealtime = SystemClock.elapsedRealtime() - pendingIntent.send(context, 0, Intent().apply { putExtra(NetworkLocationService.EXTRA_LOCATION, location) }) + pendingIntent.send(context, 0, Intent().apply { putExtra(EXTRA_LOCATION, location) }) } } \ No newline at end of file diff --git a/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/NetworkLocationService.kt b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/NetworkLocationService.kt similarity index 51% rename from play-services-location/core/src/main/kotlin/org/microg/gms/location/network/NetworkLocationService.kt rename to play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/NetworkLocationService.kt index f69597c0c8..9744ba2aad 100644 --- a/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/NetworkLocationService.kt +++ b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/NetworkLocationService.kt @@ -9,17 +9,26 @@ import android.annotation.SuppressLint import android.app.PendingIntent import android.content.Intent import android.location.Location +import android.location.LocationManager +import android.net.wifi.WifiManager import android.os.Build.VERSION.SDK_INT import android.os.Handler import android.os.HandlerThread import android.os.SystemClock import android.os.WorkSource import android.util.Log +import androidx.annotation.GuardedBy +import androidx.collection.LruCache +import androidx.core.content.getSystemService +import androidx.core.location.LocationListenerCompat +import androidx.core.location.LocationManagerCompat +import androidx.core.location.LocationRequestCompat import androidx.lifecycle.LifecycleService import androidx.lifecycle.lifecycleScope import com.android.volley.VolleyError import kotlinx.coroutines.launch -import org.microg.gms.location.elapsedMillis +import kotlinx.coroutines.withTimeout +import org.microg.gms.location.* import org.microg.gms.location.network.LocationCacheDatabase.Companion.NEGATIVE_CACHE_ENTRY import org.microg.gms.location.network.cell.CellDetails import org.microg.gms.location.network.cell.CellDetailsCallback @@ -29,18 +38,26 @@ import org.microg.gms.location.network.mozilla.ServiceException import org.microg.gms.location.network.wifi.* import java.io.FileDescriptor import java.io.PrintWriter +import java.lang.Math.pow +import java.nio.ByteBuffer +import java.util.LinkedList import kotlin.math.min class NetworkLocationService : LifecycleService(), WifiDetailsCallback, CellDetailsCallback { private lateinit var handlerThread: HandlerThread private lateinit var handler: Handler + + @GuardedBy("activeRequests") private val activeRequests = HashSet() private val highPowerScanRunnable = Runnable { this.scan(false) } private val lowPowerScanRunnable = Runnable { this.scan(true) } - private val wifiDetailsSource by lazy { WifiDetailsSource.create(this, this) } - private val cellDetailsSource by lazy { CellDetailsSource.create(this, this) } + private var wifiDetailsSource: WifiDetailsSource? = null + private var cellDetailsSource: CellDetailsSource? = null private val mozilla by lazy { MozillaLocationServiceClient(this) } private val cache by lazy { LocationCacheDatabase(this) } + private val movingWifiHelper by lazy { MovingWifiHelper(this) } + private val settings by lazy { LocationSettings(this) } + private val wifiScanCache = LruCache(100) private var lastHighPowerScanRealtime = 0L private var lastLowPowerScanRealtime = 0L @@ -55,6 +72,11 @@ class NetworkLocationService : LifecycleService(), WifiDetailsCallback, CellDeta private var lastCellLocation: Location? = null private var lastLocation: Location? = null + private val gpsLocationListener by lazy { LocationListenerCompat { onNewGpsLocation(it) } } + + @GuardedBy("gpsLocationBuffer") + private val gpsLocationBuffer = LinkedList() + private val interval: Long get() = min(highPowerIntervalMillis, lowPowerIntervalMillis) @@ -63,8 +85,21 @@ class NetworkLocationService : LifecycleService(), WifiDetailsCallback, CellDeta handlerThread = HandlerThread(NetworkLocationService::class.java.simpleName) handlerThread.start() handler = Handler(handlerThread.looper) - wifiDetailsSource.enable() - cellDetailsSource.enable() + wifiDetailsSource = WifiDetailsSource.create(this, this).apply { enable() } + cellDetailsSource = CellDetailsSource.create(this, this).apply { enable() } + try { + getSystemService()?.let { locationManager -> + LocationManagerCompat.requestLocationUpdates( + locationManager, + LocationManager.GPS_PROVIDER, + LocationRequestCompat.Builder(LocationRequestCompat.PASSIVE_INTERVAL).setMinUpdateIntervalMillis(GPS_PASSIVE_INTERVAL).build(), + gpsLocationListener, + handlerThread.looper + ) + } + } catch (e: SecurityException) { + Log.d(TAG, "GPS location retriever not initialized", e) + } } @SuppressLint("WrongConstant") @@ -72,8 +107,8 @@ class NetworkLocationService : LifecycleService(), WifiDetailsCallback, CellDeta if (!lowPower) lastHighPowerScanRealtime = SystemClock.elapsedRealtime() lastLowPowerScanRealtime = SystemClock.elapsedRealtime() val workSource = synchronized(activeRequests) { activeRequests.minByOrNull { it.intervalMillis }?.workSource } - wifiDetailsSource.startScan(workSource) - cellDetailsSource.startScan(workSource) + wifiDetailsSource?.startScan(workSource) + cellDetailsSource?.startScan(workSource) updateRequests() } @@ -147,38 +182,106 @@ class NetworkLocationService : LifecycleService(), WifiDetailsCallback, CellDeta override fun onDestroy() { handlerThread.stop() - wifiDetailsSource.disable() - cellDetailsSource.disable() + wifiDetailsSource?.disable() + wifiDetailsSource = null + cellDetailsSource?.disable() + cellDetailsSource = null super.onDestroy() } + suspend fun requestWifiLocation(requestableWifis: List, currentLocalMovingWifi: WifiDetails?): Location? { + var candidate: Location? = null + if (currentLocalMovingWifi != null && settings.wifiMoving) { + try { + withTimeout(5000L) { + candidate = movingWifiHelper.retrieveMovingLocation(currentLocalMovingWifi) + } + } catch (e: Exception) { + Log.w(TAG, "Failed retrieving location for current moving wifi ${currentLocalMovingWifi.ssid}", e) + } + } + if ((candidate?.accuracy ?: Float.MAX_VALUE) <= 50f) return candidate + if (requestableWifis.size >= 3) { + try { + candidate = when (val cacheLocation = requestableWifis.hash()?.let { wifiScanCache[it.toHexString()] } + ?.takeIf { it.time > System.currentTimeMillis() - MAX_WIFI_SCAN_CACHE_AGE }) { + NEGATIVE_CACHE_ENTRY -> null + null -> { + if (settings.wifiMls) { + val location = mozilla.retrieveMultiWifiLocation(requestableWifis) + location.time = System.currentTimeMillis() + requestableWifis.hash()?.let { wifiScanCache[it.toHexString()] = location } + location + } else { + null + } + } + + else -> cacheLocation + }?.takeIf { candidate == null || it.accuracy < candidate?.accuracy!! } ?: candidate + } catch (e: Exception) { + Log.w(TAG, "Failed retrieving location for ${requestableWifis.size} wifi networks", e) + if (e is ServiceException && e.error.code == 404 || e is VolleyError && e.networkResponse?.statusCode == 404) { + requestableWifis.hash()?.let { wifiScanCache[it.toHexString()] = NEGATIVE_CACHE_ENTRY } + } + } + } + if ((candidate?.accuracy ?: Float.MAX_VALUE) <= 50f) return candidate + if (requestableWifis.isNotEmpty() && settings.wifiLearning) { + val wifiLocations = requestableWifis.mapNotNull { wifi -> cache.getWifiLocation(wifi)?.let { wifi to it } } + if (wifiLocations.size == 1 && (candidate == null || wifiLocations.single().second.accuracy < candidate!!.accuracy)) { + return wifiLocations.single().second + } else if (wifiLocations.isNotEmpty()) { + val location = Location(LocationCacheDatabase.PROVIDER_CACHE).apply { + latitude = wifiLocations.weightedAverage { it.second.latitude to pow(10.0, it.first.signalStrength?.toDouble() ?: 1.0) } + longitude = wifiLocations.weightedAverage { it.second.longitude to pow(10.0, it.first.signalStrength?.toDouble() ?: 1.0) } + precision = wifiLocations.size.toDouble() / 4.0 + } + location.accuracy = wifiLocations.maxOf { it.second.accuracy } - wifiLocations.minOf { it.second.distanceTo(location) } / 2 + return location + } + + } + return candidate + } + + fun List.weightedAverage(f: (T) -> Pair): Double { + val valuesAndWeights = map { f(it) } + return valuesAndWeights.sumOf { it.first * it.second } / valuesAndWeights.sumOf { it.second } + } + override fun onWifiDetailsAvailable(wifis: List) { + if (wifis.isEmpty()) return val scanResultTimestamp = min(wifis.maxOf { it.timestamp ?: Long.MAX_VALUE }, System.currentTimeMillis()) val scanResultRealtimeMillis = if (SDK_INT >= 17) SystemClock.elapsedRealtime() - (System.currentTimeMillis() - scanResultTimestamp) else scanResultTimestamp - if (scanResultRealtimeMillis < lastWifiDetailsRealtimeMillis + interval / 2) { + if (scanResultRealtimeMillis < lastWifiDetailsRealtimeMillis + interval / 2 && lastWifiDetailsRealtimeMillis != 0L) { Log.d(TAG, "Ignoring wifi details, similar age as last ($scanResultRealtimeMillis < $lastWifiDetailsRealtimeMillis + $interval / 2)") return } - if (wifis.size < 3) return + @Suppress("DEPRECATION") + val currentLocalMovingWifi = getSystemService()?.connectionInfo + ?.let { wifiInfo -> wifis.filter { it.macAddress == wifiInfo.bssid && it.isMoving } } + ?.filter { movingWifiHelper.isLocallyRetrievable(it) } + ?.singleOrNull() + val requestableWifis = wifis.filter(WifiDetails::isRequestable) + if (SDK_INT >= 17 && settings.wifiLearning) { + for (wifi in requestableWifis.filter { it.timestamp != null }) { + val wifiElapsedMillis = SystemClock.elapsedRealtime() - (System.currentTimeMillis() - wifi.timestamp!!) + getGpsLocation(wifiElapsedMillis)?.let { + cache.learnWifiLocation(wifi, it) + } + } + } + if (requestableWifis.isEmpty() && currentLocalMovingWifi == null) return + val previousLastRealtimeMillis = lastWifiDetailsRealtimeMillis lastWifiDetailsRealtimeMillis = scanResultRealtimeMillis lifecycleScope.launch { - val location = try { - when (val cacheLocation = cache.getWifiScanLocation(wifis)) { - NEGATIVE_CACHE_ENTRY -> null - null -> mozilla.retrieveMultiWifiLocation(wifis).also { - it.time = System.currentTimeMillis() - cache.putWifiScanLocation(wifis, it) - } - else -> cacheLocation - } - } catch (e: Exception) { - Log.w(TAG, "Failed retrieving location for ${wifis.size} wifi networks", e) - if (e is ServiceException && e.error.code == 404 || e is VolleyError && e.networkResponse?.statusCode == 404) { - cache.putWifiScanLocation(wifis, NEGATIVE_CACHE_ENTRY) - } - null - } ?: return@launch + val location = requestWifiLocation(requestableWifis, currentLocalMovingWifi) + if (location == null) { + lastWifiDetailsRealtimeMillis = previousLastRealtimeMillis + return@launch + } location.time = scanResultTimestamp if (SDK_INT >= 17) location.elapsedRealtimeNanos = scanResultRealtimeMillis * 1_000_000L synchronized(locationLock) { @@ -188,24 +291,45 @@ class NetworkLocationService : LifecycleService(), WifiDetailsCallback, CellDeta } } + override fun onWifiSourceFailed() { + // Wifi source failed, create a new one + wifiDetailsSource?.disable() + wifiDetailsSource = WifiDetailsSource.create(this, this).apply { enable() } + } + override fun onCellDetailsAvailable(cells: List) { val scanResultTimestamp = min(cells.maxOf { it.timestamp ?: Long.MAX_VALUE }, System.currentTimeMillis()) val scanResultRealtimeMillis = if (SDK_INT >= 17) SystemClock.elapsedRealtime() - (System.currentTimeMillis() - scanResultTimestamp) else scanResultTimestamp - if (scanResultRealtimeMillis < lastCellDetailsRealtimeMillis + interval/2) { + if (scanResultRealtimeMillis < lastCellDetailsRealtimeMillis + interval / 2) { Log.d(TAG, "Ignoring cell details, similar age as last") return } + if (SDK_INT >= 17 && settings.cellLearning) { + for (cell in cells.filter { it.timestamp != null && it.location == null }) { + val cellElapsedMillis = SystemClock.elapsedRealtime() - (System.currentTimeMillis() - cell.timestamp!!) + getGpsLocation(cellElapsedMillis)?.let { + cache.learnCellLocation(cell, it) + } + } + } lastCellDetailsRealtimeMillis = scanResultRealtimeMillis lifecycleScope.launch { - val singleCell = cells.filter { it.location != NEGATIVE_CACHE_ENTRY }.maxByOrNull { it.timestamp ?: it.signalStrength?.toLong() ?: 0L } ?: return@launch + val singleCell = + cells.filter { it.location != NEGATIVE_CACHE_ENTRY }.maxByOrNull { it.timestamp ?: it.signalStrength?.toLong() ?: 0L } ?: return@launch val location = singleCell.location ?: try { - when (val cacheLocation = cache.getCellLocation(singleCell)) { + when (val cacheLocation = cache.getCellLocation(singleCell, allowLearned = settings.cellLearning)) { NEGATIVE_CACHE_ENTRY -> null - null -> mozilla.retrieveSingleCellLocation(singleCell).also { - it.time = System.currentTimeMillis() - cache.putCellLocation(singleCell, it) + + null -> if (settings.cellMls) { + mozilla.retrieveSingleCellLocation(singleCell).also { + it.time = System.currentTimeMillis() + cache.putCellLocation(singleCell, it) + } + } else { + null } + else -> cacheLocation } } catch (e: Exception) { @@ -216,7 +340,9 @@ class NetworkLocationService : LifecycleService(), WifiDetailsCallback, CellDeta null } ?: return@launch location.time = singleCell.timestamp ?: scanResultTimestamp - if (SDK_INT >= 17) location.elapsedRealtimeNanos = singleCell.timestamp?.let { SystemClock.elapsedRealtimeNanos() - (System.currentTimeMillis() - it) * 1_000_000L } ?: (scanResultRealtimeMillis * 1_000_000L) + if (SDK_INT >= 17) location.elapsedRealtimeNanos = + singleCell.timestamp?.let { SystemClock.elapsedRealtimeNanos() - (System.currentTimeMillis() - it) * 1_000_000L } + ?: (scanResultRealtimeMillis * 1_000_000L) synchronized(locationLock) { lastCellLocation = location } @@ -235,7 +361,7 @@ class NetworkLocationService : LifecycleService(), WifiDetailsCallback, CellDeta lastCellLocation!!.elapsedMillis > lastWifiLocation!!.elapsedMillis + LOCATION_TIME_CLIFF_MS -> lastCellLocation lastWifiLocation!!.elapsedMillis > lastCellLocation!!.elapsedMillis + LOCATION_TIME_CLIFF_MS -> lastWifiLocation // Wifi out of cell range with higher precision - lastCellLocation!!.precision > lastWifiLocation!!.precision && lastWifiLocation!!.distanceTo(lastCellLocation!!) > 2*lastCellLocation!!.accuracy -> lastCellLocation + lastCellLocation!!.precision > lastWifiLocation!!.precision && lastWifiLocation!!.distanceTo(lastCellLocation!!) > 2 * lastCellLocation!!.accuracy -> lastCellLocation else -> lastWifiLocation } } ?: return @@ -259,33 +385,80 @@ class NetworkLocationService : LifecycleService(), WifiDetailsCallback, CellDeta } } + private fun onNewGpsLocation(location: Location) { + if (location.accuracy > GPS_PASSIVE_MIN_ACCURACY) return + synchronized(gpsLocationBuffer) { + if (gpsLocationBuffer.isNotEmpty() && gpsLocationBuffer.last.elapsedMillis < SystemClock.elapsedRealtime() - GPS_BUFFER_SIZE * GPS_PASSIVE_INTERVAL) { + gpsLocationBuffer.clear() + } else if (gpsLocationBuffer.size >= GPS_BUFFER_SIZE) { + gpsLocationBuffer.remove() + } + gpsLocationBuffer.offer(location) + } + } + + private fun getGpsLocation(elapsedMillis: Long): Location? { + if (elapsedMillis + GPS_BUFFER_SIZE * GPS_PASSIVE_INTERVAL < SystemClock.elapsedRealtime()) return null + synchronized(gpsLocationBuffer) { + if (gpsLocationBuffer.isEmpty()) return null + for (location in gpsLocationBuffer.descendingIterator()) { + if (location.elapsedMillis in (elapsedMillis - GPS_PASSIVE_INTERVAL)..(elapsedMillis + GPS_PASSIVE_INTERVAL)) return location + if (location.elapsedMillis < elapsedMillis) return null + } + } + return null + } + override fun dump(fd: FileDescriptor?, writer: PrintWriter, args: Array?) { - writer.println("Last scan elapsed realtime: high-power: $lastHighPowerScanRealtime, low-power: $lastLowPowerScanRealtime") - writer.println("Last scan result time: wifi: $lastWifiDetailsRealtimeMillis, cells: $lastCellDetailsRealtimeMillis") - writer.println("Interval: high-power: ${highPowerIntervalMillis}ms, low-power: ${lowPowerIntervalMillis}ms") + writer.println("Last scan elapsed realtime: high-power: ${lastHighPowerScanRealtime.formatRealtime()}, low-power: ${lastLowPowerScanRealtime.formatRealtime()}") + writer.println("Last scan result time: wifi: ${lastWifiDetailsRealtimeMillis.formatRealtime()}, cells: ${lastCellDetailsRealtimeMillis.formatRealtime()}") + writer.println("Interval: high-power: ${highPowerIntervalMillis.formatDuration()}, low-power: ${lowPowerIntervalMillis.formatDuration()}") writer.println("Last wifi location: $lastWifiLocation${if (lastWifiLocation == lastLocation) " (active)" else ""}") writer.println("Last cell location: $lastCellLocation${if (lastCellLocation == lastLocation) " (active)" else ""}") + writer.println("Settings: Wi-Fi MLS=${settings.wifiMls} moving=${settings.wifiMoving} learn=${settings.wifiLearning} Cell MLS=${settings.cellMls} learn=${settings.cellLearning}") + writer.println("Wifi scan cache size=${wifiScanCache.size()} hits=${wifiScanCache.hitCount()} miss=${wifiScanCache.missCount()} puts=${wifiScanCache.putCount()} evicts=${wifiScanCache.evictionCount()}") + writer.println("GPS location buffer size=${gpsLocationBuffer.size} first=${gpsLocationBuffer.firstOrNull()?.elapsedMillis?.formatRealtime()} last=${gpsLocationBuffer.lastOrNull()?.elapsedMillis?.formatRealtime()}") + cache.dump(writer) synchronized(activeRequests) { if (activeRequests.isNotEmpty()) { writer.println("Active requests:") for (request in activeRequests) { - writer.println("- ${request.workSource} ${request.intervalMillis}ms (low power: ${request.lowPower}, bypass: ${request.bypass})") + writer.println("- ${request.workSource} ${request.intervalMillis.formatDuration()} (low power: ${request.lowPower}, bypass: ${request.bypass})") } } } } companion object { - const val ACTION_REPORT_LOCATION = "org.microg.gms.location.network.ACTION_REPORT_LOCATION" - const val EXTRA_PENDING_INTENT = "pending_intent" - const val EXTRA_ENABLE = "enable" - const val EXTRA_INTERVAL_MILLIS = "interval" - const val EXTRA_FORCE_NOW = "force_now" - const val EXTRA_LOW_POWER = "low_power" - const val EXTRA_WORK_SOURCE = "work_source" - const val EXTRA_BYPASS = "bypass" - const val EXTRA_LOCATION = "location" + const val GPS_BUFFER_SIZE = 60 + const val GPS_PASSIVE_INTERVAL = 1000L + const val GPS_PASSIVE_MIN_ACCURACY = 25f const val LOCATION_TIME_CLIFF_MS = 30000L const val DEBOUNCE_DELAY_MS = 5000L + const val MAX_WIFI_SCAN_CACHE_AGE = 1000L * 60 * 60 * 24 // 1 day + } +} + +private operator fun LruCache.set(key: K, value: V) { + put(key, value) +} + +fun List.hash(): ByteArray? { + val filtered = sortedBy { it.macClean } + .filter { it.timestamp == null || it.timestamp!! > System.currentTimeMillis() - 60000 } + .filter { it.signalStrength == null || it.signalStrength!! > -90 } + if (filtered.size < 3) return null + val maxTimestamp = maxOf { it.timestamp ?: 0L } + fun WifiDetails.hashBytes(): ByteArray { + return macBytes + byteArrayOf( + ((maxTimestamp - (timestamp ?: 0L)) / (60 * 1000)).toByte(), // timestamp + ((signalStrength ?: 0) / 10).toByte() // signal strength + ) + } + + val buffer = ByteBuffer.allocate(filtered.size * 8) + for (wifi in filtered) { + buffer.put(wifi.hashBytes()) } + return buffer.array().digest("SHA-256") } \ No newline at end of file diff --git a/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/cell/CellDetails.kt b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/cell/CellDetails.kt similarity index 100% rename from play-services-location/core/src/main/kotlin/org/microg/gms/location/network/cell/CellDetails.kt rename to play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/cell/CellDetails.kt diff --git a/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/cell/CellDetailsCallback.kt b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/cell/CellDetailsCallback.kt similarity index 100% rename from play-services-location/core/src/main/kotlin/org/microg/gms/location/network/cell/CellDetailsCallback.kt rename to play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/cell/CellDetailsCallback.kt diff --git a/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/cell/CellDetailsSource.kt b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/cell/CellDetailsSource.kt similarity index 63% rename from play-services-location/core/src/main/kotlin/org/microg/gms/location/network/cell/CellDetailsSource.kt rename to play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/cell/CellDetailsSource.kt index 750fb9ad0c..316da16aa9 100644 --- a/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/cell/CellDetailsSource.kt +++ b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/cell/CellDetailsSource.kt @@ -12,7 +12,6 @@ import android.os.WorkSource import android.telephony.CellInfo import android.telephony.TelephonyManager import androidx.core.content.getSystemService -import org.microg.gms.location.network.LocationCacheDatabase class CellDetailsSource(private val context: Context, private val callback: CellDetailsCallback) { fun enable() = Unit @@ -28,19 +27,23 @@ class CellDetailsSource(private val context: Context, private val callback: Cell if (details.isNotEmpty()) callback.onCellDetailsAvailable(details) } }) + return } else if (SDK_INT >= 17) { - val details = telephonyManager.allCellInfo.map(CellInfo::toCellDetails).map { it.repair(context) }.filter(CellDetails::isValid) - if (details.isNotEmpty()) callback.onCellDetailsAvailable(details) - } else { - val networkOperator = telephonyManager.networkOperator - var mcc: Int? = null - var mnc: Int? = null - if (networkOperator != null && networkOperator.length > 4) { - mcc = networkOperator.substring(0, 3).toIntOrNull() - mnc = networkOperator.substring(3).toIntOrNull() + val allCellInfo = telephonyManager.allCellInfo + if (allCellInfo != null) { + val details = allCellInfo.map(CellInfo::toCellDetails).map { it.repair(context) }.filter(CellDetails::isValid) + if (details.isNotEmpty()) { + callback.onCellDetailsAvailable(details) + return + } } - val detail = telephonyManager.cellLocation.toCellDetails(mcc, mnc) - if (detail.isValid) callback.onCellDetailsAvailable(listOf(detail)) + } + val networkOperator = telephonyManager.networkOperator + if (networkOperator != null && networkOperator.length > 4) { + val mcc = networkOperator.substring(0, 3).toIntOrNull() + val mnc = networkOperator.substring(3).toIntOrNull() + val detail = telephonyManager.cellLocation?.toCellDetails(mcc, mnc) + if (detail?.isValid == true) callback.onCellDetailsAvailable(listOf(detail)) } } diff --git a/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/cell/extensions.kt b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/cell/extensions.kt similarity index 99% rename from play-services-location/core/src/main/kotlin/org/microg/gms/location/network/cell/extensions.kt rename to play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/cell/extensions.kt index e5af415602..0e1b1966d6 100644 --- a/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/cell/extensions.kt +++ b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/cell/extensions.kt @@ -26,10 +26,8 @@ import android.telephony.CellLocation import android.telephony.TelephonyManager import android.telephony.cdma.CdmaCellLocation import android.telephony.gsm.GsmCellLocation -import android.util.Log import androidx.annotation.RequiresApi import androidx.core.content.getSystemService -import org.microg.gms.location.network.TAG private fun locationFromCdma(latitude: Int, longitude: Int) = if (latitude == Int.MAX_VALUE || longitude == Int.MAX_VALUE) null else Location("cdma").also { it.latitude = latitude.toDouble() / 14400.0 diff --git a/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/extensions.kt b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/extensions.kt similarity index 86% rename from play-services-location/core/src/main/kotlin/org/microg/gms/location/network/extensions.kt rename to play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/extensions.kt index e20e43ada8..565901a24b 100644 --- a/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/extensions.kt +++ b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/extensions.kt @@ -7,11 +7,10 @@ package org.microg.gms.location.network import android.location.Location import android.os.Bundle -import androidx.core.location.LocationCompat import java.security.MessageDigest const val TAG = "NetworkLocation" -internal const val LOCATION_EXTRA_PRECISION = "precision" +const val LOCATION_EXTRA_PRECISION = "precision" internal var Location.precision: Double get() = extras?.getDouble(LOCATION_EXTRA_PRECISION, 1.0) ?: 1.0 diff --git a/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/mozilla/CellTower.kt b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/mozilla/CellTower.kt similarity index 100% rename from play-services-location/core/src/main/kotlin/org/microg/gms/location/network/mozilla/CellTower.kt rename to play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/mozilla/CellTower.kt diff --git a/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/mozilla/Fallback.kt b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/mozilla/Fallback.kt similarity index 100% rename from play-services-location/core/src/main/kotlin/org/microg/gms/location/network/mozilla/Fallback.kt rename to play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/mozilla/Fallback.kt diff --git a/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/mozilla/GeolocateRequest.kt b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/mozilla/GeolocateRequest.kt similarity index 100% rename from play-services-location/core/src/main/kotlin/org/microg/gms/location/network/mozilla/GeolocateRequest.kt rename to play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/mozilla/GeolocateRequest.kt diff --git a/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/mozilla/GeolocateResponse.kt b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/mozilla/GeolocateResponse.kt similarity index 100% rename from play-services-location/core/src/main/kotlin/org/microg/gms/location/network/mozilla/GeolocateResponse.kt rename to play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/mozilla/GeolocateResponse.kt diff --git a/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/mozilla/MozillaLocationServiceClient.kt b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/mozilla/MozillaLocationServiceClient.kt similarity index 74% rename from play-services-location/core/src/main/kotlin/org/microg/gms/location/network/mozilla/MozillaLocationServiceClient.kt rename to play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/mozilla/MozillaLocationServiceClient.kt index 1138b101b6..a3ab400420 100644 --- a/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/mozilla/MozillaLocationServiceClient.kt +++ b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/mozilla/MozillaLocationServiceClient.kt @@ -7,35 +7,34 @@ package org.microg.gms.location.network.mozilla import android.content.Context import android.location.Location +import android.net.Uri import android.os.Bundle import android.util.Log import com.android.volley.Request.Method import com.android.volley.toolbox.JsonObjectRequest import com.android.volley.toolbox.Volley -import org.microg.gms.location.network.NetworkLocationService -import org.microg.gms.location.network.cell.CellDetails +import org.microg.gms.location.provider.BuildConfig import org.microg.gms.location.network.precision import org.microg.gms.location.network.wifi.WifiDetails +import org.microg.gms.location.network.wifi.isMoving import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException import kotlin.coroutines.suspendCoroutine -import kotlin.math.log10 -import kotlin.math.log2 class MozillaLocationServiceClient(context: Context) { private val queue = Volley.newRequestQueue(context) - suspend fun retrieveMultiWifiLocation(wifis: List): Location = geoLocate( + suspend fun retrieveMultiWifiLocation(wifis: List): Location = geoLocate( GeolocateRequest( considerIp = false, - wifiAccessPoints = wifis.filter { it.ssid?.endsWith("_nomap") != true }.map(WifiDetails::toWifiAccessPoint), + wifiAccessPoints = wifis.filter { it.ssid?.endsWith("_nomap") != true && !it.isMoving }.map(WifiDetails::toWifiAccessPoint), fallbacks = Fallback(lacf = false, ipf = false) ) ).apply { - precision = WIFI_BASE_PRECISION_COUNT/wifis.size.toDouble() + precision = wifis.size.toDouble()/ WIFI_BASE_PRECISION_COUNT } - suspend fun retrieveSingleCellLocation(cell: CellDetails): Location = geoLocate( + suspend fun retrieveSingleCellLocation(cell: org.microg.gms.location.network.cell.CellDetails): Location = geoLocate( GeolocateRequest( considerIp = false, cellTowers = listOf(cell.toCellTower()), @@ -66,7 +65,9 @@ class MozillaLocationServiceClient(context: Context) { } private suspend fun rawGeoLocate(request: GeolocateRequest): GeolocateResponse = suspendCoroutine { continuation -> - queue.add(JsonObjectRequest(Method.POST, GEOLOCATE_URL.format(API_KEY), request.toJson(), { + queue.add(JsonObjectRequest(Method.POST, Uri.parse(GEOLOCATE_URL).buildUpon().apply { + if (API_KEY != null) appendQueryParameter("key", API_KEY) + }.build().toString(), request.toJson(), { continuation.resume(it.toGeolocateResponse()) }, { continuation.resumeWithException(it) @@ -75,11 +76,12 @@ class MozillaLocationServiceClient(context: Context) { companion object { private const val TAG = "MozillaLocation" - private const val GEOLOCATE_URL = "https://location.services.mozilla.com/v1/geolocate?key=%s" - private const val API_KEY = "068ab754-c06b-473d-a1e5-60e7b1a2eb77" - private const val WIFI_BASE_PRECISION_COUNT = 8.0 + private const val GEOLOCATE_URL = "https://location.services.mozilla.com/v1/geolocate" + private const val WIFI_BASE_PRECISION_COUNT = 4.0 private const val CELL_DEFAULT_PRECISION = 1.0 private const val CELL_FALLBACK_PRECISION = 0.5 + @JvmField + val API_KEY: String? = BuildConfig.ICHNAEA_KEY.takeIf { it.isNotBlank() } const val LOCATION_EXTRA_FALLBACK = "fallback" } diff --git a/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/mozilla/RadioType.kt b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/mozilla/RadioType.kt similarity index 100% rename from play-services-location/core/src/main/kotlin/org/microg/gms/location/network/mozilla/RadioType.kt rename to play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/mozilla/RadioType.kt diff --git a/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/mozilla/ResponseError.kt b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/mozilla/ResponseError.kt similarity index 100% rename from play-services-location/core/src/main/kotlin/org/microg/gms/location/network/mozilla/ResponseError.kt rename to play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/mozilla/ResponseError.kt diff --git a/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/mozilla/ResponseLocation.kt b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/mozilla/ResponseLocation.kt similarity index 100% rename from play-services-location/core/src/main/kotlin/org/microg/gms/location/network/mozilla/ResponseLocation.kt rename to play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/mozilla/ResponseLocation.kt diff --git a/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/mozilla/ServiceException.kt b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/mozilla/ServiceException.kt similarity index 100% rename from play-services-location/core/src/main/kotlin/org/microg/gms/location/network/mozilla/ServiceException.kt rename to play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/mozilla/ServiceException.kt diff --git a/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/mozilla/WifiAccessPoint.kt b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/mozilla/WifiAccessPoint.kt similarity index 100% rename from play-services-location/core/src/main/kotlin/org/microg/gms/location/network/mozilla/WifiAccessPoint.kt rename to play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/mozilla/WifiAccessPoint.kt diff --git a/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/mozilla/extensions.kt b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/mozilla/extensions.kt similarity index 100% rename from play-services-location/core/src/main/kotlin/org/microg/gms/location/network/mozilla/extensions.kt rename to play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/mozilla/extensions.kt diff --git a/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/wifi/MovingWifiHelper.kt b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/wifi/MovingWifiHelper.kt new file mode 100644 index 0000000000..821c0a5b82 --- /dev/null +++ b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/wifi/MovingWifiHelper.kt @@ -0,0 +1,249 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.location.network.wifi + +import android.content.Context +import android.location.Location +import android.net.ConnectivityManager +import android.net.ConnectivityManager.TYPE_WIFI +import android.os.Build.VERSION.SDK_INT +import android.util.Log +import androidx.core.content.getSystemService +import androidx.core.location.LocationCompat +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.json.JSONArray +import org.json.JSONObject +import java.net.HttpURLConnection +import java.net.URL +import java.text.SimpleDateFormat +import java.util.* + +private val MOVING_WIFI_HOTSPOTS = setOf( + // Austria + "OEBB", + "Austrian Flynet", + // Belgium + "THALYSNET", + // Canada + "Air Canada", + // Czech Republic + "CDWiFi", + // France + "_SNCF_WIFI_INOUI", + "_SNCF_WIFI_INTERCITES", + "_WIFI_LYRIA", + "OUIFI", + "NormandieTrainConnecte", + // Germany + "WIFIonICE", + "WIFI@DB", + "WiFi@DB", + "RRX Hotspot", + "FlixBux", + "FlixBus Wi-Fi", + "FlixTrain Wi-Fi", + "FlyNet", + "Telekom_FlyNet", + "Vestische WLAN", + // Greece + "AegeanWiFi", + // Hong Kong + "Cathay Pacific", + // Hungary + "MAVSTART-WIFI", + // Netherlands + "KEOLIS Nederland", + // Sweden + "SJ", + // Switzerland + "SBB-Free", + // United Kingdom + "CrossCountryWiFi", + "GWR WiFi", + // United States + "Amtrak_WiFi", +) + +private val PHONE_HOTSPOT_KEYWORDS = setOf( + "iPhone", + "Galaxy", + "AndroidAP" +) + +/** + * A Wi-Fi hotspot that changes its location dynamically and thus is unsuitable for use with location services that assume stable locations. + * + * Some moving Wi-Fi hotspots allow to determine their location when connected or through a public network API. + */ +val WifiDetails.isMoving: Boolean + get() { + if (open && MOVING_WIFI_HOTSPOTS.contains(ssid)) { + return true + } + if (PHONE_HOTSPOT_KEYWORDS.any { ssid?.contains(it) == true }) { + return true + } + return false + } + +class MovingWifiHelper(private val context: Context) { + suspend fun retrieveMovingLocation(current: WifiDetails): Location { + if (!isLocallyRetrievable(current)) throw IllegalArgumentException() + val connectivityManager = context.getSystemService() ?: throw IllegalStateException() + val url = URL(MOVING_WIFI_HOTSPOTS_LOCALLY_RETRIEVABLE[current.ssid]) + return withContext(Dispatchers.IO) { + val network = if (isLocallyRetrievable(current) && SDK_INT >= 23) { + @Suppress("DEPRECATION") + (connectivityManager.allNetworks.singleOrNull { + val networkInfo = connectivityManager.getNetworkInfo(it) + Log.d(org.microg.gms.location.network.TAG, "Network info: $networkInfo") + networkInfo?.type == TYPE_WIFI && networkInfo.isConnected + }) + } else { + null + } + val connection = (if (SDK_INT >= 21) { + network?.openConnection(url) + } else { + null + } ?: url.openConnection()) as HttpURLConnection + try { + connection.doInput = true + if (connection.responseCode != 200) throw RuntimeException("Got error") + parseInput(current.ssid!!, connection.inputStream.readBytes()) + } finally { + connection.inputStream.close() + connection.disconnect() + } + } + } + + private fun parseWifiOnIce(location: Location, data: ByteArray): Location { + val json = JSONObject(data.decodeToString()) + if (json.getString("gpsStatus") != "VALID") throw RuntimeException("GPS not valid") + location.accuracy = 100f + location.time = json.getLong("serverTime") - 15000L + location.latitude = json.getDouble("latitude") + location.longitude = json.getDouble("longitude") + json.optDouble("speed").takeIf { !it.isNaN() }?.let { + location.speed = (it / 3.6).toFloat() + LocationCompat.setSpeedAccuracyMetersPerSecond(location, location.speed * 0.1f) + } + return location + } + + private fun parseFlixbus(location: Location, data: ByteArray): Location { + val json = JSONObject(data.decodeToString()) + location.accuracy = 100f + location.latitude = json.getDouble("latitude") + location.longitude = json.getDouble("longitude") + json.optDouble("speed").takeIf { !it.isNaN() }?.let { + location.speed = it.toFloat() + LocationCompat.setSpeedAccuracyMetersPerSecond(location, location.speed * 0.1f) + } + return location + } + + private fun parsePassengera(location: Location, data: ByteArray): Location { + val json = JSONObject(data.decodeToString()) + location.accuracy = 100f + location.latitude = json.getDouble("gpsLat") + location.longitude = json.getDouble("gpsLng") + json.optDouble("speed").takeIf { !it.isNaN() }?.let { + location.speed = (it / 3.6).toFloat() + LocationCompat.setSpeedAccuracyMetersPerSecond(location, location.speed * 0.1f) + } + json.optDouble("altitude").takeIf { !it.isNaN() }?.let { location.altitude = it } + return location + } + + private fun parseDisplayUgo(location: Location, data: ByteArray): Location { + val json = JSONArray(data.decodeToString()).getJSONObject(0) + location.accuracy = 100f + location.latitude = json.getDouble("latitude") + location.longitude = json.getDouble("longitude") + runCatching { SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.US).parse(json.getString("created_at"))?.time }.getOrNull()?.let { location.time = it } + json.optDouble("speed_kilometers_per_hour").takeIf { !it.isNaN() }?.let { + location.speed = (it / 3.6).toFloat() + LocationCompat.setSpeedAccuracyMetersPerSecond(location, location.speed * 0.1f) + } + json.optDouble("altitude_meters").takeIf { !it.isNaN() }?.let { location.altitude = it } + json.optDouble("bearing_in_degree").takeIf { !it.isNaN() }?.let { + location.bearing = it.toFloat() + LocationCompat.setBearingAccuracyDegrees(location, 90f) + } + return location + } + + private fun parsePanasonic(location: Location, data: ByteArray): Location { + val json = JSONObject(data.decodeToString()) + location.accuracy = 100f + location.latitude = json.getJSONObject("current_coordinates").getDouble("latitude") + location.longitude = json.getJSONObject("current_coordinates").getDouble("longitude") + json.optDouble("ground_speed_knots").takeIf { !it.isNaN() }?.let { + location.speed = (it * 0.5144).toFloat() + LocationCompat.setSpeedAccuracyMetersPerSecond(location, location.speed * 0.1f) + } + json.optDouble("altitude_feet").takeIf { !it.isNaN() }?.let { location.altitude = it * 0.3048 } + json.optDouble("true_heading_degree").takeIf { !it.isNaN() }?.let { + location.bearing = it.toFloat() + LocationCompat.setBearingAccuracyDegrees(location, 90f) + } + return location + } + + private fun parseBoardConnect(location: Location, data: ByteArray): Location { + val json = JSONObject(data.decodeToString()) + location.accuracy = 100f + location.latitude = json.getDouble("lat") + location.longitude = json.getDouble("lon") + runCatching { SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.US).parse(json.getString("utc"))?.time }.getOrNull()?.let { location.time = it } + json.optDouble("groundSpeed").takeIf { !it.isNaN() }?.let { + location.speed = (it * 0.5144).toFloat() + LocationCompat.setSpeedAccuracyMetersPerSecond(location, location.speed * 0.1f) + } + json.optDouble("altitude").takeIf { !it.isNaN() }?.let { location.altitude = it * 0.3048 } + json.optDouble("heading").takeIf { !it.isNaN() }?.let { + location.bearing = it.toFloat() + LocationCompat.setBearingAccuracyDegrees(location, 90f) + } + return location + } + + private fun parseInput(ssid: String, data: ByteArray): Location { + val location = Location(ssid) + return when (ssid) { + "WIFIonICE" -> parseWifiOnIce(location, data) + "FlixBus" -> parseFlixbus(location, data) + "FlixBus Wi-Fi" -> parseFlixbus(location, data) + "FlixTrain Wi-Fi" -> parseFlixbus(location, data) + "MAVSTART-WIFI" -> parsePassengera(location, data) + "AegeanWiFi" -> parseDisplayUgo(location, data) + "Telekom_FlyNet" -> parsePanasonic(location, data) + "FlyNet" -> parseBoardConnect(location, data) + else -> throw UnsupportedOperationException() + } + } + + fun isLocallyRetrievable(wifi: WifiDetails): Boolean = + MOVING_WIFI_HOTSPOTS_LOCALLY_RETRIEVABLE.containsKey(wifi.ssid) + + companion object { + private val MOVING_WIFI_HOTSPOTS_LOCALLY_RETRIEVABLE = mapOf( + "WIFIonICE" to "https://iceportal.de/api1/rs/status", + "FlixBus" to "https://media.flixbus.com/services/pis/v1/position", + "FlixBus Wi-Fi" to "https://media.flixbus.com/services/pis/v1/position", + "FlixTrain Wi-Fi" to "https://media.flixbus.com/services/pis/v1/position", + "MAVSTART-WIFI" to "http://portal.mav.hu/portal/api/vehicle/realtime", + "AegeanWiFi" to "https://api.ife.ugo.aero/navigation/positions", + "Telekom_FlyNet" to "https://services.inflightpanasonic.aero/inflight/services/flightdata/v2/flightdata", + "Cathay Pacific" to "https://services.inflightpanasonic.aero/inflight/services/flightdata/v2/flightdata", + "FlyNet" to "https://ww2.lufthansa-flynet.com/map/api/flightData", + ) + } +} + diff --git a/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/wifi/WifiDetails.kt b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/wifi/WifiDetails.kt similarity index 82% rename from play-services-location/core/src/main/kotlin/org/microg/gms/location/network/wifi/WifiDetails.kt rename to play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/wifi/WifiDetails.kt index 4ed0f6e151..28661c20ef 100644 --- a/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/wifi/WifiDetails.kt +++ b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/wifi/WifiDetails.kt @@ -11,5 +11,6 @@ data class WifiDetails( val timestamp: Long? = null, val frequency: Int? = null, val channel: Int? = null, - val signalStrength: Int? = null + val signalStrength: Int? = null, + val open: Boolean = false ) diff --git a/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/wifi/WifiDetailsCallback.kt b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/wifi/WifiDetailsCallback.kt similarity index 89% rename from play-services-location/core/src/main/kotlin/org/microg/gms/location/network/wifi/WifiDetailsCallback.kt rename to play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/wifi/WifiDetailsCallback.kt index 79a54c8ca4..82cb06d298 100644 --- a/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/wifi/WifiDetailsCallback.kt +++ b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/wifi/WifiDetailsCallback.kt @@ -7,4 +7,5 @@ package org.microg.gms.location.network.wifi interface WifiDetailsCallback { fun onWifiDetailsAvailable(wifis: List) + fun onWifiSourceFailed() } \ No newline at end of file diff --git a/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/wifi/WifiDetailsSource.kt b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/wifi/WifiDetailsSource.kt similarity index 100% rename from play-services-location/core/src/main/kotlin/org/microg/gms/location/network/wifi/WifiDetailsSource.kt rename to play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/wifi/WifiDetailsSource.kt diff --git a/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/wifi/WifiManagerSource.kt b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/wifi/WifiManagerSource.kt similarity index 94% rename from play-services-location/core/src/main/kotlin/org/microg/gms/location/network/wifi/WifiManagerSource.kt rename to play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/wifi/WifiManagerSource.kt index faa6d4d5de..71cbb4180a 100644 --- a/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/wifi/WifiManagerSource.kt +++ b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/wifi/WifiManagerSource.kt @@ -15,7 +15,6 @@ import android.net.wifi.WifiManager import android.os.WorkSource import android.util.Log import androidx.core.content.getSystemService -import org.microg.gms.location.network.TAG class WifiManagerSource(private val context: Context, private val callback: WifiDetailsCallback) : BroadcastReceiver(), WifiDetailsSource { @@ -24,7 +23,7 @@ class WifiManagerSource(private val context: Context, private val callback: Wifi try { callback.onWifiDetailsAvailable(this.context.getSystemService()?.scanResults.orEmpty().map(ScanResult::toWifiDetails)) } catch (e: Exception) { - Log.w(TAG, e) + Log.w(org.microg.gms.location.network.TAG, e) } } diff --git a/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/wifi/WifiScannerSource.kt b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/wifi/WifiScannerSource.kt similarity index 60% rename from play-services-location/core/src/main/kotlin/org/microg/gms/location/network/wifi/WifiScannerSource.kt rename to play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/wifi/WifiScannerSource.kt index 375db46f42..4cd82ae717 100644 --- a/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/wifi/WifiScannerSource.kt +++ b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/wifi/WifiScannerSource.kt @@ -5,29 +5,39 @@ package org.microg.gms.location.network.wifi +import android.Manifest import android.annotation.SuppressLint import android.content.Context +import android.content.pm.PackageManager.PERMISSION_GRANTED import android.net.wifi.ScanResult import android.net.wifi.WifiScanner +import android.os.Build.VERSION.SDK_INT import android.os.WorkSource import android.util.Log +import androidx.core.content.ContextCompat import org.microg.gms.location.network.TAG @SuppressLint("WrongConstant") class WifiScannerSource(private val context: Context, private val callback: WifiDetailsCallback) : WifiDetailsSource { override fun startScan(workSource: WorkSource?) { val scanner = context.getSystemService("wifiscanner") as WifiScanner - scanner.startScan(WifiScanner.ScanSettings(), object : WifiScanner.ScanListener { + scanner.startScan(WifiScanner.ScanSettings().apply { + band = WifiScanner.WIFI_BAND_BOTH + }, object : WifiScanner.ScanListener { override fun onSuccess() { Log.d(TAG, "Not yet implemented: onSuccess") + failed = false } override fun onFailure(reason: Int, description: String?) { - Log.d(TAG, "Not yet implemented: onFailure") + Log.d(TAG, "Not yet implemented: onFailure $reason $description") + failed = true + callback.onWifiSourceFailed() } + @Deprecated("Not supported on all devices") override fun onPeriodChanged(periodInMs: Int) { - Log.d(TAG, "Not yet implemented: onPeriodChanged") + Log.d(TAG, "Not yet implemented: onPeriodChanged $periodInMs") } override fun onResults(results: Array) { @@ -35,14 +45,15 @@ class WifiScannerSource(private val context: Context, private val callback: Wifi } override fun onFullResult(fullScanResult: ScanResult) { - Log.d(TAG, "Not yet implemented: onFullResult") + Log.d(TAG, "Not yet implemented: onFullResult $fullScanResult") } }, workSource) } companion object { + private var failed = false fun isSupported(context: Context): Boolean { - return (context.getSystemService("wifiscanner") as? WifiScanner) != null + return SDK_INT >= 26 && !failed && (context.getSystemService("wifiscanner") as? WifiScanner) != null && ContextCompat.checkSelfPermission(context, Manifest.permission.LOCATION_HARDWARE) == PERMISSION_GRANTED } } } \ No newline at end of file diff --git a/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/wifi/extensions.kt b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/wifi/extensions.kt similarity index 66% rename from play-services-location/core/src/main/kotlin/org/microg/gms/location/network/wifi/extensions.kt rename to play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/wifi/extensions.kt index 560f11ad97..ff05dfa67a 100644 --- a/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/wifi/extensions.kt +++ b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/wifi/extensions.kt @@ -6,16 +6,17 @@ package org.microg.gms.location.network.wifi import android.net.wifi.ScanResult -import android.os.Build +import android.os.Build.VERSION.SDK_INT import android.os.SystemClock internal fun ScanResult.toWifiDetails(): WifiDetails = WifiDetails( macAddress = BSSID, ssid = SSID, - timestamp = if (Build.VERSION.SDK_INT >= 19) System.currentTimeMillis() - (SystemClock.elapsedRealtime() - (timestamp / 1000)) else null, + timestamp = if (SDK_INT >= 19) System.currentTimeMillis() - (SystemClock.elapsedRealtime() - (timestamp / 1000)) else null, frequency = frequency, channel = frequencyToChannel(frequency), - signalStrength = level + signalStrength = level, + open = setOf("WEP", "WPA", "PSK", "EAP", "IEEE8021X", "PEAP", "TLS", "TTLS").none { capabilities.contains(it) } ) private const val BAND_24_GHZ_FIRST_CH_NUM = 1 @@ -56,4 +57,28 @@ internal fun frequencyToChannel(freq: Int): Int? { else -> null } -} \ No newline at end of file +} + +val WifiDetails.isNomap: Boolean + get() = ssid?.endsWith("_nomap") == true + +val WifiDetails.isHidden: Boolean + get() = ssid == "" + +val WifiDetails.isRequestable: Boolean + get() = !isNomap && !isHidden && !isMoving + +val WifiDetails.macBytes: ByteArray + get() { + val mac = macClean + return byteArrayOf( + mac.substring(0, 2).toInt(16).toByte(), + mac.substring(2, 4).toInt(16).toByte(), + mac.substring(4, 6).toInt(16).toByte(), + mac.substring(6, 8).toInt(16).toByte(), + mac.substring(8, 10).toInt(16).toByte(), + mac.substring(10, 12).toInt(16).toByte() + ) + } +val WifiDetails.macClean: String + get() = macAddress.lowercase().replace(":", "") \ No newline at end of file diff --git a/play-services-location/core/src/main/kotlin/org/microg/gms/location/provider/AbstractLocationProviderPreTiramisu.kt b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/provider/AbstractLocationProviderPreTiramisu.kt similarity index 98% rename from play-services-location/core/src/main/kotlin/org/microg/gms/location/provider/AbstractLocationProviderPreTiramisu.kt rename to play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/provider/AbstractLocationProviderPreTiramisu.kt index 74cd11c337..c737ea25ba 100644 --- a/play-services-location/core/src/main/kotlin/org/microg/gms/location/provider/AbstractLocationProviderPreTiramisu.kt +++ b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/provider/AbstractLocationProviderPreTiramisu.kt @@ -6,7 +6,6 @@ package org.microg.gms.location.provider import android.content.Context -import android.location.Location import android.location.LocationProvider import android.os.Bundle import android.os.SystemClock diff --git a/play-services-location/core/src/main/kotlin/org/microg/gms/location/provider/GenericLocationProvider.kt b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/provider/GenericLocationProvider.kt similarity index 87% rename from play-services-location/core/src/main/kotlin/org/microg/gms/location/provider/GenericLocationProvider.kt rename to play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/provider/GenericLocationProvider.kt index 4f31ffc344..4f7e1a6edc 100644 --- a/play-services-location/core/src/main/kotlin/org/microg/gms/location/provider/GenericLocationProvider.kt +++ b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/provider/GenericLocationProvider.kt @@ -14,5 +14,5 @@ interface GenericLocationProvider { fun enable() fun disable() fun dump(writer: PrintWriter) - fun reportLocation(location: Location) + fun reportLocationToSystem(location: Location) } \ No newline at end of file diff --git a/play-services-location/core/src/main/kotlin/org/microg/gms/location/provider/GeocodeProviderService.kt b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/provider/GeocodeProviderService.kt similarity index 76% rename from play-services-location/core/src/main/kotlin/org/microg/gms/location/provider/GeocodeProviderService.kt rename to play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/provider/GeocodeProviderService.kt index a99e3814b7..3f4c2034ea 100644 --- a/play-services-location/core/src/main/kotlin/org/microg/gms/location/provider/GeocodeProviderService.kt +++ b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/provider/GeocodeProviderService.kt @@ -8,6 +8,8 @@ package org.microg.gms.location.provider import android.app.Service import android.content.Intent import android.os.IBinder +import java.io.FileDescriptor +import java.io.PrintWriter class GeocodeProviderService : Service() { private var bound: Boolean = false @@ -20,4 +22,8 @@ class GeocodeProviderService : Service() { bound = true return provider?.binder } + + override fun dump(fd: FileDescriptor?, writer: PrintWriter?, args: Array?) { + provider?.dump(writer) + } } \ No newline at end of file diff --git a/play-services-location/core/src/main/kotlin/org/microg/gms/location/provider/NetworkLocationProviderPreTiramisu.kt b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/provider/NetworkLocationProviderPreTiramisu.kt similarity index 79% rename from play-services-location/core/src/main/kotlin/org/microg/gms/location/provider/NetworkLocationProviderPreTiramisu.kt rename to play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/provider/NetworkLocationProviderPreTiramisu.kt index c66ff22bd9..40972f4efb 100644 --- a/play-services-location/core/src/main/kotlin/org/microg/gms/location/provider/NetworkLocationProviderPreTiramisu.kt +++ b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/provider/NetworkLocationProviderPreTiramisu.kt @@ -14,13 +14,13 @@ import android.location.Criteria import android.location.Location import android.os.Build.VERSION.SDK_INT import android.os.WorkSource -import android.util.Log import androidx.annotation.RequiresApi import com.android.location.provider.ProviderPropertiesUnbundled import com.android.location.provider.ProviderRequestUnbundled +import org.microg.gms.location.* import org.microg.gms.location.network.LOCATION_EXTRA_PRECISION import org.microg.gms.location.network.NetworkLocationService -import org.microg.gms.location.network.NetworkLocationService.Companion.ACTION_REPORT_LOCATION +import org.microg.gms.location.provider.NetworkLocationProviderService.Companion.ACTION_REPORT_LOCATION import java.io.PrintWriter import kotlin.math.max @@ -53,16 +53,16 @@ class NetworkLocationProviderPreTiramisu : AbstractLocationProviderPreTiramisu { intervalMillis = Long.MAX_VALUE } val intent = Intent(context, NetworkLocationService::class.java) - intent.putExtra(NetworkLocationService.EXTRA_PENDING_INTENT, pendingIntent) - intent.putExtra(NetworkLocationService.EXTRA_ENABLE, true) - intent.putExtra(NetworkLocationService.EXTRA_INTERVAL_MILLIS, intervalMillis) - intent.putExtra(NetworkLocationService.EXTRA_FORCE_NOW, forceNow) + intent.putExtra(EXTRA_PENDING_INTENT, pendingIntent) + intent.putExtra(EXTRA_ENABLE, true) + intent.putExtra(EXTRA_INTERVAL_MILLIS, intervalMillis) + intent.putExtra(EXTRA_FORCE_NOW, forceNow) if (SDK_INT >= 31) { - intent.putExtra(NetworkLocationService.EXTRA_LOW_POWER, currentRequest?.isLowPower ?: false) - intent.putExtra(NetworkLocationService.EXTRA_WORK_SOURCE, currentRequest?.workSource) + intent.putExtra(EXTRA_LOW_POWER, currentRequest?.isLowPower ?: false) + intent.putExtra(EXTRA_WORK_SOURCE, currentRequest?.workSource) } if (SDK_INT >= 29) { - intent.putExtra(NetworkLocationService.EXTRA_BYPASS, currentRequest?.isLocationSettingsIgnored ?: false) + intent.putExtra(EXTRA_BYPASS, currentRequest?.isLocationSettingsIgnored ?: false) } context.startService(intent) } @@ -100,8 +100,8 @@ class NetworkLocationProviderPreTiramisu : AbstractLocationProviderPreTiramisu { synchronized(this) { if (!enabled) throw IllegalStateException() val intent = Intent(context, NetworkLocationService::class.java) - intent.putExtra(NetworkLocationService.EXTRA_PENDING_INTENT, pendingIntent) - intent.putExtra(NetworkLocationService.EXTRA_ENABLE, false) + intent.putExtra(EXTRA_PENDING_INTENT, pendingIntent) + intent.putExtra(EXTRA_ENABLE, false) context.startService(intent) pendingIntent?.cancel() pendingIntent = null @@ -110,7 +110,7 @@ class NetworkLocationProviderPreTiramisu : AbstractLocationProviderPreTiramisu { } } - override fun reportLocation(location: Location) { + override fun reportLocationToSystem(location: Location) { location.provider = "network" location.extras?.remove(LOCATION_EXTRA_PRECISION) lastReportedLocation = location diff --git a/play-services-location/core/src/main/kotlin/org/microg/gms/location/provider/NetworkLocationProviderService.kt b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/provider/NetworkLocationProviderService.kt similarity index 87% rename from play-services-location/core/src/main/kotlin/org/microg/gms/location/provider/NetworkLocationProviderService.kt rename to play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/provider/NetworkLocationProviderService.kt index 50fa462e06..fb2cd9e279 100644 --- a/play-services-location/core/src/main/kotlin/org/microg/gms/location/provider/NetworkLocationProviderService.kt +++ b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/provider/NetworkLocationProviderService.kt @@ -10,8 +10,7 @@ import android.content.Intent import android.location.Location import android.os.* import android.os.Build.VERSION.SDK_INT -import org.microg.gms.location.network.NetworkLocationService.Companion.ACTION_REPORT_LOCATION -import org.microg.gms.location.network.NetworkLocationService.Companion.EXTRA_LOCATION +import org.microg.gms.location.EXTRA_LOCATION import java.io.FileDescriptor import java.io.PrintWriter @@ -33,7 +32,7 @@ class NetworkLocationProviderService : Service() { handler.post { val location = intent.getParcelableExtra(EXTRA_LOCATION) if (location != null) { - provider?.reportLocation(location) + provider?.reportLocationToSystem(location) } } } @@ -51,7 +50,7 @@ class NetworkLocationProviderService : Service() { else -> @Suppress("DEPRECATION") - NetworkLocationProviderPreTiramisu(this, Unit) + (NetworkLocationProviderPreTiramisu(this, Unit)) } provider?.enable() } @@ -71,4 +70,8 @@ class NetworkLocationProviderService : Service() { bound = false super.onDestroy() } + + companion object { + const val ACTION_REPORT_LOCATION = "org.microg.gms.location.provider.ACTION_REPORT_LOCATION" + } } \ No newline at end of file diff --git a/play-services-location/core/src/main/kotlin/org/microg/gms/location/provider/OpenStreetMapNominatimGeocodeProvider.kt b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/provider/OpenStreetMapNominatimGeocodeProvider.kt similarity index 93% rename from play-services-location/core/src/main/kotlin/org/microg/gms/location/provider/OpenStreetMapNominatimGeocodeProvider.kt rename to play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/provider/OpenStreetMapNominatimGeocodeProvider.kt index 540e3b2565..92e08d750f 100644 --- a/play-services-location/core/src/main/kotlin/org/microg/gms/location/provider/OpenStreetMapNominatimGeocodeProvider.kt +++ b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/provider/OpenStreetMapNominatimGeocodeProvider.kt @@ -18,6 +18,8 @@ import com.android.volley.toolbox.Volley import com.google.android.gms.location.internal.ClientIdentity import org.json.JSONObject import org.microg.address.Formatter +import org.microg.gms.location.LocationSettings +import java.io.PrintWriter import java.util.* import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit @@ -28,10 +30,12 @@ class OpenStreetMapNominatimGeocodeProvider(private val context: Context) : Geoc private val queue = Volley.newRequestQueue(context) private val formatter = runCatching { Formatter() }.getOrNull() private val addressCache = LruCache(CACHE_SIZE) + private val settings by lazy { LocationSettings(context) } override fun onGetFromLocation(latitude: Double, longitude: Double, maxResults: Int, params: GeocoderParams, addresses: MutableList
): String? { val clientIdentity = params.clientIdentity ?: return "null client package" val locale = params.locale ?: return "null locale" + if (!settings.geocoderNominatim) return "disabled" val cacheKey = CacheKey(clientIdentity, locale, latitude, longitude) addressCache[cacheKey]?.let {address -> addresses.add(address) @@ -79,6 +83,7 @@ class OpenStreetMapNominatimGeocodeProvider(private val context: Context) : Geoc ): String? { val clientIdentity = params.clientIdentity ?: return "null client package" val locale = params.locale ?: return "null locale" + if (!settings.geocoderNominatim) return "disabled" val uri = Uri.Builder() .scheme("https").authority(NOMINATIM_SERVER).path("/search") .appendQueryParameter("format", "json") @@ -149,6 +154,11 @@ class OpenStreetMapNominatimGeocodeProvider(private val context: Context) : Geoc return address } + fun dump(writer: PrintWriter?) { + writer?.println("Enabled: ${settings.geocoderNominatim}") + writer?.println("Address cache: size=${addressCache.size()} hits=${addressCache.hitCount()} miss=${addressCache.missCount()} puts=${addressCache.putCount()} evicts=${addressCache.evictionCount()}") + } + companion object { private const val CACHE_SIZE = 200 diff --git a/play-services-location/core/src/main/kotlin/org/microg/gms/location/provider/extensions.kt b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/provider/extensions.kt similarity index 89% rename from play-services-location/core/src/main/kotlin/org/microg/gms/location/provider/extensions.kt rename to play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/provider/extensions.kt index eea68e661a..a99592a812 100644 --- a/play-services-location/core/src/main/kotlin/org/microg/gms/location/provider/extensions.kt +++ b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/provider/extensions.kt @@ -6,11 +6,9 @@ package org.microg.gms.location.provider import android.content.Context -import android.content.pm.PackageManager import android.location.GeocoderParams import android.os.Build.VERSION.SDK_INT import com.google.android.gms.location.internal.ClientIdentity -import org.microg.gms.utils.getApplicationLabel const val TAG = "LocationProvider" diff --git a/play-services-location/core/src/main/AndroidManifest.xml b/play-services-location/core/src/main/AndroidManifest.xml index 48bc6ae5dd..3af5571577 100644 --- a/play-services-location/core/src/main/AndroidManifest.xml +++ b/play-services-location/core/src/main/AndroidManifest.xml @@ -9,16 +9,9 @@ - - + - - - - - @@ -45,28 +36,5 @@ - - - - - - - - - - - - - - - diff --git a/play-services-location/core/src/main/kotlin/org/microg/gms/location/extensions.kt b/play-services-location/core/src/main/kotlin/org/microg/gms/location/extensions.kt deleted file mode 100644 index e8ba0cda7f..0000000000 --- a/play-services-location/core/src/main/kotlin/org/microg/gms/location/extensions.kt +++ /dev/null @@ -1,32 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2023 microG Project Team - * SPDX-License-Identifier: Apache-2.0 - */ - -package org.microg.gms.location - -import android.location.Location -import androidx.core.location.LocationCompat -import com.google.android.gms.common.Feature - -internal val Location.elapsedMillis: Long - get() = LocationCompat.getElapsedRealtimeMillis(this) - -internal val FEATURES = arrayOf( - Feature("name_ulr_private", 1), - Feature("driving_mode", 6), - Feature("name_sleep_segment_request", 1), - Feature("support_context_feature_id", 1), - Feature("get_current_location", 2), - Feature("get_last_activity_feature_id", 1), - Feature("get_last_location_with_request", 1), - Feature("set_mock_mode_with_callback", 1), - Feature("set_mock_location_with_callback", 1), - Feature("inject_location_with_callback", 1), - Feature("location_updates_with_callback", 1), - Feature("user_service_developer_features", 1), - Feature("user_service_location_accuracy", 1), - Feature("user_service_safety_and_emergency", 1), - - Feature("use_safe_parcelable_in_intents", 1) -) \ No newline at end of file diff --git a/play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/DeviceOrientationManager.kt b/play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/DeviceOrientationManager.kt index de03e4657e..558ee2cb27 100644 --- a/play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/DeviceOrientationManager.kt +++ b/play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/DeviceOrientationManager.kt @@ -23,7 +23,6 @@ import android.os.WorkSource import android.util.Log import android.view.Surface import android.view.WindowManager -import androidx.core.content.ContextCompat import androidx.core.content.getSystemService import androidx.core.location.LocationCompat import androidx.lifecycle.Lifecycle @@ -36,6 +35,7 @@ import com.google.android.gms.location.internal.ClientIdentity import com.google.android.gms.location.internal.DeviceOrientationRequestInternal import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock +import org.microg.gms.location.formatDuration import org.microg.gms.utils.WorkSourceUtil import java.io.PrintWriter import kotlin.math.* @@ -259,7 +259,7 @@ class DeviceOrientationManager(private val context: Context, private val lifecyc fun dump(writer: PrintWriter) { writer.println("Current device orientation request (started=$started)") for (request in requests.values.toList()) { - writer.println("- ${request.workSource} (pending: ${request.updatesPending} ${request.timePendingMillis}ms)") + writer.println("- ${request.workSource} (pending: ${request.updatesPending.let { if (it == Int.MAX_VALUE) "\u221e" else "$it" }} ${request.timePendingMillis.formatDuration()})") } } @@ -305,7 +305,7 @@ class DeviceOrientationManager(private val context: Context, private val lifecyc if (lastOrientation != null && abs(lastOrientation!!.headingDegrees - deviceOrientation.headingDegrees) < Math.toDegrees(request.smallestAngleChangeRadians.toDouble())) return if (lastOrientation == deviceOrientation) return listener.onDeviceOrientationChanged(deviceOrientation) - updates++ + if (request.numUpdates != Int.MAX_VALUE) updates++ if (updatesPending <= 0) throw RuntimeException("max updates reached") } } diff --git a/play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/LastLocationCapsule.kt b/play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/LastLocationCapsule.kt index ca553c8381..69ceeb7295 100644 --- a/play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/LastLocationCapsule.kt +++ b/play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/LastLocationCapsule.kt @@ -9,8 +9,6 @@ import android.content.Context import android.location.Location import android.location.LocationManager import android.os.Build.VERSION.SDK_INT -import android.os.Parcel -import android.os.Parcelable import android.os.SystemClock import android.util.Log import androidx.core.content.getSystemService @@ -68,11 +66,15 @@ class LastLocationCapsule(private val context: Context) { } fun updateCoarseLocation(location: Location) { - if (lastCoarseLocation != null && lastCoarseLocation!!.elapsedMillis > location.elapsedMillis + 30_000L && (!location.hasBearing() || !location.hasSpeed())) { - location.bearing = lastCoarseLocation!!.bearingTo(location) - LocationCompat.setBearingAccuracyDegrees(location, 180.0f) - location.speed = lastCoarseLocation!!.distanceTo(location) / ((location.elapsedMillis - lastCoarseLocation!!.elapsedMillis) / 1000) - LocationCompat.setSpeedAccuracyMetersPerSecond(location, location.speed) + if (lastCoarseLocation != null && lastCoarseLocation!!.elapsedMillis + EXTENSION_CLIFF > location.elapsedMillis) { + if (!location.hasSpeed()) { + location.speed = lastCoarseLocation!!.distanceTo(location) / ((location.elapsedMillis - lastCoarseLocation!!.elapsedMillis) / 1000) + LocationCompat.setSpeedAccuracyMetersPerSecond(location, location.speed) + } + if (!location.hasBearing() && location.speed > 0.5f) { + location.bearing = lastCoarseLocation!!.bearingTo(location) + LocationCompat.setBearingAccuracyDegrees(location, 180.0f) + } } lastCoarseLocation = newest(lastCoarseLocation, location) lastCoarseLocationTimeCoarsed = newest(lastCoarseLocationTimeCoarsed, location, TIME_COARSE_CLIFF) @@ -92,13 +94,18 @@ class LastLocationCapsule(private val context: Context) { } fun start() { + fun Location.adjustRealtime() = apply { + if (SDK_INT >= 17) { + elapsedRealtimeNanos = SystemClock.elapsedRealtimeNanos() - TimeUnit.MILLISECONDS.toNanos((System.currentTimeMillis() - time)) + } + } try { if (file.exists()) { val capsule = SafeParcelUtil.fromByteArray(file.readBytes(), LastLocationCapsuleParcelable.CREATOR) - lastFineLocation = capsule.lastFineLocation - lastCoarseLocation = capsule.lastCoarseLocation - lastFineLocationTimeCoarsed = capsule.lastFineLocationTimeCoarsed - lastCoarseLocationTimeCoarsed = capsule.lastCoarseLocationTimeCoarsed + lastFineLocation = capsule.lastFineLocation?.adjustRealtime() + lastCoarseLocation = capsule.lastCoarseLocation?.adjustRealtime() + lastFineLocationTimeCoarsed = capsule.lastFineLocationTimeCoarsed?.adjustRealtime() + lastCoarseLocationTimeCoarsed = capsule.lastCoarseLocationTimeCoarsed?.adjustRealtime() } } catch (e: Exception) { Log.w(TAG, e) @@ -126,6 +133,7 @@ class LastLocationCapsule(private val context: Context) { companion object { private const val FILE_NAME = "last_location_capsule" private const val TIME_COARSE_CLIFF = 60_000L + private const val EXTENSION_CLIFF = 30_000L private class LastLocationCapsuleParcelable( @Field(1) @JvmField val lastFineLocation: Location?, diff --git a/play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/LocationAppsDatabase.kt b/play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/LocationAppsDatabase.kt new file mode 100644 index 0000000000..f9cede3a13 --- /dev/null +++ b/play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/LocationAppsDatabase.kt @@ -0,0 +1,131 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.location.manager + +import android.content.ContentValues +import android.content.Context +import android.database.sqlite.SQLiteDatabase +import android.database.sqlite.SQLiteOpenHelper +import android.location.Location +import android.util.Log +import androidx.core.content.contentValuesOf +import androidx.core.database.getIntOrNull + +class LocationAppsDatabase(context: Context) : SQLiteOpenHelper(context, "geoapps.db", null, 2) { + override fun onCreate(db: SQLiteDatabase) { + db.execSQL("CREATE TABLE IF NOT EXISTS $TABLE_APPS($FIELD_PACKAGE TEXT NOT NULL, $FIELD_TIME INTEGER NOT NULL, $FIELD_FORCE_COARSE INTEGER);") + db.execSQL("CREATE TABLE IF NOT EXISTS $TABLE_APPS_LAST_LOCATION($FIELD_PACKAGE TEXT NOT NULL, $FIELD_TIME INTEGER NOT NULL, $FIELD_LATITUDE REAL NOT NULL, $FIELD_LONGITUDE REAL NOT NULL, $FIELD_ACCURACY REAL NOT NULL, $FIELD_PROVIDER TEXT NOT NULL);") + db.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS ${TABLE_APPS}_index ON ${TABLE_APPS}(${FIELD_PACKAGE});") + db.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS ${TABLE_APPS_LAST_LOCATION}_index ON ${TABLE_APPS_LAST_LOCATION}(${FIELD_PACKAGE});") + } + + private fun insertOrUpdateApp(packageName: String, vararg pairs: Pair) { + val values = contentValuesOf(FIELD_PACKAGE to packageName, *pairs) + if (writableDatabase.insertWithOnConflict(TABLE_APPS, null, values, SQLiteDatabase.CONFLICT_IGNORE) < 0) { + writableDatabase.update(TABLE_APPS, values, "$FIELD_PACKAGE = ?", arrayOf(packageName)) + } + close() + } + + fun noteAppUsage(packageName: String) { + insertOrUpdateApp(packageName, FIELD_TIME to System.currentTimeMillis()) + } + + fun getForceCoarse(packageName: String): Boolean { + return readableDatabase.query(TABLE_APPS, arrayOf(FIELD_FORCE_COARSE), "$FIELD_PACKAGE = ?", arrayOf(packageName), null, null, null, "1").run { + try { + if (moveToNext()) { + getIntOrNull(0) == 1 + } else { + false + } + } finally { + close() + } + } + } + + fun setForceCoarse(packageName: String, forceCoarse: Boolean) { + insertOrUpdateApp(packageName, FIELD_FORCE_COARSE to (if (forceCoarse) 1 else 0)) + } + + fun noteAppLocation(packageName: String, location: Location?) { + noteAppUsage(packageName) + if (location == null) return + val values = contentValuesOf( + FIELD_PACKAGE to packageName, + FIELD_TIME to location.time, + FIELD_LATITUDE to location.latitude, + FIELD_LONGITUDE to location.longitude, + FIELD_ACCURACY to location.accuracy, + FIELD_PROVIDER to location.provider + ) + writableDatabase.insertWithOnConflict(TABLE_APPS_LAST_LOCATION, null, values, SQLiteDatabase.CONFLICT_REPLACE) + close() + } + + fun listAppsByAccessTime(limit: Int = Int.MAX_VALUE): List> { + val res = arrayListOf>() + readableDatabase.query(TABLE_APPS, arrayOf(FIELD_PACKAGE, FIELD_TIME), null, null, null, null, "$FIELD_TIME DESC", "$limit").apply { + while (moveToNext()) { + res.add(getString(0) to getLong(1)) + } + close() + } + return res + } + + fun getAppLocation(packageName: String): Location? { + return readableDatabase.query( + TABLE_APPS_LAST_LOCATION, + arrayOf(FIELD_LATITUDE, FIELD_LONGITUDE, FIELD_ACCURACY, FIELD_TIME, FIELD_PROVIDER), + "$FIELD_PACKAGE = ?", + arrayOf(packageName), + null, + null, + null, + "1" + ).run { + try { + if (moveToNext()) { + Location(getString(4)).also { + it.latitude = getDouble(0) + it.longitude = getDouble(1) + it.accuracy = getFloat(2) + it.time = getLong(3) + } + } else { + null + } + } finally { + close() + } + } + } + + override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { + onCreate(db) + if (oldVersion < 2) { + try { + db.execSQL("ALTER TABLE $TABLE_APPS ADD COLUMN IF NOT EXISTS $FIELD_FORCE_COARSE INTEGER;") + } catch (ignored: Exception) { + // Ignoring + } + } + } + + companion object { + private const val TABLE_APPS = "apps" + private const val TABLE_APPS_LAST_LOCATION = "app_location" + private const val FIELD_PACKAGE = "package" + private const val FIELD_FORCE_COARSE = "force_coarse" + private const val FIELD_LATITUDE = "lat" + private const val FIELD_LONGITUDE = "lon" + private const val FIELD_ACCURACY = "acc" + private const val FIELD_TIME = "time" + private const val FIELD_PROVIDER = "provider" + } +} \ No newline at end of file diff --git a/play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/LocationManager.kt b/play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/LocationManager.kt index efd84bccc8..2e7af73869 100644 --- a/play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/LocationManager.kt +++ b/play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/LocationManager.kt @@ -13,6 +13,7 @@ import android.content.Context import android.content.Intent import android.content.pm.PackageManager import android.location.Location +import android.location.LocationManager import android.os.Build.VERSION.SDK_INT import android.os.IBinder import android.util.Log @@ -26,20 +27,24 @@ import androidx.lifecycle.lifecycleScope import com.google.android.gms.location.* import com.google.android.gms.location.Granularity.GRANULARITY_COARSE import com.google.android.gms.location.Granularity.GRANULARITY_FINE +import com.google.android.gms.location.Priority.PRIORITY_HIGH_ACCURACY import com.google.android.gms.location.internal.ClientIdentity -import org.microg.gms.location.GranularityUtil -import org.microg.gms.location.elapsedMillis -import org.microg.gms.location.network.NetworkLocationService +import org.microg.gms.location.* +import org.microg.gms.utils.IntentCacheManager import java.io.PrintWriter import kotlin.math.max +import kotlin.math.min import android.location.LocationManager as SystemLocationManager class LocationManager(private val context: Context, private val lifecycle: Lifecycle) : LifecycleOwner { private var coarsePendingIntent: PendingIntent? = null - private val postProcessor = LocationPostProcessor() - private val lastLocationCapsule = LastLocationCapsule(context) - private val requestManager = LocationRequestManager(context, lifecycle, postProcessor) { onRequestManagerUpdated() } - private val fineLocationListener = LocationListenerCompat { updateFineLocation(it) } + private val postProcessor by lazy { LocationPostProcessor() } + private val lastLocationCapsule by lazy { LastLocationCapsule(context) } + val database by lazy { LocationAppsDatabase(context) } + private val requestManager by lazy { LocationRequestManager(context, lifecycle, postProcessor, database) { onRequestManagerUpdated() } } + private val gpsLocationListener by lazy { LocationListenerCompat { updateGpsLocation(it) } } + private val networkLocationListener by lazy { LocationListenerCompat { updateNetworkLocation(it) } } + private var boundToSystemNetworkLocation: Boolean = false val deviceOrientationManager = DeviceOrientationManager(context, lifecycle) @@ -61,7 +66,8 @@ class LocationManager(private val context: Context, private val lifecycle: Lifec Log.w(TAG, "${clientIdentity.packageName} wants to impersonate ${request.impersonation!!.packageName}. Ignoring.") } val permissionGranularity = context.granularityFromPermission(clientIdentity) - val effectiveGranularity = getEffectiveGranularity(request.granularity, permissionGranularity) + var effectiveGranularity = getEffectiveGranularity(request.granularity, permissionGranularity) + if (effectiveGranularity == GRANULARITY_FINE && database.getForceCoarse(clientIdentity.packageName)) effectiveGranularity = GRANULARITY_COARSE val returnedLocation = if (effectiveGranularity > permissionGranularity) { // No last location available at requested granularity due to lack of permission null @@ -78,8 +84,8 @@ class LocationManager(private val context: Context, private val lifecycle: Lifec processedLocation } } - // TODO: Log request to local database - return returnedLocation + database.noteAppLocation(clientIdentity.packageName, returnedLocation) + return returnedLocation?.let { Location(it).apply { provider = "fused" } } } fun getLocationAvailability(clientIdentity: ClientIdentity, request: LocationAvailabilityRequest): LocationAvailability { @@ -97,7 +103,7 @@ class LocationManager(private val context: Context, private val lifecycle: Lifec suspend fun addBinderRequest(clientIdentity: ClientIdentity, binder: IBinder, callback: ILocationCallback, request: LocationRequest) { request.verify(context, clientIdentity) - requestManager.add(binder, clientIdentity, callback, request) + requestManager.add(binder, clientIdentity, callback, request, lastLocationCapsule) } suspend fun updateBinderRequest( @@ -108,7 +114,7 @@ class LocationManager(private val context: Context, private val lifecycle: Lifec request: LocationRequest ) { request.verify(context, clientIdentity) - requestManager.update(oldBinder, binder, clientIdentity, callback, request) + requestManager.update(oldBinder, binder, clientIdentity, callback, request, lastLocationCapsule) } suspend fun removeBinderRequest(binder: IBinder) { @@ -117,7 +123,7 @@ class LocationManager(private val context: Context, private val lifecycle: Lifec suspend fun addIntentRequest(clientIdentity: ClientIdentity, pendingIntent: PendingIntent, request: LocationRequest) { request.verify(context, clientIdentity) - requestManager.add(pendingIntent, clientIdentity, request) + requestManager.add(pendingIntent, clientIdentity, request, lastLocationCapsule) } suspend fun removeIntentRequest(pendingIntent: PendingIntent) { @@ -130,7 +136,7 @@ class LocationManager(private val context: Context, private val lifecycle: Lifec started = true } val intent = Intent(context, LocationManagerService::class.java) - intent.action = NetworkLocationService.ACTION_REPORT_LOCATION + intent.action = LocationManagerService.ACTION_REPORT_LOCATION coarsePendingIntent = PendingIntent.getService(context, 0, intent, (if (SDK_INT >= 31) FLAG_MUTABLE else 0) or FLAG_UPDATE_CURRENT) lastLocationCapsule.start() requestManager.start() @@ -145,109 +151,121 @@ class LocationManager(private val context: Context, private val lifecycle: Lifec lastLocationCapsule.stop() deviceOrientationManager.stop() - val intent = Intent(context, NetworkLocationService::class.java) - intent.putExtra(NetworkLocationService.EXTRA_PENDING_INTENT, coarsePendingIntent) - intent.putExtra(NetworkLocationService.EXTRA_ENABLE, false) - context.startService(intent) + if (context.hasNetworkLocationServiceBuiltIn()) { + val intent = Intent(ACTION_NETWORK_LOCATION_SERVICE) + intent.`package` = context.packageName + intent.putExtra(EXTRA_PENDING_INTENT, coarsePendingIntent) + intent.putExtra(EXTRA_ENABLE, false) + context.startService(intent) + } val locationManager = context.getSystemService() ?: return try { - LocationManagerCompat.removeUpdates(locationManager, fineLocationListener) + if (boundToSystemNetworkLocation) { + LocationManagerCompat.removeUpdates(locationManager, networkLocationListener) + boundToSystemNetworkLocation = false + } + LocationManagerCompat.removeUpdates(locationManager, gpsLocationListener) } catch (e: SecurityException) { // Ignore } } private fun onRequestManagerUpdated() { - val coarseInterval = when (requestManager.granularity) { + val networkInterval = when (requestManager.granularity) { GRANULARITY_COARSE -> max(requestManager.intervalMillis, MAX_COARSE_UPDATE_INTERVAL) GRANULARITY_FINE -> max(requestManager.intervalMillis, MAX_FINE_UPDATE_INTERVAL) else -> Long.MAX_VALUE } - val fineInterval = when (requestManager.granularity) { - GRANULARITY_FINE -> requestManager.intervalMillis + val gpsInterval = when (requestManager.priority to requestManager.granularity) { + PRIORITY_HIGH_ACCURACY to GRANULARITY_FINE -> requestManager.intervalMillis else -> Long.MAX_VALUE } - val intent = Intent(context, NetworkLocationService::class.java) - intent.putExtra(NetworkLocationService.EXTRA_PENDING_INTENT, coarsePendingIntent) - intent.putExtra(NetworkLocationService.EXTRA_ENABLE, true) - intent.putExtra(NetworkLocationService.EXTRA_INTERVAL_MILLIS, coarseInterval) - intent.putExtra(NetworkLocationService.EXTRA_LOW_POWER, requestManager.granularity <= GRANULARITY_COARSE) - intent.putExtra(NetworkLocationService.EXTRA_WORK_SOURCE, requestManager.workSource) - context.startService(intent) + if (context.hasNetworkLocationServiceBuiltIn()) { + val intent = Intent(ACTION_NETWORK_LOCATION_SERVICE) + intent.`package` = context.packageName + intent.putExtra(EXTRA_PENDING_INTENT, coarsePendingIntent) + intent.putExtra(EXTRA_ENABLE, true) + intent.putExtra(EXTRA_INTERVAL_MILLIS, networkInterval) + intent.putExtra(EXTRA_LOW_POWER, requestManager.granularity <= GRANULARITY_COARSE) + intent.putExtra(EXTRA_WORK_SOURCE, requestManager.workSource) + context.startService(intent) + } val locationManager = context.getSystemService() ?: return - if (fineInterval != Long.MAX_VALUE) { - try { - LocationManagerCompat.requestLocationUpdates( - locationManager, - SystemLocationManager.GPS_PROVIDER, - LocationRequestCompat.Builder(fineInterval).build(), - fineLocationListener, - context.mainLooper - ) - } catch (e: SecurityException) { - // Ignore - } - } else { - try { - LocationManagerCompat.removeUpdates(locationManager, fineLocationListener) - } catch (e: SecurityException) { - // Ignore + locationManager.requestSystemProviderUpdates(SystemLocationManager.GPS_PROVIDER, gpsInterval, gpsLocationListener) + if (!context.hasNetworkLocationServiceBuiltIn() && LocationManagerCompat.hasProvider(locationManager, SystemLocationManager.NETWORK_PROVIDER)) { + boundToSystemNetworkLocation = true + locationManager.requestSystemProviderUpdates(SystemLocationManager.NETWORK_PROVIDER, networkInterval, networkLocationListener) + } + } + + private fun SystemLocationManager.requestSystemProviderUpdates(provider: String, interval: Long, listener: LocationListenerCompat) { + try { + if (interval != Long.MAX_VALUE) { + LocationManagerCompat.requestLocationUpdates(this, provider, LocationRequestCompat.Builder(interval).build(), listener, context.mainLooper) + } else { + LocationManagerCompat.requestLocationUpdates(this, provider, LocationRequestCompat.Builder(LocationRequestCompat.PASSIVE_INTERVAL).setMinUpdateIntervalMillis(MAX_FINE_UPDATE_INTERVAL).build(), listener, context.mainLooper) } + } catch (e: SecurityException) { + // Ignore } } - fun updateCoarseLocation(location: Location) { + fun updateNetworkLocation(location: Location) { val lastLocation = lastLocationCapsule.getLocation(GRANULARITY_FINE, Long.MAX_VALUE) - if (lastLocation == null || lastLocation.accuracy > location.accuracy || lastLocation.elapsedMillis + UPDATE_CLIFF_MS < location.elapsedMillis) { + + // Ignore outdated location + if (lastLocation != null && location.elapsedMillis + UPDATE_CLIFF_MS < lastLocation.elapsedMillis) return + + if (lastLocation == null || + lastLocation.accuracy > location.accuracy || + lastLocation.elapsedMillis + min(requestManager.intervalMillis * 2, UPDATE_CLIFF_MS) < location.elapsedMillis || + lastLocation.accuracy + ((location.elapsedMillis - lastLocation.elapsedMillis) / 1000.0) > location.accuracy + ) { lastLocationCapsule.updateCoarseLocation(location) - sendNewLocation(location) + sendNewLocation() } } - fun updateFineLocation(location: Location) { + fun updateGpsLocation(location: Location) { lastLocationCapsule.updateFineLocation(location) - sendNewLocation(location) + sendNewLocation() } - fun sendNewLocation(location: Location) { + fun sendNewLocation() { lifecycleScope.launchWhenStarted { - requestManager.processNewLocation(location) + requestManager.processNewLocation(lastLocationCapsule) } - deviceOrientationManager.onLocationChanged(location) + lastLocationCapsule.getLocation(GRANULARITY_FINE, Long.MAX_VALUE)?.let { deviceOrientationManager.onLocationChanged(it) } } fun dump(writer: PrintWriter) { writer.println("Location availability: ${lastLocationCapsule.locationAvailability}") - writer.println( - "Last coarse location: ${ - postProcessor.process( - lastLocationCapsule.getLocation(GRANULARITY_COARSE, Long.MAX_VALUE), - GRANULARITY_COARSE, - true - ) - }" - ) - writer.println( - "Last fine location: ${ - postProcessor.process( - lastLocationCapsule.getLocation(GRANULARITY_FINE, Long.MAX_VALUE), - GRANULARITY_FINE, - true - ) - }" - ) - if (requestManager.granularity > 0) { - requestManager.dump(writer) - } + writer.println("Last coarse location: ${postProcessor.process(lastLocationCapsule.getLocation(GRANULARITY_COARSE, Long.MAX_VALUE), GRANULARITY_COARSE, true)}") + writer.println("Last fine location: ${postProcessor.process(lastLocationCapsule.getLocation(GRANULARITY_FINE, Long.MAX_VALUE), GRANULARITY_FINE, true)}") + writer.println("Network location: built-in=${context.hasNetworkLocationServiceBuiltIn()} system=$boundToSystemNetworkLocation") + requestManager.dump(writer) deviceOrientationManager.dump(writer) } + fun handleCacheIntent(intent: Intent) { + when (IntentCacheManager.getType(intent)) { + LocationRequestManager.CACHE_TYPE -> { + requestManager.handleCacheIntent(intent) + } + + else -> { + Log.w(TAG, "Unknown cache intent: $intent") + } + } + } + companion object { const val MAX_COARSE_UPDATE_INTERVAL = 20_000L const val MAX_FINE_UPDATE_INTERVAL = 10_000L + const val EXTENSION_CLIFF_MS = 10_000L const val UPDATE_CLIFF_MS = 30_000L } } \ No newline at end of file diff --git a/play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/LocationManagerInstance.kt b/play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/LocationManagerInstance.kt index 26426f5b05..f9d8a08b5f 100644 --- a/play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/LocationManagerInstance.kt +++ b/play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/LocationManagerInstance.kt @@ -10,10 +10,13 @@ import android.app.PendingIntent import android.content.Context import android.content.pm.PackageManager.PERMISSION_GRANTED import android.location.Location +import android.location.LocationManager.GPS_PROVIDER +import android.location.LocationManager.NETWORK_PROVIDER import android.os.IBinder import android.os.Parcel import android.os.SystemClock import android.util.Log +import androidx.core.content.getSystemService import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope @@ -151,8 +154,17 @@ class LocationManagerInstance( override fun requestLocationSettingsDialog(settingsRequest: LocationSettingsRequest?, callback: ISettingsCallbacks?, packageName: String?) { Log.d(TAG, "requestLocationSettingsDialog by ${getClientIdentity().packageName}") - Log.d(TAG, "Not yet implemented: requestLocationSettingsDialog") - callback?.onLocationSettingsResult(LocationSettingsResult(Status.SUCCESS)) + val clientIdentity = getClientIdentity() + lifecycleScope.launchWhenStarted { + val locationManager = context.getSystemService() + val gpsPresent = locationManager?.allProviders?.contains(GPS_PROVIDER) == true + val networkPresent = locationManager?.allProviders?.contains(NETWORK_PROVIDER) == true + val gpsUsable = gpsPresent && locationManager?.isProviderEnabled(GPS_PROVIDER) == true && + context.packageManager.checkPermission(ACCESS_FINE_LOCATION, clientIdentity.packageName) == PERMISSION_GRANTED + val networkUsable = networkPresent && locationManager?.isProviderEnabled(NETWORK_PROVIDER) == true && + context.packageManager.checkPermission(ACCESS_COARSE_LOCATION, clientIdentity.packageName) == PERMISSION_GRANTED + callback?.onLocationSettingsResult(LocationSettingsResult(LocationSettingsStates(gpsUsable, networkUsable, false, gpsPresent, networkPresent, true), Status.SUCCESS)) + } } // region Mock locations @@ -314,5 +326,5 @@ class LocationManagerInstance( override fun getLifecycle(): Lifecycle = lifecycle override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean = - warnOnTransactionIssues(code, reply, flags) { super.onTransact(code, data, reply, flags) } + warnOnTransactionIssues(code, reply, flags, TAG) { super.onTransact(code, data, reply, flags) } } \ No newline at end of file diff --git a/play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/LocationManagerService.kt b/play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/LocationManagerService.kt index a40d8c83cd..194cdbd711 100644 --- a/play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/LocationManagerService.kt +++ b/play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/LocationManagerService.kt @@ -9,7 +9,6 @@ import android.content.Intent import android.location.Location import android.os.Binder import android.os.Process -import com.google.android.gms.common.Feature import com.google.android.gms.common.api.CommonStatusCodes import com.google.android.gms.common.internal.ConnectionInfo import com.google.android.gms.common.internal.GetServiceRequest @@ -17,9 +16,8 @@ import com.google.android.gms.common.internal.IGmsCallbacks import org.microg.gms.BaseService import org.microg.gms.common.GmsService import org.microg.gms.common.PackageUtils -import org.microg.gms.location.FEATURES -import org.microg.gms.location.network.NetworkLocationService.Companion.ACTION_REPORT_LOCATION -import org.microg.gms.location.network.NetworkLocationService.Companion.EXTRA_LOCATION +import org.microg.gms.location.EXTRA_LOCATION +import org.microg.gms.utils.IntentCacheManager import java.io.FileDescriptor import java.io.PrintWriter @@ -32,9 +30,12 @@ class LocationManagerService : BaseService(TAG, GmsService.LOCATION_MANAGER) { if (Binder.getCallingUid() == Process.myUid() && intent?.action == ACTION_REPORT_LOCATION) { val location = intent.getParcelableExtra(EXTRA_LOCATION) if (location != null) { - locationManager.updateCoarseLocation(location) + locationManager.updateNetworkLocation(location) } } + if (intent != null && IntentCacheManager.isCache(intent)) { + locationManager.handleCacheIntent(intent) + } return super.onStartCommand(intent, flags, startId) } @@ -58,4 +59,8 @@ class LocationManagerService : BaseService(TAG, GmsService.LOCATION_MANAGER) { super.dump(fd, writer, args) locationManager.dump(writer) } + + companion object { + const val ACTION_REPORT_LOCATION = "org.microg.gms.location.manager.ACTION_REPORT_LOCATION" + } } \ No newline at end of file diff --git a/play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/LocationRequestManager.kt b/play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/LocationRequestManager.kt index 1b406fa1d3..39dcc603b9 100644 --- a/play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/LocationRequestManager.kt +++ b/play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/LocationRequestManager.kt @@ -9,33 +9,34 @@ import android.app.PendingIntent import android.content.Context import android.content.Intent import android.location.Location -import android.os.IBinder -import android.os.SystemClock -import android.os.WorkSource +import android.os.* import android.util.Log import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope -import com.google.android.gms.location.Granularity -import com.google.android.gms.location.Granularity.GRANULARITY_FINE -import com.google.android.gms.location.Granularity.GRANULARITY_PERMISSION_LEVEL -import com.google.android.gms.location.ILocationCallback -import com.google.android.gms.location.LocationRequest -import com.google.android.gms.location.LocationResult +import com.google.android.gms.location.* +import com.google.android.gms.location.Granularity.* +import com.google.android.gms.location.Priority.* import com.google.android.gms.location.internal.ClientIdentity import kotlinx.coroutines.delay import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import org.microg.gms.location.GranularityUtil +import org.microg.gms.location.PriorityUtil import org.microg.gms.location.elapsedMillis +import org.microg.gms.location.formatDuration +import org.microg.gms.utils.IntentCacheManager import org.microg.gms.utils.WorkSourceUtil import java.io.PrintWriter +import kotlin.math.max -class LocationRequestManager(private val context: Context, private val lifecycle: Lifecycle, private val postProcessor: LocationPostProcessor, private val requestDetailsUpdatedCallback: () -> Unit) : +class LocationRequestManager(private val context: Context, private val lifecycle: Lifecycle, private val postProcessor: LocationPostProcessor, private val database: LocationAppsDatabase = LocationAppsDatabase(context), private val requestDetailsUpdatedCallback: () -> Unit) : IBinder.DeathRecipient, LifecycleOwner { private val lock = Mutex() private val binderRequests = mutableMapOf() private val pendingIntentRequests = mutableMapOf() + private val cacheManager by lazy { IntentCacheManager.create(context, CACHE_TYPE) } + var priority: @Priority Int = PRIORITY_PASSIVE var granularity: @Granularity Int = GRANULARITY_PERMISSION_LEVEL private set var intervalMillis: Long = Long.MAX_VALUE @@ -43,7 +44,7 @@ class LocationRequestManager(private val context: Context, private val lifecycle var workSource = WorkSource() private set private var requestDetailsUpdated = false - private var checkingWhileFine = false + private var checkingWhileHighAccuracy = false override fun getLifecycle(): Lifecycle = lifecycle @@ -60,11 +61,16 @@ class LocationRequestManager(private val context: Context, private val lifecycle } } - suspend fun add(binder: IBinder, clientIdentity: ClientIdentity, callback: ILocationCallback, request: LocationRequest) { + suspend fun add(binder: IBinder, clientIdentity: ClientIdentity, callback: ILocationCallback, request: LocationRequest, lastLocationCapsule: LastLocationCapsule) { lock.withLock { val holder = LocationRequestHolder(context, clientIdentity, request, callback, null) try { - holder.start() + holder.start().also { + var effectiveGranularity = it.effectiveGranularity + if (effectiveGranularity == GRANULARITY_FINE && database.getForceCoarse(it.clientIdentity.packageName)) effectiveGranularity = GRANULARITY_COARSE + val lastLocation = lastLocationCapsule.getLocation(effectiveGranularity, request.maxUpdateAgeMillis) + if (lastLocation != null) it.processNewLocation(lastLocation) + } binderRequests[binder] = holder binder.linkToDeath(this, 0) } catch (e: Exception) { @@ -75,12 +81,17 @@ class LocationRequestManager(private val context: Context, private val lifecycle notifyRequestDetailsUpdated() } - suspend fun update(oldBinder: IBinder, binder: IBinder, clientIdentity: ClientIdentity, callback: ILocationCallback, request: LocationRequest) { + suspend fun update(oldBinder: IBinder, binder: IBinder, clientIdentity: ClientIdentity, callback: ILocationCallback, request: LocationRequest, lastLocationCapsule: LastLocationCapsule) { lock.withLock { oldBinder.unlinkToDeath(this, 0) val holder = binderRequests.remove(oldBinder) try { - val startedHolder = holder?.update(callback, request) ?: LocationRequestHolder(context, clientIdentity, request, callback, null).start() + val startedHolder = holder?.update(callback, request) ?: LocationRequestHolder(context, clientIdentity, request, callback, null).start().also { + var effectiveGranularity = it.effectiveGranularity + if (effectiveGranularity == GRANULARITY_FINE && database.getForceCoarse(it.clientIdentity.packageName)) effectiveGranularity = GRANULARITY_COARSE + val lastLocation = lastLocationCapsule.getLocation(effectiveGranularity, request.maxUpdateAgeMillis) + if (lastLocation != null) it.processNewLocation(lastLocation) + } binderRequests[binder] = startedHolder binder.linkToDeath(this, 0) } catch (e: Exception) { @@ -99,10 +110,16 @@ class LocationRequestManager(private val context: Context, private val lifecycle notifyRequestDetailsUpdated() } - suspend fun add(pendingIntent: PendingIntent, clientIdentity: ClientIdentity, request: LocationRequest) { + suspend fun add(pendingIntent: PendingIntent, clientIdentity: ClientIdentity, request: LocationRequest, lastLocationCapsule: LastLocationCapsule) { lock.withLock { try { - pendingIntentRequests[pendingIntent] = LocationRequestHolder(context, clientIdentity, request, null, pendingIntent).start() + pendingIntentRequests[pendingIntent] = LocationRequestHolder(context, clientIdentity, request, null, pendingIntent).start().also { + cacheManager.add(it.asParcelable()) { it.pendingIntent == pendingIntent } + var effectiveGranularity = it.effectiveGranularity + if (effectiveGranularity == GRANULARITY_FINE && database.getForceCoarse(it.clientIdentity.packageName)) effectiveGranularity = GRANULARITY_COARSE + val lastLocation = lastLocationCapsule.getLocation(effectiveGranularity, request.maxUpdateAgeMillis) + if (lastLocation != null) it.processNewLocation(lastLocation) + } } catch (e: Exception) { // Ignore } @@ -113,33 +130,45 @@ class LocationRequestManager(private val context: Context, private val lifecycle suspend fun remove(pendingIntent: PendingIntent) { lock.withLock { + cacheManager.removeIf { it.pendingIntent == pendingIntent } if (pendingIntentRequests.remove(pendingIntent) != null) recalculateRequests() } notifyRequestDetailsUpdated() } - private fun processNewLocation(location: Location, map: Map): Set { + private fun processNewLocation(lastLocationCapsule: LastLocationCapsule, map: Map): Pair, Set> { val toRemove = mutableSetOf() + val updated = mutableSetOf() for ((key, holder) in map) { try { - postProcessor.process(location, holder.effectiveGranularity, holder.clientIdentity.isGoogle(context))?.let { - holder.processNewLocation(it) + var effectiveGranularity = holder.effectiveGranularity + if (effectiveGranularity == GRANULARITY_FINE && database.getForceCoarse(holder.clientIdentity.packageName)) effectiveGranularity = GRANULARITY_COARSE + val location = lastLocationCapsule.getLocation(effectiveGranularity, holder.maxUpdateDelayMillis) + postProcessor.process(location, effectiveGranularity, holder.clientIdentity.isGoogle(context))?.let { + if (holder.processNewLocation(it)) { + database.noteAppLocation(holder.clientIdentity.packageName, it) + updated.add(key) + } } } catch (e: Exception) { - Log.w(TAG, "Exception while processing for ${holder.workSource}", e) + Log.w(TAG, "Exception while processing for ${holder.workSource}: ${e.message}") toRemove.add(key) } } - return toRemove + return toRemove to updated } - suspend fun processNewLocation(location: Location) { + suspend fun processNewLocation(lastLocationCapsule: LastLocationCapsule) { lock.withLock { - val pendingIntentsToRemove = processNewLocation(location, pendingIntentRequests) + val (pendingIntentsToRemove, pendingIntentsUpdated) = processNewLocation(lastLocationCapsule, pendingIntentRequests) for (pendingIntent in pendingIntentsToRemove) { + cacheManager.removeIf { it.pendingIntent == pendingIntent } pendingIntentRequests.remove(pendingIntent) } - val bindersToRemove = processNewLocation(location, binderRequests) + for (pendingIntent in pendingIntentsUpdated) { + cacheManager.add(pendingIntentRequests[pendingIntent]!!.asParcelable()) { it.pendingIntent == pendingIntent } + } + val (bindersToRemove, _) = processNewLocation(lastLocationCapsule, binderRequests) for (binder in bindersToRemove) { try { binderRequests[binder]?.cancel() @@ -158,13 +187,15 @@ class LocationRequestManager(private val context: Context, private val lifecycle private fun recalculateRequests() { val merged = binderRequests.values + pendingIntentRequests.values val newGranularity = merged.maxOfOrNull { it.effectiveGranularity } ?: GRANULARITY_PERMISSION_LEVEL + val newPriority = merged.minOfOrNull { it.effectivePriority } ?: PRIORITY_PASSIVE val newIntervalMillis = merged.minOfOrNull { it.intervalMillis } ?: Long.MAX_VALUE val newWorkSource = WorkSource() for (holder in merged) { newWorkSource.add(holder.workSource) } - if (newGranularity == GRANULARITY_FINE && granularity != GRANULARITY_FINE) lifecycleScope.launchWhenStarted { checkWhileFine() } - if (newGranularity != granularity || newIntervalMillis != intervalMillis || newWorkSource != workSource) { + if (newPriority == PRIORITY_HIGH_ACCURACY && priority != PRIORITY_HIGH_ACCURACY) lifecycleScope.launchWhenStarted { checkWhileHighAccuracy() } + if (newPriority != priority || newGranularity != granularity || newIntervalMillis != intervalMillis || newWorkSource != workSource) { + priority = newPriority granularity = newGranularity intervalMillis = newIntervalMillis workSource = newWorkSource @@ -184,6 +215,7 @@ class LocationRequestManager(private val context: Context, private val lifecycle } } for (pendingIntent in pendingIntentsToRemove) { + cacheManager.removeIf { it.pendingIntent == pendingIntent } pendingIntentRequests.remove(pendingIntent) } val bindersToRemove = mutableSetOf() @@ -210,14 +242,14 @@ class LocationRequestManager(private val context: Context, private val lifecycle notifyRequestDetailsUpdated() } - private suspend fun checkWhileFine() { - if (checkingWhileFine) return - checkingWhileFine = true - while (granularity == GRANULARITY_FINE) { + private suspend fun checkWhileHighAccuracy() { + if (checkingWhileHighAccuracy) return + checkingWhileHighAccuracy = true + while (priority == PRIORITY_HIGH_ACCURACY) { check() delay(1000) } - checkingWhileFine = false + checkingWhileHighAccuracy = false } private fun notifyRequestDetailsUpdated() { @@ -238,13 +270,69 @@ class LocationRequestManager(private val context: Context, private val lifecycle } fun dump(writer: PrintWriter) { - writer.println("Current location request (${GranularityUtil.granularityToString(granularity)}, ${intervalMillis}ms from ${workSource})") + writer.println("Request cache: id=${cacheManager.getId()} size=${cacheManager.getEntries().size}") + writer.println("Current location request (${GranularityUtil.granularityToString(granularity)}, ${PriorityUtil.priorityToString(priority)}, ${intervalMillis.formatDuration()} from ${workSource})") for (request in binderRequests.values.toList()) { - writer.println("- ${request.workSource} ${request.intervalMillis}ms ${GranularityUtil.granularityToString(request.effectiveGranularity)} (pending: ${request.updatesPending} ${request.timePendingMillis}ms)") + writer.println("- bound ${request.workSource} ${request.intervalMillis.formatDuration()} ${GranularityUtil.granularityToString(request.effectiveGranularity)}, ${PriorityUtil.priorityToString(request.effectivePriority)} (pending: ${request.updatesPending.let { if (it == Int.MAX_VALUE) "\u221e" else "$it" }} ${request.timePendingMillis.formatDuration()})") + } + for (request in pendingIntentRequests.values.toList()) { + writer.println("- pending intent ${request.workSource} ${request.intervalMillis.formatDuration()} ${GranularityUtil.granularityToString(request.effectiveGranularity)}, ${PriorityUtil.priorityToString(request.effectivePriority)} (pending: ${request.updatesPending.let { if (it == Int.MAX_VALUE) "\u221e" else "$it" }} ${request.timePendingMillis.formatDuration()})") } } + fun handleCacheIntent(intent: Intent) { + cacheManager.processIntent(intent) + for (parcelable in cacheManager.getEntries()) { + pendingIntentRequests[parcelable.pendingIntent] = LocationRequestHolder(context, parcelable) + } + recalculateRequests() + notifyRequestDetailsUpdated() + } + companion object { + const val CACHE_TYPE = 1 + + private class LocationRequestHolderParcelable( + val clientIdentity: ClientIdentity, + val request: LocationRequest, + val pendingIntent: PendingIntent, + val start: Long, + val updates: Int, + val lastLocation: Location? + ) : Parcelable { + constructor(parcel: Parcel) : this( + parcel.readParcelable(ClientIdentity::class.java.classLoader)!!, + parcel.readParcelable(LocationRequest::class.java.classLoader)!!, + parcel.readParcelable(PendingIntent::class.java.classLoader)!!, + parcel.readLong(), + parcel.readInt(), + parcel.readParcelable(Location::class.java.classLoader) + ) + + override fun writeToParcel(parcel: Parcel, flags: Int) { + parcel.writeParcelable(clientIdentity, flags) + parcel.writeParcelable(request, flags) + parcel.writeParcelable(pendingIntent, flags) + parcel.writeLong(start) + parcel.writeInt(updates) + parcel.writeParcelable(lastLocation, flags) + } + + override fun describeContents(): Int { + return 0 + } + + companion object CREATOR : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): LocationRequestHolderParcelable { + return LocationRequestHolderParcelable(parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } + } + private class LocationRequestHolder( private val context: Context, val clientIdentity: ClientIdentity, @@ -256,10 +344,27 @@ class LocationRequestManager(private val context: Context, private val lifecycle private var updates = 0 private var lastLocation: Location? = null + constructor(context: Context, parcelable: LocationRequestHolderParcelable) : this(context, parcelable.clientIdentity, parcelable.request, null, parcelable.pendingIntent) { + start = parcelable.start + updates = parcelable.updates + lastLocation = parcelable.lastLocation + } + + fun asParcelable() = LocationRequestHolderParcelable(clientIdentity, request, pendingIntent!!, start, updates, lastLocation) + val permissionGranularity: @Granularity Int get() = context.granularityFromPermission(clientIdentity) val effectiveGranularity: @Granularity Int get() = getEffectiveGranularity(request.granularity, permissionGranularity) + val effectivePriority: @Priority Int + get() { + if (request.priority == PRIORITY_HIGH_ACCURACY && permissionGranularity < GRANULARITY_FINE) { + return PRIORITY_BALANCED_POWER_ACCURACY + } + return request.priority + } + val maxUpdateDelayMillis: Long + get() = max(request.maxUpdateDelayMillis, intervalMillis) val intervalMillis: Long get() = request.intervalMillis val updatesPending: Int @@ -268,28 +373,40 @@ class LocationRequestManager(private val context: Context, private val lifecycle get() = request.durationMillis - (SystemClock.elapsedRealtime() - start) var workSource: WorkSource = WorkSource(request.workSource).also { WorkSourceUtil.add(it, clientIdentity.uid, clientIdentity.packageName) } private set + val shouldPersistOp: Boolean + get() = request.intervalMillis < 60000 || effectivePriority == PRIORITY_HIGH_ACCURACY fun update(callback: ILocationCallback, request: LocationRequest): LocationRequestHolder { val changedGranularity = request.granularity != this.request.granularity - if (changedGranularity) context.finishAppOpForEffectiveGranularity(clientIdentity, effectiveGranularity) + if (changedGranularity && shouldPersistOp) context.finishAppOpForEffectiveGranularity(clientIdentity, effectiveGranularity) this.callback = callback this.request = request this.start = SystemClock.elapsedRealtime() this.updates = 0 this.workSource = WorkSource(request.workSource).also { WorkSourceUtil.add(it, clientIdentity.uid, clientIdentity.packageName) } - if (changedGranularity && !context.startAppOpForEffectiveGranularity(clientIdentity, effectiveGranularity)) throw RuntimeException("Lack of permission") + if (changedGranularity) { + if (shouldPersistOp) { + if (!context.startAppOpForEffectiveGranularity(clientIdentity, effectiveGranularity)) throw RuntimeException("Lack of permission") + } else { + if (!context.checkAppOpForEffectiveGranularity(clientIdentity, effectiveGranularity)) throw RuntimeException("Lack of permission") + } + } return this } fun start(): LocationRequestHolder { - if (!context.startAppOpForEffectiveGranularity(clientIdentity, effectiveGranularity)) throw RuntimeException("Lack of permission") + if (shouldPersistOp) { + if (!context.startAppOpForEffectiveGranularity(clientIdentity, effectiveGranularity)) throw RuntimeException("Lack of permission") + } else { + if (!context.checkAppOpForEffectiveGranularity(clientIdentity, effectiveGranularity)) throw RuntimeException("Lack of permission") + } // TODO: Register app op watch return this } fun cancel() { try { - context.finishAppOpForEffectiveGranularity(clientIdentity, effectiveGranularity) + if (shouldPersistOp) context.finishAppOpForEffectiveGranularity(clientIdentity, effectiveGranularity) callback?.cancel() } catch (e: Exception) { Log.w(TAG, e) @@ -298,33 +415,33 @@ class LocationRequestManager(private val context: Context, private val lifecycle fun check() { if (!context.checkAppOpForEffectiveGranularity(clientIdentity, effectiveGranularity)) throw RuntimeException("Lack of permission") - if (timePendingMillis < 0) throw RuntimeException("duration limit reached (active for ${SystemClock.elapsedRealtime() - start}ms, duration ${request.durationMillis}ms)") + if (timePendingMillis < 0) throw RuntimeException("duration limit reached (active for ${(SystemClock.elapsedRealtime() - start).formatDuration()}, duration ${request.durationMillis.formatDuration()})") if (updatesPending <= 0) throw RuntimeException("max updates reached") if (callback?.asBinder()?.isBinderAlive == false) throw RuntimeException("Binder died") } - fun processNewLocation(location: Location) { + fun processNewLocation(location: Location): Boolean { check() - if (lastLocation != null && location.elapsedMillis - lastLocation!!.elapsedMillis < request.minUpdateIntervalMillis) return - if (lastLocation != null && location.distanceTo(lastLocation!!) < request.minUpdateDistanceMeters) return - if (lastLocation == location) return + if (lastLocation != null && location.elapsedMillis - lastLocation!!.elapsedMillis < request.minUpdateIntervalMillis) return false + if (lastLocation != null && location.distanceTo(lastLocation!!) < request.minUpdateDistanceMeters) return false + if (lastLocation == location) return false val returnedLocation = if (effectiveGranularity > permissionGranularity) { throw RuntimeException("lack of permission") } else { if (!context.noteAppOpForEffectiveGranularity(clientIdentity, effectiveGranularity)) { throw RuntimeException("app op denied") } else if (clientIdentity.isSelfProcess()) { - // When the request is coming from us, we want to make sure to return a new object to not accidentally modify the internal state Location(location) } else { - location + Location(location).apply { provider = "fused" } } } val result = LocationResult.create(listOf(returnedLocation)) callback?.onLocationResult(result) pendingIntent?.send(context, 0, Intent().apply { putExtra(LocationResult.EXTRA_LOCATION_RESULT, result) }) - updates++ + if (request.maxUpdates != Int.MAX_VALUE) updates++ check() + return true } init { diff --git a/play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/extensions.kt b/play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/extensions.kt index c790141684..9780ee17bb 100644 --- a/play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/extensions.kt +++ b/play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/extensions.kt @@ -10,7 +10,6 @@ import android.app.AppOpsManager import android.content.Context import android.content.pm.PackageManager import android.os.Binder -import android.os.Build import android.os.Build.VERSION.SDK_INT import android.os.Process import android.os.WorkSource @@ -18,6 +17,7 @@ import android.util.Log import androidx.annotation.RequiresApi import androidx.core.app.AppOpsManagerCompat import androidx.core.content.getSystemService +import com.google.android.gms.common.Feature import com.google.android.gms.location.* import com.google.android.gms.location.internal.ClientIdentity import com.google.android.gms.location.internal.IFusedLocationProviderCallback @@ -27,6 +27,25 @@ import org.microg.gms.utils.WorkSourceUtil const val TAG = "LocationManager" +internal val FEATURES = arrayOf( + Feature("name_ulr_private", 1), + Feature("driving_mode", 6), + Feature("name_sleep_segment_request", 1), + Feature("support_context_feature_id", 1), + Feature("get_current_location", 2), + Feature("get_last_activity_feature_id", 1), + Feature("get_last_location_with_request", 1), + Feature("set_mock_mode_with_callback", 1), + Feature("set_mock_location_with_callback", 1), + Feature("inject_location_with_callback", 1), + Feature("location_updates_with_callback", 1), + Feature("user_service_developer_features", 1), + Feature("user_service_location_accuracy", 1), + Feature("user_service_safety_and_emergency", 1), + + Feature("use_safe_parcelable_in_intents", 1) +) + fun ILocationListener.asCallback(): ILocationCallback { return object : ILocationCallback.Stub() { override fun onLocationResult(result: LocationResult) { diff --git a/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/LocationCacheDatabase.kt b/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/LocationCacheDatabase.kt deleted file mode 100644 index 73b80326be..0000000000 --- a/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/LocationCacheDatabase.kt +++ /dev/null @@ -1,206 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2023 microG Project Team - * SPDX-License-Identifier: Apache-2.0 - */ - -package org.microg.gms.location.network - -import android.content.ContentValues -import android.content.Context -import android.database.Cursor -import android.database.sqlite.SQLiteDatabase -import android.database.sqlite.SQLiteOpenHelper -import android.location.Location -import android.util.Log -import org.microg.gms.location.network.cell.CellDetails -import org.microg.gms.location.network.cell.isValid -import org.microg.gms.location.network.wifi.WifiDetails -import org.microg.gms.utils.toHexString -import java.nio.ByteBuffer - -internal class LocationCacheDatabase(context: Context?) : SQLiteOpenHelper(context, "geocache.db", null, 1) { - fun getCellLocation(cell: CellDetails): Location? { - readableDatabase.query( - TABLE_CELLS, - arrayOf(FIELD_LATITUDE, FIELD_LONGITUDE, FIELD_ACCURACY, FIELD_TIME, FIELD_PRECISION), - CELLS_SELECTION, - getCellSelectionArgs(cell), - null, - null, - null - ).use { cursor -> - if (cursor.moveToNext()) { - cursor.getLocation(MAX_CELL_AGE).let { return it } - } - } - readableDatabase.query(TABLE_CELLS_PRE, arrayOf(FIELD_TIME), CELLS_PRE_SELECTION, getCellPreSelectionArgs(cell), null, null, null).use { cursor -> - if (cursor.moveToNext()) { - if (cursor.getLong(1) > System.currentTimeMillis() - MAX_CELL_AGE) { - return NEGATIVE_CACHE_ENTRY - } - } - } - return null - } - - fun putCellLocation(cell: CellDetails, location: Location) { - if (!cell.isValid) return - val cv = ContentValues().apply { - put(FIELD_MCC, cell.mcc) - put(FIELD_MNC, cell.mnc) - put(FIELD_LAC_TAC, cell.lac ?: cell.tac ?: 0) - put(FIELD_TYPE, cell.type.ordinal) - put(FIELD_CID, cell.cid) - put(FIELD_PSC, cell.psc ?: 0) - putLocation(location) - } - writableDatabase.insertWithOnConflict(TABLE_CELLS, null, cv, SQLiteDatabase.CONFLICT_REPLACE) - } - - fun getWifiScanLocation(wifis: List): Location? { - val hash = wifis.hash() ?: return null - readableDatabase.query( - TABLE_WIFI_SCANS, - arrayOf(FIELD_LATITUDE, FIELD_LONGITUDE, FIELD_ACCURACY, FIELD_TIME, FIELD_PRECISION), - "$FIELD_SCAN_HASH = x'${hash.toHexString()}'", - arrayOf(), - null, - null, - null - ).use { cursor -> - if (cursor.moveToNext()) { - cursor.getLocation(MAX_WIFI_AGE).let { return it } - } - } - return null - } - - fun putWifiScanLocation(wifis: List, location: Location) { - val cv = ContentValues().apply { - put(FIELD_SCAN_HASH, wifis.hash()) - putLocation(location) - } - writableDatabase.insertWithOnConflict(TABLE_WIFI_SCANS, null, cv, SQLiteDatabase.CONFLICT_REPLACE) - } - - override fun onCreate(db: SQLiteDatabase) { - db.execSQL("DROP TABLE IF EXISTS $TABLE_CELLS;") - db.execSQL("DROP TABLE IF EXISTS $TABLE_CELLS_PRE;") - db.execSQL("DROP TABLE IF EXISTS $TABLE_WIFIS;") - db.execSQL("DROP TABLE IF EXISTS $TABLE_WIFI_SCANS;") - db.execSQL("CREATE TABLE $TABLE_CELLS($FIELD_MCC INTEGER NOT NULL, $FIELD_MNC INTEGER NOT NULL, $FIELD_TYPE INTEGER NOT NULL, $FIELD_LAC_TAC INTEGER NOT NULL, $FIELD_CID INTEGER NOT NULL, $FIELD_PSC INTEGER NOT NULL, $FIELD_LATITUDE REAL NOT NULL, $FIELD_LONGITUDE REAL NOT NULL, $FIELD_ACCURACY REAL NOT NULL, $FIELD_TIME INTEGER NOT NULL, $FIELD_PRECISION REAL NOT NULL);") - db.execSQL("CREATE TABLE $TABLE_CELLS_PRE($FIELD_MCC INTEGER NOT NULL, $FIELD_MNC INTEGER NOT NULL, $FIELD_TIME INTEGER NOT NULL);") - db.execSQL("CREATE TABLE $TABLE_WIFIS($FIELD_MAC BLOB, $FIELD_LATITUDE REAL NOT NULL, $FIELD_LONGITUDE REAL NOT NULL, $FIELD_ACCURACY REAL NOT NULL, $FIELD_TIME INTEGER NOT NULL, $FIELD_PRECISION REAL NOT NULL);") - db.execSQL("CREATE TABLE $TABLE_WIFI_SCANS($FIELD_SCAN_HASH BLOB, $FIELD_LATITUDE REAL NOT NULL, $FIELD_LONGITUDE REAL NOT NULL, $FIELD_ACCURACY REAL NOT NULL, $FIELD_TIME INTEGER NOT NULL, $FIELD_PRECISION REAL NOT NULL);") - db.execSQL("CREATE UNIQUE INDEX ${TABLE_CELLS}_index ON $TABLE_CELLS($FIELD_MCC, $FIELD_MNC, $FIELD_TYPE, $FIELD_LAC_TAC, $FIELD_CID, $FIELD_PSC);") - db.execSQL("CREATE UNIQUE INDEX ${TABLE_CELLS_PRE}_index ON $TABLE_CELLS_PRE($FIELD_MCC, $FIELD_MNC);") - db.execSQL("CREATE UNIQUE INDEX ${TABLE_WIFIS}_index ON $TABLE_WIFIS($FIELD_MAC);") - db.execSQL("CREATE UNIQUE INDEX ${TABLE_WIFI_SCANS}_index ON $TABLE_WIFI_SCANS($FIELD_SCAN_HASH);") - } - - override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { - onCreate(db) - } - - companion object { - private const val PROVIDER_CACHE = "cache" - val NEGATIVE_CACHE_ENTRY = Location(PROVIDER_CACHE) - private const val MAX_CELL_AGE = 1000L * 60 * 60 * 24 * 28 // 28 days - private const val MAX_WIFI_AGE = 1000L * 60 * 60 * 24 * 14 // 14 days - private const val TABLE_CELLS = "cells" - private const val TABLE_CELLS_PRE = "cells_pre" - private const val TABLE_WIFIS = "wifis" - private const val TABLE_WIFI_SCANS = "wifi_scans" - private const val FIELD_MCC = "mcc" - private const val FIELD_MNC = "mnc" - private const val FIELD_TYPE = "type" - private const val FIELD_LAC_TAC = "lac" - private const val FIELD_CID = "cid" - private const val FIELD_PSC = "psc" - private const val FIELD_LATITUDE = "lat" - private const val FIELD_LONGITUDE = "lon" - private const val FIELD_ACCURACY = "acc" - private const val FIELD_TIME = "time" - private const val FIELD_PRECISION = "prec" - private const val FIELD_MAC = "mac" - private const val FIELD_SCAN_HASH = "hash" - private const val CELLS_SELECTION = "$FIELD_MCC = ? AND $FIELD_MNC = ? AND $FIELD_TYPE = ? AND $FIELD_LAC_TAC = ? AND $FIELD_CID = ? AND $FIELD_PSC = ?" - private const val CELLS_PRE_SELECTION = "$FIELD_MCC = ? AND $FIELD_MNC = ?" - private fun getCellSelectionArgs(cell: CellDetails): Array { - return arrayOf( - cell.mcc.toString(), - cell.mnc.toString(), - cell.type.ordinal.toString(), - (cell.lac ?: cell.tac ?: 0).toString(), - cell.cid.toString(), - (cell.psc ?: 0).toString(), - ) - } - - private fun getCellPreSelectionArgs(cell: CellDetails): Array { - return arrayOf( - cell.mcc.toString(), - cell.mnc.toString() - ) - } - - private val WifiDetails.macClean: String - get() = macAddress.lowercase().replace(":", "") - - private fun List.hash(): ByteArray? { - val filtered = sortedBy { it.macClean } - .filter { it.timestamp == null || it.timestamp > System.currentTimeMillis() - 60000 } - .filter { it.signalStrength == null || it.signalStrength > -90 } - if (filtered.size < 3) return null - val maxTimestamp = maxOf { it.timestamp ?: 0L } - fun WifiDetails.hashBytes(): ByteArray { - val mac = macClean - return byteArrayOf( - mac.substring(0, 2).toInt(16).toByte(), - mac.substring(2, 4).toInt(16).toByte(), - mac.substring(4, 6).toInt(16).toByte(), - mac.substring(6, 8).toInt(16).toByte(), - mac.substring(8, 10).toInt(16).toByte(), - mac.substring(10, 12).toInt(16).toByte(), - ((maxTimestamp - (timestamp ?: 0L)) / (60 * 1000)).toByte(), // timestamp - ((signalStrength ?: 0) / 10).toByte() // signal strength - ) - } - - val buffer = ByteBuffer.allocate(filtered.size * 8) - for (wifi in filtered) { - buffer.put(wifi.hashBytes()) - } - return buffer.array().digest("SHA-256") - } - - private fun Cursor.getLocation(maxAge: Long): Location? { - if (getLong(3) > System.currentTimeMillis() - maxAge) { - if (getDouble(2) == 0.0) return NEGATIVE_CACHE_ENTRY - return Location(PROVIDER_CACHE).apply { - latitude = getDouble(0) - longitude = getDouble(1) - accuracy = getDouble(2).toFloat() - precision = getDouble(4) - } - } - return null - } - - private fun ContentValues.putLocation(location: Location) { - if (location != NEGATIVE_CACHE_ENTRY) { - put(FIELD_LATITUDE, location.latitude) - put(FIELD_LONGITUDE, location.longitude) - put(FIELD_ACCURACY, location.accuracy) - put(FIELD_TIME, location.time) - put(FIELD_PRECISION, location.precision) - } else { - put(FIELD_LATITUDE, 0.0) - put(FIELD_LONGITUDE, 0.0) - put(FIELD_ACCURACY, 0.0) - put(FIELD_TIME, System.currentTimeMillis()) - put(FIELD_PRECISION, 0.0) - } - } - } -} \ No newline at end of file diff --git a/play-services-location/core/src/main/kotlin/org/microg/gms/location/reporting/ReportingAndroidService.kt b/play-services-location/core/src/main/kotlin/org/microg/gms/location/reporting/ReportingAndroidService.kt index 68f260875b..2ee0d84ee2 100644 --- a/play-services-location/core/src/main/kotlin/org/microg/gms/location/reporting/ReportingAndroidService.kt +++ b/play-services-location/core/src/main/kotlin/org/microg/gms/location/reporting/ReportingAndroidService.kt @@ -12,7 +12,7 @@ import com.google.android.gms.common.internal.IGmsCallbacks import org.microg.gms.BaseService import org.microg.gms.common.GmsService import org.microg.gms.common.PackageUtils -import org.microg.gms.location.FEATURES +import org.microg.gms.location.manager.FEATURES class ReportingAndroidService : BaseService("GmsLocReportingSvc", GmsService.LOCATION_REPORTING) { @Throws(RemoteException::class) diff --git a/play-services-location/core/src/main/kotlin/org/microg/gms/location/reporting/ReportingServiceInstance.kt b/play-services-location/core/src/main/kotlin/org/microg/gms/location/reporting/ReportingServiceInstance.kt index df633590da..e2590f5967 100644 --- a/play-services-location/core/src/main/kotlin/org/microg/gms/location/reporting/ReportingServiceInstance.kt +++ b/play-services-location/core/src/main/kotlin/org/microg/gms/location/reporting/ReportingServiceInstance.kt @@ -60,5 +60,5 @@ class ReportingServiceInstance(private val context: Context, private val package } override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean = - warnOnTransactionIssues(code, reply, flags) { super.onTransact(code, data, reply, flags) } + warnOnTransactionIssues(code, reply, flags, TAG) { super.onTransact(code, data, reply, flags) } } diff --git a/play-services-location/core/src/main/kotlin/org/microg/gms/location/ui/LocationAllAppsFragment.kt b/play-services-location/core/src/main/kotlin/org/microg/gms/location/ui/LocationAllAppsFragment.kt new file mode 100644 index 0000000000..c47192ea2a --- /dev/null +++ b/play-services-location/core/src/main/kotlin/org/microg/gms/location/ui/LocationAllAppsFragment.kt @@ -0,0 +1,86 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.location.ui + +import android.os.Bundle +import android.text.format.DateUtils +import androidx.appcompat.content.res.AppCompatResources +import androidx.core.os.bundleOf +import androidx.lifecycle.lifecycleScope +import androidx.navigation.fragment.findNavController +import androidx.preference.Preference +import androidx.preference.PreferenceCategory +import androidx.preference.PreferenceFragmentCompat +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.microg.gms.location.manager.LocationAppsDatabase +import org.microg.gms.ui.AppIconPreference +import org.microg.gms.ui.getApplicationInfoIfExists +import org.microg.gms.ui.navigate +import org.microg.gms.location.core.R + +class LocationAllAppsFragment : PreferenceFragmentCompat() { + private lateinit var progress: Preference + private lateinit var locationApps: PreferenceCategory + private lateinit var database: LocationAppsDatabase + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + database = LocationAppsDatabase(requireContext()) + } + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + addPreferencesFromResource(R.xml.preferences_location_all_apps) + progress = preferenceScreen.findPreference("pref_location_apps_all_progress") ?: progress + locationApps = preferenceScreen.findPreference("prefcat_location_apps") ?: locationApps + } + + override fun onResume() { + super.onResume() + updateContent() + } + + override fun onPause() { + super.onPause() + database.close() + } + + + private fun updateContent() { + lifecycleScope.launchWhenResumed { + val context = requireContext() + val apps = withContext(Dispatchers.IO) { + val res = database.listAppsByAccessTime().map { app -> + app to context.packageManager.getApplicationInfoIfExists(app.first) + }.map { (app, applicationInfo) -> + val pref = AppIconPreference(context) + pref.title = applicationInfo?.loadLabel(context.packageManager) ?: app.first + pref.summary = getString(R.string.location_app_last_access_at, DateUtils.getRelativeTimeSpanString(app.second)) + pref.icon = applicationInfo?.loadIcon(context.packageManager) ?: AppCompatResources.getDrawable(context, android.R.mipmap.sym_def_app_icon) + pref.onPreferenceClickListener = Preference.OnPreferenceClickListener { + findNavController().navigate(requireContext(), R.id.openLocationAppDetailsFromAll, bundleOf("package" to app.first)) + true + } + pref.key = "pref_location_app_" + app.first + pref + }.sortedBy { + it.title.toString().toLowerCase() + }.mapIndexed { idx, pair -> + pair.order = idx + pair + } + database.close() + res + } + locationApps.removeAll() + locationApps.isVisible = true + for (app in apps) { + locationApps.addPreference(app) + } + progress.isVisible = false + } + } +} \ No newline at end of file diff --git a/play-services-location/core/src/main/kotlin/org/microg/gms/location/ui/LocationAppFragment.kt b/play-services-location/core/src/main/kotlin/org/microg/gms/location/ui/LocationAppFragment.kt new file mode 100644 index 0000000000..f9390f8033 --- /dev/null +++ b/play-services-location/core/src/main/kotlin/org/microg/gms/location/ui/LocationAppFragment.kt @@ -0,0 +1,127 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.location.ui + +import android.annotation.SuppressLint +import android.content.Intent +import android.location.Geocoder +import android.net.Uri +import android.os.Build.VERSION.SDK_INT +import android.os.Bundle +import android.text.format.DateUtils +import android.util.Log +import androidx.lifecycle.lifecycleScope +import androidx.preference.Preference +import androidx.preference.PreferenceCategory +import androidx.preference.PreferenceFragmentCompat +import androidx.preference.TwoStatePreference +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.microg.gms.location.core.R +import org.microg.gms.location.manager.LocationAppsDatabase +import org.microg.gms.ui.AppHeadingPreference +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlin.coroutines.suspendCoroutine + +class LocationAppFragment : PreferenceFragmentCompat() { + private lateinit var appHeadingPreference: AppHeadingPreference + private lateinit var lastLocationCategory: PreferenceCategory + private lateinit var lastLocation: Preference + private lateinit var lastLocationMap: LocationMapPreference + private lateinit var forceCoarse: TwoStatePreference + private lateinit var database: LocationAppsDatabase + + private val packageName: String? + get() = arguments?.getString("package") + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + database = LocationAppsDatabase(requireContext()) + } + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + addPreferencesFromResource(R.xml.preferences_location_app_details) + } + + @SuppressLint("RestrictedApi") + override fun onBindPreferences() { + appHeadingPreference = preferenceScreen.findPreference("pref_location_app_heading") ?: appHeadingPreference + lastLocationCategory = preferenceScreen.findPreference("prefcat_location_app_last_location") ?: lastLocationCategory + lastLocation = preferenceScreen.findPreference("pref_location_app_last_location") ?: lastLocation + lastLocationMap = preferenceScreen.findPreference("pref_location_app_last_location_map") ?: lastLocationMap + forceCoarse = preferenceScreen.findPreference("pref_location_app_force_coarse") ?: forceCoarse + forceCoarse.setOnPreferenceChangeListener { _, newValue -> + packageName?.let { database.setForceCoarse(it, newValue as Boolean); true} == true + } + } + + override fun onResume() { + super.onResume() + updateContent() + } + + override fun onPause() { + super.onPause() + database.close() + } + + fun Double.toStringWithDigits(digits: Int): String { + val s = this.toString() + val i = s.indexOf('.') + if (i <= 0 || s.length - i - 1 < digits) return s + if (digits == 0) return s.substring(0, i) + return s.substring(0, s.indexOf('.') + digits + 1) + } + + fun updateContent() { + val context = requireContext() + lifecycleScope.launchWhenResumed { + appHeadingPreference.packageName = packageName + forceCoarse.isChecked = packageName?.let { database.getForceCoarse(it) } == true + val location = packageName?.let { database.getAppLocation(it) } + if (location != null) { + lastLocationCategory.isVisible = true + lastLocation.title = DateUtils.getRelativeTimeSpanString(location.time) + lastLocation.intent = Intent(Intent.ACTION_VIEW, Uri.parse("geo:${location.latitude},${location.longitude}")) + lastLocationMap.location = location + val address = try { + if (SDK_INT > 33) { + suspendCoroutine { continuation -> + try { + Geocoder(context).getFromLocation(location.latitude, location.longitude, 1) { + continuation.resume(it.firstOrNull()) + } + } catch (e: Exception) { + continuation.resumeWithException(e) + } + } + } else { + withContext(Dispatchers.IO) { Geocoder(context).getFromLocation(location.latitude, location.longitude, 1)?.firstOrNull() } + } + } catch (e: Exception) { + Log.w(TAG, e) + null + } + if (address != null) { + val addressLine = StringBuilder() + var i = 0 + addressLine.append(address.getAddressLine(i)) + while (addressLine.length < 32 && address.maxAddressLineIndex > i) { + i++ + addressLine.append(", ") + addressLine.append(address.getAddressLine(i)) + } + lastLocation.summary = addressLine.toString() + } else { + lastLocation.summary = "${location.latitude.toStringWithDigits(6)}, ${location.longitude.toStringWithDigits(6)}" + } + } else { + lastLocationCategory.isVisible = false + } + } + } +} \ No newline at end of file diff --git a/play-services-location/core/src/main/kotlin/org/microg/gms/location/ui/LocationMapPreference.kt b/play-services-location/core/src/main/kotlin/org/microg/gms/location/ui/LocationMapPreference.kt new file mode 100644 index 0000000000..a7745c022f --- /dev/null +++ b/play-services-location/core/src/main/kotlin/org/microg/gms/location/ui/LocationMapPreference.kt @@ -0,0 +1,110 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.location.ui + +import android.content.Context +import android.location.Location +import android.util.AttributeSet +import android.util.Log +import android.view.View +import android.view.ViewGroup +import android.view.ViewGroup.LayoutParams +import android.widget.FrameLayout +import androidx.preference.Preference +import androidx.preference.PreferenceViewHolder +import com.google.android.gms.maps.CameraUpdateFactory +import com.google.android.gms.maps.GoogleMapOptions +import com.google.android.gms.maps.MapView +import com.google.android.gms.maps.model.CameraPosition +import com.google.android.gms.maps.model.Circle +import com.google.android.gms.maps.model.CircleOptions +import com.google.android.gms.maps.model.LatLng +import org.microg.gms.location.core.R +import org.microg.gms.ui.resolveColor +import kotlin.math.log2 + +class LocationMapPreference : Preference { + constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) + constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) + constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) + constructor(context: Context) : super(context) + + init { + layoutResource = R.layout.preference_full_container + } + + var location: Location? = null + set(value) { + field = value + notifyChanged() + } + + private var mapView: View? = null + private var circle1: Any? = null + private var circle2: Any? = null + + override fun onBindViewHolder(holder: PreferenceViewHolder) { + super.onBindViewHolder(holder) + holder.isDividerAllowedAbove = false + holder.isDividerAllowedBelow = false + if (location != null) { + if (isAvailable) { + val latLng = LatLng(location!!.latitude, location!!.longitude) + val camera = CameraPosition.fromLatLngZoom(latLng, (21 - log2(location!!.accuracy)).coerceIn(2f, 22f)) + val container = holder.itemView as ViewGroup + if (mapView == null) { + val options = GoogleMapOptions().liteMode(true).scrollGesturesEnabled(false).zoomGesturesEnabled(false).camera(camera) + mapView = MapView(context, options) + mapView?.layoutParams = FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT, (height * context.resources.displayMetrics.density).toInt()) + container.addView(mapView) + (mapView as MapView).onCreate(null) + } else { + (mapView as MapView).getMapAsync { + it.moveCamera(CameraUpdateFactory.newCameraPosition(camera)) + } + } + (circle1 as? Circle?)?.remove() + (circle2 as? Circle?)?.remove() + (mapView as MapView).getMapAsync { + val strokeColor = (context.resolveColor(androidx.appcompat.R.attr.colorAccent) ?: 0xff009688L.toInt()) + val fillColor = strokeColor and 0x60ffffff + circle1 = it.addCircle(CircleOptions().center(latLng).radius(location!!.accuracy.toDouble()).fillColor(fillColor).strokeWidth(1f).strokeColor(strokeColor)) + circle2 = it.addCircle(CircleOptions().center(latLng).radius(location!!.accuracy.toDouble() * 2).fillColor(fillColor).strokeWidth(1f).strokeColor(strokeColor)) + } + } else { + Log.d(TAG, "MapView not available") + } + } else if (mapView != null) { + (mapView as MapView).onDestroy() + (mapView?.parent as? ViewGroup?)?.removeView(mapView) + circle1 = null + circle2 = null + mapView = null + } + } + + override fun onDetached() { + super.onDetached() + if (mapView != null) { + (mapView as MapView).onDestroy() + circle1 = null + circle2 = null + mapView = null + } + } + + companion object { + const val height = 200f + + val isAvailable: Boolean + get() = try { + Class.forName("com.google.android.gms.maps.MapView") + true + } catch (e: ClassNotFoundException) { + false + } + } +} \ No newline at end of file diff --git a/play-services-location/core/src/main/kotlin/org/microg/gms/location/ui/LocationPreferencesFragment.kt b/play-services-location/core/src/main/kotlin/org/microg/gms/location/ui/LocationPreferencesFragment.kt new file mode 100644 index 0000000000..99b02de86c --- /dev/null +++ b/play-services-location/core/src/main/kotlin/org/microg/gms/location/ui/LocationPreferencesFragment.kt @@ -0,0 +1,148 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.location.ui + +import android.annotation.SuppressLint +import android.os.Build.VERSION.SDK_INT +import android.os.Bundle +import androidx.core.os.bundleOf +import androidx.lifecycle.lifecycleScope +import androidx.navigation.fragment.findNavController +import androidx.preference.Preference +import androidx.preference.PreferenceCategory +import androidx.preference.PreferenceFragmentCompat +import androidx.preference.TwoStatePreference +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.microg.gms.location.LocationSettings +import org.microg.gms.location.core.R +import org.microg.gms.location.hasMozillaLocationServiceSupport +import org.microg.gms.location.hasNetworkLocationServiceBuiltIn +import org.microg.gms.location.manager.LocationAppsDatabase +import org.microg.gms.ui.AppIconPreference +import org.microg.gms.ui.getApplicationInfoIfExists +import org.microg.gms.ui.navigate + +class LocationPreferencesFragment : PreferenceFragmentCompat() { + private lateinit var locationApps: PreferenceCategory + private lateinit var locationAppsAll: Preference + private lateinit var locationAppsNone: Preference + private lateinit var networkProviderCategory: PreferenceCategory + private lateinit var wifiMls: TwoStatePreference + private lateinit var wifiMoving: TwoStatePreference + private lateinit var wifiLearning: TwoStatePreference + private lateinit var cellMls: TwoStatePreference + private lateinit var cellLearning: TwoStatePreference + private lateinit var nominatim: TwoStatePreference + private lateinit var database: LocationAppsDatabase + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + database = LocationAppsDatabase(requireContext()) + } + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + addPreferencesFromResource(R.xml.preferences_location) + + locationApps = preferenceScreen.findPreference("prefcat_location_apps") ?: locationApps + locationAppsAll = preferenceScreen.findPreference("pref_location_apps_all") ?: locationAppsAll + locationAppsNone = preferenceScreen.findPreference("pref_location_apps_none") ?: locationAppsNone + networkProviderCategory = preferenceScreen.findPreference("prefcat_location_network_provider") ?: networkProviderCategory + wifiMls = preferenceScreen.findPreference("pref_location_wifi_mls_enabled") ?: wifiMls + wifiMoving = preferenceScreen.findPreference("pref_location_wifi_moving_enabled") ?: wifiMoving + wifiLearning = preferenceScreen.findPreference("pref_location_wifi_learning_enabled") ?: wifiLearning + cellMls = preferenceScreen.findPreference("pref_location_cell_mls_enabled") ?: cellMls + cellLearning = preferenceScreen.findPreference("pref_location_cell_learning_enabled") ?: cellLearning + nominatim = preferenceScreen.findPreference("pref_geocoder_nominatim_enabled") ?: nominatim + + locationAppsAll.setOnPreferenceClickListener { + findNavController().navigate(requireContext(), R.id.openAllLocationApps) + true + } + wifiMls.setOnPreferenceChangeListener { _, newValue -> + LocationSettings(requireContext()).wifiMls = newValue as Boolean + true + } + wifiMoving.setOnPreferenceChangeListener { _, newValue -> + LocationSettings(requireContext()).wifiMoving = newValue as Boolean + true + } + wifiLearning.setOnPreferenceChangeListener { _, newValue -> + LocationSettings(requireContext()).wifiLearning = newValue as Boolean + true + } + cellMls.setOnPreferenceChangeListener { _, newValue -> + LocationSettings(requireContext()).cellMls = newValue as Boolean + true + } + cellLearning.setOnPreferenceChangeListener { _, newValue -> + LocationSettings(requireContext()).cellLearning = newValue as Boolean + true + } + nominatim.setOnPreferenceChangeListener { _, newValue -> + LocationSettings(requireContext()).geocoderNominatim = newValue as Boolean + true + } + + networkProviderCategory.isVisible = requireContext().hasNetworkLocationServiceBuiltIn() + wifiMls.isVisible = requireContext().hasMozillaLocationServiceSupport() + cellMls.isVisible = requireContext().hasMozillaLocationServiceSupport() + wifiLearning.isVisible = SDK_INT >= 17 + cellLearning.isVisible = SDK_INT >= 17 + } + + override fun onResume() { + super.onResume() + updateContent() + } + + override fun onPause() { + super.onPause() + database.close() + } + + private fun updateContent() { + lifecycleScope.launchWhenResumed { + val context = requireContext() + wifiMls.isChecked = LocationSettings(context).wifiMls + wifiMoving.isChecked = LocationSettings(context).wifiMoving + wifiLearning.isChecked = LocationSettings(context).wifiLearning + cellMls.isChecked = LocationSettings(context).cellMls + cellLearning.isChecked = LocationSettings(context).cellLearning + nominatim.isChecked = LocationSettings(context).geocoderNominatim + val (apps, showAll) = withContext(Dispatchers.IO) { + val apps = database.listAppsByAccessTime() + val res = apps.map { app -> + app to context.packageManager.getApplicationInfoIfExists(app.first) + }.mapNotNull { (app, info) -> + if (info == null) null else app to info + }.take(3).mapIndexed { idx, (app, applicationInfo) -> + val pref = AppIconPreference(context) + pref.order = idx + pref.applicationInfo = applicationInfo + pref.onPreferenceClickListener = Preference.OnPreferenceClickListener { + findNavController().navigate(requireContext(), R.id.openLocationAppDetails, bundleOf("package" to app.first)) + true + } + pref.key = "pref_location_app_" + app.first + pref + }.let { it to (it.size < apps.size) } + database.close() + res + } + locationAppsAll.isVisible = showAll + locationApps.removeAll() + for (app in apps) { + locationApps.addPreference(app) + } + if (showAll) { + locationApps.addPreference(locationAppsAll) + } else if (apps.isEmpty()) { + locationApps.addPreference(locationAppsNone) + } + } + } +} \ No newline at end of file diff --git a/play-services-location/core/src/main/kotlin/org/microg/gms/location/ui/extensions.kt b/play-services-location/core/src/main/kotlin/org/microg/gms/location/ui/extensions.kt new file mode 100644 index 0000000000..c771c06497 --- /dev/null +++ b/play-services-location/core/src/main/kotlin/org/microg/gms/location/ui/extensions.kt @@ -0,0 +1,9 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.location.ui + +const val TAG = "LocationUi" + diff --git a/play-services-location/core/src/main/res/layout/preference_full_container.xml b/play-services-location/core/src/main/res/layout/preference_full_container.xml new file mode 100644 index 0000000000..264dd65b4e --- /dev/null +++ b/play-services-location/core/src/main/res/layout/preference_full_container.xml @@ -0,0 +1,12 @@ + + + + + \ No newline at end of file diff --git a/play-services-location/core/src/main/res/navigation/nav_location.xml b/play-services-location/core/src/main/res/navigation/nav_location.xml new file mode 100644 index 0000000000..4e50df0fa6 --- /dev/null +++ b/play-services-location/core/src/main/res/navigation/nav_location.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/play-services-location/core/src/main/res/values/strings.xml b/play-services-location/core/src/main/res/values/strings.xml new file mode 100644 index 0000000000..c228158735 --- /dev/null +++ b/play-services-location/core/src/main/res/values/strings.xml @@ -0,0 +1,30 @@ + + + + Location + Recent access + Wi-Fi location + Mobile network location + Address resolver + Request from Mozilla + Fetch Wi-Fi-based location from Mozilla Location Service. + Request from Hotspot + Fetch Wi-Fi location directly from supported hotspots when connected. + Remember from GPS + Store Wi-Fi locations locally when GPS is used. + Request from Mozilla + Fetch mobile network cell tower locations from Mozilla Location Service. + Remember from GPS + Store mobile network locations locally when GPS is used. + Use Nominatim + Resolve addresses using OpenStreetMap Nominatim. + + Apps with location access + Last access: %1$s + + Force coarse location + Always return coarse locations to this app, ignoring its permission level. + \ No newline at end of file diff --git a/play-services-location/core/src/main/res/xml/preferences_location.xml b/play-services-location/core/src/main/res/xml/preferences_location.xml new file mode 100644 index 0000000000..adaa849343 --- /dev/null +++ b/play-services-location/core/src/main/res/xml/preferences_location.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/play-services-location/core/src/main/res/xml/preferences_location_all_apps.xml b/play-services-location/core/src/main/res/xml/preferences_location_all_apps.xml new file mode 100644 index 0000000000..07b57e681d --- /dev/null +++ b/play-services-location/core/src/main/res/xml/preferences_location_all_apps.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/play-services-location/core/src/main/res/xml/preferences_location_app_details.xml b/play-services-location/core/src/main/res/xml/preferences_location_app_details.xml new file mode 100644 index 0000000000..daca94ce75 --- /dev/null +++ b/play-services-location/core/src/main/res/xml/preferences_location_app_details.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/play-services-location/system-api/build.gradle b/play-services-location/core/system-api/build.gradle similarity index 100% rename from play-services-location/system-api/build.gradle rename to play-services-location/core/system-api/build.gradle diff --git a/play-services-location/system-api/src/main/AndroidManifest.xml b/play-services-location/core/system-api/src/main/AndroidManifest.xml similarity index 100% rename from play-services-location/system-api/src/main/AndroidManifest.xml rename to play-services-location/core/system-api/src/main/AndroidManifest.xml diff --git a/play-services-location/system-api/src/main/java/android/location/GeocoderParams.java b/play-services-location/core/system-api/src/main/java/android/location/GeocoderParams.java similarity index 100% rename from play-services-location/system-api/src/main/java/android/location/GeocoderParams.java rename to play-services-location/core/system-api/src/main/java/android/location/GeocoderParams.java diff --git a/play-services-location/system-api/src/main/java/android/location/Geofence.java b/play-services-location/core/system-api/src/main/java/android/location/Geofence.java similarity index 100% rename from play-services-location/system-api/src/main/java/android/location/Geofence.java rename to play-services-location/core/system-api/src/main/java/android/location/Geofence.java diff --git a/play-services-location/system-api/src/main/java/android/location/Location.java b/play-services-location/core/system-api/src/main/java/android/location/Location.java similarity index 100% rename from play-services-location/system-api/src/main/java/android/location/Location.java rename to play-services-location/core/system-api/src/main/java/android/location/Location.java diff --git a/play-services-location/system-api/src/main/java/android/location/LocationManager.java b/play-services-location/core/system-api/src/main/java/android/location/LocationManager.java similarity index 100% rename from play-services-location/system-api/src/main/java/android/location/LocationManager.java rename to play-services-location/core/system-api/src/main/java/android/location/LocationManager.java diff --git a/play-services-location/system-api/src/main/java/android/location/LocationRequest.java b/play-services-location/core/system-api/src/main/java/android/location/LocationRequest.java similarity index 100% rename from play-services-location/system-api/src/main/java/android/location/LocationRequest.java rename to play-services-location/core/system-api/src/main/java/android/location/LocationRequest.java diff --git a/play-services-location/system-api/src/main/java/android/net/wifi/WifiScanner.java b/play-services-location/core/system-api/src/main/java/android/net/wifi/WifiScanner.java similarity index 100% rename from play-services-location/system-api/src/main/java/android/net/wifi/WifiScanner.java rename to play-services-location/core/system-api/src/main/java/android/net/wifi/WifiScanner.java diff --git a/play-services-location/system-api/src/main/java/com/android/internal/location/ProviderProperties.java b/play-services-location/core/system-api/src/main/java/com/android/internal/location/ProviderProperties.java similarity index 100% rename from play-services-location/system-api/src/main/java/com/android/internal/location/ProviderProperties.java rename to play-services-location/core/system-api/src/main/java/com/android/internal/location/ProviderProperties.java diff --git a/play-services-location/system-api/src/main/java/com/android/internal/location/ProviderRequest.java b/play-services-location/core/system-api/src/main/java/com/android/internal/location/ProviderRequest.java similarity index 100% rename from play-services-location/system-api/src/main/java/com/android/internal/location/ProviderRequest.java rename to play-services-location/core/system-api/src/main/java/com/android/internal/location/ProviderRequest.java diff --git a/play-services-location/system-api/src/main/java/com/android/location/provider/GeocodeProvider.java b/play-services-location/core/system-api/src/main/java/com/android/location/provider/GeocodeProvider.java similarity index 100% rename from play-services-location/system-api/src/main/java/com/android/location/provider/GeocodeProvider.java rename to play-services-location/core/system-api/src/main/java/com/android/location/provider/GeocodeProvider.java diff --git a/play-services-location/system-api/src/main/java/com/android/location/provider/LocationProvider.java b/play-services-location/core/system-api/src/main/java/com/android/location/provider/LocationProvider.java similarity index 100% rename from play-services-location/system-api/src/main/java/com/android/location/provider/LocationProvider.java rename to play-services-location/core/system-api/src/main/java/com/android/location/provider/LocationProvider.java diff --git a/play-services-location/system-api/src/main/java/com/android/location/provider/LocationProviderBase.java b/play-services-location/core/system-api/src/main/java/com/android/location/provider/LocationProviderBase.java similarity index 100% rename from play-services-location/system-api/src/main/java/com/android/location/provider/LocationProviderBase.java rename to play-services-location/core/system-api/src/main/java/com/android/location/provider/LocationProviderBase.java diff --git a/play-services-location/system-api/src/main/java/com/android/location/provider/LocationRequestUnbundled.java b/play-services-location/core/system-api/src/main/java/com/android/location/provider/LocationRequestUnbundled.java similarity index 100% rename from play-services-location/system-api/src/main/java/com/android/location/provider/LocationRequestUnbundled.java rename to play-services-location/core/system-api/src/main/java/com/android/location/provider/LocationRequestUnbundled.java diff --git a/play-services-location/system-api/src/main/java/com/android/location/provider/ProviderPropertiesUnbundled.java b/play-services-location/core/system-api/src/main/java/com/android/location/provider/ProviderPropertiesUnbundled.java similarity index 100% rename from play-services-location/system-api/src/main/java/com/android/location/provider/ProviderPropertiesUnbundled.java rename to play-services-location/core/system-api/src/main/java/com/android/location/provider/ProviderPropertiesUnbundled.java diff --git a/play-services-location/system-api/src/main/java/com/android/location/provider/ProviderRequestUnbundled.java b/play-services-location/core/system-api/src/main/java/com/android/location/provider/ProviderRequestUnbundled.java similarity index 100% rename from play-services-location/system-api/src/main/java/com/android/location/provider/ProviderRequestUnbundled.java rename to play-services-location/core/system-api/src/main/java/com/android/location/provider/ProviderRequestUnbundled.java diff --git a/play-services-location/src/main/java/com/google/android/gms/location/LocationSettingsStates.java b/play-services-location/src/main/java/com/google/android/gms/location/LocationSettingsStates.java index 194982b2b7..bc0dffd87f 100644 --- a/play-services-location/src/main/java/com/google/android/gms/location/LocationSettingsStates.java +++ b/play-services-location/src/main/java/com/google/android/gms/location/LocationSettingsStates.java @@ -1,23 +1,18 @@ /* - * Copyright (C) 2013-2017 microG Project Team - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * SPDX-FileCopyrightText: 2015 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.location; +import android.app.Activity; +import android.content.Intent; import org.microg.gms.common.PublicApi; import org.microg.safeparcel.AutoSafeParcelable; +import org.microg.safeparcel.SafeParcelUtil; import org.microg.safeparcel.SafeParceled; /** @@ -26,55 +21,83 @@ @PublicApi public class LocationSettingsStates extends AutoSafeParcelable { - @SafeParceled(1000) + @Field(1000) private int versionCode = 2; - @SafeParceled(1) + @Field(1) private boolean gpsUsable; - @SafeParceled(2) + @Field(2) private boolean networkLocationUsable; - @SafeParceled(3) + @Field(3) private boolean bleUsable; - @SafeParceled(4) + @Field(4) private boolean gpsPresent; - @SafeParceled(5) + @Field(5) private boolean networkLocationPresent; - @SafeParceled(6) + @Field(6) private boolean blePresent; + /** + * Whether BLE is present on the device. + */ public boolean isBlePresent() { return blePresent; } + /** + * Whether BLE is enabled and is usable by the app. + */ public boolean isBleUsable() { return bleUsable; } + /** + * Whether GPS provider is present on the device. + */ public boolean isGpsPresent() { return gpsPresent; } + /** + * Whether GPS provider is enabled and is usable by the app. + */ public boolean isGpsUsable() { return gpsUsable; } + /** + * Whether location is present on the device. + *

+ * This method returns true when either GPS or network location provider is present. + */ public boolean isLocationPresent() { return isGpsPresent() || isNetworkLocationPresent(); } + /** + * Whether location is enabled and is usable by the app. + *

+ * This method returns true when either GPS or network location provider is usable. + */ public boolean isLocationUsable() { return isGpsUsable() || isNetworkLocationUsable(); } + /** + * Whether network location provider is present on the device. + */ public boolean isNetworkLocationPresent() { return networkLocationPresent; } + /** + * Whether network location provider is enabled and usable by the app. + */ public boolean isNetworkLocationUsable() { return networkLocationUsable; } @@ -88,5 +111,17 @@ public LocationSettingsStates(boolean gpsUsable, boolean networkLocationUsable, this.blePresent = blePresent; } + /** + * Retrieves the location settings states from the intent extras. When the location settings dialog finishes, you can use this method to retrieve the + * current location settings states from the intent in your {@link Activity#onActivityResult(int, int, Intent)}; + */ + public static LocationSettingsStates fromIntent(Intent intent) { + byte[] bytes = intent.getByteArrayExtra(EXTRA_NAME); + if (bytes == null) return null; + return SafeParcelUtil.fromByteArray(bytes, CREATOR); + } + public static final Creator CREATOR = new AutoCreator(LocationSettingsStates.class); + + private static final String EXTRA_NAME = "com.google.android.gms.location.LOCATION_SETTINGS_STATES"; } diff --git a/play-services-maps-core-mapbox/build.gradle b/play-services-maps-core-mapbox/build.gradle index da676944aa..a0e48a4a0e 100644 --- a/play-services-maps-core-mapbox/build.gradle +++ b/play-services-maps-core-mapbox/build.gradle @@ -20,12 +20,13 @@ apply plugin: 'kotlin-android' dependencies { implementation project(':play-services-maps') implementation project(':play-services-base-core') - implementation("org.maplibre.gl:android-sdk:9.6.0") { - exclude group: 'com.google.android.gms' - } - implementation("org.maplibre.gl:android-plugin-annotation-v9:1.0.0") { + implementation project(':play-services-location') + implementation("org.maplibre.gl:android-sdk:10.2.0") + implementation("org.maplibre.gl:android-plugin-annotation-v9:2.0.0") { exclude group: 'com.google.android.gms' } + implementation 'org.maplibre.gl:android-sdk-turf:5.9.0' + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion" } @@ -38,16 +39,6 @@ def execResult(...args) { return stdout.toString().trim() } -def mapboxKey() { - Properties properties = new Properties() - try { - properties.load(project.rootProject.file('local.properties').newDataInputStream()) - } catch (ignored) { - // Ignore - } - return properties.getProperty("mapbox.key", "invalid") -} - android { compileSdkVersion androidCompileSdk buildToolsVersion "$androidBuildVersionTools" @@ -56,7 +47,7 @@ android { versionName version minSdkVersion androidMinSdk targetSdkVersion androidTargetSdk - buildConfigField "String", "MAPBOX_KEY", "\"${mapboxKey()}\"" + buildConfigField "String", "MAPBOX_KEY", "\"${localProperties.getProperty("mapbox.key", System.getenv('MAPBOX_VECTOR_TILES_KEY') ?: "")}\"" ndk { abiFilters "armeabi", "armeabi-v7a", "arm64-v8a", "x86", "x86_64" diff --git a/play-services-maps-core-mapbox/src/main/AndroidManifest.xml b/play-services-maps-core-mapbox/src/main/AndroidManifest.xml index eef9445961..0dfbdfe99c 100644 --- a/play-services-maps-core-mapbox/src/main/AndroidManifest.xml +++ b/play-services-maps-core-mapbox/src/main/AndroidManifest.xml @@ -29,6 +29,8 @@ + + diff --git a/play-services-maps-core-mapbox/src/main/assets/style-mapbox-outdoors-v12.json b/play-services-maps-core-mapbox/src/main/assets/style-mapbox-outdoors-v12.json new file mode 100644 index 0000000000..21c4d40f55 --- /dev/null +++ b/play-services-maps-core-mapbox/src/main/assets/style-mapbox-outdoors-v12.json @@ -0,0 +1,13845 @@ +{ + "name": "Mapbox Outdoors", + "sprite": "mapbox://sprites/mapbox/outdoors-v12", + "glyphs": "mapbox://fonts/mapbox/{fontstack}/{range}.pbf", + "center": [ + 9.1, + 42.2 + ], + "zoom": 7.5, + "fog": { + "range": [ + 1, + 20 + ], + "color": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 4, + "hsl(200, 100%, 100%)", + 6, + "hsl(200, 50%, 90%)" + ], + "high-color": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 4, + "hsl(200, 100%, 60%)", + 6, + "hsl(310, 60%, 80%)" + ], + "space-color": [ + "interpolate", + [ + "exponential", + 1.2 + ], + [ + "zoom" + ], + 4, + "hsl(205, 10%, 10%)", + 6, + "hsl(205, 60%, 50%)" + ], + "horizon-blend": [ + "interpolate", + [ + "exponential", + 1.2 + ], + [ + "zoom" + ], + 4, + 0.01, + 6, + 0.1 + ], + "star-intensity": [ + "interpolate", + [ + "exponential", + 1.2 + ], + [ + "zoom" + ], + 4, + 0.1, + 6, + 0 + ] + }, + "projection": { + "name": "globe" + }, + "visibility": "public", + "version": 8, + "layers": [ + { + "id": "land", + "type": "background", + "layout": {}, + "minzoom": 0, + "paint": { + "background-color": "hsl(60, 20%, 85%)" + }, + "metadata": { + "mapbox:featureComponent": "land-and-water", + "mapbox:group": "Land & water, land", + "microg:gms-type-feature": "landscape.natural.landcover", + "microg:gms-type-element": "geometry.fill" + } + }, + { + "id": "landcover", + "type": "fill", + "source": "composite", + "source-layer": "landcover", + "minzoom": 0, + "maxzoom": 12, + "layout": {}, + "paint": { + "fill-color": [ + "match", + [ + "get", + "class" + ], + "wood", + "hsla(103, 50%, 60%, 0.8)", + "scrub", + "hsla(98, 47%, 68%, 0.6)", + "crop", + "hsla(68, 55%, 70%, 0.6)", + "grass", + "hsla(98, 50%, 74%, 0.6)", + "snow", + "hsl(205, 45%, 95%)", + "hsl(98, 48%, 67%)" + ], + "fill-opacity": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 8, + 0.8, + 12, + 0 + ], + "fill-antialias": false + }, + "metadata": { + "mapbox:featureComponent": "land-and-water", + "mapbox:group": "Land & water, land", + "microg:gms-type-feature": "landscape.natural.landcover", + "microg:gms-type-element": "geometry.fill" + } + }, + { + "id": "national-park", + "type": "fill", + "source": "composite", + "source-layer": "landuse_overlay", + "minzoom": 5, + "filter": [ + "==", + [ + "get", + "class" + ], + "national_park" + ], + "layout": {}, + "paint": { + "fill-color": "hsl(98, 38%, 68%)", + "fill-opacity": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 5, + 0, + 6, + 0.6, + 12, + 0.2 + ] + }, + "metadata": { + "mapbox:featureComponent": "land-and-water", + "mapbox:group": "Land & water, land", + "microg:gms-type-feature": "landscape.natural.landcover", + "microg:gms-type-element": "geometry.fill" + } + }, + { + "id": "national-park_tint-band", + "type": "line", + "source": "composite", + "source-layer": "landuse_overlay", + "minzoom": 9, + "filter": [ + "==", + [ + "get", + "class" + ], + "national_park" + ], + "layout": {}, + "paint": { + "line-color": "hsl(98, 38%, 68%)", + "line-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 9, + 1, + 14, + 8 + ], + "line-blur": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 9, + 1, + 14, + 8 + ] + }, + "metadata": { + "mapbox:featureComponent": "land-and-water", + "mapbox:group": "Land & water, land", + "microg:gms-type-feature": "landscape.natural.landcover", + "microg:gms-type-element": "geometry.stroke" + } + }, + { + "id": "landuse", + "type": "fill", + "source": "composite", + "source-layer": "landuse", + "minzoom": 5, + "filter": [ + "all", + [ + ">=", + [ + "to-number", + [ + "get", + "sizerank" + ] + ], + 0 + ], + [ + "match", + [ + "get", + "class" + ], + [ + "agriculture", + "wood", + "grass", + "scrub", + "glacier", + "pitch", + "sand" + ], + [ + "step", + [ + "zoom" + ], + false, + 11, + true + ], + "residential", + [ + "step", + [ + "zoom" + ], + true, + 10, + false + ], + [ + "park", + "airport" + ], + [ + "step", + [ + "zoom" + ], + false, + 8, + [ + "case", + [ + "==", + [ + "get", + "sizerank" + ], + 1 + ], + true, + false + ], + 10, + true + ], + [ + "facility", + "industrial" + ], + [ + "step", + [ + "zoom" + ], + false, + 12, + true + ], + "rock", + [ + "step", + [ + "zoom" + ], + false, + 11, + true + ], + "cemetery", + [ + "step", + [ + "zoom" + ], + false, + 11, + true + ], + "school", + [ + "step", + [ + "zoom" + ], + false, + 11, + true + ], + "hospital", + [ + "step", + [ + "zoom" + ], + false, + 11, + true + ], + "commercial_area", + [ + "step", + [ + "zoom" + ], + false, + 11, + true + ], + false + ], + [ + "<=", + [ + "-", + [ + "to-number", + [ + "get", + "sizerank" + ] + ], + [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 12, + 0, + 18, + 14 + ] + ], + 14 + ] + ], + "layout": {}, + "paint": { + "fill-color": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 15, + [ + "match", + [ + "get", + "class" + ], + "wood", + "hsla(103, 50%, 60%, 0.8)", + "scrub", + "hsla(98, 47%, 68%, 0.6)", + "agriculture", + "hsla(98, 50%, 74%, 0.6)", + "park", + [ + "match", + [ + "get", + "type" + ], + [ + "garden", + "playground", + "zoo" + ], + "hsl(98, 38%, 68%)", + "hsl(98, 55%, 70%)" + ], + "grass", + "hsla(98, 50%, 74%, 0.6)", + "airport", + "hsl(230, 40%, 82%)", + "cemetery", + "hsl(98, 45%, 75%)", + "glacier", + "hsl(205, 45%, 95%)", + "hospital", + "hsl(20, 45%, 82%)", + "pitch", + "hsl(88, 65%, 75%)", + "sand", + "hsl(69, 60%, 72%)", + "rock", + "hsl(60, 0%, 85%)", + "school", + "hsl(40, 45%, 78%)", + "commercial_area", + "hsl(55, 45%, 85%)", + "residential", + "hsl(60, 7%, 87%)", + [ + "facility", + "industrial" + ], + "hsl(230, 20%, 85%)", + "hsl(60, 22%, 72%)" + ], + 16, + [ + "match", + [ + "get", + "class" + ], + "wood", + "hsla(103, 50%, 60%, 0.8)", + "scrub", + "hsla(98, 47%, 68%, 0.6)", + "agriculture", + "hsla(98, 50%, 74%, 0.6)", + "park", + [ + "match", + [ + "get", + "type" + ], + [ + "garden", + "playground", + "zoo" + ], + "hsl(98, 38%, 68%)", + "hsl(98, 55%, 70%)" + ], + "grass", + "hsla(98, 50%, 74%, 0.6)", + "airport", + "hsl(230, 40%, 82%)", + "cemetery", + "hsl(98, 45%, 75%)", + "glacier", + "hsl(205, 45%, 95%)", + "hospital", + "hsl(20, 45%, 82%)", + "pitch", + "hsl(88, 65%, 75%)", + "sand", + "hsl(69, 60%, 72%)", + "rock", + "hsla(60, 0%, 85%, 0.5)", + "school", + "hsl(40, 45%, 78%)", + "commercial_area", + "hsla(55, 45%, 85%, 0.5)", + [ + "facility", + "industrial" + ], + "hsl(230, 20%, 85%)", + "hsl(60, 22%, 72%)" + ] + ], + "fill-opacity": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 8, + [ + "match", + [ + "get", + "class" + ], + "residential", + 0.8, + 0.2 + ], + 10, + [ + "match", + [ + "get", + "class" + ], + "residential", + 0, + 1 + ] + ], + "fill-antialias": false + }, + "metadata": { + "mapbox:featureComponent": "land-and-water", + "mapbox:group": "Land & water, land", + "microg:gms-type-feature": "landscape.man_made", + "microg:gms-type-element": "geometry.fill" + } + }, + { + "id": "pitch-outline", + "type": "line", + "source": "composite", + "source-layer": "landuse", + "minzoom": 15, + "filter": [ + "==", + [ + "get", + "class" + ], + "pitch" + ], + "layout": {}, + "paint": { + "line-color": "hsl(88, 60%, 65%)" + }, + "metadata": { + "mapbox:featureComponent": "land-and-water", + "mapbox:group": "Land & water, land", + "microg:gms-type-feature": "landscape.natural.landcover", + "microg:gms-type-element": "geometry.stroke" + } + }, + { + "id": "waterway-shadow", + "type": "line", + "source": "composite", + "source-layer": "waterway", + "minzoom": 10, + "layout": { + "line-cap": [ + "step", + [ + "zoom" + ], + "butt", + 11, + "round" + ], + "line-join": [ + "step", + [ + "zoom" + ], + "miter", + 11, + "round" + ] + }, + "paint": { + "line-color": "hsl(224, 79%, 69%)", + "line-width": [ + "interpolate", + [ + "exponential", + 1.3 + ], + [ + "zoom" + ], + 9, + [ + "match", + [ + "get", + "class" + ], + [ + "canal", + "river" + ], + 0.1, + 0 + ], + 20, + [ + "match", + [ + "get", + "class" + ], + [ + "canal", + "river" + ], + 8, + 3 + ] + ], + "line-translate": [ + "interpolate", + [ + "exponential", + 1.2 + ], + [ + "zoom" + ], + 7, + [ + "literal", + [ + 0, + 0 + ] + ], + 16, + [ + "literal", + [ + -1, + -1 + ] + ] + ], + "line-translate-anchor": "viewport", + "line-opacity": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 8, + 0, + 8.5, + 1 + ] + }, + "metadata": { + "mapbox:featureComponent": "land-and-water", + "mapbox:group": "Land & water, water", + "microg:gms-type-feature": "water", + "microg:gms-type-element": "geometry.stroke" + } + }, + { + "id": "water-shadow", + "type": "fill", + "source": "composite", + "source-layer": "water", + "minzoom": 10, + "layout": {}, + "paint": { + "fill-color": "hsl(224, 79%, 69%)", + "fill-translate": [ + "interpolate", + [ + "exponential", + 1.2 + ], + [ + "zoom" + ], + 7, + [ + "literal", + [ + 0, + 0 + ] + ], + 16, + [ + "literal", + [ + -1, + -1 + ] + ] + ], + "fill-translate-anchor": "viewport" + }, + "metadata": { + "mapbox:featureComponent": "land-and-water", + "mapbox:group": "Land & water, water", + "microg:gms-type-feature": "water", + "microg:gms-type-element": "geometry.stroke" + } + }, + { + "id": "waterway", + "type": "line", + "source": "composite", + "source-layer": "waterway", + "minzoom": 8, + "layout": { + "line-cap": [ + "step", + [ + "zoom" + ], + "butt", + 11, + "round" + ], + "line-join": [ + "step", + [ + "zoom" + ], + "miter", + 11, + "round" + ] + }, + "paint": { + "line-color": "hsl(205, 75%, 70%)", + "line-width": [ + "interpolate", + [ + "exponential", + 1.3 + ], + [ + "zoom" + ], + 9, + [ + "match", + [ + "get", + "class" + ], + [ + "canal", + "river" + ], + 0.1, + 0 + ], + 20, + [ + "match", + [ + "get", + "class" + ], + [ + "canal", + "river" + ], + 8, + 3 + ] + ], + "line-opacity": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 8, + 0, + 8.5, + 1 + ] + }, + "metadata": { + "mapbox:featureComponent": "land-and-water", + "mapbox:group": "Land & water, water", + "microg:gms-type-feature": "water", + "microg:gms-type-element": "geometry.stroke" + } + }, + { + "id": "water", + "type": "fill", + "source": "composite", + "source-layer": "water", + "minzoom": 0, + "layout": {}, + "paint": { + "fill-color": "hsl(205, 75%, 70%)" + }, + "metadata": { + "mapbox:featureComponent": "land-and-water", + "mapbox:group": "Land & water, water", + "microg:gms-type-feature": "water", + "microg:gms-type-element": "geometry.fill" + } + }, + { + "id": "water-depth", + "type": "fill", + "source": "composite", + "source-layer": "depth", + "minzoom": 0, + "maxzoom": 8, + "layout": {}, + "paint": { + "fill-antialias": false, + "fill-color": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 6, + [ + "interpolate", + [ + "linear" + ], + [ + "get", + "min_depth" + ], + 0, + "hsla(205, 75%, 70%, 0.35)", + 200, + "hsla(205, 75%, 63%, 0.35)", + 7000, + "hsla(205, 75%, 56%, 0.35)" + ], + 8, + [ + "interpolate", + [ + "linear" + ], + [ + "get", + "min_depth" + ], + 0, + "hsla(205, 75%, 70%, 0)", + 200, + "hsla(205, 75%, 63%, 0)", + 7000, + "hsla(205, 75%, 53%, 0)" + ] + ] + }, + "metadata": { + "mapbox:featureComponent": "land-and-water", + "mapbox:group": "Land & water, water", + "microg:gms-type-feature": "water", + "microg:gms-type-element": "geometry.fill" + } + }, + { + "id": "wetland", + "type": "fill", + "source": "composite", + "source-layer": "landuse_overlay", + "minzoom": 5, + "filter": [ + "match", + [ + "get", + "class" + ], + [ + "wetland", + "wetland_noveg" + ], + true, + false + ], + "paint": { + "fill-color": "hsl(194, 38%, 74%)", + "fill-opacity": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 10, + 0.25, + 10.5, + 0.15 + ] + }, + "metadata": { + "mapbox:featureComponent": "land-and-water", + "mapbox:group": "Land & water, water", + "microg:gms-type-feature": "water", + "microg:gms-type-element": "geometry.fill" + } + }, + { + "id": "wetland-pattern", + "type": "fill", + "source": "composite", + "source-layer": "landuse_overlay", + "minzoom": 5, + "filter": [ + "match", + [ + "get", + "class" + ], + [ + "wetland", + "wetland_noveg" + ], + true, + false + ], + "paint": { + "fill-color": "hsl(194, 38%, 74%)", + "fill-opacity": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 10, + 0, + 10.5, + 1 + ], + "fill-pattern": "wetland", + "fill-translate-anchor": "viewport" + }, + "metadata": { + "mapbox:featureComponent": "land-and-water", + "mapbox:group": "Land & water, water", + "microg:gms-type-feature": "water", + "microg:gms-type-element": "geometry.fill" + } + }, + { + "id": "hillshade", + "type": "fill", + "source": "composite", + "source-layer": "hillshade", + "filter": [ + "all", + [ + "step", + [ + "zoom" + ], + [ + "==", + [ + "get", + "class" + ], + "shadow" + ], + 11, + true + ], + [ + "match", + [ + "get", + "level" + ], + 89, + true, + 78, + [ + "step", + [ + "zoom" + ], + false, + 5, + true + ], + 67, + [ + "step", + [ + "zoom" + ], + false, + 9, + true + ], + 56, + [ + "step", + [ + "zoom" + ], + false, + 6, + true + ], + 94, + [ + "step", + [ + "zoom" + ], + false, + 11, + true + ], + 90, + [ + "step", + [ + "zoom" + ], + false, + 12, + true + ], + false + ] + ], + "minzoom": 0, + "maxzoom": 16, + "layout": {}, + "paint": { + "fill-color": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 14, + [ + "match", + [ + "get", + "class" + ], + "shadow", + "hsla(66, 38%, 17%, 0.08)", + "hsla(60, 20%, 95%, 0.14)" + ], + 16, + [ + "match", + [ + "get", + "class" + ], + "shadow", + "hsla(66, 38%, 17%, 0)", + "hsla(60, 20%, 95%, 0)" + ] + ], + "fill-antialias": false + }, + "metadata": { + "mapbox:featureComponent": "terrain", + "mapbox:group": "Terrain, land", + "microg:gms-type-feature": "landscape.natural.terrain", + "microg:gms-type-element": "geometry.fill" + } + }, + { + "id": "contour-line", + "type": "line", + "source": "composite", + "source-layer": "contour", + "minzoom": 11, + "filter": [ + "!=", + [ + "get", + "index" + ], + -1 + ], + "layout": {}, + "paint": { + "line-opacity": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 11, + [ + "match", + [ + "get", + "index" + ], + [ + 1, + 2 + ], + 0.15, + 0.3 + ], + 13, + [ + "match", + [ + "get", + "index" + ], + [ + 1, + 2 + ], + 0.3, + 0.5 + ] + ], + "line-color": "hsl(60, 10%, 35%)", + "line-width": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 13, + [ + "match", + [ + "get", + "index" + ], + [ + 1, + 2 + ], + 0.5, + 0.6 + ], + 16, + [ + "match", + [ + "get", + "index" + ], + [ + 1, + 2 + ], + 0.8, + 1.2 + ] + ] + }, + "metadata": { + "mapbox:featureComponent": "terrain", + "mapbox:group": "Terrain, land", + "microg:gms-type-feature": "landscape.natural.terrain", + "microg:gms-type-element": "geometry.stroke" + } + }, + { + "id": "land-structure-polygon", + "type": "fill", + "source": "composite", + "source-layer": "structure", + "minzoom": 13, + "filter": [ + "all", + [ + "==", + [ + "get", + "class" + ], + "land" + ], + [ + "==", + [ + "geometry-type" + ], + "Polygon" + ] + ], + "layout": {}, + "paint": { + "fill-color": "hsl(60, 20%, 85%)" + }, + "metadata": { + "mapbox:featureComponent": "land-and-water", + "mapbox:group": "Land & water, built", + "microg:gms-type-feature": "landscape.natural.terrain", + "microg:gms-type-element": "geometry.fill" + } + }, + { + "id": "land-structure-line", + "type": "line", + "source": "composite", + "source-layer": "structure", + "minzoom": 13, + "filter": [ + "all", + [ + "==", + [ + "get", + "class" + ], + "land" + ], + [ + "==", + [ + "geometry-type" + ], + "LineString" + ] + ], + "layout": { + "line-cap": "square" + }, + "paint": { + "line-width": [ + "interpolate", + [ + "exponential", + 1.99 + ], + [ + "zoom" + ], + 14, + 0.75, + 20, + 40 + ], + "line-color": "hsl(60, 20%, 85%)" + }, + "metadata": { + "mapbox:featureComponent": "land-and-water", + "mapbox:group": "Land & water, built", + "microg:gms-type-feature": "landscape.natural.terrain", + "microg:gms-type-element": "geometry.stroke" + } + }, + { + "id": "aeroway-polygon", + "type": "fill", + "source": "composite", + "source-layer": "aeroway", + "minzoom": 11, + "filter": [ + "all", + [ + "match", + [ + "get", + "type" + ], + [ + "runway", + "taxiway", + "helipad" + ], + true, + false + ], + [ + "==", + [ + "geometry-type" + ], + "Polygon" + ] + ], + "paint": { + "fill-color": "hsl(230, 36%, 74%)", + "fill-opacity": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 10, + 0, + 11, + 1 + ] + }, + "metadata": { + "mapbox:featureComponent": "transit", + "mapbox:group": "Transit, built", + "microg:gms-type-feature": "transit.station.airport", + "microg:gms-type-element": "geometry.fill" + } + }, + { + "id": "aeroway-line", + "type": "line", + "source": "composite", + "source-layer": "aeroway", + "minzoom": 9, + "filter": [ + "==", + [ + "geometry-type" + ], + "LineString" + ], + "paint": { + "line-color": "hsl(230, 36%, 74%)", + "line-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 9, + [ + "match", + [ + "get", + "type" + ], + "runway", + 1, + 0.5 + ], + 18, + [ + "match", + [ + "get", + "type" + ], + "runway", + 80, + 20 + ] + ], + "line-opacity": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 10, + 0, + 11, + 1 + ] + }, + "metadata": { + "mapbox:featureComponent": "transit", + "mapbox:group": "Transit, built", + "microg:gms-type-feature": "transit.station.airport", + "microg:gms-type-element": "geometry.stroke" + } + }, + { + "id": "building", + "type": "fill", + "source": "composite", + "source-layer": "building", + "minzoom": 15, + "filter": [ + "all", + [ + "!=", + [ + "get", + "type" + ], + "building:part" + ], + [ + "==", + [ + "get", + "underground" + ], + "false" + ] + ], + "layout": {}, + "paint": { + "fill-color": "hsl(50, 15%, 75%)", + "fill-opacity": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 15, + 0, + 16, + 1 + ], + "fill-outline-color": "hsl(60, 10%, 65%)" + }, + "metadata": { + "mapbox:featureComponent": "buildings", + "mapbox:group": "Buildings, built", + "microg:gms-type-feature": "landscape.man_made", + "microg:gms-type-element": "geometry.fill" + } + }, + { + "id": "building-underground", + "type": "fill", + "source": "composite", + "source-layer": "building", + "minzoom": 15, + "filter": [ + "all", + [ + "==", + [ + "get", + "underground" + ], + "true" + ], + [ + "==", + [ + "geometry-type" + ], + "Polygon" + ] + ], + "layout": {}, + "paint": { + "fill-color": "hsl(260, 60%, 85%)", + "fill-opacity": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 15, + 0, + 16, + 0.5 + ] + }, + "metadata": { + "mapbox:featureComponent": "buildings", + "mapbox:group": "Buildings, built", + "microg:gms-type-feature": "landscape.man_made", + "microg:gms-type-element": "geometry.fill" + } + }, + { + "id": "tunnel-minor-case", + "type": "line", + "source": "composite", + "source-layer": "road", + "minzoom": 13, + "filter": [ + "all", + [ + "==", + [ + "get", + "structure" + ], + "tunnel" + ], + [ + "match", + [ + "get", + "class" + ], + [ + "track" + ], + true, + "service", + [ + "step", + [ + "zoom" + ], + false, + 14, + true + ], + false + ], + [ + "==", + [ + "geometry-type" + ], + "LineString" + ] + ], + "paint": { + "line-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 14, + 0.8, + 22, + 2 + ], + "line-color": "hsl(60, 3%, 57%)", + "line-gap-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 14, + 1, + 18, + 10, + 22, + 100 + ], + "line-dasharray": [ + 3, + 3 + ] + }, + "metadata": { + "mapbox:featureComponent": "road-network", + "mapbox:group": "Road network, tunnels-case", + "microg:gms-type-feature": "road.local", + "microg:gms-type-element": "geometry.stroke" + } + }, + { + "id": "tunnel-street-case", + "type": "line", + "source": "composite", + "source-layer": "road", + "minzoom": 14, + "filter": [ + "all", + [ + "==", + [ + "get", + "structure" + ], + "tunnel" + ], + [ + "match", + [ + "get", + "class" + ], + [ + "street", + "street_limited" + ], + true, + false + ], + [ + "==", + [ + "geometry-type" + ], + "LineString" + ] + ], + "paint": { + "line-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 14, + 0.8, + 22, + 2 + ], + "line-color": "hsl(60, 3%, 57%)", + "line-gap-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 12, + 0.5, + 18, + 20, + 22, + 200 + ], + "line-opacity": [ + "step", + [ + "zoom" + ], + 0, + 14, + 1 + ], + "line-dasharray": [ + 3, + 3 + ] + }, + "metadata": { + "mapbox:featureComponent": "road-network", + "mapbox:group": "Road network, tunnels-case", + "microg:gms-type-feature": "road.local", + "microg:gms-type-element": "geometry.stroke" + } + }, + { + "id": "tunnel-minor-link-case", + "type": "line", + "source": "composite", + "source-layer": "road", + "minzoom": 13, + "filter": [ + "all", + [ + "match", + [ + "get", + "class" + ], + [ + "primary_link", + "secondary_link", + "tertiary_link" + ], + true, + false + ], + [ + "==", + [ + "get", + "structure" + ], + "tunnel" + ], + [ + "==", + [ + "geometry-type" + ], + "LineString" + ] + ], + "layout": { + "line-cap": [ + "step", + [ + "zoom" + ], + "butt", + 14, + "round" + ], + "line-join": [ + "step", + [ + "zoom" + ], + "miter", + 14, + "round" + ] + }, + "paint": { + "line-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 14, + 0.8, + 22, + 2 + ], + "line-color": "hsl(60, 10%, 70%)", + "line-gap-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 12, + 0.4, + 18, + 18, + 22, + 180 + ], + "line-opacity": [ + "step", + [ + "zoom" + ], + 0, + 11, + 1 + ] + }, + "metadata": { + "mapbox:featureComponent": "road-network", + "mapbox:group": "Road network, tunnels-case", + "microg:gms-type-feature": "road.local", + "microg:gms-type-element": "geometry.stroke" + } + }, + { + "id": "tunnel-secondary-tertiary-case", + "type": "line", + "source": "composite", + "source-layer": "road", + "minzoom": 11, + "filter": [ + "all", + [ + "==", + [ + "get", + "structure" + ], + "tunnel" + ], + [ + "match", + [ + "get", + "class" + ], + [ + "secondary", + "tertiary" + ], + true, + false + ], + [ + "==", + [ + "geometry-type" + ], + "LineString" + ] + ], + "paint": { + "line-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 14, + 1, + 22, + 2 + ], + "line-color": "hsl(60, 3%, 57%)", + "line-gap-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 3, + 0, + 18, + 26, + 22, + 260 + ], + "line-dasharray": [ + 3, + 3 + ] + }, + "metadata": { + "mapbox:featureComponent": "road-network", + "mapbox:group": "Road network, tunnels-case", + "microg:gms-type-feature": "road.local", + "microg:gms-type-element": "geometry.stroke" + } + }, + { + "id": "tunnel-primary-case", + "type": "line", + "source": "composite", + "source-layer": "road", + "minzoom": 10, + "filter": [ + "all", + [ + "==", + [ + "get", + "structure" + ], + "tunnel" + ], + [ + "==", + [ + "get", + "class" + ], + "primary" + ], + [ + "==", + [ + "geometry-type" + ], + "LineString" + ] + ], + "paint": { + "line-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 14, + 1, + 22, + 2 + ], + "line-color": "hsl(60, 3%, 57%)", + "line-gap-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 3, + 0.8, + 18, + 28, + 22, + 280 + ], + "line-dasharray": [ + 3, + 3 + ] + }, + "metadata": { + "mapbox:featureComponent": "road-network", + "mapbox:group": "Road network, tunnels-case", + "microg:gms-type-feature": "road.highway", + "microg:gms-type-element": "geometry.stroke" + } + }, + { + "id": "tunnel-major-link-case", + "type": "line", + "source": "composite", + "source-layer": "road", + "minzoom": 12, + "filter": [ + "all", + [ + "==", + [ + "get", + "structure" + ], + "tunnel" + ], + [ + "match", + [ + "get", + "class" + ], + [ + "motorway_link", + "trunk_link" + ], + true, + false + ], + [ + "==", + [ + "geometry-type" + ], + "LineString" + ] + ], + "paint": { + "line-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 14, + 0.8, + 22, + 2 + ], + "line-color": "hsl(0, 0%, 95%)", + "line-gap-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 12, + 0.8, + 18, + 20, + 22, + 200 + ], + "line-dasharray": [ + 3, + 3 + ] + }, + "metadata": { + "mapbox:featureComponent": "road-network", + "mapbox:group": "Road network, tunnels-case", + "microg:gms-type-feature": "road.highway", + "microg:gms-type-element": "geometry.stroke" + } + }, + { + "id": "tunnel-motorway-trunk-case", + "type": "line", + "source": "composite", + "source-layer": "road", + "minzoom": 13, + "filter": [ + "all", + [ + "==", + [ + "get", + "structure" + ], + "tunnel" + ], + [ + "match", + [ + "get", + "class" + ], + [ + "motorway", + "trunk" + ], + true, + false + ], + [ + "==", + [ + "geometry-type" + ], + "LineString" + ] + ], + "paint": { + "line-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 14, + 1, + 22, + 2 + ], + "line-color": "hsl(60, 10%, 82%)", + "line-gap-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 3, + 0.8, + 18, + 30, + 22, + 300 + ], + "line-dasharray": [ + 3, + 3 + ] + }, + "metadata": { + "mapbox:featureComponent": "road-network", + "mapbox:group": "Road network, tunnels-case", + "microg:gms-type-feature": "road.arterial", + "microg:gms-type-element": "geometry.stroke" + } + }, + { + "id": "tunnel-path-trail", + "type": "line", + "source": "composite", + "source-layer": "road", + "minzoom": 14, + "filter": [ + "all", + [ + "==", + [ + "get", + "structure" + ], + "tunnel" + ], + [ + "==", + [ + "get", + "class" + ], + "path" + ], + [ + "match", + [ + "get", + "type" + ], + [ + "hiking", + "mountain_bike", + "trail" + ], + true, + false + ], + [ + "==", + [ + "geometry-type" + ], + "LineString" + ] + ], + "layout": {}, + "paint": { + "line-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 15, + 1, + 18, + 4 + ], + "line-color": "hsl(60, 32%, 90%)", + "line-dasharray": [ + "step", + [ + "zoom" + ], + [ + "literal", + [ + 5, + 0.5 + ] + ], + 15, + [ + "literal", + [ + 4, + 0.5 + ] + ], + 16, + [ + "literal", + [ + 4, + 0.45 + ] + ] + ] + }, + "metadata": { + "mapbox:featureComponent": "walking-cycling", + "mapbox:group": "Walking, cycling, etc., tunnels", + "microg:gms-type-feature": "road.local", + "microg:gms-type-element": "geometry.stroke" + } + }, + { + "id": "tunnel-path-cycleway-piste", + "type": "line", + "source": "composite", + "source-layer": "road", + "minzoom": 12, + "filter": [ + "all", + [ + "==", + [ + "get", + "structure" + ], + "tunnel" + ], + [ + "==", + [ + "get", + "class" + ], + "path" + ], + [ + "match", + [ + "get", + "type" + ], + [ + "cycleway", + "piste" + ], + true, + false + ], + [ + "==", + [ + "geometry-type" + ], + "LineString" + ] + ], + "layout": {}, + "paint": { + "line-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 15, + 1, + 18, + 4 + ], + "line-color": "hsl(60, 32%, 90%)", + "line-dasharray": [ + 10, + 0 + ] + }, + "metadata": { + "mapbox:featureComponent": "walking-cycling", + "mapbox:group": "Walking, cycling, etc., tunnels", + "microg:gms-type-feature": "road.local", + "microg:gms-type-element": "geometry.fill" + } + }, + { + "id": "tunnel-path", + "type": "line", + "source": "composite", + "source-layer": "road", + "minzoom": 14, + "filter": [ + "all", + [ + "==", + [ + "get", + "structure" + ], + "tunnel" + ], + [ + "==", + [ + "get", + "class" + ], + "path" + ], + [ + "!=", + [ + "get", + "type" + ], + "steps" + ], + [ + "==", + [ + "geometry-type" + ], + "LineString" + ] + ], + "layout": {}, + "paint": { + "line-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 15, + 1, + 18, + 4 + ], + "line-color": "hsl(60, 32%, 90%)", + "line-dasharray": [ + "step", + [ + "zoom" + ], + [ + "literal", + [ + 1, + 0 + ] + ], + 15, + [ + "literal", + [ + 1.75, + 1 + ] + ], + 16, + [ + "literal", + [ + 1, + 0.75 + ] + ], + 17, + [ + "literal", + [ + 1, + 0.5 + ] + ] + ] + }, + "metadata": { + "mapbox:featureComponent": "walking-cycling", + "mapbox:group": "Walking, cycling, etc., tunnels", + "microg:gms-type-feature": "road.local", + "microg:gms-type-element": "geometry.fill" + } + }, + { + "id": "tunnel-steps", + "type": "line", + "source": "composite", + "source-layer": "road", + "minzoom": 14, + "filter": [ + "all", + [ + "==", + [ + "get", + "structure" + ], + "tunnel" + ], + [ + "==", + [ + "get", + "type" + ], + "steps" + ], + [ + "==", + [ + "geometry-type" + ], + "LineString" + ] + ], + "paint": { + "line-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 15, + 1, + 16, + 1.6, + 18, + 6 + ], + "line-color": "hsl(60, 32%, 90%)", + "line-dasharray": [ + "step", + [ + "zoom" + ], + [ + "literal", + [ + 1, + 0 + ] + ], + 15, + [ + "literal", + [ + 1.75, + 1 + ] + ], + 16, + [ + "literal", + [ + 1, + 0.75 + ] + ], + 17, + [ + "literal", + [ + 0.3, + 0.3 + ] + ] + ] + }, + "metadata": { + "mapbox:featureComponent": "walking-cycling", + "mapbox:group": "Walking, cycling, etc., tunnels", + "microg:gms-type-feature": "road.local", + "microg:gms-type-element": "geometry.fill" + } + }, + { + "id": "tunnel-pedestrian", + "type": "line", + "source": "composite", + "source-layer": "road", + "minzoom": 13, + "filter": [ + "all", + [ + "==", + [ + "get", + "structure" + ], + "tunnel" + ], + [ + "==", + [ + "get", + "class" + ], + "pedestrian" + ], + [ + "==", + [ + "geometry-type" + ], + "LineString" + ] + ], + "paint": { + "line-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 14, + 0.5, + 18, + 12 + ], + "line-color": "hsl(0, 0%, 95%)", + "line-dasharray": [ + "step", + [ + "zoom" + ], + [ + "literal", + [ + 1, + 0 + ] + ], + 15, + [ + "literal", + [ + 1.5, + 0.4 + ] + ], + 16, + [ + "literal", + [ + 1, + 0.2 + ] + ] + ] + }, + "metadata": { + "mapbox:featureComponent": "walking-cycling", + "mapbox:group": "Walking, cycling, etc., tunnels", + "microg:gms-type-feature": "road.local", + "microg:gms-type-element": "geometry.fill" + } + }, + { + "id": "tunnel-construction", + "type": "line", + "source": "composite", + "source-layer": "road", + "minzoom": 14, + "filter": [ + "all", + [ + "==", + [ + "get", + "structure" + ], + "tunnel" + ], + [ + "==", + [ + "get", + "class" + ], + "construction" + ], + [ + "==", + [ + "geometry-type" + ], + "LineString" + ] + ], + "paint": { + "line-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 14, + 2, + 18, + 20, + 22, + 200 + ], + "line-color": "hsl(60, 10%, 70%)", + "line-dasharray": [ + "step", + [ + "zoom" + ], + [ + "literal", + [ + 0.4, + 0.8 + ] + ], + 15, + [ + "literal", + [ + 0.3, + 0.6 + ] + ], + 16, + [ + "literal", + [ + 0.2, + 0.3 + ] + ], + 17, + [ + "literal", + [ + 0.2, + 0.25 + ] + ], + 18, + [ + "literal", + [ + 0.15, + 0.15 + ] + ] + ] + }, + "metadata": { + "mapbox:featureComponent": "road-network", + "mapbox:group": "Road network, tunnels", + "microg:gms-type-feature": "road.local", + "microg:gms-type-element": "geometry.fill" + } + }, + { + "id": "tunnel-minor", + "type": "line", + "source": "composite", + "source-layer": "road", + "minzoom": 13, + "filter": [ + "all", + [ + "==", + [ + "get", + "structure" + ], + "tunnel" + ], + [ + "match", + [ + "get", + "class" + ], + [ + "track" + ], + true, + "service", + [ + "step", + [ + "zoom" + ], + false, + 14, + true + ], + false + ], + [ + "==", + [ + "geometry-type" + ], + "LineString" + ] + ], + "paint": { + "line-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 14, + 1, + 18, + 10, + 22, + 100 + ], + "line-color": [ + "match", + [ + "get", + "class" + ], + "street_limited", + "hsl(60, 22%, 80%)", + "hsl(0, 0%, 95%)" + ] + }, + "metadata": { + "mapbox:featureComponent": "road-network", + "mapbox:group": "Road network, tunnels", + "microg:gms-type-feature": "road.local", + "microg:gms-type-element": "geometry.fill" + } + }, + { + "id": "tunnel-minor-link", + "type": "line", + "source": "composite", + "source-layer": "road", + "minzoom": 13, + "filter": [ + "all", + [ + "match", + [ + "get", + "class" + ], + [ + "primary_link", + "secondary_link", + "tertiary_link" + ], + true, + false + ], + [ + "==", + [ + "get", + "structure" + ], + "tunnel" + ], + [ + "==", + [ + "geometry-type" + ], + "LineString" + ] + ], + "layout": { + "line-cap": [ + "step", + [ + "zoom" + ], + "butt", + 13, + "round" + ], + "line-join": [ + "step", + [ + "zoom" + ], + "miter", + 13, + "round" + ] + }, + "paint": { + "line-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 12, + 0.4, + 18, + 18, + 22, + 180 + ], + "line-color": "hsl(0, 0%, 95%)" + }, + "metadata": { + "mapbox:featureComponent": "road-network", + "mapbox:group": "Road network, tunnels", + "microg:gms-type-feature": "road.local", + "microg:gms-type-element": "geometry.fill" + } + }, + { + "id": "tunnel-major-link", + "type": "line", + "source": "composite", + "source-layer": "road", + "minzoom": 12, + "filter": [ + "all", + [ + "==", + [ + "get", + "structure" + ], + "tunnel" + ], + [ + "match", + [ + "get", + "class" + ], + [ + "motorway_link", + "trunk_link" + ], + true, + false + ], + [ + "==", + [ + "geometry-type" + ], + "LineString" + ] + ], + "paint": { + "line-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 12, + 0.8, + 18, + 20, + 22, + 200 + ], + "line-color": [ + "match", + [ + "get", + "class" + ], + "motorway_link", + "hsl(15, 100%, 85%)", + "hsl(35, 78%, 85%)" + ] + }, + "metadata": { + "mapbox:featureComponent": "road-network", + "mapbox:group": "Road network, tunnels", + "microg:gms-type-feature": "road.highway", + "microg:gms-type-element": "geometry.fill" + } + }, + { + "id": "tunnel-street", + "type": "line", + "source": "composite", + "source-layer": "road", + "minzoom": 13, + "filter": [ + "all", + [ + "==", + [ + "get", + "structure" + ], + "tunnel" + ], + [ + "match", + [ + "get", + "class" + ], + [ + "street", + "street_limited" + ], + true, + false + ], + [ + "==", + [ + "geometry-type" + ], + "LineString" + ] + ], + "paint": { + "line-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 12, + 0.5, + 18, + 20, + 22, + 200 + ], + "line-color": [ + "match", + [ + "get", + "class" + ], + "street_limited", + "hsl(60, 22%, 80%)", + "hsl(0, 0%, 95%)" + ], + "line-opacity": [ + "step", + [ + "zoom" + ], + 0, + 14, + 1 + ] + }, + "metadata": { + "mapbox:featureComponent": "road-network", + "mapbox:group": "Road network, tunnels", + "microg:gms-type-feature": "road.local", + "microg:gms-type-element": "geometry.fill" + } + }, + { + "id": "tunnel-street-low", + "type": "line", + "source": "composite", + "source-layer": "road", + "minzoom": 13, + "maxzoom": 14, + "filter": [ + "all", + [ + "==", + [ + "get", + "structure" + ], + "tunnel" + ], + [ + "match", + [ + "get", + "class" + ], + [ + "street", + "street_limited" + ], + true, + false + ], + [ + "==", + [ + "geometry-type" + ], + "LineString" + ] + ], + "layout": { + "line-cap": [ + "step", + [ + "zoom" + ], + "butt", + 14, + "round" + ], + "line-join": [ + "step", + [ + "zoom" + ], + "miter", + 14, + "round" + ] + }, + "paint": { + "line-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 12, + 0.5, + 18, + 20, + 22, + 200 + ], + "line-color": "hsl(0, 0%, 95%)" + }, + "metadata": { + "mapbox:featureComponent": "road-network", + "mapbox:group": "Road network, tunnels", + "microg:gms-type-feature": "road.local", + "microg:gms-type-element": "geometry.fill" + } + }, + { + "id": "tunnel-secondary-tertiary", + "type": "line", + "source": "composite", + "source-layer": "road", + "minzoom": 13, + "filter": [ + "all", + [ + "==", + [ + "get", + "structure" + ], + "tunnel" + ], + [ + "match", + [ + "get", + "class" + ], + [ + "secondary", + "tertiary" + ], + true, + false + ], + [ + "==", + [ + "geometry-type" + ], + "LineString" + ] + ], + "paint": { + "line-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 3, + 0, + 18, + 26, + 22, + 260 + ], + "line-color": "hsl(0, 0%, 95%)" + }, + "metadata": { + "mapbox:featureComponent": "road-network", + "mapbox:group": "Road network, tunnels", + "microg:gms-type-feature": "road.local", + "microg:gms-type-element": "geometry.fill" + } + }, + { + "id": "tunnel-primary", + "type": "line", + "source": "composite", + "source-layer": "road", + "minzoom": 13, + "filter": [ + "all", + [ + "==", + [ + "get", + "structure" + ], + "tunnel" + ], + [ + "==", + [ + "get", + "class" + ], + "primary" + ], + [ + "==", + [ + "geometry-type" + ], + "LineString" + ] + ], + "paint": { + "line-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 3, + 0.8, + 18, + 28, + 22, + 280 + ], + "line-color": "hsl(0, 0%, 95%)" + }, + "metadata": { + "mapbox:featureComponent": "road-network", + "mapbox:group": "Road network, tunnels", + "microg:gms-type-feature": "road.highway", + "microg:gms-type-element": "geometry.fill" + } + }, + { + "id": "tunnel-motorway-trunk", + "type": "line", + "source": "composite", + "source-layer": "road", + "minzoom": 13, + "filter": [ + "all", + [ + "==", + [ + "get", + "structure" + ], + "tunnel" + ], + [ + "match", + [ + "get", + "class" + ], + [ + "motorway", + "trunk" + ], + true, + false + ], + [ + "==", + [ + "geometry-type" + ], + "LineString" + ] + ], + "paint": { + "line-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 3, + 0.8, + 18, + 30, + 22, + 300 + ], + "line-color": [ + "match", + [ + "get", + "class" + ], + "motorway", + "hsl(15, 100%, 85%)", + "hsl(35, 78%, 85%)" + ] + }, + "metadata": { + "mapbox:featureComponent": "road-network", + "mapbox:group": "Road network, tunnels", + "microg:gms-type-feature": "road.arterial", + "microg:gms-type-element": "geometry.fill" + } + }, + { + "id": "tunnel-oneway-arrow-blue", + "type": "symbol", + "source": "composite", + "source-layer": "road", + "minzoom": 16, + "filter": [ + "all", + [ + "==", + [ + "get", + "structure" + ], + "tunnel" + ], + [ + "==", + [ + "get", + "oneway" + ], + "true" + ], + [ + "step", + [ + "zoom" + ], + [ + "match", + [ + "get", + "class" + ], + [ + "primary", + "secondary", + "street", + "street_limited", + "tertiary" + ], + true, + false + ], + 16, + [ + "match", + [ + "get", + "class" + ], + [ + "primary", + "secondary", + "tertiary", + "street", + "street_limited", + "primary_link", + "secondary_link", + "tertiary_link", + "service", + "track" + ], + true, + false + ] + ] + ], + "layout": { + "symbol-placement": "line", + "icon-image": [ + "step", + [ + "zoom" + ], + "oneway-small", + 18, + "oneway-large" + ], + "symbol-spacing": 200, + "icon-rotation-alignment": "map", + "icon-allow-overlap": true, + "icon-ignore-placement": true + }, + "paint": {}, + "metadata": { + "mapbox:featureComponent": "road-network", + "mapbox:group": "Road network, tunnels", + "microg:gms-type-feature": "road", + "microg:gms-type-element": "labels.icon" + } + }, + { + "id": "tunnel-oneway-arrow-white", + "type": "symbol", + "source": "composite", + "source-layer": "road", + "minzoom": 16, + "filter": [ + "all", + [ + "==", + [ + "get", + "structure" + ], + "tunnel" + ], + [ + "match", + [ + "get", + "class" + ], + [ + "motorway", + "motorway_link", + "trunk", + "trunk_link" + ], + true, + false + ], + [ + "==", + [ + "get", + "oneway" + ], + "true" + ] + ], + "layout": { + "symbol-placement": "line", + "icon-image": [ + "step", + [ + "zoom" + ], + "oneway-white-small", + 18, + "oneway-white-large" + ], + "symbol-spacing": 200, + "icon-rotation-alignment": "map", + "icon-allow-overlap": true, + "icon-ignore-placement": true + }, + "paint": {}, + "metadata": { + "mapbox:featureComponent": "road-network", + "mapbox:group": "Road network, tunnels", + "microg:gms-type-feature": "road.arterial", + "microg:gms-type-element": "labels.icon" + } + }, + { + "id": "cliff", + "type": "line", + "source": "composite", + "source-layer": "structure", + "minzoom": 15, + "filter": [ + "==", + [ + "get", + "class" + ], + "cliff" + ], + "layout": { + "line-cap": "round", + "line-join": "round" + }, + "paint": { + "line-opacity": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 15, + 0, + 15.25, + 1 + ], + "line-width": 10, + "line-pattern": "cliff" + }, + "metadata": { + "mapbox:featureComponent": "terrain", + "mapbox:group": "Terrain, surface", + "microg:gms-type-feature": "landscape.natural.terrain", + "microg:gms-type-element": "geometry.stroke" + } + }, + { + "id": "ferry", + "type": "line", + "source": "composite", + "source-layer": "road", + "minzoom": 8, + "filter": [ + "==", + [ + "get", + "type" + ], + "ferry" + ], + "paint": { + "line-color": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 15, + "hsl(214, 68%, 63%)", + 17, + "hsl(239, 68%, 63%)" + ], + "line-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 14, + 0.5, + 20, + 1 + ], + "line-dasharray": [ + "step", + [ + "zoom" + ], + [ + "literal", + [ + 1, + 0 + ] + ], + 13, + [ + "literal", + [ + 12, + 4 + ] + ] + ] + }, + "metadata": { + "mapbox:featureComponent": "transit", + "mapbox:group": "Transit, ferries", + "microg:gms-type-feature": "transit.line", + "microg:gms-type-element": "geometry.fill" + } + }, + { + "id": "ferry-auto", + "type": "line", + "source": "composite", + "source-layer": "road", + "minzoom": 8, + "filter": [ + "==", + [ + "get", + "type" + ], + "ferry_auto" + ], + "paint": { + "line-color": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 15, + "hsl(214, 68%, 63%)", + 17, + "hsl(239, 68%, 63%)" + ], + "line-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 14, + 0.5, + 20, + 1 + ] + }, + "metadata": { + "mapbox:featureComponent": "transit", + "mapbox:group": "Transit, ferries", + "microg:gms-type-feature": "transit.line", + "microg:gms-type-element": "geometry.fill" + } + }, + { + "id": "road-pedestrian-polygon-fill", + "type": "fill", + "source": "composite", + "source-layer": "road", + "minzoom": 14, + "filter": [ + "all", + [ + "match", + [ + "get", + "class" + ], + [ + "path", + "pedestrian" + ], + true, + false + ], + [ + "match", + [ + "get", + "structure" + ], + [ + "none", + "ford" + ], + true, + false + ], + [ + "case", + [ + "has", + "layer" + ], + [ + ">=", + [ + "get", + "layer" + ], + 0 + ], + true + ], + [ + "==", + [ + "geometry-type" + ], + "Polygon" + ] + ], + "paint": { + "fill-color": "hsl(60, 20%, 85%)" + }, + "metadata": { + "mapbox:featureComponent": "walking-cycling", + "mapbox:group": "Walking, cycling, etc., surface", + "microg:gms-type-feature": "road.local", + "microg:gms-type-element": "geometry.fill" + } + }, + { + "id": "road-pedestrian-polygon-pattern", + "type": "fill", + "source": "composite", + "source-layer": "road", + "minzoom": 16, + "filter": [ + "all", + [ + "match", + [ + "get", + "class" + ], + [ + "path", + "pedestrian" + ], + true, + false + ], + [ + "match", + [ + "get", + "structure" + ], + [ + "none", + "ford" + ], + true, + false + ], + [ + "case", + [ + "has", + "layer" + ], + [ + ">=", + [ + "get", + "layer" + ], + 0 + ], + true + ], + [ + "==", + [ + "geometry-type" + ], + "Polygon" + ] + ], + "paint": { + "fill-pattern": "pedestrian-polygon", + "fill-opacity": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 16, + 0, + 17, + 1 + ] + }, + "metadata": { + "mapbox:featureComponent": "walking-cycling", + "mapbox:group": "Walking, cycling, etc., surface", + "microg:gms-type-feature": "road.local", + "microg:gms-type-element": "geometry.fill" + } + }, + { + "id": "road-path-bg", + "type": "line", + "source": "composite", + "source-layer": "road", + "minzoom": 12, + "filter": [ + "all", + [ + "==", + [ + "get", + "class" + ], + "path" + ], + [ + "step", + [ + "zoom" + ], + [ + "!", + [ + "match", + [ + "get", + "type" + ], + [ + "steps", + "sidewalk", + "crossing" + ], + true, + false + ] + ], + 16, + [ + "!=", + [ + "get", + "type" + ], + "steps" + ] + ], + [ + "match", + [ + "get", + "structure" + ], + [ + "none", + "ford" + ], + true, + false + ], + [ + "==", + [ + "geometry-type" + ], + "LineString" + ] + ], + "layout": { + "line-join": [ + "step", + [ + "zoom" + ], + "miter", + 14, + "round" + ] + }, + "paint": { + "line-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 15, + 2, + 18, + 7 + ], + "line-color": [ + "match", + [ + "get", + "type" + ], + "piste", + "hsl(215, 80%, 48%)", + [ + "mountain_bike", + "hiking", + "trail", + "cycleway", + "footway", + "path", + "bridleway" + ], + "hsl(35, 80%, 48%)", + "hsl(60, 1%, 64%)" + ] + }, + "metadata": { + "mapbox:featureComponent": "walking-cycling", + "mapbox:group": "Walking, cycling, etc., surface", + "microg:gms-type-feature": "road.local", + "microg:gms-type-element": "geometry.stroke" + } + }, + { + "id": "road-steps-bg", + "type": "line", + "source": "composite", + "source-layer": "road", + "minzoom": 14, + "filter": [ + "all", + [ + "==", + [ + "get", + "type" + ], + "steps" + ], + [ + "match", + [ + "get", + "structure" + ], + [ + "none", + "ford" + ], + true, + false + ], + [ + "==", + [ + "geometry-type" + ], + "LineString" + ] + ], + "layout": { + "line-join": "round" + }, + "paint": { + "line-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 15, + 2, + 17, + 4.6, + 18, + 7 + ], + "line-color": "hsl(35, 80%, 48%)", + "line-opacity": 0.75 + }, + "metadata": { + "mapbox:featureComponent": "walking-cycling", + "mapbox:group": "Walking, cycling, etc., surface", + "microg:gms-type-feature": "road.local", + "microg:gms-type-element": "geometry.stroke" + } + }, + { + "id": "road-pedestrian-case", + "type": "line", + "source": "composite", + "source-layer": "road", + "minzoom": 14, + "filter": [ + "all", + [ + "==", + [ + "get", + "class" + ], + "pedestrian" + ], + [ + "match", + [ + "get", + "structure" + ], + [ + "none", + "ford" + ], + true, + false + ], + [ + "case", + [ + "has", + "layer" + ], + [ + ">=", + [ + "get", + "layer" + ], + 0 + ], + true + ], + [ + "==", + [ + "geometry-type" + ], + "LineString" + ] + ], + "layout": { + "line-join": [ + "step", + [ + "zoom" + ], + "miter", + 14, + "round" + ] + }, + "paint": { + "line-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 14, + 2, + 18, + 14.5 + ], + "line-color": "hsl(60, 10%, 70%)" + }, + "metadata": { + "mapbox:featureComponent": "walking-cycling", + "mapbox:group": "Walking, cycling, etc., surface", + "microg:gms-type-feature": "road.local", + "microg:gms-type-element": "geometry.stroke" + } + }, + { + "id": "road-path-trail", + "type": "line", + "source": "composite", + "source-layer": "road", + "minzoom": 12, + "filter": [ + "all", + [ + "==", + [ + "get", + "class" + ], + "path" + ], + [ + "match", + [ + "get", + "type" + ], + [ + "hiking", + "mountain_bike", + "trail" + ], + true, + false + ], + [ + "match", + [ + "get", + "structure" + ], + [ + "none", + "ford" + ], + true, + false + ], + [ + "==", + [ + "geometry-type" + ], + "LineString" + ] + ], + "layout": { + "line-join": [ + "step", + [ + "zoom" + ], + "miter", + 14, + "round" + ] + }, + "paint": { + "line-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 15, + 1, + 18, + 4 + ], + "line-color": "hsl(0, 0%, 95%)", + "line-dasharray": [ + "step", + [ + "zoom" + ], + [ + "literal", + [ + 5, + 0.5 + ] + ], + 15, + [ + "literal", + [ + 4, + 0.5 + ] + ], + 16, + [ + "literal", + [ + 4, + 0.45 + ] + ] + ] + }, + "metadata": { + "mapbox:featureComponent": "walking-cycling", + "mapbox:group": "Walking, cycling, etc., surface", + "microg:gms-type-feature": "road.local", + "microg:gms-type-element": "geometry.fill" + } + }, + { + "id": "road-path-cycleway-piste", + "type": "line", + "source": "composite", + "source-layer": "road", + "minzoom": 12, + "filter": [ + "all", + [ + "==", + [ + "get", + "class" + ], + "path" + ], + [ + "match", + [ + "get", + "type" + ], + [ + "cycleway", + "piste" + ], + true, + false + ], + [ + "match", + [ + "get", + "structure" + ], + [ + "none", + "ford" + ], + true, + false + ], + [ + "==", + [ + "geometry-type" + ], + "LineString" + ] + ], + "layout": { + "line-join": [ + "step", + [ + "zoom" + ], + "miter", + 14, + "round" + ] + }, + "paint": { + "line-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 15, + 1, + 18, + 4 + ], + "line-color": "hsl(0, 0%, 95%)", + "line-dasharray": [ + 10, + 0 + ] + }, + "metadata": { + "mapbox:featureComponent": "walking-cycling", + "mapbox:group": "Walking, cycling, etc., surface", + "microg:gms-type-feature": "road.local", + "microg:gms-type-element": "geometry.fill" + } + }, + { + "id": "road-path", + "type": "line", + "source": "composite", + "source-layer": "road", + "minzoom": 12, + "filter": [ + "all", + [ + "==", + [ + "get", + "class" + ], + "path" + ], + [ + "step", + [ + "zoom" + ], + [ + "!", + [ + "match", + [ + "get", + "type" + ], + [ + "steps", + "sidewalk", + "crossing" + ], + true, + false + ] + ], + 16, + [ + "!=", + [ + "get", + "type" + ], + "steps" + ] + ], + [ + "match", + [ + "get", + "structure" + ], + [ + "none", + "ford" + ], + true, + false + ], + [ + "==", + [ + "geometry-type" + ], + "LineString" + ] + ], + "layout": { + "line-join": [ + "step", + [ + "zoom" + ], + "miter", + 14, + "round" + ] + }, + "paint": { + "line-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 13, + 0.5, + 14, + 1, + 15, + 1, + 18, + 4 + ], + "line-color": "hsl(0, 0%, 95%)", + "line-dasharray": [ + "step", + [ + "zoom" + ], + [ + "literal", + [ + 4, + 0.3 + ] + ], + 15, + [ + "literal", + [ + 1.75, + 0.3 + ] + ], + 16, + [ + "literal", + [ + 1, + 0.3 + ] + ], + 17, + [ + "literal", + [ + 1, + 0.25 + ] + ] + ] + }, + "metadata": { + "mapbox:featureComponent": "walking-cycling", + "mapbox:group": "Walking, cycling, etc., surface", + "microg:gms-type-feature": "road.local", + "microg:gms-type-element": "geometry.fill" + } + }, + { + "id": "road-steps", + "type": "line", + "source": "composite", + "source-layer": "road", + "minzoom": 14, + "filter": [ + "all", + [ + "==", + [ + "get", + "type" + ], + "steps" + ], + [ + "match", + [ + "get", + "structure" + ], + [ + "none", + "ford" + ], + true, + false + ], + [ + "==", + [ + "geometry-type" + ], + "LineString" + ] + ], + "layout": { + "line-join": "round" + }, + "paint": { + "line-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 15, + 1, + 16, + 1.6, + 18, + 6 + ], + "line-color": "hsl(0, 0%, 95%)", + "line-dasharray": [ + "step", + [ + "zoom" + ], + [ + "literal", + [ + 1, + 0 + ] + ], + 15, + [ + "literal", + [ + 1.75, + 1 + ] + ], + 16, + [ + "literal", + [ + 1, + 0.75 + ] + ], + 17, + [ + "literal", + [ + 0.3, + 0.3 + ] + ] + ] + }, + "metadata": { + "mapbox:featureComponent": "walking-cycling", + "mapbox:group": "Walking, cycling, etc., surface", + "microg:gms-type-feature": "road.local", + "microg:gms-type-element": "geometry.fill" + } + }, + { + "id": "road-pedestrian", + "type": "line", + "source": "composite", + "source-layer": "road", + "minzoom": 12, + "filter": [ + "all", + [ + "==", + [ + "get", + "class" + ], + "pedestrian" + ], + [ + "match", + [ + "get", + "structure" + ], + [ + "none", + "ford" + ], + true, + false + ], + [ + "case", + [ + "has", + "layer" + ], + [ + ">=", + [ + "get", + "layer" + ], + 0 + ], + true + ], + [ + "==", + [ + "geometry-type" + ], + "LineString" + ] + ], + "layout": { + "line-join": [ + "step", + [ + "zoom" + ], + "miter", + 14, + "round" + ] + }, + "paint": { + "line-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 14, + 0.5, + 18, + 12 + ], + "line-color": "hsl(0, 0%, 95%)", + "line-dasharray": [ + "step", + [ + "zoom" + ], + [ + "literal", + [ + 1, + 0 + ] + ], + 15, + [ + "literal", + [ + 1.5, + 0.4 + ] + ], + 16, + [ + "literal", + [ + 1, + 0.2 + ] + ] + ] + }, + "metadata": { + "mapbox:featureComponent": "walking-cycling", + "mapbox:group": "Walking, cycling, etc., surface", + "microg:gms-type-feature": "road.local", + "microg:gms-type-element": "geometry.fill" + } + }, + { + "id": "golf-hole-line", + "type": "line", + "source": "composite", + "source-layer": "road", + "minzoom": 16, + "filter": [ + "==", + [ + "get", + "class" + ], + "golf" + ], + "paint": { + "line-color": "hsl(98, 26%, 56%)" + }, + "metadata": { + "mapbox:featureComponent": "walking-cycling", + "mapbox:group": "Walking, cycling, etc., surface", + "microg:gms-type-feature": "poi.attraction", + "microg:gms-type-element": "geometry.stroke" + } + }, + { + "id": "road-polygon", + "type": "fill", + "source": "composite", + "source-layer": "road", + "minzoom": 12, + "filter": [ + "all", + [ + "match", + [ + "get", + "class" + ], + [ + "primary", + "secondary", + "tertiary", + "primary_link", + "secondary_link", + "tertiary_link", + "trunk", + "trunk_link", + "street", + "street_limited", + "track", + "service" + ], + true, + false + ], + [ + "match", + [ + "get", + "structure" + ], + [ + "none", + "ford" + ], + true, + false + ], + [ + "==", + [ + "geometry-type" + ], + "Polygon" + ] + ], + "paint": { + "fill-color": "hsl(0, 0%, 95%)", + "fill-outline-color": "hsl(60, 10%, 70%)" + }, + "metadata": { + "mapbox:featureComponent": "road-network", + "mapbox:group": "Road network, surface", + "microg:gms-type-feature": "road", + "microg:gms-type-element": "geometry.fill" + } + }, + { + "id": "turning-feature-outline", + "type": "circle", + "source": "composite", + "source-layer": "road", + "minzoom": 15, + "filter": [ + "all", + [ + "match", + [ + "get", + "class" + ], + [ + "turning_circle", + "turning_loop" + ], + true, + false + ], + [ + "==", + [ + "geometry-type" + ], + "Point" + ] + ], + "paint": { + "circle-radius": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 15, + 4.5, + 16, + 8, + 18, + 20, + 22, + 200 + ], + "circle-color": "hsl(0, 0%, 95%)", + "circle-stroke-width": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 15, + 0.8, + 16, + 1.2, + 18, + 2 + ], + "circle-stroke-color": "hsl(60, 10%, 70%)", + "circle-pitch-alignment": "map" + }, + "metadata": { + "mapbox:featureComponent": "road-network", + "mapbox:group": "Road network, surface", + "microg:gms-type-feature": "road", + "microg:gms-type-element": "geometry.stroke" + } + }, + { + "id": "road-minor-case", + "type": "line", + "source": "composite", + "source-layer": "road", + "minzoom": 13, + "filter": [ + "all", + [ + "match", + [ + "get", + "class" + ], + [ + "track" + ], + true, + "service", + [ + "step", + [ + "zoom" + ], + false, + 14, + true + ], + false + ], + [ + "match", + [ + "get", + "structure" + ], + [ + "none", + "ford" + ], + true, + false + ], + [ + "==", + [ + "geometry-type" + ], + "LineString" + ] + ], + "layout": { + "line-cap": [ + "step", + [ + "zoom" + ], + "butt", + 14, + "round" + ], + "line-join": [ + "step", + [ + "zoom" + ], + "miter", + 14, + "round" + ] + }, + "paint": { + "line-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 14, + 0.8, + 22, + 2 + ], + "line-color": [ + "match", + [ + "get", + "class" + ], + "track", + "hsl(35, 80%, 48%)", + "hsl(60, 10%, 70%)" + ], + "line-gap-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 14, + 1, + 18, + 10, + 22, + 100 + ] + }, + "metadata": { + "mapbox:featureComponent": "road-network", + "mapbox:group": "Road network, surface", + "microg:gms-type-feature": "road.local", + "microg:gms-type-element": "geometry.stroke" + } + }, + { + "id": "road-street-case", + "type": "line", + "source": "composite", + "source-layer": "road", + "minzoom": 14, + "filter": [ + "all", + [ + "match", + [ + "get", + "class" + ], + [ + "street", + "street_limited" + ], + true, + false + ], + [ + "match", + [ + "get", + "structure" + ], + [ + "none", + "ford" + ], + true, + false + ], + [ + "==", + [ + "geometry-type" + ], + "LineString" + ] + ], + "layout": { + "line-cap": [ + "step", + [ + "zoom" + ], + "butt", + 14, + "round" + ], + "line-join": [ + "step", + [ + "zoom" + ], + "miter", + 14, + "round" + ] + }, + "paint": { + "line-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 14, + 0.8, + 22, + 2 + ], + "line-color": "hsl(60, 10%, 70%)", + "line-gap-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 12, + 0.5, + 18, + 20, + 22, + 200 + ], + "line-opacity": [ + "step", + [ + "zoom" + ], + 0, + 14, + 1 + ] + }, + "metadata": { + "mapbox:featureComponent": "road-network", + "mapbox:group": "Road network, surface", + "microg:gms-type-feature": "road.local", + "microg:gms-type-element": "geometry.stroke" + } + }, + { + "id": "road-minor-link-case", + "type": "line", + "source": "composite", + "source-layer": "road", + "minzoom": 13, + "filter": [ + "all", + [ + "match", + [ + "get", + "class" + ], + [ + "primary_link", + "secondary_link", + "tertiary_link" + ], + true, + false + ], + [ + "match", + [ + "get", + "structure" + ], + [ + "none", + "ford" + ], + true, + false + ], + [ + "==", + [ + "geometry-type" + ], + "LineString" + ] + ], + "layout": { + "line-cap": [ + "step", + [ + "zoom" + ], + "butt", + 14, + "round" + ], + "line-join": [ + "step", + [ + "zoom" + ], + "miter", + 14, + "round" + ] + }, + "paint": { + "line-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 14, + 0.8, + 22, + 2 + ], + "line-color": "hsl(60, 10%, 70%)", + "line-gap-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 12, + 0.4, + 18, + 18, + 22, + 180 + ], + "line-opacity": [ + "step", + [ + "zoom" + ], + 0, + 11, + 1 + ] + }, + "metadata": { + "mapbox:featureComponent": "road-network", + "mapbox:group": "Road network, surface", + "microg:gms-type-feature": "road.local", + "microg:gms-type-element": "geometry.stroke" + } + }, + { + "id": "road-secondary-tertiary-case", + "type": "line", + "source": "composite", + "source-layer": "road", + "minzoom": 11, + "filter": [ + "all", + [ + "match", + [ + "get", + "class" + ], + [ + "secondary", + "tertiary" + ], + true, + false + ], + [ + "match", + [ + "get", + "structure" + ], + [ + "none", + "ford" + ], + true, + false + ], + [ + "==", + [ + "geometry-type" + ], + "LineString" + ] + ], + "layout": { + "line-cap": [ + "step", + [ + "zoom" + ], + "butt", + 14, + "round" + ], + "line-join": [ + "step", + [ + "zoom" + ], + "miter", + 14, + "round" + ] + }, + "paint": { + "line-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 14, + 0.8, + 22, + 2 + ], + "line-color": "hsl(60, 10%, 70%)", + "line-gap-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 3, + 0, + 18, + 26, + 22, + 260 + ] + }, + "metadata": { + "mapbox:featureComponent": "road-network", + "mapbox:group": "Road network, surface", + "microg:gms-type-feature": "road.local", + "microg:gms-type-element": "geometry.stroke" + } + }, + { + "id": "road-primary-case", + "type": "line", + "source": "composite", + "source-layer": "road", + "minzoom": 10, + "filter": [ + "all", + [ + "==", + [ + "get", + "class" + ], + "primary" + ], + [ + "match", + [ + "get", + "structure" + ], + [ + "none", + "ford" + ], + true, + false + ], + [ + "==", + [ + "geometry-type" + ], + "LineString" + ] + ], + "layout": { + "line-cap": [ + "step", + [ + "zoom" + ], + "butt", + 14, + "round" + ], + "line-join": [ + "step", + [ + "zoom" + ], + "miter", + 14, + "round" + ] + }, + "paint": { + "line-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 14, + 1, + 22, + 2 + ], + "line-color": "hsl(60, 10%, 70%)", + "line-gap-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 3, + 0.8, + 18, + 28, + 22, + 280 + ] + }, + "metadata": { + "mapbox:featureComponent": "road-network", + "mapbox:group": "Road network, surface", + "microg:gms-type-feature": "road.highway", + "microg:gms-type-element": "geometry.stroke" + } + }, + { + "id": "road-major-link-case", + "type": "line", + "source": "composite", + "source-layer": "road", + "minzoom": 12, + "filter": [ + "all", + [ + "match", + [ + "get", + "class" + ], + [ + "motorway_link", + "trunk_link" + ], + true, + false + ], + [ + "match", + [ + "get", + "structure" + ], + [ + "none", + "ford" + ], + true, + false + ], + [ + "==", + [ + "geometry-type" + ], + "LineString" + ] + ], + "layout": { + "line-cap": [ + "step", + [ + "zoom" + ], + "butt", + 14, + "round" + ], + "line-join": [ + "step", + [ + "zoom" + ], + "miter", + 14, + "round" + ] + }, + "paint": { + "line-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 14, + 0.8, + 22, + 2 + ], + "line-color": "hsl(60, 10%, 82%)", + "line-gap-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 12, + 0.8, + 18, + 20, + 22, + 200 + ], + "line-opacity": [ + "step", + [ + "zoom" + ], + 0, + 11, + 1 + ] + }, + "metadata": { + "mapbox:featureComponent": "road-network", + "mapbox:group": "Road network, surface", + "microg:gms-type-feature": "road.highway", + "microg:gms-type-element": "geometry.stroke" + } + }, + { + "id": "road-motorway-trunk-case", + "type": "line", + "source": "composite", + "source-layer": "road", + "minzoom": 3, + "filter": [ + "all", + [ + "step", + [ + "zoom" + ], + [ + "match", + [ + "get", + "class" + ], + [ + "motorway", + "trunk" + ], + true, + false + ], + 5, + [ + "all", + [ + "match", + [ + "get", + "class" + ], + [ + "motorway", + "trunk" + ], + true, + false + ], + [ + "match", + [ + "get", + "structure" + ], + [ + "none", + "ford" + ], + true, + false + ] + ] + ], + [ + "==", + [ + "geometry-type" + ], + "LineString" + ] + ], + "layout": { + "line-cap": [ + "step", + [ + "zoom" + ], + "butt", + 14, + "round" + ], + "line-join": [ + "step", + [ + "zoom" + ], + "miter", + 14, + "round" + ] + }, + "paint": { + "line-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 14, + 1, + 22, + 2 + ], + "line-color": "hsl(60, 10%, 82%)", + "line-gap-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 3, + 0.8, + 18, + 30, + 22, + 300 + ], + "line-opacity": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 3, + 0, + 3.5, + 1 + ] + }, + "metadata": { + "mapbox:featureComponent": "road-network", + "mapbox:group": "Road network, surface", + "microg:gms-type-feature": "road.arterial", + "microg:gms-type-element": "geometry.stroke" + } + }, + { + "id": "turning-feature", + "type": "circle", + "source": "composite", + "source-layer": "road", + "minzoom": 15, + "filter": [ + "all", + [ + "match", + [ + "get", + "class" + ], + [ + "turning_circle", + "turning_loop" + ], + true, + false + ], + [ + "==", + [ + "geometry-type" + ], + "Point" + ] + ], + "paint": { + "circle-radius": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 15, + 4.5, + 16, + 8, + 18, + 20, + 22, + 200 + ], + "circle-color": "hsl(0, 0%, 95%)", + "circle-pitch-alignment": "map" + }, + "metadata": { + "mapbox:featureComponent": "road-network", + "mapbox:group": "Road network, surface", + "microg:gms-type-feature": "road", + "microg:gms-type-element": "geometry.fill" + } + }, + { + "id": "road-construction", + "type": "line", + "source": "composite", + "source-layer": "road", + "minzoom": 14, + "filter": [ + "all", + [ + "==", + [ + "get", + "class" + ], + "construction" + ], + [ + "match", + [ + "get", + "structure" + ], + [ + "none", + "ford" + ], + true, + false + ], + [ + "==", + [ + "geometry-type" + ], + "LineString" + ] + ], + "paint": { + "line-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 14, + 2, + 18, + 20, + 22, + 200 + ], + "line-color": "hsl(0, 0%, 95%)", + "line-dasharray": [ + "step", + [ + "zoom" + ], + [ + "literal", + [ + 0.4, + 0.8 + ] + ], + 15, + [ + "literal", + [ + 0.3, + 0.6 + ] + ], + 16, + [ + "literal", + [ + 0.2, + 0.3 + ] + ], + 17, + [ + "literal", + [ + 0.2, + 0.25 + ] + ], + 18, + [ + "literal", + [ + 0.15, + 0.15 + ] + ] + ] + }, + "metadata": { + "mapbox:featureComponent": "road-network", + "mapbox:group": "Road network, surface", + "microg:gms-type-feature": "road.local", + "microg:gms-type-element": "geometry.fill" + } + }, + { + "id": "road-minor", + "type": "line", + "source": "composite", + "source-layer": "road", + "minzoom": 13, + "filter": [ + "all", + [ + "match", + [ + "get", + "class" + ], + [ + "track" + ], + true, + "service", + [ + "step", + [ + "zoom" + ], + false, + 14, + true + ], + false + ], + [ + "match", + [ + "get", + "structure" + ], + [ + "none", + "ford" + ], + true, + false + ], + [ + "==", + [ + "geometry-type" + ], + "LineString" + ] + ], + "layout": { + "line-cap": [ + "step", + [ + "zoom" + ], + "butt", + 14, + "round" + ], + "line-join": [ + "step", + [ + "zoom" + ], + "miter", + 14, + "round" + ] + }, + "paint": { + "line-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 14, + 1, + 18, + 10, + 22, + 100 + ], + "line-color": "hsl(0, 0%, 95%)" + }, + "metadata": { + "mapbox:featureComponent": "road-network", + "mapbox:group": "Road network, surface", + "microg:gms-type-feature": "road.local", + "microg:gms-type-element": "geometry.fill" + } + }, + { + "id": "road-minor-link", + "type": "line", + "source": "composite", + "source-layer": "road", + "minzoom": 12, + "filter": [ + "all", + [ + "match", + [ + "get", + "class" + ], + [ + "primary_link", + "secondary_link", + "tertiary_link" + ], + true, + false + ], + [ + "match", + [ + "get", + "structure" + ], + [ + "none", + "ford" + ], + true, + false + ], + [ + "==", + [ + "geometry-type" + ], + "LineString" + ] + ], + "layout": { + "line-cap": [ + "step", + [ + "zoom" + ], + "butt", + 13, + "round" + ], + "line-join": [ + "step", + [ + "zoom" + ], + "miter", + 13, + "round" + ] + }, + "paint": { + "line-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 12, + 0.4, + 18, + 18, + 22, + 180 + ], + "line-color": "hsl(0, 0%, 95%)" + }, + "metadata": { + "mapbox:featureComponent": "road-network", + "mapbox:group": "Road network, surface", + "microg:gms-type-feature": "road.local", + "microg:gms-type-element": "geometry.fill" + } + }, + { + "id": "road-major-link", + "type": "line", + "source": "composite", + "source-layer": "road", + "minzoom": 12, + "filter": [ + "all", + [ + "match", + [ + "get", + "class" + ], + [ + "motorway_link", + "trunk_link" + ], + true, + false + ], + [ + "match", + [ + "get", + "structure" + ], + [ + "none", + "ford" + ], + true, + false + ], + [ + "==", + [ + "geometry-type" + ], + "LineString" + ] + ], + "layout": { + "line-cap": [ + "step", + [ + "zoom" + ], + "butt", + 13, + "round" + ], + "line-join": [ + "step", + [ + "zoom" + ], + "miter", + 13, + "round" + ] + }, + "paint": { + "line-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 12, + 0.8, + 18, + 20, + 22, + 200 + ], + "line-color": [ + "match", + [ + "get", + "class" + ], + "motorway_link", + "hsl(15, 100%, 75%)", + "hsl(35, 89%, 75%)" + ] + }, + "metadata": { + "mapbox:featureComponent": "road-network", + "mapbox:group": "Road network, surface", + "microg:gms-type-feature": "road.local", + "microg:gms-type-element": "geometry.fill" + } + }, + { + "id": "road-street", + "type": "line", + "source": "composite", + "source-layer": "road", + "minzoom": 13, + "filter": [ + "all", + [ + "match", + [ + "get", + "class" + ], + [ + "street", + "street_limited" + ], + true, + false + ], + [ + "match", + [ + "get", + "structure" + ], + [ + "none", + "ford" + ], + true, + false + ], + [ + "==", + [ + "geometry-type" + ], + "LineString" + ] + ], + "layout": { + "line-cap": [ + "step", + [ + "zoom" + ], + "butt", + 14, + "round" + ], + "line-join": [ + "step", + [ + "zoom" + ], + "miter", + 14, + "round" + ] + }, + "paint": { + "line-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 12, + 0.5, + 18, + 20, + 22, + 200 + ], + "line-color": [ + "match", + [ + "get", + "class" + ], + "street_limited", + "hsl(60, 22%, 80%)", + "hsl(0, 0%, 95%)" + ], + "line-opacity": [ + "step", + [ + "zoom" + ], + 0, + 14, + 1 + ] + }, + "metadata": { + "mapbox:featureComponent": "road-network", + "mapbox:group": "Road network, surface", + "microg:gms-type-feature": "road.local", + "microg:gms-type-element": "geometry.fill" + } + }, + { + "id": "road-street-low", + "type": "line", + "source": "composite", + "source-layer": "road", + "minzoom": 11, + "maxzoom": 14, + "filter": [ + "all", + [ + "match", + [ + "get", + "class" + ], + [ + "street", + "street_limited" + ], + true, + false + ], + [ + "match", + [ + "get", + "structure" + ], + [ + "none", + "ford" + ], + true, + false + ], + [ + "==", + [ + "geometry-type" + ], + "LineString" + ] + ], + "layout": { + "line-cap": [ + "step", + [ + "zoom" + ], + "butt", + 14, + "round" + ], + "line-join": [ + "step", + [ + "zoom" + ], + "miter", + 14, + "round" + ] + }, + "paint": { + "line-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 12, + 0.5, + 18, + 20, + 22, + 200 + ], + "line-color": "hsl(0, 0%, 95%)" + }, + "metadata": { + "mapbox:featureComponent": "road-network", + "mapbox:group": "Road network, surface", + "microg:gms-type-feature": "road.local", + "microg:gms-type-element": "geometry.fill" + } + }, + { + "id": "road-secondary-tertiary", + "type": "line", + "source": "composite", + "source-layer": "road", + "minzoom": 9, + "filter": [ + "all", + [ + "match", + [ + "get", + "class" + ], + [ + "secondary", + "tertiary" + ], + true, + false + ], + [ + "match", + [ + "get", + "structure" + ], + [ + "none", + "ford" + ], + true, + false + ], + [ + "==", + [ + "geometry-type" + ], + "LineString" + ] + ], + "layout": { + "line-cap": [ + "step", + [ + "zoom" + ], + "butt", + 14, + "round" + ], + "line-join": [ + "step", + [ + "zoom" + ], + "miter", + 14, + "round" + ] + }, + "paint": { + "line-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 3, + 0, + 18, + 26, + 22, + 260 + ], + "line-color": "hsl(0, 0%, 95%)" + }, + "metadata": { + "mapbox:featureComponent": "road-network", + "mapbox:group": "Road network, surface", + "microg:gms-type-feature": "road.local", + "microg:gms-type-element": "geometry.fill" + } + }, + { + "id": "road-primary", + "type": "line", + "source": "composite", + "source-layer": "road", + "minzoom": 6, + "filter": [ + "all", + [ + "==", + [ + "get", + "class" + ], + "primary" + ], + [ + "match", + [ + "get", + "structure" + ], + [ + "none", + "ford" + ], + true, + false + ], + [ + "==", + [ + "geometry-type" + ], + "LineString" + ] + ], + "layout": { + "line-cap": [ + "step", + [ + "zoom" + ], + "butt", + 14, + "round" + ], + "line-join": [ + "step", + [ + "zoom" + ], + "miter", + 14, + "round" + ] + }, + "paint": { + "line-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 3, + 0.8, + 18, + 28, + 22, + 280 + ], + "line-color": "hsl(0, 0%, 95%)" + }, + "metadata": { + "mapbox:featureComponent": "road-network", + "mapbox:group": "Road network, surface", + "microg:gms-type-feature": "road.highway", + "microg:gms-type-element": "geometry.fill" + } + }, + { + "id": "road-motorway-trunk", + "type": "line", + "source": "composite", + "source-layer": "road", + "minzoom": 3, + "filter": [ + "all", + [ + "step", + [ + "zoom" + ], + [ + "match", + [ + "get", + "class" + ], + [ + "motorway", + "trunk" + ], + true, + false + ], + 5, + [ + "all", + [ + "match", + [ + "get", + "class" + ], + [ + "motorway", + "trunk" + ], + true, + false + ], + [ + "match", + [ + "get", + "structure" + ], + [ + "none", + "ford" + ], + true, + false + ] + ] + ], + [ + "==", + [ + "geometry-type" + ], + "LineString" + ] + ], + "layout": { + "line-cap": [ + "step", + [ + "zoom" + ], + "butt", + 13, + "round" + ], + "line-join": [ + "step", + [ + "zoom" + ], + "miter", + 13, + "round" + ] + }, + "paint": { + "line-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 3, + 0.8, + 18, + 30, + 22, + 300 + ], + "line-color": [ + "step", + [ + "zoom" + ], + [ + "match", + [ + "get", + "class" + ], + "motorway", + "hsl(15, 88%, 69%)", + "trunk", + "hsl(35, 81%, 59%)", + "hsl(60, 18%, 85%)" + ], + 9, + [ + "match", + [ + "get", + "class" + ], + "motorway", + "hsl(15, 100%, 75%)", + "hsl(35, 89%, 75%)" + ] + ], + "line-opacity": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 3, + 0, + 3.5, + 1 + ] + }, + "metadata": { + "mapbox:featureComponent": "road-network", + "mapbox:group": "Road network, surface", + "microg:gms-type-feature": "road.arterial", + "microg:gms-type-element": "geometry.fill" + } + }, + { + "id": "road-rail", + "type": "line", + "source": "composite", + "source-layer": "road", + "minzoom": 13, + "filter": [ + "all", + [ + "match", + [ + "get", + "class" + ], + [ + "major_rail", + "minor_rail" + ], + true, + false + ], + [ + "match", + [ + "get", + "structure" + ], + [ + "none", + "ford" + ], + true, + false + ] + ], + "paint": { + "line-color": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 13, + "hsl(75, 25%, 68%)", + 16, + "hsl(60, 0%, 56%)" + ], + "line-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 14, + 0.5, + 20, + 1 + ] + }, + "metadata": { + "mapbox:featureComponent": "transit", + "mapbox:group": "Transit, surface", + "microg:gms-type-feature": "transit.line", + "microg:gms-type-element": "geometry.fill" + } + }, + { + "id": "road-rail-tracks", + "type": "line", + "source": "composite", + "source-layer": "road", + "minzoom": 13, + "filter": [ + "all", + [ + "match", + [ + "get", + "class" + ], + [ + "major_rail", + "minor_rail" + ], + true, + false + ], + [ + "match", + [ + "get", + "structure" + ], + [ + "none", + "ford" + ], + true, + false + ] + ], + "paint": { + "line-color": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 13, + "hsl(75, 25%, 68%)", + 16, + "hsl(60, 0%, 56%)" + ], + "line-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 14, + 4, + 20, + 8 + ], + "line-dasharray": [ + 0.1, + 15 + ], + "line-opacity": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 13.75, + 0, + 14, + 1 + ] + }, + "metadata": { + "mapbox:featureComponent": "transit", + "mapbox:group": "Transit, surface", + "microg:gms-type-feature": "transit.line", + "microg:gms-type-element": "geometry.fill" + } + }, + { + "id": "level-crossing", + "type": "symbol", + "source": "composite", + "source-layer": "road", + "minzoom": 16, + "filter": [ + "==", + [ + "get", + "class" + ], + "level_crossing" + ], + "layout": { + "icon-image": "level-crossing", + "icon-rotation-alignment": "map", + "icon-allow-overlap": true, + "icon-ignore-placement": true + }, + "paint": {}, + "metadata": { + "mapbox:featureComponent": "road-network", + "mapbox:group": "Road network, surface-icons", + "microg:gms-type-feature": "road.local", + "microg:gms-type-element": "labels.icon" + } + }, + { + "id": "road-oneway-arrow-blue", + "type": "symbol", + "source": "composite", + "source-layer": "road", + "minzoom": 16, + "filter": [ + "all", + [ + "==", + [ + "get", + "oneway" + ], + "true" + ], + [ + "step", + [ + "zoom" + ], + [ + "match", + [ + "get", + "class" + ], + [ + "primary", + "secondary", + "tertiary", + "street", + "street_limited" + ], + true, + false + ], + 16, + [ + "match", + [ + "get", + "class" + ], + [ + "primary", + "secondary", + "tertiary", + "street", + "street_limited", + "primary_link", + "secondary_link", + "tertiary_link", + "service", + "track" + ], + true, + false + ] + ], + [ + "match", + [ + "get", + "structure" + ], + [ + "none", + "ford" + ], + true, + false + ] + ], + "layout": { + "symbol-placement": "line", + "icon-image": [ + "step", + [ + "zoom" + ], + "oneway-small", + 18, + "oneway-large" + ], + "symbol-spacing": 200, + "icon-rotation-alignment": "map", + "icon-allow-overlap": true, + "icon-ignore-placement": true + }, + "paint": {}, + "metadata": { + "mapbox:featureComponent": "road-network", + "mapbox:group": "Road network, surface-icons", + "microg:gms-type-feature": "road", + "microg:gms-type-element": "labels.icon" + } + }, + { + "id": "road-oneway-arrow-white", + "type": "symbol", + "source": "composite", + "source-layer": "road", + "minzoom": 16, + "filter": [ + "all", + [ + "==", + [ + "get", + "oneway" + ], + "true" + ], + [ + "match", + [ + "get", + "class" + ], + [ + "motorway", + "trunk", + "motorway_link", + "trunk_link" + ], + true, + false + ], + [ + "match", + [ + "get", + "structure" + ], + [ + "none", + "ford" + ], + true, + false + ] + ], + "layout": { + "symbol-placement": "line", + "icon-image": [ + "step", + [ + "zoom" + ], + "oneway-white-small", + 18, + "oneway-white-large" + ], + "symbol-spacing": 200, + "icon-rotation-alignment": "map", + "icon-allow-overlap": true, + "icon-ignore-placement": true + }, + "paint": {}, + "metadata": { + "mapbox:featureComponent": "road-network", + "mapbox:group": "Road network, surface-icons", + "microg:gms-type-feature": "road.arterial", + "microg:gms-type-element": "labels.icon" + } + }, + { + "id": "crosswalks", + "type": "symbol", + "source": "composite", + "source-layer": "structure", + "minzoom": 17, + "filter": [ + "all", + [ + "==", + [ + "get", + "type" + ], + "crosswalk" + ], + [ + "==", + [ + "geometry-type" + ], + "Point" + ] + ], + "layout": { + "icon-size": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 16, + 0.1, + 18, + 0.2, + 19, + 0.5, + 22, + 1.5 + ], + "icon-image": [ + "step", + [ + "zoom" + ], + "crosswalk-small", + 18, + "crosswalk-large" + ], + "icon-rotate": [ + "get", + "direction" + ], + "icon-rotation-alignment": "map", + "icon-allow-overlap": true, + "icon-ignore-placement": true + }, + "paint": {}, + "metadata": { + "mapbox:featureComponent": "road-network", + "mapbox:group": "Road network, surface-icons", + "microg:gms-type-feature": "road.local", + "microg:gms-type-element": "labels.icon" + } + }, + { + "id": "gate-fence-hedge", + "type": "line", + "source": "composite", + "source-layer": "structure", + "minzoom": 16, + "filter": [ + "match", + [ + "get", + "class" + ], + [ + "gate", + "fence", + "hedge" + ], + true, + false + ], + "layout": {}, + "paint": { + "line-color": [ + "match", + [ + "get", + "class" + ], + "hedge", + "hsl(98, 32%, 56%)", + "hsl(60, 25%, 63%)" + ], + "line-width": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 16, + 1, + 20, + 3 + ], + "line-opacity": [ + "match", + [ + "get", + "class" + ], + "gate", + 0.5, + 1 + ], + "line-dasharray": [ + 1, + 2, + 5, + 2, + 1, + 2 + ] + }, + "metadata": { + "mapbox:featureComponent": "walking-cycling", + "mapbox:group": "Walking, cycling, etc., barriers-bridges", + "microg:gms-type-feature": "landscape.man_made", + "microg:gms-type-element": "geometry.stroke" + } + }, + { + "id": "bridge-path-bg", + "type": "line", + "source": "composite", + "source-layer": "road", + "minzoom": 14, + "filter": [ + "all", + [ + "==", + [ + "get", + "structure" + ], + "bridge" + ], + [ + "==", + [ + "get", + "class" + ], + "path" + ], + [ + "step", + [ + "zoom" + ], + [ + "!", + [ + "match", + [ + "get", + "type" + ], + [ + "steps", + "sidewalk", + "crossing" + ], + true, + false + ] + ], + 16, + [ + "!=", + [ + "get", + "type" + ], + "steps" + ] + ], + [ + "==", + [ + "geometry-type" + ], + "LineString" + ] + ], + "layout": { + "line-cap": [ + "step", + [ + "zoom" + ], + "butt", + 14, + "round" + ] + }, + "paint": { + "line-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 15, + 2, + 18, + 7 + ], + "line-color": [ + "match", + [ + "get", + "type" + ], + "piste", + "hsl(215, 80%, 48%)", + [ + "mountain_bike", + "hiking", + "trail", + "cycleway", + "footway", + "path", + "bridleway" + ], + "hsl(35, 80%, 48%)", + "hsl(60, 1%, 64%)" + ] + }, + "metadata": { + "mapbox:featureComponent": "walking-cycling", + "mapbox:group": "Walking, cycling, etc., barriers-bridges", + "microg:gms-type-feature": "road.local", + "microg:gms-type-element": "geometry.fill" + } + }, + { + "id": "bridge-steps-bg", + "type": "line", + "source": "composite", + "source-layer": "road", + "minzoom": 14, + "filter": [ + "all", + [ + "==", + [ + "get", + "type" + ], + "steps" + ], + [ + "==", + [ + "get", + "structure" + ], + "bridge" + ], + [ + "==", + [ + "geometry-type" + ], + "LineString" + ] + ], + "paint": { + "line-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 15, + 2, + 17, + 4.6, + 18, + 7 + ], + "line-color": "hsl(35, 80%, 48%)", + "line-opacity": 0.75 + }, + "metadata": { + "mapbox:featureComponent": "walking-cycling", + "mapbox:group": "Walking, cycling, etc., barriers-bridges", + "microg:gms-type-feature": "road.local", + "microg:gms-type-element": "geometry.stroke" + } + }, + { + "id": "bridge-pedestrian-case", + "type": "line", + "source": "composite", + "source-layer": "road", + "minzoom": 14, + "filter": [ + "all", + [ + "==", + [ + "get", + "structure" + ], + "bridge" + ], + [ + "==", + [ + "get", + "class" + ], + "pedestrian" + ], + [ + "==", + [ + "geometry-type" + ], + "LineString" + ] + ], + "paint": { + "line-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 14, + 2, + 18, + 14.5 + ], + "line-color": "hsl(60, 10%, 70%)" + }, + "metadata": { + "mapbox:featureComponent": "walking-cycling", + "mapbox:group": "Walking, cycling, etc., barriers-bridges", + "microg:gms-type-feature": "road.local", + "microg:gms-type-element": "geometry.stroke" + } + }, + { + "id": "bridge-path-trail", + "type": "line", + "source": "composite", + "source-layer": "road", + "minzoom": 14, + "filter": [ + "all", + [ + "==", + [ + "get", + "structure" + ], + "bridge" + ], + [ + "==", + [ + "get", + "class" + ], + "path" + ], + [ + "match", + [ + "get", + "type" + ], + [ + "hiking", + "mountain_bike", + "trail" + ], + true, + false + ], + [ + "==", + [ + "geometry-type" + ], + "LineString" + ] + ], + "layout": {}, + "paint": { + "line-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 15, + 1, + 18, + 4 + ], + "line-color": "hsl(0, 0%, 95%)", + "line-dasharray": [ + "step", + [ + "zoom" + ], + [ + "literal", + [ + 5, + 0.5 + ] + ], + 15, + [ + "literal", + [ + 4, + 0.5 + ] + ], + 16, + [ + "literal", + [ + 4, + 0.45 + ] + ] + ] + }, + "metadata": { + "mapbox:featureComponent": "walking-cycling", + "mapbox:group": "Walking, cycling, etc., barriers-bridges", + "microg:gms-type-feature": "road.local", + "microg:gms-type-element": "geometry.stroke" + } + }, + { + "id": "bridge-path-cycleway-piste", + "type": "line", + "source": "composite", + "source-layer": "road", + "minzoom": 14, + "filter": [ + "all", + [ + "==", + [ + "get", + "structure" + ], + "bridge" + ], + [ + "==", + [ + "get", + "class" + ], + "path" + ], + [ + "match", + [ + "get", + "type" + ], + [ + "cycleway", + "piste" + ], + true, + false + ], + [ + "==", + [ + "geometry-type" + ], + "LineString" + ] + ], + "layout": {}, + "paint": { + "line-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 15, + 1, + 18, + 4 + ], + "line-color": "hsl(0, 0%, 95%)", + "line-dasharray": [ + 10, + 0 + ] + }, + "metadata": { + "mapbox:featureComponent": "walking-cycling", + "mapbox:group": "Walking, cycling, etc., barriers-bridges", + "microg:gms-type-feature": "road.local", + "microg:gms-type-element": "geometry.fill" + } + }, + { + "id": "bridge-path", + "type": "line", + "source": "composite", + "source-layer": "road", + "minzoom": 14, + "filter": [ + "all", + [ + "==", + [ + "get", + "structure" + ], + "bridge" + ], + [ + "==", + [ + "get", + "class" + ], + "path" + ], + [ + "!=", + [ + "get", + "type" + ], + "steps" + ], + [ + "==", + [ + "geometry-type" + ], + "LineString" + ] + ], + "layout": {}, + "paint": { + "line-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 15, + 1, + 18, + 4 + ], + "line-color": "hsl(0, 0%, 95%)", + "line-dasharray": [ + "step", + [ + "zoom" + ], + [ + "literal", + [ + 4, + 0.3 + ] + ], + 15, + [ + "literal", + [ + 1.75, + 0.3 + ] + ], + 16, + [ + "literal", + [ + 1, + 0.3 + ] + ], + 17, + [ + "literal", + [ + 1, + 0.25 + ] + ] + ] + }, + "metadata": { + "mapbox:featureComponent": "walking-cycling", + "mapbox:group": "Walking, cycling, etc., barriers-bridges", + "microg:gms-type-feature": "road.local", + "microg:gms-type-element": "geometry.fill" + } + }, + { + "id": "bridge-steps", + "type": "line", + "source": "composite", + "source-layer": "road", + "minzoom": 14, + "filter": [ + "all", + [ + "==", + [ + "get", + "type" + ], + "steps" + ], + [ + "==", + [ + "get", + "structure" + ], + "bridge" + ], + [ + "==", + [ + "geometry-type" + ], + "LineString" + ] + ], + "paint": { + "line-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 15, + 1, + 16, + 1.6, + 18, + 6 + ], + "line-color": "hsl(0, 0%, 95%)", + "line-dasharray": [ + "step", + [ + "zoom" + ], + [ + "literal", + [ + 1, + 0 + ] + ], + 15, + [ + "literal", + [ + 1.75, + 1 + ] + ], + 16, + [ + "literal", + [ + 1, + 0.75 + ] + ], + 17, + [ + "literal", + [ + 0.3, + 0.3 + ] + ] + ] + }, + "metadata": { + "mapbox:featureComponent": "walking-cycling", + "mapbox:group": "Walking, cycling, etc., barriers-bridges", + "microg:gms-type-feature": "road.local", + "microg:gms-type-element": "geometry.fill" + } + }, + { + "id": "bridge-pedestrian", + "type": "line", + "source": "composite", + "source-layer": "road", + "minzoom": 13, + "filter": [ + "all", + [ + "==", + [ + "get", + "structure" + ], + "bridge" + ], + [ + "==", + [ + "get", + "class" + ], + "pedestrian" + ], + [ + "==", + [ + "geometry-type" + ], + "LineString" + ] + ], + "paint": { + "line-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 14, + 0.5, + 18, + 12 + ], + "line-color": "hsl(0, 0%, 95%)", + "line-dasharray": [ + "step", + [ + "zoom" + ], + [ + "literal", + [ + 1, + 0 + ] + ], + 15, + [ + "literal", + [ + 1.5, + 0.4 + ] + ], + 16, + [ + "literal", + [ + 1, + 0.2 + ] + ] + ] + }, + "metadata": { + "mapbox:featureComponent": "walking-cycling", + "mapbox:group": "Walking, cycling, etc., barriers-bridges", + "microg:gms-type-feature": "road.local", + "microg:gms-type-element": "geometry.fill" + } + }, + { + "id": "gate-label", + "type": "symbol", + "source": "composite", + "source-layer": "structure", + "minzoom": 16, + "filter": [ + "==", + [ + "get", + "class" + ], + "gate" + ], + "layout": { + "icon-image": [ + "match", + [ + "get", + "type" + ], + "gate", + "gate", + "lift_gate", + "lift-gate", + "" + ] + }, + "paint": {}, + "metadata": { + "mapbox:featureComponent": "walking-cycling", + "mapbox:group": "Walking, cycling, etc., barriers-bridges", + "microg:gms-type-feature": "road", + "microg:gms-type-element": "labels.icon" + } + }, + { + "id": "bridge-minor-case", + "type": "line", + "source": "composite", + "source-layer": "road", + "minzoom": 13, + "filter": [ + "all", + [ + "==", + [ + "get", + "structure" + ], + "bridge" + ], + [ + "match", + [ + "get", + "class" + ], + [ + "track" + ], + true, + "service", + [ + "step", + [ + "zoom" + ], + false, + 14, + true + ], + false + ], + [ + "==", + [ + "geometry-type" + ], + "LineString" + ] + ], + "paint": { + "line-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 14, + 0.8, + 22, + 2 + ], + "line-color": [ + "match", + [ + "get", + "class" + ], + "track", + "hsl(35, 80%, 48%)", + "hsl(60, 10%, 70%)" + ], + "line-gap-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 14, + 1, + 18, + 10, + 22, + 100 + ] + }, + "metadata": { + "mapbox:featureComponent": "road-network", + "mapbox:group": "Road network, bridges", + "microg:gms-type-feature": "road.local", + "microg:gms-type-element": "geometry.stroke" + } + }, + { + "id": "bridge-street-case", + "type": "line", + "source": "composite", + "source-layer": "road", + "minzoom": 14, + "filter": [ + "all", + [ + "==", + [ + "get", + "structure" + ], + "bridge" + ], + [ + "match", + [ + "get", + "class" + ], + [ + "street", + "street_limited" + ], + true, + false + ], + [ + "==", + [ + "geometry-type" + ], + "LineString" + ] + ], + "paint": { + "line-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 14, + 0.8, + 22, + 2 + ], + "line-color": [ + "match", + [ + "get", + "class" + ], + "track", + "hsl(35, 80%, 48%)", + "hsl(60, 10%, 70%)" + ], + "line-gap-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 12, + 0.5, + 18, + 20, + 22, + 200 + ], + "line-opacity": [ + "step", + [ + "zoom" + ], + 0, + 14, + 1 + ] + }, + "metadata": { + "mapbox:featureComponent": "road-network", + "mapbox:group": "Road network, bridges", + "microg:gms-type-feature": "road.local", + "microg:gms-type-element": "geometry.stroke" + } + }, + { + "id": "bridge-minor-link-case", + "type": "line", + "source": "composite", + "source-layer": "road", + "minzoom": 13, + "filter": [ + "all", + [ + "match", + [ + "get", + "class" + ], + [ + "primary_link", + "secondary_link", + "tertiary_link" + ], + true, + false + ], + [ + "==", + [ + "get", + "structure" + ], + "bridge" + ], + [ + "==", + [ + "geometry-type" + ], + "LineString" + ] + ], + "layout": { + "line-join": [ + "step", + [ + "zoom" + ], + "miter", + 14, + "round" + ] + }, + "paint": { + "line-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 14, + 0.8, + 22, + 2 + ], + "line-color": "hsl(60, 10%, 70%)", + "line-gap-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 12, + 0.4, + 18, + 18, + 22, + 180 + ], + "line-opacity": [ + "step", + [ + "zoom" + ], + 0, + 11, + 1 + ] + }, + "metadata": { + "mapbox:featureComponent": "road-network", + "mapbox:group": "Road network, bridges", + "microg:gms-type-feature": "road.local", + "microg:gms-type-element": "geometry.stroke" + } + }, + { + "id": "bridge-secondary-tertiary-case", + "type": "line", + "source": "composite", + "source-layer": "road", + "minzoom": 11, + "filter": [ + "all", + [ + "==", + [ + "get", + "structure" + ], + "bridge" + ], + [ + "match", + [ + "get", + "class" + ], + [ + "secondary", + "tertiary" + ], + true, + false + ], + [ + "==", + [ + "geometry-type" + ], + "LineString" + ] + ], + "paint": { + "line-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 14, + 1, + 22, + 2 + ], + "line-color": "hsl(60, 10%, 70%)", + "line-gap-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 3, + 0, + 18, + 26, + 22, + 260 + ], + "line-opacity": [ + "step", + [ + "zoom" + ], + 0, + 10, + 1 + ] + }, + "metadata": { + "mapbox:featureComponent": "road-network", + "mapbox:group": "Road network, bridges", + "microg:gms-type-feature": "road.local", + "microg:gms-type-element": "geometry.stroke" + } + }, + { + "id": "bridge-primary-case", + "type": "line", + "source": "composite", + "source-layer": "road", + "minzoom": 10, + "filter": [ + "all", + [ + "==", + [ + "get", + "structure" + ], + "bridge" + ], + [ + "==", + [ + "get", + "class" + ], + "primary" + ], + [ + "==", + [ + "geometry-type" + ], + "LineString" + ] + ], + "paint": { + "line-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 14, + 1, + 22, + 2 + ], + "line-color": "hsl(60, 10%, 70%)", + "line-gap-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 3, + 0.8, + 18, + 28, + 22, + 280 + ], + "line-opacity": [ + "step", + [ + "zoom" + ], + 0, + 10, + 1 + ] + }, + "metadata": { + "mapbox:featureComponent": "road-network", + "mapbox:group": "Road network, bridges", + "microg:gms-type-feature": "road.highway", + "microg:gms-type-element": "geometry.stroke" + } + }, + { + "id": "bridge-major-link-case", + "type": "line", + "source": "composite", + "source-layer": "road", + "minzoom": 12, + "filter": [ + "all", + [ + "==", + [ + "get", + "structure" + ], + "bridge" + ], + [ + "match", + [ + "get", + "class" + ], + [ + "motorway_link", + "trunk_link" + ], + true, + false + ], + [ + "<=", + [ + "get", + "layer" + ], + 1 + ], + [ + "==", + [ + "geometry-type" + ], + "LineString" + ] + ], + "paint": { + "line-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 14, + 0.8, + 22, + 2 + ], + "line-color": "hsl(60, 10%, 82%)", + "line-gap-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 12, + 0.8, + 18, + 20, + 22, + 200 + ] + }, + "metadata": { + "mapbox:featureComponent": "road-network", + "mapbox:group": "Road network, bridges", + "microg:gms-type-feature": "road.highway", + "microg:gms-type-element": "geometry.stroke" + } + }, + { + "id": "bridge-motorway-trunk-case", + "type": "line", + "source": "composite", + "source-layer": "road", + "minzoom": 13, + "filter": [ + "all", + [ + "==", + [ + "get", + "structure" + ], + "bridge" + ], + [ + "match", + [ + "get", + "class" + ], + [ + "motorway", + "trunk" + ], + true, + false + ], + [ + "<=", + [ + "get", + "layer" + ], + 1 + ], + [ + "==", + [ + "geometry-type" + ], + "LineString" + ] + ], + "paint": { + "line-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 14, + 1, + 22, + 2 + ], + "line-color": "hsl(60, 10%, 82%)", + "line-gap-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 3, + 0.8, + 18, + 30, + 22, + 300 + ] + }, + "metadata": { + "mapbox:featureComponent": "road-network", + "mapbox:group": "Road network, bridges", + "microg:gms-type-feature": "road.arterial", + "microg:gms-type-element": "geometry.stroke" + } + }, + { + "id": "bridge-construction", + "type": "line", + "source": "composite", + "source-layer": "road", + "minzoom": 14, + "filter": [ + "all", + [ + "==", + [ + "get", + "structure" + ], + "bridge" + ], + [ + "==", + [ + "get", + "class" + ], + "construction" + ], + [ + "==", + [ + "geometry-type" + ], + "LineString" + ] + ], + "paint": { + "line-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 14, + 2, + 18, + 20, + 22, + 200 + ], + "line-color": "hsl(60, 10%, 70%)", + "line-dasharray": [ + "step", + [ + "zoom" + ], + [ + "literal", + [ + 0.4, + 0.8 + ] + ], + 15, + [ + "literal", + [ + 0.3, + 0.6 + ] + ], + 16, + [ + "literal", + [ + 0.2, + 0.3 + ] + ], + 17, + [ + "literal", + [ + 0.2, + 0.25 + ] + ], + 18, + [ + "literal", + [ + 0.15, + 0.15 + ] + ] + ] + }, + "metadata": { + "mapbox:featureComponent": "road-network", + "mapbox:group": "Road network, bridges", + "microg:gms-type-feature": "road.local", + "microg:gms-type-element": "geometry.fill" + } + }, + { + "id": "bridge-minor", + "type": "line", + "source": "composite", + "source-layer": "road", + "minzoom": 13, + "filter": [ + "all", + [ + "==", + [ + "get", + "structure" + ], + "bridge" + ], + [ + "match", + [ + "get", + "class" + ], + [ + "track" + ], + true, + "service", + [ + "step", + [ + "zoom" + ], + false, + 14, + true + ], + false + ], + [ + "==", + [ + "geometry-type" + ], + "LineString" + ] + ], + "layout": { + "line-cap": [ + "step", + [ + "zoom" + ], + "butt", + 14, + "round" + ] + }, + "paint": { + "line-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 14, + 1, + 18, + 10, + 22, + 100 + ], + "line-color": "hsl(0, 0%, 95%)" + }, + "metadata": { + "mapbox:featureComponent": "road-network", + "mapbox:group": "Road network, bridges", + "microg:gms-type-feature": "road.local", + "microg:gms-type-element": "geometry.fill" + } + }, + { + "id": "bridge-minor-link", + "type": "line", + "source": "composite", + "source-layer": "road", + "minzoom": 13, + "filter": [ + "all", + [ + "match", + [ + "get", + "class" + ], + [ + "primary_link", + "secondary_link", + "tertiary_link" + ], + true, + false + ], + [ + "==", + [ + "get", + "structure" + ], + "bridge" + ], + [ + "==", + [ + "geometry-type" + ], + "LineString" + ] + ], + "layout": { + "line-cap": [ + "step", + [ + "zoom" + ], + "butt", + 14, + "round" + ] + }, + "paint": { + "line-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 12, + 0.4, + 18, + 18, + 22, + 180 + ], + "line-color": "hsl(0, 0%, 95%)" + }, + "metadata": { + "mapbox:featureComponent": "road-network", + "mapbox:group": "Road network, bridges", + "microg:gms-type-feature": "road.local", + "microg:gms-type-element": "geometry.fill" + } + }, + { + "id": "bridge-major-link", + "type": "line", + "source": "composite", + "source-layer": "road", + "minzoom": 12, + "filter": [ + "all", + [ + "==", + [ + "get", + "structure" + ], + "bridge" + ], + [ + "match", + [ + "get", + "class" + ], + [ + "motorway_link", + "trunk_link" + ], + true, + false + ], + [ + "<=", + [ + "get", + "layer" + ], + 1 + ], + [ + "==", + [ + "geometry-type" + ], + "LineString" + ] + ], + "layout": { + "line-cap": [ + "step", + [ + "zoom" + ], + "butt", + 13, + "round" + ] + }, + "paint": { + "line-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 12, + 0.8, + 18, + 20, + 22, + 200 + ], + "line-color": [ + "match", + [ + "get", + "class" + ], + "motorway_link", + "hsl(15, 100%, 75%)", + "hsl(35, 89%, 75%)" + ] + }, + "metadata": { + "mapbox:featureComponent": "road-network", + "mapbox:group": "Road network, bridges", + "microg:gms-type-feature": "road.local", + "microg:gms-type-element": "geometry.fill" + } + }, + { + "id": "bridge-street", + "type": "line", + "source": "composite", + "source-layer": "road", + "minzoom": 13, + "filter": [ + "all", + [ + "==", + [ + "get", + "structure" + ], + "bridge" + ], + [ + "match", + [ + "get", + "class" + ], + [ + "street", + "street_limited" + ], + true, + false + ], + [ + "==", + [ + "geometry-type" + ], + "LineString" + ] + ], + "layout": { + "line-cap": [ + "step", + [ + "zoom" + ], + "butt", + 14, + "round" + ] + }, + "paint": { + "line-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 12, + 0.5, + 18, + 20, + 22, + 200 + ], + "line-color": [ + "match", + [ + "get", + "class" + ], + "street_limited", + "hsl(60, 22%, 80%)", + "hsl(0, 0%, 95%)" + ], + "line-opacity": [ + "step", + [ + "zoom" + ], + 0, + 14, + 1 + ] + }, + "metadata": { + "mapbox:featureComponent": "road-network", + "mapbox:group": "Road network, bridges", + "microg:gms-type-feature": "road.local", + "microg:gms-type-element": "geometry.fill" + } + }, + { + "id": "bridge-street-low", + "type": "line", + "source": "composite", + "source-layer": "road", + "minzoom": 13, + "maxzoom": 14, + "filter": [ + "all", + [ + "==", + [ + "get", + "structure" + ], + "bridge" + ], + [ + "match", + [ + "get", + "class" + ], + [ + "street", + "street_limited" + ], + true, + false + ], + [ + "==", + [ + "geometry-type" + ], + "LineString" + ] + ], + "layout": { + "line-cap": [ + "step", + [ + "zoom" + ], + "butt", + 14, + "round" + ], + "line-join": [ + "step", + [ + "zoom" + ], + "miter", + 14, + "round" + ] + }, + "paint": { + "line-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 12, + 0.5, + 18, + 20, + 22, + 200 + ], + "line-color": "hsl(0, 0%, 95%)" + }, + "metadata": { + "mapbox:featureComponent": "road-network", + "mapbox:group": "Road network, bridges", + "microg:gms-type-feature": "road.local", + "microg:gms-type-element": "geometry.fill" + } + }, + { + "id": "bridge-secondary-tertiary", + "type": "line", + "source": "composite", + "source-layer": "road", + "minzoom": 13, + "filter": [ + "all", + [ + "==", + [ + "get", + "structure" + ], + "bridge" + ], + [ + "match", + [ + "get", + "class" + ], + [ + "secondary", + "tertiary" + ], + true, + false + ], + [ + "==", + [ + "geometry-type" + ], + "LineString" + ] + ], + "layout": { + "line-cap": [ + "step", + [ + "zoom" + ], + "butt", + 14, + "round" + ] + }, + "paint": { + "line-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 3, + 0, + 18, + 26, + 22, + 260 + ], + "line-color": "hsl(0, 0%, 95%)" + }, + "metadata": { + "mapbox:featureComponent": "road-network", + "mapbox:group": "Road network, bridges", + "microg:gms-type-feature": "road.local", + "microg:gms-type-element": "geometry.fill" + } + }, + { + "id": "bridge-primary", + "type": "line", + "source": "composite", + "source-layer": "road", + "minzoom": 13, + "filter": [ + "all", + [ + "==", + [ + "get", + "structure" + ], + "bridge" + ], + [ + "==", + [ + "get", + "class" + ], + "primary" + ], + [ + "==", + [ + "geometry-type" + ], + "LineString" + ] + ], + "layout": { + "line-cap": [ + "step", + [ + "zoom" + ], + "butt", + 14, + "round" + ] + }, + "paint": { + "line-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 3, + 0.8, + 18, + 28, + 22, + 280 + ], + "line-color": "hsl(0, 0%, 95%)" + }, + "metadata": { + "mapbox:featureComponent": "road-network", + "mapbox:group": "Road network, bridges", + "microg:gms-type-feature": "road.highway", + "microg:gms-type-element": "geometry.fill" + } + }, + { + "id": "bridge-motorway-trunk", + "type": "line", + "source": "composite", + "source-layer": "road", + "minzoom": 13, + "filter": [ + "all", + [ + "==", + [ + "get", + "structure" + ], + "bridge" + ], + [ + "match", + [ + "get", + "class" + ], + [ + "motorway", + "trunk" + ], + true, + false + ], + [ + "<=", + [ + "get", + "layer" + ], + 1 + ], + [ + "==", + [ + "geometry-type" + ], + "LineString" + ] + ], + "layout": { + "line-cap": [ + "step", + [ + "zoom" + ], + "butt", + 14, + "round" + ] + }, + "paint": { + "line-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 3, + 0.8, + 18, + 30, + 22, + 300 + ], + "line-color": [ + "match", + [ + "get", + "class" + ], + "motorway", + "hsl(15, 100%, 75%)", + "hsl(35, 89%, 75%)" + ] + }, + "metadata": { + "mapbox:featureComponent": "road-network", + "mapbox:group": "Road network, bridges", + "microg:gms-type-feature": "road.arterial", + "microg:gms-type-element": "geometry.fill" + } + }, + { + "id": "bridge-major-link-2-case", + "type": "line", + "source": "composite", + "source-layer": "road", + "minzoom": 12, + "filter": [ + "all", + [ + "==", + [ + "get", + "structure" + ], + "bridge" + ], + [ + ">=", + [ + "get", + "layer" + ], + 2 + ], + [ + "match", + [ + "get", + "class" + ], + [ + "motorway_link", + "trunk_link" + ], + true, + false + ], + [ + "==", + [ + "geometry-type" + ], + "LineString" + ] + ], + "paint": { + "line-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 14, + 0.8, + 22, + 2 + ], + "line-color": "hsl(60, 10%, 82%)", + "line-gap-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 12, + 0.8, + 18, + 20, + 22, + 200 + ] + }, + "metadata": { + "mapbox:featureComponent": "road-network", + "mapbox:group": "Road network, bridges", + "microg:gms-type-feature": "road.highway", + "microg:gms-type-element": "geometry.stroke" + } + }, + { + "id": "bridge-motorway-trunk-2-case", + "type": "line", + "source": "composite", + "source-layer": "road", + "minzoom": 13, + "filter": [ + "all", + [ + "==", + [ + "get", + "structure" + ], + "bridge" + ], + [ + ">=", + [ + "get", + "layer" + ], + 2 + ], + [ + "match", + [ + "get", + "class" + ], + [ + "motorway", + "trunk" + ], + true, + false + ], + [ + "==", + [ + "geometry-type" + ], + "LineString" + ] + ], + "paint": { + "line-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 14, + 1, + 22, + 2 + ], + "line-color": "hsl(60, 10%, 82%)", + "line-gap-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 3, + 0.8, + 18, + 30, + 22, + 300 + ] + }, + "metadata": { + "mapbox:featureComponent": "road-network", + "mapbox:group": "Road network, bridges", + "microg:gms-type-feature": "road.arterial", + "microg:gms-type-element": "geometry.stroke" + } + }, + { + "id": "bridge-major-link-2", + "type": "line", + "source": "composite", + "source-layer": "road", + "minzoom": 12, + "filter": [ + "all", + [ + "==", + [ + "get", + "structure" + ], + "bridge" + ], + [ + ">=", + [ + "get", + "layer" + ], + 2 + ], + [ + "match", + [ + "get", + "class" + ], + [ + "motorway_link", + "trunk_link" + ], + true, + false + ], + [ + "==", + [ + "geometry-type" + ], + "LineString" + ] + ], + "layout": { + "line-cap": [ + "step", + [ + "zoom" + ], + "butt", + 13, + "round" + ] + }, + "paint": { + "line-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 12, + 0.8, + 18, + 20, + 22, + 200 + ], + "line-color": [ + "match", + [ + "get", + "class" + ], + "motorway_link", + "hsl(15, 100%, 75%)", + "hsl(35, 89%, 75%)" + ] + }, + "metadata": { + "mapbox:featureComponent": "road-network", + "mapbox:group": "Road network, bridges", + "microg:gms-type-feature": "road.highway", + "microg:gms-type-element": "geometry.fill" + } + }, + { + "id": "bridge-motorway-trunk-2", + "type": "line", + "source": "composite", + "source-layer": "road", + "minzoom": 13, + "filter": [ + "all", + [ + "==", + [ + "get", + "structure" + ], + "bridge" + ], + [ + ">=", + [ + "get", + "layer" + ], + 2 + ], + [ + "match", + [ + "get", + "class" + ], + [ + "motorway", + "trunk" + ], + true, + false + ], + [ + "==", + [ + "geometry-type" + ], + "LineString" + ] + ], + "layout": { + "line-cap": [ + "step", + [ + "zoom" + ], + "butt", + 14, + "round" + ] + }, + "paint": { + "line-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 3, + 0.8, + 18, + 30, + 22, + 300 + ], + "line-color": [ + "match", + [ + "get", + "class" + ], + "motorway", + "hsl(15, 100%, 75%)", + "hsl(35, 89%, 75%)" + ] + }, + "metadata": { + "mapbox:featureComponent": "road-network", + "mapbox:group": "Road network, bridges", + "microg:gms-type-feature": "road.arterial", + "microg:gms-type-element": "geometry.fill" + } + }, + { + "id": "bridge-oneway-arrow-blue", + "type": "symbol", + "source": "composite", + "source-layer": "road", + "minzoom": 16, + "filter": [ + "all", + [ + "==", + [ + "get", + "structure" + ], + "bridge" + ], + [ + "==", + [ + "get", + "oneway" + ], + "true" + ], + [ + "step", + [ + "zoom" + ], + [ + "match", + [ + "get", + "class" + ], + [ + "primary", + "secondary", + "tertiary", + "street", + "street_limited" + ], + true, + false + ], + 16, + [ + "match", + [ + "get", + "class" + ], + [ + "primary", + "secondary", + "tertiary", + "street", + "street_limited", + "primary_link", + "secondary_link", + "tertiary_link", + "service", + "track" + ], + true, + false + ] + ] + ], + "layout": { + "symbol-placement": "line", + "icon-image": [ + "step", + [ + "zoom" + ], + "oneway-small", + 18, + "oneway-large" + ], + "symbol-spacing": 200, + "icon-rotation-alignment": "map", + "icon-allow-overlap": true, + "icon-ignore-placement": true + }, + "paint": {}, + "metadata": { + "mapbox:featureComponent": "road-network", + "mapbox:group": "Road network, bridges", + "microg:gms-type-feature": "road", + "microg:gms-type-element": "labels.icon" + } + }, + { + "id": "bridge-oneway-arrow-white", + "type": "symbol", + "source": "composite", + "source-layer": "road", + "minzoom": 16, + "filter": [ + "all", + [ + "==", + [ + "get", + "structure" + ], + "bridge" + ], + [ + "match", + [ + "get", + "class" + ], + [ + "motorway", + "trunk", + "motorway_link", + "trunk_link" + ], + true, + false + ], + [ + "==", + [ + "get", + "oneway" + ], + "true" + ] + ], + "layout": { + "symbol-placement": "line", + "icon-image": "oneway-white-small", + "symbol-spacing": 200, + "icon-rotation-alignment": "map", + "icon-allow-overlap": true, + "icon-ignore-placement": true + }, + "paint": {}, + "metadata": { + "mapbox:featureComponent": "road-network", + "mapbox:group": "Road network, bridges", + "microg:gms-type-feature": "road.arterial", + "microg:gms-type-element": "labels.icon" + } + }, + { + "id": "bridge-rail", + "type": "line", + "source": "composite", + "source-layer": "road", + "minzoom": 13, + "filter": [ + "all", + [ + "==", + [ + "get", + "structure" + ], + "bridge" + ], + [ + "match", + [ + "get", + "class" + ], + [ + "major_rail", + "minor_rail" + ], + true, + false + ] + ], + "paint": { + "line-color": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 13, + "hsl(75, 25%, 68%)", + 16, + "hsl(60, 0%, 56%)" + ], + "line-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 14, + 0.5, + 20, + 1 + ] + }, + "metadata": { + "mapbox:featureComponent": "transit", + "mapbox:group": "Transit, bridges", + "microg:gms-type-feature": "transit.line", + "microg:gms-type-element": "geometry.fill" + } + }, + { + "id": "bridge-rail-tracks", + "type": "line", + "source": "composite", + "source-layer": "road", + "minzoom": 13, + "filter": [ + "all", + [ + "==", + [ + "get", + "structure" + ], + "bridge" + ], + [ + "match", + [ + "get", + "class" + ], + [ + "major_rail", + "minor_rail" + ], + true, + false + ] + ], + "paint": { + "line-color": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 13, + "hsl(75, 25%, 68%)", + 16, + "hsl(60, 0%, 56%)" + ], + "line-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 14, + 4, + 20, + 8 + ], + "line-dasharray": [ + 0.1, + 15 + ], + "line-opacity": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 13.75, + 0, + 14, + 1 + ] + }, + "metadata": { + "mapbox:featureComponent": "transit", + "mapbox:group": "Transit, bridges", + "microg:gms-type-feature": "transit.line", + "microg:gms-type-element": "geometry.fill" + } + }, + { + "id": "aerialway", + "type": "line", + "source": "composite", + "source-layer": "road", + "minzoom": 12, + "filter": [ + "==", + [ + "get", + "class" + ], + "aerialway" + ], + "paint": { + "line-color": "hsl(230, 50%, 60%)", + "line-width": [ + "interpolate", + [ + "exponential", + 1.5 + ], + [ + "zoom" + ], + 14, + 1, + 20, + 2 + ], + "line-dasharray": [ + 4, + 1 + ] + }, + "metadata": { + "mapbox:featureComponent": "transit", + "mapbox:group": "Transit, elevated", + "microg:gms-type-feature": "transit.line", + "microg:gms-type-element": "geometry.fill" + } + }, + { + "id": "admin-1-boundary-bg", + "type": "line", + "source": "composite", + "source-layer": "admin", + "minzoom": 7, + "filter": [ + "all", + [ + "==", + [ + "get", + "admin_level" + ], + 1 + ], + [ + "==", + [ + "get", + "maritime" + ], + "false" + ], + [ + "match", + [ + "get", + "worldview" + ], + [ + "all", + "US" + ], + true, + false + ] + ], + "paint": { + "line-color": "hsl(350, 90%, 88%)", + "line-width": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 3, + 3, + 12, + 6 + ], + "line-opacity": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 7, + 0, + 8, + 0.5 + ], + "line-dasharray": [ + 1, + 0 + ], + "line-blur": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 3, + 0, + 12, + 3 + ] + }, + "metadata": { + "mapbox:featureComponent": "admin-boundaries", + "mapbox:group": "Administrative boundaries, admin", + "microg:gms-type-feature": "administrative.province", + "microg:gms-type-element": "geometry.stroke" + } + }, + { + "id": "admin-0-boundary-bg", + "type": "line", + "source": "composite", + "source-layer": "admin", + "minzoom": 1, + "filter": [ + "all", + [ + "==", + [ + "get", + "admin_level" + ], + 0 + ], + [ + "==", + [ + "get", + "maritime" + ], + "false" + ], + [ + "match", + [ + "get", + "worldview" + ], + [ + "all", + "US" + ], + true, + false + ] + ], + "paint": { + "line-width": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 3, + 4, + 12, + 8 + ], + "line-color": "hsl(350, 90%, 88%)", + "line-opacity": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 3, + 0, + 4, + 0.5 + ], + "line-blur": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 3, + 0, + 12, + 2 + ] + }, + "metadata": { + "mapbox:featureComponent": "admin-boundaries", + "mapbox:group": "Administrative boundaries, admin", + "microg:gms-type-feature": "administrative.country", + "microg:gms-type-element": "geometry.stroke" + } + }, + { + "id": "admin-1-boundary", + "type": "line", + "source": "composite", + "source-layer": "admin", + "minzoom": 2, + "filter": [ + "all", + [ + "==", + [ + "get", + "admin_level" + ], + 1 + ], + [ + "==", + [ + "get", + "maritime" + ], + "false" + ], + [ + "match", + [ + "get", + "worldview" + ], + [ + "all", + "US" + ], + true, + false + ] + ], + "layout": {}, + "paint": { + "line-dasharray": [ + "step", + [ + "zoom" + ], + [ + "literal", + [ + 2, + 0 + ] + ], + 7, + [ + "literal", + [ + 2, + 2, + 6, + 2 + ] + ] + ], + "line-width": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 3, + 0.3, + 12, + 1.5 + ], + "line-opacity": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 2, + 0, + 3, + 1 + ], + "line-color": "hsl(350, 30%, 55%)" + }, + "metadata": { + "mapbox:featureComponent": "admin-boundaries", + "mapbox:group": "Administrative boundaries, admin", + "microg:gms-type-feature": "administrative.province", + "microg:gms-type-element": "geometry.fill" + } + }, + { + "id": "admin-0-boundary", + "type": "line", + "source": "composite", + "source-layer": "admin", + "minzoom": 1, + "filter": [ + "all", + [ + "==", + [ + "get", + "admin_level" + ], + 0 + ], + [ + "==", + [ + "get", + "disputed" + ], + "false" + ], + [ + "==", + [ + "get", + "maritime" + ], + "false" + ], + [ + "match", + [ + "get", + "worldview" + ], + [ + "all", + "US" + ], + true, + false + ] + ], + "layout": {}, + "paint": { + "line-color": "hsl(350, 30%, 50%)", + "line-width": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 3, + 0.5, + 12, + 2 + ], + "line-dasharray": [ + "step", + [ + "zoom" + ], + [ + "literal", + [ + 2, + 0 + ] + ], + 7, + [ + "literal", + [ + 2, + 2, + 6, + 2 + ] + ] + ] + }, + "metadata": { + "mapbox:featureComponent": "admin-boundaries", + "mapbox:group": "Administrative boundaries, admin", + "microg:gms-type-feature": "administrative.country", + "microg:gms-type-element": "geometry.fill" + } + }, + { + "id": "admin-0-boundary-disputed", + "type": "line", + "source": "composite", + "source-layer": "admin", + "minzoom": 1, + "filter": [ + "all", + [ + "==", + [ + "get", + "disputed" + ], + "true" + ], + [ + "==", + [ + "get", + "admin_level" + ], + 0 + ], + [ + "==", + [ + "get", + "maritime" + ], + "false" + ], + [ + "match", + [ + "get", + "worldview" + ], + [ + "all", + "US" + ], + true, + false + ] + ], + "paint": { + "line-color": "hsl(350, 30%, 50%)", + "line-width": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 3, + 0.5, + 12, + 2 + ], + "line-dasharray": [ + "step", + [ + "zoom" + ], + [ + "literal", + [ + 3, + 2, + 5 + ] + ], + 7, + [ + "literal", + [ + 2, + 1.5 + ] + ] + ] + }, + "metadata": { + "mapbox:featureComponent": "admin-boundaries", + "mapbox:group": "Administrative boundaries, admin", + "microg:gms-type-feature": "administrative.country", + "microg:gms-type-element": "geometry.fill" + } + }, + { + "id": "contour-label", + "type": "symbol", + "source": "composite", + "source-layer": "contour", + "minzoom": 11, + "filter": [ + "any", + [ + "==", + [ + "get", + "index" + ], + 10 + ], + [ + "==", + [ + "get", + "index" + ], + 5 + ] + ], + "layout": { + "text-field": [ + "concat", + [ + "get", + "ele" + ], + " m" + ], + "symbol-placement": "line", + "text-pitch-alignment": "viewport", + "text-max-angle": 25, + "text-padding": 5, + "text-font": [ + "DIN Pro Medium", + "Arial Unicode MS Regular" + ], + "text-size": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 15, + 9.5, + 20, + 12 + ] + }, + "paint": { + "text-color": "hsl(60, 10%, 35%)", + "text-halo-width": 1, + "text-halo-color": "hsl(60, 10%, 85%)" + }, + "metadata": { + "mapbox:featureComponent": "terrain", + "mapbox:group": "Terrain, terrain-labels", + "microg:gms-type-feature": "landscape.natural.terrain", + "microg:gms-type-element": "labels.text" + } + }, + { + "id": "building-entrance", + "type": "symbol", + "source": "composite", + "source-layer": "structure", + "minzoom": 18, + "filter": [ + "==", + [ + "get", + "class" + ], + "entrance" + ], + "layout": { + "icon-image": "marker", + "text-field": [ + "get", + "ref" + ], + "text-size": 10, + "text-offset": [ + 0, + -0.5 + ], + "text-font": [ + "DIN Pro Italic", + "Arial Unicode MS Regular" + ] + }, + "paint": { + "text-color": "hsl(60, 8%, 38%)", + "text-halo-color": "hsl(60, 13%, 77%)", + "text-halo-width": 1, + "icon-opacity": 0.4 + }, + "metadata": { + "mapbox:featureComponent": "buildings", + "mapbox:group": "Buildings, building-labels", + "microg:gms-type-feature": "poi", + "microg:gms-type-element": "labels.icon" + } + }, + { + "id": "building-number-label", + "type": "symbol", + "source": "composite", + "source-layer": "housenum_label", + "minzoom": 17, + "layout": { + "text-field": [ + "get", + "house_num" + ], + "text-font": [ + "DIN Pro Italic", + "Arial Unicode MS Regular" + ], + "text-padding": 4, + "text-max-width": 7, + "text-size": 10 + }, + "paint": { + "text-color": "hsl(60, 8%, 38%)", + "text-halo-color": "hsl(60, 13%, 77%)", + "text-halo-width": 1 + }, + "metadata": { + "mapbox:featureComponent": "buildings", + "mapbox:group": "Buildings, building-labels", + "microg:gms-type-feature": "landscape.man_made", + "microg:gms-type-element": "labels.text" + } + }, + { + "id": "block-number-label", + "type": "symbol", + "source": "composite", + "source-layer": "place_label", + "minzoom": 16, + "filter": [ + "all", + [ + "==", + [ + "get", + "class" + ], + "settlement_subdivision" + ], + [ + "==", + [ + "get", + "type" + ], + "block" + ] + ], + "layout": { + "text-field": [ + "get", + "name" + ], + "text-font": [ + "DIN Pro Italic", + "Arial Unicode MS Regular" + ], + "text-max-width": 7, + "text-size": 11 + }, + "paint": { + "text-color": "hsl(60, 18%, 44%)", + "text-halo-color": "hsl(60, 17%, 84%)", + "text-halo-width": 0.5, + "text-halo-blur": 0.5 + }, + "metadata": { + "mapbox:featureComponent": "buildings", + "mapbox:group": "Buildings, building-labels", + "microg:gms-type-feature": "landscape.man_made", + "microg:gms-type-element": "labels.text" + } + }, + { + "id": "road-label", + "type": "symbol", + "source": "composite", + "source-layer": "road", + "minzoom": 10, + "filter": [ + "all", + [ + "has", + "name" + ], + [ + "step", + [ + "zoom" + ], + [ + "match", + [ + "get", + "class" + ], + [ + "motorway", + "trunk", + "primary", + "secondary", + "tertiary" + ], + true, + false + ], + 12, + [ + "match", + [ + "get", + "class" + ], + [ + "motorway", + "trunk", + "primary", + "secondary", + "tertiary", + "street", + "street_limited", + "track" + ], + true, + false + ], + 15, + [ + "match", + [ + "get", + "class" + ], + [ + "path", + "pedestrian", + "golf", + "ferry", + "aerialway" + ], + false, + true + ] + ] + ], + "layout": { + "text-size": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 10, + [ + "match", + [ + "get", + "class" + ], + [ + "motorway", + "trunk", + "primary", + "secondary", + "tertiary" + ], + 10, + [ + "motorway_link", + "trunk_link", + "primary_link", + "secondary_link", + "tertiary_link", + "street", + "street_limited", + "track" + ], + 9, + 6.5 + ], + 18, + [ + "match", + [ + "get", + "class" + ], + [ + "motorway", + "trunk", + "primary", + "secondary", + "tertiary" + ], + 16, + [ + "motorway_link", + "trunk_link", + "primary_link", + "secondary_link", + "tertiary_link", + "street", + "street_limited", + "track" + ], + 14, + 13 + ] + ], + "text-max-angle": 30, + "text-font": [ + "DIN Pro Regular", + "Arial Unicode MS Regular" + ], + "symbol-placement": "line", + "text-padding": 1, + "text-rotation-alignment": "map", + "text-pitch-alignment": "viewport", + "text-field": [ + "coalesce", + [ + "get", + "name_en" + ], + [ + "get", + "name" + ] + ], + "text-letter-spacing": 0.01 + }, + "paint": { + "text-color": "hsl(0,0%, 0%)", + "text-halo-color": [ + "match", + [ + "get", + "class" + ], + [ + "motorway", + "trunk" + ], + "hsla(60, 25%, 100%, 0.75)", + "hsl(60, 25%, 100%)" + ], + "text-halo-width": 1, + "text-halo-blur": 1 + }, + "metadata": { + "mapbox:featureComponent": "road-network", + "mapbox:group": "Road network, road-labels", + "microg:gms-type-feature": "road", + "microg:gms-type-element": "labels.text" + } + }, + { + "id": "road-intersection", + "type": "symbol", + "source": "composite", + "source-layer": "road", + "minzoom": 15, + "filter": [ + "all", + [ + "==", + [ + "get", + "class" + ], + "intersection" + ], + [ + "has", + "name" + ] + ], + "layout": { + "text-field": [ + "coalesce", + [ + "get", + "name_en" + ], + [ + "get", + "name" + ] + ], + "icon-image": "intersection", + "icon-text-fit": "both", + "icon-text-fit-padding": [ + 1, + 2, + 1, + 2 + ], + "text-size": [ + "interpolate", + [ + "exponential", + 1.2 + ], + [ + "zoom" + ], + 15, + 9, + 18, + 12 + ], + "text-font": [ + "DIN Pro Bold", + "Arial Unicode MS Bold" + ] + }, + "paint": { + "text-color": "hsl(230, 36%, 64%)" + }, + "metadata": { + "mapbox:featureComponent": "road-network", + "mapbox:group": "Road network, road-labels", + "microg:gms-type-feature": "road", + "microg:gms-type-element": "labels.text" + } + }, + { + "id": "road-number-shield", + "type": "symbol", + "source": "composite", + "source-layer": "road", + "minzoom": 6, + "filter": [ + "all", + [ + "has", + "reflen" + ], + [ + "<=", + [ + "get", + "reflen" + ], + 6 + ], + [ + "match", + [ + "get", + "class" + ], + [ + "pedestrian", + "service" + ], + false, + true + ], + [ + "step", + [ + "zoom" + ], + [ + "==", + [ + "geometry-type" + ], + "Point" + ], + 11, + [ + ">", + [ + "get", + "len" + ], + 5000 + ], + 12, + [ + ">", + [ + "get", + "len" + ], + 2500 + ], + 13, + [ + ">", + [ + "get", + "len" + ], + 1000 + ], + 14, + true + ] + ], + "layout": { + "text-size": 9, + "icon-image": [ + "case", + [ + "has", + "shield_beta" + ], + [ + "coalesce", + [ + "image", + [ + "concat", + [ + "get", + "shield_beta" + ], + "-", + [ + "to-string", + [ + "get", + "reflen" + ] + ] + ] + ], + [ + "image", + [ + "concat", + "default-", + [ + "to-string", + [ + "get", + "reflen" + ] + ] + ] + ] + ], + [ + "concat", + [ + "get", + "shield" + ], + "-", + [ + "to-string", + [ + "get", + "reflen" + ] + ] + ] + ], + "icon-rotation-alignment": "viewport", + "text-max-angle": 38, + "symbol-spacing": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 11, + 400, + 14, + 600 + ], + "text-font": [ + "DIN Pro Bold", + "Arial Unicode MS Bold" + ], + "symbol-placement": [ + "step", + [ + "zoom" + ], + "point", + 11, + "line" + ], + "text-rotation-alignment": "viewport", + "text-field": [ + "get", + "ref" + ], + "text-letter-spacing": 0.05 + }, + "paint": { + "text-color": [ + "case", + [ + "all", + [ + "has", + "shield_text_color_beta" + ], + [ + "to-boolean", + [ + "coalesce", + [ + "image", + [ + "concat", + [ + "get", + "shield_beta" + ], + "-", + [ + "to-string", + [ + "get", + "reflen" + ] + ] + ] + ], + "" + ] + ] + ], + [ + "match", + [ + "get", + "shield_text_color_beta" + ], + "white", + "hsl(0, 0%, 100%)", + "yellow", + "hsl(50, 63%, 70%)", + "orange", + "hsl(25, 63%, 75%)", + "blue", + "hsl(230, 36%, 44%)", + "red", + "hsl(0, 54%, 59%)", + "green", + "hsl(140, 46%, 37%)", + "hsl(230, 11%, 13%)" + ], + [ + "match", + [ + "get", + "shield_text_color" + ], + "white", + "hsl(0, 0%, 100%)", + "yellow", + "hsl(50, 63%, 70%)", + "orange", + "hsl(25, 63%, 75%)", + "blue", + "hsl(230, 36%, 44%)", + "red", + "hsl(0, 54%, 59%)", + "green", + "hsl(140, 46%, 37%)", + "hsl(230, 11%, 13%)" + ] + ] + }, + "metadata": { + "mapbox:featureComponent": "road-network", + "mapbox:group": "Road network, road-labels", + "microg:gms-type-feature": "road", + "microg:gms-type-element": "labels.icon" + } + }, + { + "id": "road-exit-shield", + "type": "symbol", + "source": "composite", + "source-layer": "motorway_junction", + "minzoom": 14, + "filter": [ + "all", + [ + "has", + "reflen" + ], + [ + "<=", + [ + "get", + "reflen" + ], + 9 + ] + ], + "layout": { + "text-field": [ + "get", + "ref" + ], + "text-size": 9, + "icon-image": [ + "concat", + "motorway-exit-", + [ + "to-string", + [ + "get", + "reflen" + ] + ] + ], + "text-font": [ + "DIN Pro Bold", + "Arial Unicode MS Bold" + ] + }, + "paint": { + "text-color": "hsl(0, 0%, 100%)", + "text-translate": [ + 0, + 0 + ] + }, + "metadata": { + "mapbox:featureComponent": "road-network", + "mapbox:group": "Road network, road-labels", + "microg:gms-type-feature": "road", + "microg:gms-type-element": "labels.icon" + } + }, + { + "id": "path-pedestrian-label", + "type": "symbol", + "source": "composite", + "source-layer": "road", + "minzoom": 12, + "filter": [ + "all", + [ + "case", + [ + "has", + "layer" + ], + [ + ">=", + [ + "get", + "layer" + ], + 0 + ], + true + ], + [ + "step", + [ + "zoom" + ], + [ + "match", + [ + "get", + "class" + ], + [ + "pedestrian" + ], + true, + false + ], + 15, + [ + "match", + [ + "get", + "class" + ], + [ + "path", + "pedestrian" + ], + true, + false + ] + ] + ], + "layout": { + "text-size": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 10, + [ + "match", + [ + "get", + "class" + ], + "pedestrian", + 9, + 6.5 + ], + 18, + [ + "match", + [ + "get", + "class" + ], + "pedestrian", + 14, + 13 + ] + ], + "text-max-angle": 30, + "text-font": [ + "DIN Pro Regular", + "Arial Unicode MS Regular" + ], + "symbol-placement": "line", + "text-padding": 1, + "text-rotation-alignment": "map", + "text-pitch-alignment": "viewport", + "text-field": [ + "coalesce", + [ + "get", + "name_en" + ], + [ + "get", + "name" + ] + ], + "text-letter-spacing": 0.01 + }, + "paint": { + "text-color": "hsl(0,0%, 0%)", + "text-halo-color": "hsl(60, 25%, 100%)", + "text-halo-width": 1, + "text-halo-blur": 1 + }, + "metadata": { + "mapbox:featureComponent": "walking-cycling", + "mapbox:group": "Walking, cycling, etc., walking-cycling-labels", + "microg:gms-type-feature": "road.local", + "microg:gms-type-element": "labels.text" + } + }, + { + "id": "golf-hole-label", + "type": "symbol", + "source": "composite", + "source-layer": "road", + "minzoom": 16, + "filter": [ + "==", + [ + "get", + "class" + ], + "golf" + ], + "layout": { + "text-field": [ + "coalesce", + [ + "get", + "name_en" + ], + [ + "get", + "name" + ] + ], + "text-font": [ + "DIN Pro Medium", + "Arial Unicode MS Regular" + ], + "text-size": 12 + }, + "paint": { + "text-halo-color": "hsl(98, 60%, 55%)", + "text-halo-width": 0.5, + "text-halo-blur": 0.5, + "text-color": "hsl(100, 80%, 18%)" + }, + "metadata": { + "mapbox:featureComponent": "walking-cycling", + "mapbox:group": "Walking, cycling, etc., walking-cycling-labels", + "microg:gms-type-feature": "poi.attraction", + "microg:gms-type-element": "labels.text" + } + }, + { + "id": "ferry-aerialway-label", + "type": "symbol", + "source": "composite", + "source-layer": "road", + "minzoom": 15, + "filter": [ + "match", + [ + "get", + "class" + ], + "aerialway", + true, + "ferry", + true, + false + ], + "layout": { + "text-size": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 10, + 6.5, + 18, + 13 + ], + "text-max-angle": 30, + "text-font": [ + "DIN Pro Regular", + "Arial Unicode MS Regular" + ], + "symbol-placement": "line", + "text-padding": 1, + "text-rotation-alignment": "map", + "text-pitch-alignment": "viewport", + "text-field": [ + "coalesce", + [ + "get", + "name_en" + ], + [ + "get", + "name" + ] + ], + "text-letter-spacing": 0.01 + }, + "paint": { + "text-color": [ + "match", + [ + "get", + "class" + ], + "ferry", + "hsl(205, 43%, 100%)", + "hsl(230, 50%, 60%)" + ], + "text-halo-color": [ + "match", + [ + "get", + "class" + ], + "ferry", + "hsl(205, 75%, 70%)", + "hsl(60, 20%, 100%)" + ], + "text-halo-width": 1, + "text-halo-blur": 1 + }, + "metadata": { + "mapbox:featureComponent": "transit", + "mapbox:group": "Transit, ferry-aerialway-labels", + "microg:gms-type-feature": "transit.line", + "microg:gms-type-element": "labels.text" + } + }, + { + "id": "waterway-label", + "type": "symbol", + "source": "composite", + "source-layer": "natural_label", + "minzoom": 13, + "filter": [ + "all", + [ + "match", + [ + "get", + "class" + ], + [ + "canal", + "river", + "stream", + "disputed_canal", + "disputed_river", + "disputed_stream" + ], + [ + "match", + [ + "get", + "worldview" + ], + [ + "all", + "US" + ], + true, + false + ], + false + ], + [ + "==", + [ + "geometry-type" + ], + "LineString" + ] + ], + "layout": { + "text-font": [ + "DIN Pro Italic", + "Arial Unicode MS Regular" + ], + "text-max-angle": 30, + "symbol-spacing": [ + "interpolate", + [ + "linear", + 1 + ], + [ + "zoom" + ], + 15, + 250, + 17, + 400 + ], + "text-size": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 13, + 12, + 18, + 18 + ], + "symbol-placement": "line", + "text-pitch-alignment": "viewport", + "text-field": [ + "coalesce", + [ + "get", + "name_en" + ], + [ + "get", + "name" + ] + ] + }, + "paint": { + "text-color": "hsl(205, 43%, 90%)", + "text-halo-color": "hsla(60, 17%, 84%, 0.5)" + }, + "metadata": { + "mapbox:featureComponent": "natural-features", + "mapbox:group": "Natural features, natural-labels", + "microg:gms-type-feature": "water", + "microg:gms-type-element": "labels.text" + } + }, + { + "id": "natural-line-label", + "type": "symbol", + "metadata": { + "mapbox:featureComponent": "natural-features", + "mapbox:group": "Natural features, natural-labels", + "microg:gms-type-feature": "landscape.natural.landcover", + "microg:gms-type-element": "labels.text" + }, + "source": "composite", + "source-layer": "natural_label", + "minzoom": 4, + "filter": [ + "all", + [ + "match", + [ + "get", + "class" + ], + [ + "glacier", + "landform", + "disputed_glacier", + "disputed_landform" + ], + [ + "match", + [ + "get", + "worldview" + ], + [ + "all", + "US" + ], + true, + false + ], + false + ], + [ + "<=", + [ + "get", + "filterrank" + ], + 4 + ], + [ + "==", + [ + "geometry-type" + ], + "LineString" + ] + ], + "layout": { + "text-size": [ + "step", + [ + "zoom" + ], + [ + "step", + [ + "get", + "sizerank" + ], + 18, + 5, + 12 + ], + 17, + [ + "step", + [ + "get", + "sizerank" + ], + 18, + 13, + 12 + ] + ], + "text-max-angle": 30, + "text-field": [ + "coalesce", + [ + "get", + "name_en" + ], + [ + "get", + "name" + ] + ], + "text-font": [ + "DIN Pro Medium", + "Arial Unicode MS Regular" + ], + "symbol-placement": "line-center", + "text-pitch-alignment": "viewport" + }, + "paint": { + "text-halo-width": 0.5, + "text-halo-color": "hsl(60, 17%, 84%)", + "text-halo-blur": 0.5, + "text-color": "hsl(340, 10%, 38%)" + } + }, + { + "id": "natural-point-label", + "type": "symbol", + "source": "composite", + "source-layer": "natural_label", + "minzoom": 4, + "filter": [ + "all", + [ + "match", + [ + "get", + "class" + ], + [ + "dock", + "glacier", + "landform", + "water_feature", + "wetland", + "disputed_dock", + "disputed_glacier", + "disputed_landform", + "disputed_water_feature", + "disputed_wetland" + ], + [ + "match", + [ + "get", + "worldview" + ], + [ + "all", + "US" + ], + true, + false + ], + false + ], + [ + "<=", + [ + "get", + "filterrank" + ], + 4 + ], + [ + "==", + [ + "geometry-type" + ], + "Point" + ] + ], + "layout": { + "text-size": [ + "step", + [ + "zoom" + ], + [ + "step", + [ + "get", + "sizerank" + ], + 18, + 5, + 12 + ], + 17, + [ + "step", + [ + "get", + "sizerank" + ], + 18, + 13, + 12 + ] + ], + "icon-image": [ + "get", + "maki" + ], + "text-font": [ + "DIN Pro Medium", + "Arial Unicode MS Regular" + ], + "text-offset": [ + "step", + [ + "zoom" + ], + [ + "step", + [ + "get", + "sizerank" + ], + [ + "literal", + [ + 0, + 0 + ] + ], + 5, + [ + "literal", + [ + 0, + 0.8 + ] + ] + ], + 17, + [ + "step", + [ + "get", + "sizerank" + ], + [ + "literal", + [ + 0, + 0 + ] + ], + 13, + [ + "literal", + [ + 0, + 0.8 + ] + ] + ] + ], + "text-anchor": [ + "step", + [ + "zoom" + ], + [ + "step", + [ + "get", + "sizerank" + ], + "center", + 5, + "top" + ], + 17, + [ + "step", + [ + "get", + "sizerank" + ], + "center", + 13, + "top" + ] + ], + "text-field": [ + "coalesce", + [ + "get", + "name_en" + ], + [ + "get", + "name" + ] + ] + }, + "paint": { + "icon-opacity": [ + "step", + [ + "zoom" + ], + [ + "step", + [ + "get", + "sizerank" + ], + 0, + 5, + 1 + ], + 17, + [ + "step", + [ + "get", + "sizerank" + ], + 0, + 13, + 1 + ] + ], + "text-halo-color": "hsl(60, 20%, 100%)", + "text-halo-width": 0.5, + "text-halo-blur": 0.5, + "text-color": "hsl(340, 10%, 38%)" + }, + "metadata": { + "mapbox:featureComponent": "natural-features", + "mapbox:group": "Natural features, natural-labels", + "microg:gms-type-feature": "landscape.natural.landcover", + "microg:gms-type-element": "labels.text" + } + }, + { + "id": "water-line-label", + "type": "symbol", + "metadata": { + "mapbox:featureComponent": "natural-features", + "mapbox:group": "Natural features, natural-labels", + "microg:gms-type-feature": "water", + "microg:gms-type-element": "labels.text" + }, + "source": "composite", + "source-layer": "natural_label", + "minzoom": 1, + "filter": [ + "all", + [ + "match", + [ + "get", + "class" + ], + [ + "bay", + "ocean", + "reservoir", + "sea", + "water", + "disputed_bay", + "disputed_ocean", + "disputed_reservoir", + "disputed_sea", + "disputed_water" + ], + [ + "match", + [ + "get", + "worldview" + ], + [ + "all", + "US" + ], + true, + false + ], + false + ], + [ + "==", + [ + "geometry-type" + ], + "LineString" + ] + ], + "layout": { + "text-size": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 0, + [ + "*", + [ + "-", + 16, + [ + "sqrt", + [ + "get", + "sizerank" + ] + ] + ], + 1 + ], + 22, + [ + "*", + [ + "-", + 22, + [ + "sqrt", + [ + "get", + "sizerank" + ] + ] + ], + 1 + ] + ], + "text-max-angle": 30, + "text-letter-spacing": [ + "match", + [ + "get", + "class" + ], + "ocean", + 0.25, + [ + "sea", + "bay" + ], + 0.15, + 0 + ], + "text-font": [ + "DIN Pro Italic", + "Arial Unicode MS Regular" + ], + "symbol-placement": "line-center", + "text-pitch-alignment": "viewport", + "text-field": [ + "coalesce", + [ + "get", + "name_en" + ], + [ + "get", + "name" + ] + ] + }, + "paint": { + "text-color": [ + "match", + [ + "get", + "class" + ], + [ + "bay", + "ocean", + "sea" + ], + "hsl(205, 71%, 90%)", + "hsl(205, 43%, 90%)" + ], + "text-halo-color": "hsla(60, 17%, 84%, 0.5)" + } + }, + { + "id": "water-point-label", + "type": "symbol", + "source": "composite", + "source-layer": "natural_label", + "minzoom": 1, + "filter": [ + "all", + [ + "match", + [ + "get", + "class" + ], + [ + "bay", + "ocean", + "reservoir", + "sea", + "water", + "disputed_bay", + "disputed_ocean", + "disputed_reservoir", + "disputed_sea", + "disputed_water" + ], + [ + "match", + [ + "get", + "worldview" + ], + [ + "all", + "US" + ], + true, + false + ], + false + ], + [ + "==", + [ + "geometry-type" + ], + "Point" + ] + ], + "layout": { + "text-line-height": 1.3, + "text-size": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 0, + [ + "*", + [ + "-", + 16, + [ + "sqrt", + [ + "get", + "sizerank" + ] + ] + ], + 1 + ], + 22, + [ + "*", + [ + "-", + 22, + [ + "sqrt", + [ + "get", + "sizerank" + ] + ] + ], + 1 + ] + ], + "text-font": [ + "DIN Pro Italic", + "Arial Unicode MS Regular" + ], + "text-field": [ + "coalesce", + [ + "get", + "name_en" + ], + [ + "get", + "name" + ] + ], + "text-letter-spacing": [ + "match", + [ + "get", + "class" + ], + "ocean", + 0.25, + [ + "bay", + "sea" + ], + 0.15, + 0.01 + ], + "text-max-width": [ + "match", + [ + "get", + "class" + ], + "ocean", + 4, + "sea", + 5, + [ + "bay", + "water" + ], + 7, + 10 + ] + }, + "paint": { + "text-color": [ + "match", + [ + "get", + "class" + ], + [ + "bay", + "ocean", + "sea" + ], + "hsl(205, 71%, 90%)", + "hsl(205, 43%, 90%)" + ], + "text-halo-color": "hsla(60, 17%, 84%, 0.5)" + }, + "metadata": { + "mapbox:featureComponent": "natural-features", + "mapbox:group": "Natural features, natural-labels", + "microg:gms-type-feature": "water", + "microg:gms-type-element": "labels.text" + } + }, + { + "id": "poi-label", + "type": "symbol", + "source": "composite", + "source-layer": "poi_label", + "minzoom": 6, + "filter": [ + "<=", + [ + "get", + "filterrank" + ], + [ + "+", + [ + "step", + [ + "zoom" + ], + 0, + 16, + 1, + 17, + 2 + ], + [ + "match", + [ + "get", + "class" + ], + "food_and_drink_stores", + 3, + "historic", + 3, + "landmark", + 3, + "medical", + 3, + "motorist", + 3, + "park_like", + 4, + "sport_and_leisure", + 4, + "visitor_amenities", + 4, + 2 + ] + ] + ], + "layout": { + "text-size": [ + "step", + [ + "zoom" + ], + [ + "step", + [ + "get", + "sizerank" + ], + 18, + 5, + 12 + ], + 17, + [ + "step", + [ + "get", + "sizerank" + ], + 18, + 13, + 12 + ] + ], + "icon-image": [ + "case", + [ + "has", + "maki_beta" + ], + [ + "coalesce", + [ + "image", + [ + "get", + "maki_beta" + ] + ], + [ + "image", + [ + "get", + "maki" + ] + ] + ], + [ + "image", + [ + "get", + "maki" + ] + ] + ], + "text-font": [ + "DIN Pro Medium", + "Arial Unicode MS Regular" + ], + "text-offset": [ + "step", + [ + "zoom" + ], + [ + "step", + [ + "get", + "sizerank" + ], + [ + "literal", + [ + 0, + 0 + ] + ], + 5, + [ + "literal", + [ + 0, + 0.8 + ] + ] + ], + 17, + [ + "step", + [ + "get", + "sizerank" + ], + [ + "literal", + [ + 0, + 0 + ] + ], + 13, + [ + "literal", + [ + 0, + 0.8 + ] + ] + ] + ], + "text-anchor": [ + "step", + [ + "zoom" + ], + [ + "step", + [ + "get", + "sizerank" + ], + "center", + 5, + "top" + ], + 17, + [ + "step", + [ + "get", + "sizerank" + ], + "center", + 13, + "top" + ] + ], + "text-field": [ + "coalesce", + [ + "get", + "name_en" + ], + [ + "get", + "name" + ] + ] + }, + "paint": { + "icon-opacity": [ + "step", + [ + "zoom" + ], + [ + "step", + [ + "get", + "sizerank" + ], + 0, + 5, + 1 + ], + 17, + [ + "step", + [ + "get", + "sizerank" + ], + 0, + 13, + 1 + ] + ], + "text-halo-color": "hsl(60, 20%, 100%)", + "text-halo-width": 0.5, + "text-halo-blur": 0.5, + "text-color": [ + "match", + [ + "get", + "class" + ], + "food_and_drink", + "hsl(35, 80%, 38%)", + "park_like", + "hsl(100, 80%, 18%)", + "education", + "hsl(30, 60%, 28%)", + "medical", + "hsl(10, 60%, 43%)", + "sport_and_leisure", + "hsl(210, 60%, 38%)", + "hsl(340, 10%, 38%)" + ] + }, + "metadata": { + "mapbox:featureComponent": "point-of-interest-labels", + "mapbox:group": "Point of interest labels, poi-labels", + "microg:gms-type-feature": "poi", + "microg:gms-type-element": "labels.text" + } + }, + { + "id": "transit-label", + "type": "symbol", + "source": "composite", + "source-layer": "transit_stop_label", + "minzoom": 12, + "filter": [ + "step", + [ + "zoom" + ], + [ + "all", + [ + "<=", + [ + "get", + "filterrank" + ], + 4 + ], + [ + "match", + [ + "get", + "mode" + ], + "rail", + true, + "metro_rail", + true, + false + ], + [ + "!=", + [ + "get", + "stop_type" + ], + "entrance" + ] + ], + 14, + [ + "all", + [ + "match", + [ + "get", + "mode" + ], + "rail", + true, + "metro_rail", + true, + false + ], + [ + "!=", + [ + "get", + "stop_type" + ], + "entrance" + ] + ], + 15, + [ + "all", + [ + "match", + [ + "get", + "mode" + ], + "rail", + true, + "metro_rail", + true, + "ferry", + true, + "light_rail", + true, + false + ], + [ + "!=", + [ + "get", + "stop_type" + ], + "entrance" + ] + ], + 16, + [ + "all", + [ + "match", + [ + "get", + "mode" + ], + "bus", + false, + true + ], + [ + "!=", + [ + "get", + "stop_type" + ], + "entrance" + ] + ], + 17, + [ + "!=", + [ + "get", + "stop_type" + ], + "entrance" + ], + 19, + true + ], + "layout": { + "text-size": 12, + "icon-image": [ + "get", + "network" + ], + "text-font": [ + "DIN Pro Medium", + "Arial Unicode MS Regular" + ], + "text-justify": [ + "match", + [ + "get", + "stop_type" + ], + "entrance", + "left", + "center" + ], + "text-offset": [ + "match", + [ + "get", + "stop_type" + ], + "entrance", + [ + "literal", + [ + 1, + 0 + ] + ], + [ + "literal", + [ + 0, + 0.8 + ] + ] + ], + "text-anchor": [ + "match", + [ + "get", + "stop_type" + ], + "entrance", + "left", + "top" + ], + "text-field": [ + "step", + [ + "zoom" + ], + "", + 13, + [ + "match", + [ + "get", + "mode" + ], + [ + "rail", + "metro_rail" + ], + [ + "coalesce", + [ + "get", + "name_en" + ], + [ + "get", + "name" + ] + ], + "" + ], + 14, + [ + "match", + [ + "get", + "mode" + ], + [ + "bus", + "bicycle" + ], + "", + [ + "coalesce", + [ + "get", + "name_en" + ], + [ + "get", + "name" + ] + ] + ], + 18, + [ + "coalesce", + [ + "get", + "name_en" + ], + [ + "get", + "name" + ] + ] + ], + "text-letter-spacing": 0.01, + "text-max-width": [ + "match", + [ + "get", + "stop_type" + ], + "entrance", + 15, + 9 + ] + }, + "paint": { + "text-halo-color": "hsl(60, 20%, 100%)", + "text-color": [ + "match", + [ + "get", + "network" + ], + "tokyo-metro", + "hsl(180, 30%, 30%)", + "mexico-city-metro", + "hsl(25, 63%, 63%)", + [ + "barcelona-metro", + "delhi-metro", + "hong-kong-mtr", + "milan-metro", + "osaka-subway" + ], + "hsl(0, 57%, 47%)", + [ + "boston-t", + "washington-metro" + ], + "hsl(230, 11%, 20%)", + [ + "chongqing-rail-transit", + "kiev-metro", + "singapore-mrt", + "taipei-metro" + ], + "hsl(140, 56%, 25%)", + "hsl(230, 50%, 60%)" + ], + "text-halo-blur": 0.5, + "text-halo-width": 0.5 + }, + "metadata": { + "mapbox:featureComponent": "transit", + "mapbox:group": "Transit, transit-labels", + "microg:gms-type-feature": "transit.station.bus", + "microg:gms-type-element": "labels.text" + } + }, + { + "id": "airport-label", + "type": "symbol", + "source": "composite", + "source-layer": "airport_label", + "minzoom": 8, + "filter": [ + "match", + [ + "get", + "class" + ], + [ + "military", + "civil", + "disputed_military", + "disputed_civil" + ], + [ + "match", + [ + "get", + "worldview" + ], + [ + "all", + "US" + ], + true, + false + ], + false + ], + "layout": { + "text-line-height": 1.1, + "text-size": [ + "step", + [ + "get", + "sizerank" + ], + 18, + 9, + 12 + ], + "icon-image": [ + "get", + "maki" + ], + "text-font": [ + "DIN Pro Medium", + "Arial Unicode MS Regular" + ], + "text-offset": [ + 0, + 0.8 + ], + "text-rotation-alignment": "viewport", + "text-anchor": "top", + "text-field": [ + "step", + [ + "get", + "sizerank" + ], + [ + "case", + [ + "has", + "ref" + ], + [ + "concat", + [ + "get", + "ref" + ], + " -\n", + [ + "coalesce", + [ + "get", + "name_en" + ], + [ + "get", + "name" + ] + ] + ], + [ + "coalesce", + [ + "get", + "name_en" + ], + [ + "get", + "name" + ] + ] + ], + 15, + [ + "get", + "ref" + ] + ], + "text-letter-spacing": 0.01, + "text-max-width": 9 + }, + "paint": { + "text-color": "hsl(230, 40%, 55%)", + "text-halo-color": "hsl(60, 20%, 100%)", + "text-halo-width": 1 + }, + "metadata": { + "mapbox:featureComponent": "transit", + "mapbox:group": "Transit, transit-labels", + "microg:gms-type-feature": "transit.station.airport", + "microg:gms-type-element": "labels.text" + } + }, + { + "id": "settlement-subdivision-label", + "type": "symbol", + "source": "composite", + "source-layer": "place_label", + "minzoom": 10, + "maxzoom": 15, + "filter": [ + "all", + [ + "match", + [ + "get", + "class" + ], + [ + "settlement_subdivision", + "disputed_settlement_subdivision" + ], + [ + "match", + [ + "get", + "worldview" + ], + [ + "all", + "US" + ], + true, + false + ], + false + ], + [ + "<=", + [ + "get", + "filterrank" + ], + 3 + ] + ], + "layout": { + "text-field": [ + "coalesce", + [ + "get", + "name_en" + ], + [ + "get", + "name" + ] + ], + "text-transform": "uppercase", + "text-font": [ + "DIN Pro Regular", + "Arial Unicode MS Regular" + ], + "text-letter-spacing": [ + "match", + [ + "get", + "type" + ], + "suburb", + 0.15, + 0.05 + ], + "text-max-width": 7, + "text-padding": 3, + "text-size": [ + "interpolate", + [ + "cubic-bezier", + 0.5, + 0, + 1, + 1 + ], + [ + "zoom" + ], + 11, + [ + "match", + [ + "get", + "type" + ], + "suburb", + 11, + 10.5 + ], + 15, + [ + "match", + [ + "get", + "type" + ], + "suburb", + 15, + 14 + ] + ] + }, + "paint": { + "text-halo-color": "hsla(60, 25%, 100%, 0.75)", + "text-halo-width": 1, + "text-color": "hsl(230, 29%, 36%)", + "text-halo-blur": 0.5 + }, + "metadata": { + "mapbox:featureComponent": "place-labels", + "mapbox:group": "Place labels, place-labels", + "microg:gms-type-feature": "administrative.neighborhood", + "microg:gms-type-element": "labels.text" + } + }, + { + "id": "settlement-minor-label", + "type": "symbol", + "source": "composite", + "source-layer": "place_label", + "minzoom": 2, + "maxzoom": 13, + "filter": [ + "all", + [ + "<=", + [ + "get", + "filterrank" + ], + 3 + ], + [ + "match", + [ + "get", + "class" + ], + [ + "settlement", + "disputed_settlement" + ], + [ + "match", + [ + "get", + "worldview" + ], + [ + "all", + "US" + ], + true, + false + ], + false + ], + [ + "step", + [ + "zoom" + ], + [ + ">", + [ + "get", + "symbolrank" + ], + 6 + ], + 4, + [ + ">=", + [ + "get", + "symbolrank" + ], + 7 + ], + 6, + [ + ">=", + [ + "get", + "symbolrank" + ], + 8 + ], + 7, + [ + ">=", + [ + "get", + "symbolrank" + ], + 10 + ], + 10, + [ + ">=", + [ + "get", + "symbolrank" + ], + 11 + ], + 11, + [ + ">=", + [ + "get", + "symbolrank" + ], + 13 + ], + 12, + [ + ">=", + [ + "get", + "symbolrank" + ], + 15 + ] + ] + ], + "layout": { + "symbol-sort-key": [ + "get", + "symbolrank" + ], + "icon-image": [ + "step", + [ + "zoom" + ], + [ + "case", + [ + "==", + [ + "get", + "capital" + ], + 2 + ], + "border-dot-13", + [ + "step", + [ + "get", + "symbolrank" + ], + "dot-11", + 9, + "dot-10", + 11, + "dot-9" + ] + ], + 8, + "" + ], + "text-font": [ + "DIN Pro Regular", + "Arial Unicode MS Regular" + ], + "text-radial-offset": [ + "step", + [ + "zoom" + ], + [ + "match", + [ + "get", + "capital" + ], + 2, + 0.6, + 0.55 + ], + 8, + 0 + ], + "text-anchor": [ + "step", + [ + "zoom" + ], + [ + "get", + "text_anchor" + ], + 8, + "center" + ], + "text-justify": "auto", + "text-field": [ + "coalesce", + [ + "get", + "name_en" + ], + [ + "get", + "name" + ] + ], + "text-max-width": 7, + "text-line-height": 1.1, + "text-size": [ + "interpolate", + [ + "cubic-bezier", + 0.2, + 0, + 0.9, + 1 + ], + [ + "zoom" + ], + 3, + [ + "step", + [ + "get", + "symbolrank" + ], + 11, + 9, + 10 + ], + 6, + [ + "step", + [ + "get", + "symbolrank" + ], + 14, + 9, + 12, + 12, + 10 + ], + 8, + [ + "step", + [ + "get", + "symbolrank" + ], + 16, + 9, + 14, + 12, + 12, + 15, + 10 + ], + 13, + [ + "step", + [ + "get", + "symbolrank" + ], + 22, + 9, + 20, + 12, + 16, + 15, + 14 + ] + ] + }, + "paint": { + "text-color": "hsl(230, 29%, 0%)", + "text-halo-color": "hsl(60, 25%, 100%)", + "text-halo-width": 1, + "text-halo-blur": 1 + }, + "metadata": { + "mapbox:featureComponent": "place-labels", + "mapbox:group": "Place labels, place-labels", + "microg:gms-type-feature": "administrative.locality", + "microg:gms-type-element": "labels.text" + } + }, + { + "id": "settlement-major-label", + "type": "symbol", + "source": "composite", + "source-layer": "place_label", + "minzoom": 2, + "maxzoom": 15, + "filter": [ + "all", + [ + "<=", + [ + "get", + "filterrank" + ], + 3 + ], + [ + "match", + [ + "get", + "class" + ], + [ + "settlement", + "disputed_settlement" + ], + [ + "match", + [ + "get", + "worldview" + ], + [ + "all", + "US" + ], + true, + false + ], + false + ], + [ + "step", + [ + "zoom" + ], + false, + 2, + [ + "<=", + [ + "get", + "symbolrank" + ], + 6 + ], + 4, + [ + "<", + [ + "get", + "symbolrank" + ], + 7 + ], + 6, + [ + "<", + [ + "get", + "symbolrank" + ], + 8 + ], + 7, + [ + "<", + [ + "get", + "symbolrank" + ], + 10 + ], + 10, + [ + "<", + [ + "get", + "symbolrank" + ], + 11 + ], + 11, + [ + "<", + [ + "get", + "symbolrank" + ], + 13 + ], + 12, + [ + "<", + [ + "get", + "symbolrank" + ], + 15 + ], + 13, + [ + ">=", + [ + "get", + "symbolrank" + ], + 11 + ], + 14, + [ + ">=", + [ + "get", + "symbolrank" + ], + 15 + ] + ] + ], + "layout": { + "symbol-sort-key": [ + "get", + "symbolrank" + ], + "icon-image": [ + "step", + [ + "zoom" + ], + [ + "case", + [ + "==", + [ + "get", + "capital" + ], + 2 + ], + "border-dot-13", + [ + "step", + [ + "get", + "symbolrank" + ], + "dot-11", + 9, + "dot-10", + 11, + "dot-9" + ] + ], + 8, + "" + ], + "text-font": [ + "DIN Pro Medium", + "Arial Unicode MS Regular" + ], + "text-radial-offset": [ + "step", + [ + "zoom" + ], + [ + "match", + [ + "get", + "capital" + ], + 2, + 0.6, + 0.55 + ], + 8, + 0 + ], + "text-anchor": [ + "step", + [ + "zoom" + ], + [ + "get", + "text_anchor" + ], + 8, + "center" + ], + "text-justify": [ + "step", + [ + "zoom" + ], + [ + "match", + [ + "get", + "text_anchor" + ], + [ + "left", + "bottom-left", + "top-left" + ], + "left", + [ + "right", + "bottom-right", + "top-right" + ], + "right", + "center" + ], + 8, + "center" + ], + "text-field": [ + "coalesce", + [ + "get", + "name_en" + ], + [ + "get", + "name" + ] + ], + "text-max-width": 7, + "text-line-height": 1.1, + "text-size": [ + "interpolate", + [ + "cubic-bezier", + 0.2, + 0, + 0.9, + 1 + ], + [ + "zoom" + ], + 3, + [ + "step", + [ + "get", + "symbolrank" + ], + 13, + 6, + 11 + ], + 6, + [ + "step", + [ + "get", + "symbolrank" + ], + 18, + 6, + 16, + 7, + 14 + ], + 8, + [ + "step", + [ + "get", + "symbolrank" + ], + 20, + 9, + 16, + 10, + 14 + ], + 15, + [ + "step", + [ + "get", + "symbolrank" + ], + 24, + 9, + 20, + 12, + 16, + 15, + 14 + ] + ] + }, + "paint": { + "text-color": "hsl(230, 29%, 0%)", + "text-halo-color": "hsl(60, 25%, 100%)", + "text-halo-width": 1, + "text-halo-blur": 1 + }, + "metadata": { + "mapbox:featureComponent": "place-labels", + "mapbox:group": "Place labels, place-labels", + "microg:gms-type-feature": "administrative.land_parcel", + "microg:gms-type-element": "labels.text" + } + }, + { + "id": "state-label", + "type": "symbol", + "source": "composite", + "source-layer": "place_label", + "minzoom": 3, + "maxzoom": 9, + "filter": [ + "match", + [ + "get", + "class" + ], + [ + "state", + "disputed_state" + ], + [ + "match", + [ + "get", + "worldview" + ], + [ + "all", + "US" + ], + true, + false + ], + false + ], + "layout": { + "text-size": [ + "interpolate", + [ + "cubic-bezier", + 0.85, + 0.7, + 0.65, + 1 + ], + [ + "zoom" + ], + 4, + [ + "step", + [ + "get", + "symbolrank" + ], + 9, + 6, + 8, + 7, + 7 + ], + 9, + [ + "step", + [ + "get", + "symbolrank" + ], + 21, + 6, + 16, + 7, + 14 + ] + ], + "text-transform": "uppercase", + "text-font": [ + "DIN Pro Bold", + "Arial Unicode MS Bold" + ], + "text-field": [ + "coalesce", + [ + "get", + "name_en" + ], + [ + "get", + "name" + ] + ], + "text-letter-spacing": 0.15, + "text-max-width": 6 + }, + "paint": { + "text-color": "hsl(230, 29%, 0%)", + "text-halo-color": "hsl(60, 25%, 100%)", + "text-halo-width": 1, + "text-opacity": 0.5 + }, + "metadata": { + "mapbox:featureComponent": "place-labels", + "mapbox:group": "Place labels, place-labels", + "microg:gms-type-feature": "administrative.province", + "microg:gms-type-element": "labels.text" + } + }, + { + "id": "country-label", + "type": "symbol", + "source": "composite", + "source-layer": "place_label", + "minzoom": 1, + "maxzoom": 10, + "filter": [ + "match", + [ + "get", + "class" + ], + [ + "country", + "disputed_country" + ], + [ + "match", + [ + "get", + "worldview" + ], + [ + "all", + "US" + ], + true, + false + ], + false + ], + "layout": { + "icon-image": "", + "text-field": [ + "coalesce", + [ + "get", + "name_en" + ], + [ + "get", + "name" + ] + ], + "text-line-height": 1.1, + "text-max-width": 6, + "text-font": [ + "DIN Pro Medium", + "Arial Unicode MS Regular" + ], + "text-radial-offset": [ + "step", + [ + "zoom" + ], + 0.6, + 8, + 0 + ], + "text-justify": [ + "step", + [ + "zoom" + ], + [ + "match", + [ + "get", + "text_anchor" + ], + [ + "left", + "bottom-left", + "top-left" + ], + "left", + [ + "right", + "bottom-right", + "top-right" + ], + "right", + "center" + ], + 7, + "auto" + ], + "text-size": [ + "interpolate", + [ + "cubic-bezier", + 0.2, + 0, + 0.7, + 1 + ], + [ + "zoom" + ], + 1, + [ + "step", + [ + "get", + "symbolrank" + ], + 11, + 4, + 9, + 5, + 8 + ], + 9, + [ + "step", + [ + "get", + "symbolrank" + ], + 22, + 4, + 19, + 5, + 17 + ] + ] + }, + "paint": { + "icon-opacity": [ + "step", + [ + "zoom" + ], + [ + "case", + [ + "has", + "text_anchor" + ], + 1, + 0 + ], + 7, + 0 + ], + "text-color": "hsl(230, 29%, 0%)", + "text-halo-color": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 2, + "hsla(60, 25%, 100%, 0.75)", + 3, + "hsl(60, 25%, 100%)" + ], + "text-halo-width": 1.25 + }, + "metadata": { + "mapbox:featureComponent": "place-labels", + "mapbox:group": "Place labels, place-labels", + "microg:gms-type-feature": "administrative.country", + "microg:gms-type-element": "labels.text" + } + }, + { + "id": "continent-label", + "type": "symbol", + "source": "composite", + "source-layer": "natural_label", + "minzoom": 0.75, + "maxzoom": 3, + "filter": [ + "==", + [ + "get", + "class" + ], + "continent" + ], + "layout": { + "text-field": [ + "coalesce", + [ + "get", + "name_en" + ], + [ + "get", + "name" + ] + ], + "text-line-height": 1.1, + "text-max-width": 6, + "text-font": [ + "DIN Pro Medium", + "Arial Unicode MS Regular" + ], + "text-size": [ + "interpolate", + [ + "exponential", + 0.5 + ], + [ + "zoom" + ], + 0, + 10, + 2.5, + 15 + ], + "text-transform": "uppercase", + "text-letter-spacing": 0.05 + }, + "paint": { + "text-color": "hsl(230, 29%, 0%)", + "text-halo-color": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 0, + "hsla(60, 25%, 100%, 0.75)", + 3, + "hsl(60, 25%, 100%)" + ], + "text-halo-width": 1.5, + "text-opacity": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 0, + 0.8, + 1.5, + 0.5, + 2.5, + 0 + ] + }, + "metadata": { + "mapbox:featureComponent": "place-labels", + "mapbox:group": "Place labels, place-labels", + "microg:gms-type-feature": "administrative.country", + "microg:gms-type-element": "labels.text" + } + } + ], + "sources": { + "composite": { + "url": "mapbox://mapbox.mapbox-streets-v8,mapbox.mapbox-terrain-v2,mapbox.mapbox-bathymetry-v2", + "type": "vector" + } + }, + "created": "1970-01-01T00:00:00.000Z", + "modified": "1970-01-01T00:00:00.000Z", + "owner": "mapbox", + "id": "outdoors-v12", + "draft": false +} diff --git a/play-services-maps-core-mapbox/src/main/assets/style-microg-normal.json b/play-services-maps-core-mapbox/src/main/assets/style-microg-normal.json new file mode 100644 index 0000000000..295ffad446 --- /dev/null +++ b/play-services-maps-core-mapbox/src/main/assets/style-microg-normal.json @@ -0,0 +1,4392 @@ +{ + "version": 8, + "name": "Mountain View Full", + "metadata": { + "mapbox:origin": "basic-template", + "mapbox:autocomposite": true, + "mapbox:type": "template", + "mapbox:sdk-support": { + "js": "0.50.0", + "android": "6.7.0", + "ios": "4.6.0" + }, + "mapbox:trackposition": false, + "mapbox:groups": { + "f51b507d2a17e572c70a5db74b0fec7e": { + "name": "Base", + "collapsed": false + }, + "3f48b8dc54ff2e6544b9ef9cedbf2990": { + "name": "Streets", + "collapsed": true + }, + "29bb589e8d1b9b402583363648b70302": { + "name": "Buildings", + "collapsed": true + }, + "3c26e9cbc75335c6f0ba8de5439cf1fa": { + "name": "Country borders", + "collapsed": true + }, + "7b44201d7f1682d99f7140188aff23ce": { + "name": "Labels", + "collapsed": true + }, + "24306bdccbff03e2ee08d5d1a4ca7312": { + "name": "Street name", + "collapsed": true + }, + "124a9d7a8e5226775d947c592110dfad": { + "name": "POI", + "collapsed": true + } + }, + "mapbox:uiParadigm": "layers" + }, + "center": [ + 12.819420849458652, + 50.03325860617235 + ], + "zoom": 3.315829104862067, + "bearing": 0, + "pitch": 1.5, + "light": { + "intensity": 0.5, + "color": "hsl(0, 0%, 100%)", + "anchor": "viewport" + }, + "sources": { + "composite": { + "url": "mapbox://mapbox.mapbox-streets-v8,mapbox.mapbox-terrain-v2", + "type": "vector" + } + }, + "sprite": "mapbox://sprites/microg/cjui4020201oo1fmca7yuwbor/8fkcj5fgn4mftlzuak3guz1f9", + "glyphs": "mapbox://fonts/microg/{fontstack}/{range}.pbf", + "layers": [ + { + "id": "background", + "type": "background", + "layout": {}, + "paint": { + "background-color": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 4, + "hsl(43, 30%, 91%)", + 5, + "hsl(0, 0%, 96%)", + 8, + "hsl(0, 0%, 96%)", + 9, + "#efeee8", + 16, + "hsl(0, 0%, 95%)", + 18, + "#f8f9fb" + ] + }, + "metadata": { + "microg:gms-type-feature": "landscape.natural.landcover", + "microg:gms-type-element": "geometry.fill" + } + }, + { + "id": "grass", + "type": "fill", + "metadata": { + "mapbox:group": "f51b507d2a17e572c70a5db74b0fec7e", + "microg:gms-type-feature": "landscape.natural.landcover", + "microg:gms-type-element": "geometry.fill" + }, + "source": "composite", + "source-layer": "landcover", + "filter": [ + "match", + [ + "get", + "class" + ], + [ + "grass" + ], + true, + false + ], + "layout": {}, + "paint": { + "fill-color": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 9, + "hsl(124, 30%, 90%)", + 11, + "hsl(107, 30%, 94%)", + 12.5, + "hsl(107, 30%, 94%)", + 13.5, + "hsl(45, 12%, 93%)" + ] + } + }, + { + "id": "forrest", + "type": "fill", + "metadata": { + "mapbox:group": "f51b507d2a17e572c70a5db74b0fec7e", + "microg:gms-type-feature": "landscape.natural.landcover", + "microg:gms-type-element": "geometry.fill" + }, + "source": "composite", + "source-layer": "landcover", + "filter": [ + "match", + [ + "get", + "class" + ], + [ + "wood", + "scrub" + ], + true, + false + ], + "layout": {}, + "paint": { + "fill-color": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 9, + "hsl(124, 42%, 86%)", + 11, + "hsl(107, 47%, 94%)", + 12.5, + "hsl(107, 47%, 94%)", + 13.5, + "hsl(45, 12%, 93%)" + ] + } + }, + { + "id": "national_park", + "type": "fill", + "metadata": { + "mapbox:group": "f51b507d2a17e572c70a5db74b0fec7e", + "microg:gms-type-feature": "landscape.natural.landcover", + "microg:gms-type-element": "geometry.fill" + }, + "source": "composite", + "source-layer": "landuse_overlay", + "filter": [ + "match", + [ + "get", + "class" + ], + [ + "national_park" + ], + true, + false + ], + "layout": {}, + "paint": { + "fill-color": "hsl(106, 58%, 85%)" + } + }, + { + "id": "snow", + "type": "fill", + "metadata": { + "mapbox:group": "f51b507d2a17e572c70a5db74b0fec7e", + "microg:gms-type-feature": "landscape.natural.landcover", + "microg:gms-type-element": "geometry.fill" + }, + "source": "composite", + "source-layer": "landcover", + "filter": [ + "match", + [ + "get", + "class" + ], + [ + "snow" + ], + true, + false + ], + "layout": {}, + "paint": { + "fill-color": "#f9fafc" + } + }, + { + "id": "hillshade", + "type": "fill", + "metadata": { + "mapbox:group": "f51b507d2a17e572c70a5db74b0fec7e", + "microg:gms-type-feature": "landscape.natural.terrain", + "microg:gms-type-element": "geometry.fill" + }, + "source": "composite", + "source-layer": "hillshade", + "layout": {}, + "paint": { + "fill-opacity": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 9, + 0.03, + 13, + 0 + ] + } + }, + { + "id": "park", + "type": "fill", + "metadata": { + "mapbox:group": "f51b507d2a17e572c70a5db74b0fec7e", + "microg:gms-type-feature": "poi.park", + "microg:gms-type-element": "geometry.fill" + }, + "source": "composite", + "source-layer": "landuse", + "filter": [ + "match", + [ + "get", + "class" + ], + [ + "park", + "scrub" + ], + true, + false + ], + "layout": {}, + "paint": { + "fill-color": "#c1ecaf" + } + }, + { + "id": "pitch", + "type": "fill", + "metadata": { + "mapbox:group": "f51b507d2a17e572c70a5db74b0fec7e", + "microg:gms-type-feature": "landscape.natural.terrain", + "microg:gms-type-element": "geometry.fill" + }, + "source": "composite", + "source-layer": "landuse", + "filter": [ + "match", + [ + "get", + "class" + ], + [ + "pitch" + ], + true, + false + ], + "layout": {}, + "paint": { + "fill-color": "#c8efbb" + } + }, + { + "id": "landuse", + "type": "fill", + "metadata": { + "mapbox:group": "f51b507d2a17e572c70a5db74b0fec7e", + "microg:gms-type-feature": "landscape.man_made", + "microg:gms-type-element": "geometry.fill" + }, + "source": "composite", + "source-layer": "landuse", + "filter": [ + "match", + [ + "get", + "class" + ], + [ + "airport", + "school", + "hospital" + ], + true, + false + ], + "layout": {}, + "paint": { + "fill-color": "hsl(202, 26%, 94%)" + } + }, + { + "id": "river", + "type": "fill", + "metadata": { + "mapbox:group": "f51b507d2a17e572c70a5db74b0fec7e", + "microg:gms-type-feature": "water", + "microg:gms-type-element": "geometry.fill" + }, + "source": "composite", + "source-layer": "water", + "layout": {}, + "paint": { + "fill-color": "hsl(206, 100%, 83%)" + } + }, + { + "id": "path", + "type": "line", + "metadata": { + "mapbox:group": "3f48b8dc54ff2e6544b9ef9cedbf2990", + "microg:gms-type-feature": "road.local", + "microg:gms-type-element": "geometry.fill" + }, + "source": "composite", + "source-layer": "road", + "filter": [ + "all", + [ + "match", + [ + "get", + "class" + ], + [ + "path" + ], + true, + false + ], + [ + "match", + [ + "get", + "type" + ], + [ + "platform", + "steps" + ], + false, + true + ] + ], + "layout": {}, + "paint": { + "line-color": "hsl(118, 34%, 66%)", + "line-dasharray": [ + 4, + 2 + ] + } + }, + { + "id": "steps", + "type": "line", + "metadata": { + "mapbox:group": "3f48b8dc54ff2e6544b9ef9cedbf2990", + "microg:gms-type-feature": "road.local", + "microg:gms-type-element": "geometry.fill" + }, + "source": "composite", + "source-layer": "road", + "filter": [ + "all", + [ + "match", + [ + "get", + "class" + ], + [ + "path" + ], + true, + false + ], + [ + "match", + [ + "get", + "type" + ], + [ + "steps" + ], + true, + false + ] + ], + "layout": {}, + "paint": { + "line-color": "hsl(118, 5%, 66%)", + "line-dasharray": [ + 1, + 1 + ], + "line-gap-width": 1 + } + }, + { + "id": "platform", + "type": "fill", + "metadata": { + "mapbox:group": "3f48b8dc54ff2e6544b9ef9cedbf2990", + "microg:gms-type-feature": "transit.station.rail", + "microg:gms-type-element": "geometry.fill" + }, + "source": "composite", + "source-layer": "road", + "filter": [ + "all", + [ + "match", + [ + "get", + "type" + ], + [ + "platform" + ], + true, + false + ], + [ + "match", + [ + "get", + "structure" + ], + [ + "tunnel" + ], + false, + true + ] + ], + "layout": {}, + "paint": { + "fill-color": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 15, + "hsl(2, 20%, 92%)", + 16, + "hsl(2, 95%, 92%)" + ], + "fill-outline-color": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 15, + "hsl(1, 10%, 76%)", + 16, + "hsl(1, 74%, 76%)" + ], + "fill-opacity": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 14, + 0, + 16, + 1 + ] + } + }, + { + "id": "primary_tunnel_border", + "type": "line", + "metadata": { + "mapbox:group": "3f48b8dc54ff2e6544b9ef9cedbf2990", + "microg:gms-type-feature": "road.highway", + "microg:gms-type-element": "geometry.stroke" + }, + "source": "composite", + "source-layer": "road", + "filter": [ + "all", + [ + "match", + [ + "get", + "class" + ], + [ + "primary_link", + "primary", + "motorway_link" + ], + true, + false + ], + [ + "match", + [ + "get", + "structure" + ], + [ + "tunnel" + ], + true, + false + ] + ], + "layout": {}, + "paint": { + "line-gap-width": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 7, + 0, + 8, + 1, + 12, + 4, + 14, + 6, + 16, + 10, + 22, + 64 + ], + "line-color": "#cbcccd" + } + }, + { + "id": "primary_tunnel", + "type": "line", + "metadata": { + "mapbox:group": "3f48b8dc54ff2e6544b9ef9cedbf2990", + "microg:gms-type-feature": "road.highway", + "microg:gms-type-element": "geometry.fill" + }, + "source": "composite", + "source-layer": "road", + "filter": [ + "all", + [ + "match", + [ + "get", + "class" + ], + [ + "primary_link", + "primary", + "motorway_link" + ], + true, + false + ], + [ + "match", + [ + "get", + "structure" + ], + [ + "tunnel" + ], + true, + false + ] + ], + "layout": {}, + "paint": { + "line-width": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 8, + 1, + 12, + 4, + 14, + 6, + 16, + 10, + 22, + 64 + ], + "line-color": "hsl(0, 0%, 89%)" + } + }, + { + "id": "aeroway", + "type": "line", + "metadata": { + "mapbox:group": "3f48b8dc54ff2e6544b9ef9cedbf2990", + "microg:gms-type-feature": "transit.station.airport", + "microg:gms-type-element": "geometry.fill" + }, + "source": "composite", + "source-layer": "aeroway", + "layout": {}, + "paint": { + "line-color": "hsla(0, 0%, 0%, 0.1)" + } + }, + { + "id": "service_road", + "type": "line", + "metadata": { + "mapbox:group": "3f48b8dc54ff2e6544b9ef9cedbf2990", + "microg:gms-type-feature": "road.local", + "microg:gms-type-element": "geometry.fill" + }, + "source": "composite", + "source-layer": "road", + "filter": [ + "match", + [ + "get", + "class" + ], + [ + "service" + ], + true, + false + ], + "layout": {}, + "paint": { + "line-color": "hsla(0, 0%, 0%, 0.1)" + } + }, + { + "id": "pedestrian_border", + "type": "line", + "metadata": { + "mapbox:group": "3f48b8dc54ff2e6544b9ef9cedbf2990", + "microg:gms-type-feature": "road.local", + "microg:gms-type-element": "geometry.stroke" + }, + "source": "composite", + "source-layer": "road", + "filter": [ + "all", + [ + "match", + [ + "get", + "class" + ], + [ + "pedestrian" + ], + true, + false + ], + [ + "has", + "name" + ] + ], + "layout": {}, + "paint": { + "line-gap-width": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 13, + 1, + 16, + 4, + 22, + 32 + ], + "line-color": "#e3e3e3", + "line-opacity": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 12.5, + 0, + 13.5, + 1 + ] + } + }, + { + "id": "street_border", + "type": "line", + "metadata": { + "mapbox:group": "3f48b8dc54ff2e6544b9ef9cedbf2990", + "microg:gms-type-feature": "road.local", + "microg:gms-type-element": "geometry.stroke" + }, + "source": "composite", + "source-layer": "road", + "filter": [ + "match", + [ + "get", + "class" + ], + [ + "street" + ], + true, + false + ], + "layout": {}, + "paint": { + "line-gap-width": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 12, + 1, + 16, + 6, + 22, + 40 + ], + "line-color": "#e3e3e3" + } + }, + { + "id": "secondary_border", + "type": "line", + "metadata": { + "mapbox:group": "3f48b8dc54ff2e6544b9ef9cedbf2990", + "microg:gms-type-feature": "road.local", + "microg:gms-type-element": "geometry.stroke" + }, + "source": "composite", + "source-layer": "road", + "filter": [ + "match", + [ + "get", + "class" + ], + [ + "secondary", + "secondary_link", + "tertiary_link", + "tertiary", + "trunk_link", + "trunk" + ], + true, + false + ], + "layout": {}, + "paint": { + "line-gap-width": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 10, + 1, + 17, + 10, + 22, + 48 + ], + "line-color": "#e3e3e3", + "line-width": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 16, + 1, + 20, + 2 + ] + } + }, + { + "id": "primary_border", + "type": "line", + "metadata": { + "mapbox:group": "3f48b8dc54ff2e6544b9ef9cedbf2990", + "microg:gms-type-feature": "road.highway", + "microg:gms-type-element": "geometry.stroke" + }, + "source": "composite", + "source-layer": "road", + "filter": [ + "all", + [ + "match", + [ + "get", + "class" + ], + [ + "primary_link", + "primary", + "motorway_link" + ], + true, + false + ], + [ + "match", + [ + "get", + "structure" + ], + [ + "tunnel" + ], + false, + true + ] + ], + "layout": { + "line-cap": "round" + }, + "paint": { + "line-gap-width": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 9, + 1, + 16, + 8, + 22, + 64 + ], + "line-color": "#ecd283" + } + }, + { + "id": "motorway_border", + "type": "line", + "metadata": { + "mapbox:group": "3f48b8dc54ff2e6544b9ef9cedbf2990", + "microg:gms-type-feature": "road.arterial", + "microg:gms-type-element": "geometry.stroke" + }, + "source": "composite", + "source-layer": "road", + "filter": [ + "match", + [ + "get", + "class" + ], + [ + "motorway" + ], + true, + false + ], + "layout": {}, + "paint": { + "line-gap-width": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 8, + 1, + 15.5, + 8, + 22, + 78 + ], + "line-color": "#ecd283", + "line-width": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 8, + 1, + 15, + 2 + ] + } + }, + { + "id": "railway", + "type": "line", + "metadata": { + "mapbox:group": "3f48b8dc54ff2e6544b9ef9cedbf2990", + "microg:gms-type-feature": "transit.line", + "microg:gms-type-element": "geometry.fill" + }, + "source": "composite", + "source-layer": "road", + "filter": [ + "all", + [ + "match", + [ + "get", + "class" + ], + [ + "major_rail", + "minor_rail", + "service_rail" + ], + true, + false + ], + [ + "match", + [ + "get", + "structure" + ], + [ + "tunnel" + ], + false, + true + ] + ], + "layout": {}, + "paint": { + "line-color": "hsl(220, 4%, 85%)" + } + }, + { + "id": "pedestrian", + "type": "line", + "metadata": { + "mapbox:group": "3f48b8dc54ff2e6544b9ef9cedbf2990", + "microg:gms-type-feature": "road.local", + "microg:gms-type-element": "geometry.fill" + }, + "source": "composite", + "source-layer": "road", + "filter": [ + "all", + [ + "match", + [ + "get", + "class" + ], + [ + "pedestrian" + ], + true, + false + ], + [ + "has", + "name" + ], + [ + "match", + [ + "get", + "type" + ], + [ + "platform" + ], + false, + true + ] + ], + "layout": {}, + "paint": { + "line-color": "#ffffff", + "line-width": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 13, + 1, + 16, + 4, + 22, + 32 + ], + "line-opacity": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 12.5, + 0, + 13.5, + 1 + ] + } + }, + { + "id": "street", + "type": "line", + "metadata": { + "mapbox:group": "3f48b8dc54ff2e6544b9ef9cedbf2990", + "microg:gms-type-feature": "road.local", + "microg:gms-type-element": "geometry.fill" + }, + "source": "composite", + "source-layer": "road", + "filter": [ + "match", + [ + "get", + "class" + ], + [ + "street" + ], + true, + false + ], + "layout": {}, + "paint": { + "line-color": "#ffffff", + "line-width": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 12, + 1, + 16, + 6, + 22, + 40 + ] + } + }, + { + "id": "secondary", + "type": "line", + "metadata": { + "mapbox:group": "3f48b8dc54ff2e6544b9ef9cedbf2990", + "microg:gms-type-feature": "road.local", + "microg:gms-type-element": "geometry.fill" + }, + "source": "composite", + "source-layer": "road", + "filter": [ + "match", + [ + "get", + "class" + ], + [ + "secondary", + "secondary_link", + "trunk_link", + "tertiary", + "tertiary_link", + "trunk" + ], + true, + false + ], + "layout": {}, + "paint": { + "line-color": "#ffffff", + "line-width": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 10, + 1, + 17, + 10, + 22, + 48 + ] + } + }, + { + "id": "primary", + "type": "line", + "metadata": { + "mapbox:group": "3f48b8dc54ff2e6544b9ef9cedbf2990", + "microg:gms-type-feature": "road.highway", + "microg:gms-type-element": "geometry.fill" + }, + "source": "composite", + "source-layer": "road", + "filter": [ + "all", + [ + "match", + [ + "get", + "class" + ], + [ + "primary_link", + "primary", + "motorway_link" + ], + true, + false + ], + [ + "match", + [ + "get", + "structure" + ], + [ + "tunnel" + ], + false, + true + ] + ], + "layout": { + "line-cap": "round" + }, + "paint": { + "line-color": [ + "step", + [ + "zoom" + ], + "hsl(50, 100%, 75%)", + 7, + "hsl(50, 100%, 85%)" + ], + "line-width": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 9, + 1, + 16, + 8, + 22, + 64 + ] + } + }, + { + "id": "motorway", + "type": "line", + "metadata": { + "mapbox:group": "3f48b8dc54ff2e6544b9ef9cedbf2990", + "microg:gms-type-feature": "road.arterial", + "microg:gms-type-element": "geometry.fill" + }, + "source": "composite", + "source-layer": "road", + "filter": [ + "match", + [ + "get", + "class" + ], + [ + "motorway" + ], + true, + false + ], + "layout": {}, + "paint": { + "line-color": "#ffeba3", + "line-width": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 8, + 1, + 15.5, + 8, + 22, + 78 + ] + } + }, + { + "id": "primary_bridge_border", + "type": "line", + "metadata": { + "mapbox:group": "3f48b8dc54ff2e6544b9ef9cedbf2990", + "microg:gms-type-feature": "road.highway", + "microg:gms-type-element": "geometry.stroke" + }, + "source": "composite", + "source-layer": "road", + "filter": [ + "all", + [ + "match", + [ + "get", + "class" + ], + [ + "primary_link", + "primary", + "motorway_link" + ], + true, + false + ], + [ + "match", + [ + "get", + "structure" + ], + [ + "bridge" + ], + true, + false + ] + ], + "layout": {}, + "paint": { + "line-gap-width": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 9, + 1, + 16, + 8, + 22, + 64 + ], + "line-color": "hsl(45, 73%, 72%)" + } + }, + { + "id": "primary_bridge", + "type": "line", + "metadata": { + "mapbox:group": "3f48b8dc54ff2e6544b9ef9cedbf2990", + "microg:gms-type-feature": "road.highway", + "microg:gms-type-element": "geometry.fill" + }, + "source": "composite", + "source-layer": "road", + "filter": [ + "all", + [ + "match", + [ + "get", + "class" + ], + [ + "primary_link", + "primary", + "motorway_link" + ], + true, + false + ], + [ + "match", + [ + "get", + "structure" + ], + [ + "bridge" + ], + true, + false + ] + ], + "layout": {}, + "paint": { + "line-color": [ + "step", + [ + "zoom" + ], + "hsl(50, 100%, 75%)", + 7, + "hsl(50, 100%, 85%)" + ], + "line-width": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 9, + 1, + 16, + 8, + 22, + 64 + ] + } + }, + { + "id": "building", + "type": "fill", + "metadata": { + "mapbox:group": "29bb589e8d1b9b402583363648b70302", + "microg:gms-type-feature": "landscape.man_made", + "microg:gms-type-element": "geometry.fill" + }, + "source": "composite", + "source-layer": "building", + "filter": [ + "match", + [ + "get", + "type" + ], + [ + "roof" + ], + false, + true + ], + "layout": {}, + "paint": { + "fill-color": [ + "match", + [ + "get", + "type" + ], + [ + "store", + "retail", + "church", + "kiosk", + "civic", + "hotel", + "supermarket", + "pub", + "dormitory" + ], + "hsl(33, 100%, 96%)", + "#ededed" + ], + "fill-opacity": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 15, + 0, + 17, + 1 + ] + } + }, + { + "id": "building_border", + "type": "line", + "metadata": { + "mapbox:group": "29bb589e8d1b9b402583363648b70302", + "microg:gms-type-feature": "landscape.man_made", + "microg:gms-type-element": "geometry.stroke" + }, + "source": "composite", + "source-layer": "building", + "filter": [ + "match", + [ + "get", + "type" + ], + [ + "roof" + ], + false, + true + ], + "layout": {}, + "paint": { + "line-color": [ + "match", + [ + "get", + "type" + ], + [ + "store", + "retail", + "church", + "kiosk", + "civic", + "commercial", + "hotel", + "supermarket", + "pub" + ], + "#f8e1c7", + "#dcdcdc" + ], + "line-opacity": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 15, + 0, + 17, + 1 + ] + } + }, + { + "id": "building_3d", + "type": "fill-extrusion", + "metadata": { + "mapbox:group": "29bb589e8d1b9b402583363648b70302", + "microg:gms-type-feature": "landscape.man_made", + "microg:gms-type-element": "geometry.fill" + }, + "source": "composite", + "source-layer": "building", + "filter": [ + "match", + [ + "get", + "extrude" + ], + [ + "true" + ], + true, + false + ], + "layout": {}, + "paint": { + "fill-extrusion-height": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 15, + 0, + 16, + [ + "get", + "height" + ] + ], + "fill-extrusion-opacity": 0.3, + "fill-extrusion-color": "hsl(0, 0%, 93%)" + } + }, + { + "id": "admin_0", + "type": "line", + "metadata": { + "mapbox:group": "3c26e9cbc75335c6f0ba8de5439cf1fa", + "microg:gms-type-feature": "administrative.country", + "microg:gms-type-element": "geometry.stroke" + }, + "source": "composite", + "source-layer": "admin", + "filter": [ + "all", + [ + "match", + [ + "get", + "maritime" + ], + [ + "false" + ], + true, + false + ], + [ + "match", + [ + "get", + "admin_level" + ], + [ + 0 + ], + true, + false + ] + ], + "layout": {}, + "paint": { + "line-color": [ + "case", + [ + "==", + [ + "get", + "disputed" + ], + true + ], + "hsl(0, 24%, 48%)", + "#787a7b" + ] + } + }, + { + "id": "admin_1", + "type": "line", + "metadata": { + "mapbox:group": "3c26e9cbc75335c6f0ba8de5439cf1fa", + "microg:gms-type-feature": "administrative.province", + "microg:gms-type-element": "geometry.stroke" + }, + "source": "composite", + "source-layer": "admin", + "filter": [ + "all", + [ + "match", + [ + "get", + "maritime" + ], + [ + "false" + ], + true, + false + ], + [ + "match", + [ + "get", + "admin_level" + ], + [ + 1 + ], + true, + false + ] + ], + "layout": {}, + "paint": { + "line-color": [ + "case", + [ + "==", + [ + "get", + "disputed" + ], + true + ], + "hsl(0, 24%, 48%)", + "#787a7b" + ], + "line-dasharray": [ + 1, + 1 + ] + } + }, + { + "id": "river_name", + "type": "symbol", + "metadata": { + "mapbox:group": "7b44201d7f1682d99f7140188aff23ce", + "microg:gms-type-feature": "water", + "microg:gms-type-element": "labels.text" + }, + "source": "composite", + "source-layer": "natural_label", + "filter": [ + "match", + [ + "get", + "class" + ], + [ + "river" + ], + true, + false + ], + "layout": { + "text-field": [ + "to-string", + [ + "get", + "name" + ] + ], + "symbol-placement": "line", + "symbol-spacing": 500, + "text-font": [ + "Roboto Regular" + ] + }, + "paint": { + "text-color": "#5083c1", + "text-halo-color": "#5083c1", + "text-halo-blur": 1 + } + }, + { + "id": "city_label_right", + "type": "symbol", + "metadata": { + "mapbox:group": "7b44201d7f1682d99f7140188aff23ce", + "microg:gms-type-feature": "administrative.locality", + "microg:gms-type-element": "labels.text" + }, + "source": "composite", + "source-layer": "place_label", + "maxzoom": 8, + "filter": [ + "all", + [ + "<=", + [ + "get", + "filterrank" + ], + [ + "/", + [ + "zoom" + ], + 3 + ] + ], + [ + "match", + [ + "get", + "class" + ], + [ + "settlement", + "settlement_subdivision" + ], + true, + false + ] + ], + "layout": { + "text-optional": true, + "text-size": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 0, + [ + "-", + 14, + [ + "max", + 0, + [ + "-", + [ + "get", + "symbolrank" + ], + 8 + ] + ] + ], + 22, + [ + "-", + 20, + [ + "/", + [ + "get", + "symbolrank" + ], + 4 + ] + ] + ], + "icon-image": [ + "match", + [ + "get", + "capital" + ], + [ + 0, + 2 + ], + "capital", + "city" + ], + "text-font": [ + "step", + [ + "get", + "symbolrank" + ], + [ + "literal", + [ + "Roboto Medium" + ] + ], + 10, + [ + "literal", + [ + "Roboto Regular" + ] + ] + ], + "text-justify": "left", + "text-offset": [ + 0.5, + 0.1 + ], + "icon-size": [ + "/", + 5, + [ + "get", + "symbolrank" + ] + ], + "text-anchor": "left", + "text-field": [ + "coalesce", + [ + "get", + "name_en" + ], + [ + "get", + "name" + ] + ] + }, + "paint": { + "text-color": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 0, + [ + "concat", + "hsl(213, 11%, ", + [ + "+", + [ + "*", + [ + "get", + "symbolrank" + ], + 2 + ], + 5 + ], + "%)" + ], + 22, + [ + "concat", + "hsl(213, 11%, ", + [ + "+", + [ + "get", + "symbolrank" + ], + 25 + ], + "%)" + ] + ], + "text-halo-width": 1, + "text-halo-blur": 1, + "text-halo-color": "hsl(0, 0%, 100%)", + "icon-opacity": 0.8 + } + }, + { + "id": "city_label_left", + "type": "symbol", + "metadata": { + "mapbox:group": "7b44201d7f1682d99f7140188aff23ce", + "microg:gms-type-feature": "administrative.locality", + "microg:gms-type-element": "labels.text" + }, + "source": "composite", + "source-layer": "place_label", + "maxzoom": 8, + "filter": [ + "all", + [ + "<=", + [ + "get", + "filterrank" + ], + [ + "/", + [ + "zoom" + ], + 3 + ] + ], + [ + "match", + [ + "get", + "class" + ], + [ + "settlement", + "settlement_subdivision" + ], + true, + false + ] + ], + "layout": { + "text-size": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 0, + [ + "-", + 14, + [ + "max", + 0, + [ + "-", + [ + "get", + "symbolrank" + ], + 8 + ] + ] + ], + 22, + [ + "-", + 20, + [ + "/", + [ + "get", + "symbolrank" + ], + 4 + ] + ] + ], + "icon-image": [ + "match", + [ + "get", + "capital" + ], + [ + 0, + 2 + ], + "capital", + "city" + ], + "text-font": [ + "step", + [ + "get", + "symbolrank" + ], + [ + "literal", + [ + "Roboto Medium" + ] + ], + 10, + [ + "literal", + [ + "Roboto Regular" + ] + ] + ], + "text-justify": "right", + "text-offset": [ + -0.5, + 0.1 + ], + "icon-size": [ + "/", + 5, + [ + "get", + "symbolrank" + ] + ], + "text-anchor": "right", + "text-field": [ + "coalesce", + [ + "get", + "name_en" + ], + [ + "get", + "name" + ] + ] + }, + "paint": { + "text-color": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 0, + [ + "concat", + "hsl(213, 11%, ", + [ + "+", + [ + "*", + [ + "get", + "symbolrank" + ], + 2 + ], + 5 + ], + "%)" + ], + 22, + [ + "concat", + "hsl(213, 11%, ", + [ + "+", + [ + "get", + "symbolrank" + ], + 25 + ], + "%)" + ] + ], + "text-halo-width": 1, + "text-halo-blur": 1, + "text-halo-color": "hsl(0, 0%, 100%)", + "icon-opacity": 0.8 + } + }, + { + "id": "city_label_below", + "type": "symbol", + "metadata": { + "mapbox:group": "7b44201d7f1682d99f7140188aff23ce", + "microg:gms-type-feature": "administrative.locality", + "microg:gms-type-element": "labels.text" + }, + "source": "composite", + "source-layer": "place_label", + "maxzoom": 8, + "filter": [ + "all", + [ + "<=", + [ + "get", + "filterrank" + ], + [ + "/", + [ + "zoom" + ], + 3 + ] + ], + [ + "match", + [ + "get", + "class" + ], + [ + "settlement", + "settlement_subdivision" + ], + true, + false + ] + ], + "layout": { + "text-size": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 0, + [ + "-", + 14, + [ + "max", + 0, + [ + "-", + [ + "get", + "symbolrank" + ], + 8 + ] + ] + ], + 22, + [ + "-", + 20, + [ + "/", + [ + "get", + "symbolrank" + ], + 4 + ] + ] + ], + "icon-image": [ + "match", + [ + "get", + "capital" + ], + [ + 0, + 2 + ], + "capital", + "city" + ], + "text-font": [ + "step", + [ + "get", + "symbolrank" + ], + [ + "literal", + [ + "Roboto Medium" + ] + ], + 10, + [ + "literal", + [ + "Roboto Regular" + ] + ] + ], + "text-offset": [ + 0, + 0.4 + ], + "icon-size": [ + "/", + 5, + [ + "get", + "symbolrank" + ] + ], + "text-anchor": "top", + "text-field": [ + "coalesce", + [ + "get", + "name_en" + ], + [ + "get", + "name" + ] + ] + }, + "paint": { + "text-color": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 0, + [ + "concat", + "hsl(213, 11%, ", + [ + "+", + [ + "*", + [ + "get", + "symbolrank" + ], + 2 + ], + 5 + ], + "%)" + ], + 22, + [ + "concat", + "hsl(213, 11%, ", + [ + "+", + [ + "get", + "symbolrank" + ], + 25 + ], + "%)" + ] + ], + "text-halo-width": 1, + "text-halo-blur": 1, + "text-halo-color": "hsl(0, 0%, 100%)", + "icon-opacity": 0.8 + } + }, + { + "id": "city_name", + "type": "symbol", + "metadata": { + "mapbox:group": "7b44201d7f1682d99f7140188aff23ce", + "microg:gms-type-feature": "administrative.locality", + "microg:gms-type-element": "labels.text" + }, + "source": "composite", + "source-layer": "place_label", + "filter": [ + "all", + [ + "<=", + [ + "get", + "filterrank" + ], + [ + "/", + [ + "zoom" + ], + 3 + ] + ], + [ + "match", + [ + "get", + "class" + ], + [ + "settlement", + "settlement_subdivision" + ], + true, + false + ] + ], + "layout": { + "text-size": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 0, + [ + "-", + 14, + [ + "max", + 0, + [ + "-", + [ + "get", + "symbolrank" + ], + 8 + ] + ] + ], + 22, + [ + "-", + 20, + [ + "/", + [ + "get", + "symbolrank" + ], + 4 + ] + ] + ], + "icon-image": [ + "step", + [ + "zoom" + ], + [ + "match", + [ + "get", + "capital" + ], + [ + 0, + 2 + ], + "capital", + "city" + ], + 8, + "" + ], + "text-transform": [ + "step", + [ + "get", + "symbolrank" + ], + "none", + 15, + "uppercase" + ], + "text-font": [ + "step", + [ + "get", + "symbolrank" + ], + [ + "literal", + [ + "Roboto Medium" + ] + ], + 10, + [ + "literal", + [ + "Roboto Regular" + ] + ] + ], + "text-offset": [ + "step", + [ + "zoom" + ], + [ + "literal", + [ + 0, + -0.2 + ] + ], + 8, + [ + "literal", + [ + 0, + 0 + ] + ] + ], + "icon-size": [ + "/", + 5, + [ + "get", + "symbolrank" + ] + ], + "text-anchor": [ + "step", + [ + "zoom" + ], + "bottom", + 8, + "center" + ], + "text-field": [ + "coalesce", + [ + "get", + "name_en" + ], + [ + "get", + "name" + ] + ], + "text-letter-spacing": [ + "interpolate", + [ + "linear" + ], + [ + "get", + "symbolrank" + ], + 0, + 0, + 8, + 0, + 12, + 0.1, + 16, + 0.2 + ] + }, + "paint": { + "text-color": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 0, + [ + "concat", + "hsl(213, 11%, ", + [ + "+", + [ + "*", + [ + "get", + "symbolrank" + ], + 2 + ], + 5 + ], + "%)" + ], + 22, + [ + "concat", + "hsl(213, 11%, ", + [ + "+", + [ + "get", + "symbolrank" + ], + 25 + ], + "%)" + ] + ], + "text-halo-width": 1, + "text-halo-blur": 1, + "text-halo-color": "hsl(0, 0%, 100%)", + "icon-opacity": 0.8 + } + }, + { + "id": "park_name", + "type": "symbol", + "metadata": { + "mapbox:group": "7b44201d7f1682d99f7140188aff23ce", + "microg:gms-type-feature": "poi.park", + "microg:gms-type-element": "labels.text" + }, + "source": "composite", + "source-layer": "poi_label", + "filter": [ + "all", + [ + "has", + "name" + ], + [ + "match", + [ + "get", + "maki" + ], + [ + "park", + "cemetery" + ], + true, + false + ], + [ + "<=", + [ + "get", + "sizerank" + ], + 10 + ] + ], + "layout": { + "text-field": [ + "to-string", + [ + "get", + "name" + ] + ], + "text-font": [ + "Roboto Regular" + ], + "text-size": 14 + }, + "paint": { + "text-color": "#297925", + "text-halo-color": "hsl(0, 0%, 100%)", + "text-halo-width": 1, + "text-halo-blur": 1 + } + }, + { + "id": "road-number-shield", + "type": "symbol", + "metadata": { + "mapbox:group": "24306bdccbff03e2ee08d5d1a4ca7312", + "microg:gms-type-feature": "road", + "microg:gms-type-element": "labels.icon" + }, + "source": "composite", + "source-layer": "road", + "filter": [ + "all", + [ + "has", + "ref" + ], + [ + "<=", + [ + "get", + "reflen" + ], + 6 + ] + ], + "layout": { + "text-size": 9, + "icon-image": [ + "case", + [ + "match", + [ + "get", + "shield" + ], + [ + "de-motorway", + "rectangle-green", + "rectangle-yellow", + "rectangle-white", + "rectangle-blue", + "rectangle-red", + "us-interstate" + ], + true, + false + ], + [ + "concat", + "shield_", + [ + "get", + "shield" + ], + "_", + [ + "get", + "reflen" + ] + ], + [ + "match", + [ + "get", + "shield_text_color" + ], + [ + "white" + ], + true, + false + ], + [ + "concat", + "shield_rectangle-blue_", + [ + "get", + "reflen" + ] + ], + [ + "concat", + "shield_rectangle-white_", + [ + "get", + "reflen" + ] + ] + ], + "icon-rotation-alignment": "viewport", + "text-font": [ + "match", + [ + "get", + "shield_text_color" + ], + [ + "white" + ], + [ + "literal", + [ + "Roboto Bold" + ] + ], + [ + "black" + ], + [ + "literal", + [ + "Roboto Medium" + ] + ], + [ + "literal", + [ + "Roboto Bold" + ] + ] + ], + "symbol-placement": [ + "step", + [ + "zoom" + ], + "point", + 11, + "line" + ], + "text-offset": [ + 0, + 0.1 + ], + "text-rotation-alignment": "viewport", + "icon-size": 0.75, + "text-field": [ + "get", + "ref" + ], + "text-letter-spacing": 0.05 + }, + "paint": { + "text-color": [ + "case", + [ + "match", + [ + "get", + "shield_text_color" + ], + [ + "white" + ], + true, + false + ], + "#ffffff", + [ + "match", + [ + "get", + "shield_text_color" + ], + [ + "black" + ], + true, + false + ], + "hsl(0, 0%, 7%)", + [ + "match", + [ + "get", + "shield_text_color" + ], + [ + "yellow" + ], + true, + false + ], + "hsl(50, 100%, 70%)", + [ + "match", + [ + "get", + "shield_text_color" + ], + [ + "orange" + ], + true, + false + ], + "hsl(25, 100%, 75%)", + [ + "match", + [ + "get", + "shield_text_color" + ], + [ + "blue" + ], + true, + false + ], + "hsl(230, 48%, 34%)", + "#ffffff" + ] + } + }, + { + "id": "country_name", + "type": "symbol", + "metadata": { + "mapbox:group": "24306bdccbff03e2ee08d5d1a4ca7312", + "microg:gms-type-feature": "administrative.country", + "microg:gms-type-element": "labels.text" + }, + "source": "composite", + "source-layer": "place_label", + "filter": [ + "match", + [ + "get", + "class" + ], + [ + "country" + ], + true, + false + ], + "layout": { + "text-letter-spacing": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 1, + 0, + 3, + 0.15 + ], + "text-font": [ + "Roboto Medium" + ], + "text-size": [ + "interpolate", + [ + "exponential", + 1.2 + ], + [ + "zoom" + ], + 1, + 12, + 7, + [ + "/", + 100, + [ + "get", + "symbolrank" + ] + ] + ], + "text-field": [ + "coalesce", + [ + "get", + "name_en" + ], + [ + "get", + "name" + ] + ] + }, + "paint": { + "text-color": "hsl(0, 0%, 10%)", + "text-halo-color": "hsl(0, 0%, 100%)", + "text-halo-width": 1, + "text-halo-blur": 1, + "text-opacity": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 6, + 1, + 7, + 0 + ] + } + }, + { + "id": "pedestrian_name", + "type": "symbol", + "metadata": { + "mapbox:group": "24306bdccbff03e2ee08d5d1a4ca7312", + "microg:gms-type-feature": "road.local", + "microg:gms-type-element": "labels.text" + }, + "source": "composite", + "source-layer": "road", + "minzoom": 16, + "filter": [ + "all", + [ + "has", + "name" + ], + [ + "match", + [ + "get", + "class" + ], + [ + "pedestrian" + ], + true, + false + ], + [ + "match", + [ + "get", + "type" + ], + [ + "platform" + ], + false, + true + ] + ], + "layout": { + "text-field": [ + "get", + "name" + ], + "symbol-placement": "line", + "text-font": [ + "Roboto Regular" + ], + "text-size": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 17, + 10, + 22, + 14 + ], + "text-padding": 5 + }, + "paint": { + "text-color": "#575757", + "text-halo-color": "#ffffff", + "text-halo-width": 1, + "text-halo-blur": 1 + } + }, + { + "id": "street_name", + "type": "symbol", + "metadata": { + "mapbox:group": "24306bdccbff03e2ee08d5d1a4ca7312", + "microg:gms-type-feature": "road.local", + "microg:gms-type-element": "labels.text" + }, + "source": "composite", + "source-layer": "road", + "filter": [ + "all", + [ + "has", + "name" + ], + [ + "match", + [ + "get", + "class" + ], + [ + "street" + ], + true, + false + ] + ], + "layout": { + "text-field": [ + "get", + "name" + ], + "symbol-placement": "line", + "text-font": [ + "Roboto Regular" + ], + "text-size": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 8, + 6, + 16, + 10 + ], + "text-padding": 5 + }, + "paint": { + "text-color": "#575757", + "text-halo-color": "#ffffff", + "text-halo-width": 1, + "text-halo-blur": 1 + } + }, + { + "id": "secondary_name", + "type": "symbol", + "metadata": { + "mapbox:group": "24306bdccbff03e2ee08d5d1a4ca7312", + "microg:gms-type-feature": "road.local", + "microg:gms-type-element": "labels.text" + }, + "source": "composite", + "source-layer": "road", + "filter": [ + "all", + [ + "has", + "name" + ], + [ + "match", + [ + "get", + "class" + ], + [ + "secondary", + "secondary_link", + "tertiary_link", + "tertiary", + "trunk_link", + "trunk" + ], + true, + false + ] + ], + "layout": { + "text-field": [ + "get", + "name" + ], + "symbol-placement": "line", + "text-font": [ + "Roboto Regular" + ], + "text-size": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 8, + 8, + 16, + 13 + ], + "symbol-spacing": 300, + "text-padding": 25 + }, + "paint": { + "text-color": "hsl(196, 0%, 34%)", + "text-halo-width": 1, + "text-halo-color": "#ffffff", + "text-halo-blur": 1 + } + }, + { + "id": "primary_name", + "type": "symbol", + "metadata": { + "mapbox:group": "24306bdccbff03e2ee08d5d1a4ca7312", + "microg:gms-type-feature": "road.highway", + "microg:gms-type-element": "labels.text" + }, + "source": "composite", + "source-layer": "road", + "filter": [ + "all", + [ + "has", + "name" + ], + [ + "match", + [ + "get", + "class" + ], + [ + "primary", + "primary_link" + ], + true, + false + ] + ], + "layout": { + "text-field": [ + "get", + "name" + ], + "symbol-placement": "line", + "text-font": [ + "Roboto Regular" + ], + "text-size": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 8, + 10, + 18, + 14 + ], + "symbol-spacing": 800, + "text-padding": 50 + }, + "paint": { + "text-color": "#6e481d", + "text-halo-width": 1, + "text-halo-color": "#ffffff", + "text-halo-blur": 1 + } + }, + { + "id": "poi_label_below", + "type": "symbol", + "metadata": { + "mapbox:group": "124a9d7a8e5226775d947c592110dfad", + "microg:gms-type-element": "labels.text", + "microg:gms-type-feature": "poi" + }, + "source": "composite", + "source-layer": "poi_label", + "filter": [ + "all", + [ + "has", + "name" + ], + [ + "<=", + [ + "get", + "filterrank" + ], + [ + "/", + [ + "zoom" + ], + 3.5 + ] + ], + [ + "!", + [ + "all", + [ + "match", + [ + "get", + "maki" + ], + [ + "park", + "cemetery" + ], + true, + false + ], + [ + "<=", + [ + "get", + "sizerank" + ], + 10 + ] + ] + ] + ], + "layout": { + "text-optional": true, + "text-line-height": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 14, + 1, + 15, + 1.2 + ], + "text-size": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 14, + 13, + 15, + 14 + ], + "icon-offset": [ + 0, + -36 + ], + "icon-image": [ + "match", + [ + "get", + "maki" + ], + [ + "museum", + "lodging", + "theatre", + "grocery", + "restaurant" + ], + [ + "concat", + "poi_", + [ + "get", + "maki" + ] + ], + [ + "fitness-centre", + "golf", + "campsite", + "bowling-alley", + "park", + "garden", + "farm", + "picnic-site", + "zoo", + "stadium", + "dog-park", + "pitch", + "cemetery" + ], + "poi_generic_green", + [ + "bank", + "parking", + "parking-garage" + ], + "poi_generic_purple", + [ + "bar", + "cafe", + "bakery" + ], + "poi_generic_orange", + [ + "alcohol-shop", + "shop", + "shoe", + "convenience", + "clothing-store", + "jewelry-store" + ], + "poi_generic_blue", + [ + "casino", + "castle", + "art-gallery", + "attraction", + "cinema", + "music", + "monument" + ], + "poi_generic_teal", + [ + "hospital", + "doctor" + ], + "poi_generic_red", + [ + "restaurant-pizza", + "restaurant-seafood", + "restaurant-noodle", + "fast-food", + "ice-cream" + ], + "poi_restaurant", + "poi_generic" + ], + "text-font": [ + "Roboto Regular" + ], + "text-offset": [ + 0, + 0.5 + ], + "icon-size": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 14, + 0.25, + 15, + 0.32 + ], + "text-anchor": "top", + "text-field": [ + "to-string", + [ + "get", + "name" + ] + ] + }, + "paint": { + "text-color": [ + "match", + [ + "get", + "maki" + ], + [ + "museum", + "casino", + "castle", + "theatre", + "art-gallery", + "attraction", + "cinema", + "music", + "monument" + ], + "hsl(186, 78%, 44%)", + [ + "lodging" + ], + "#df7db1", + [ + "fitness-centre", + "golf", + "campsite", + "park", + "garden", + "farm", + "picnic-site", + "zoo", + "dog-park", + "stadium", + "bowling-alley", + "pitch", + "cemetery" + ], + "hsl(117, 53%, 31%)", + [ + "restaurant-pizza", + "restaurant-seafood", + "restaurant", + "restaurant-noodle", + "bar", + "cafe", + "fast-food", + "bakery", + "ice-cream" + ], + "#c77d57", + [ + "shop", + "shoe", + "alcohol-shop", + "convenience", + "grocery", + "clothing-store", + "jewelry-store" + ], + "hsl(213, 40%, 48%)", + [ + "bank", + "parking", + "parking-garage" + ], + "#737b9b", + [ + "hospital", + "doctor" + ], + "#a47172", + "#67747b" + ], + "text-halo-color": "#ffffff", + "text-halo-width": 1, + "icon-translate": [ + 0, + 0 + ], + "text-translate": [ + 0, + 0 + ], + "text-halo-blur": 1 + } + }, + { + "id": "poi_label_above", + "type": "symbol", + "metadata": { + "mapbox:group": "124a9d7a8e5226775d947c592110dfad", + "microg:gms-type-element": "labels.text", + "microg:gms-type-feature": "poi" + }, + "source": "composite", + "source-layer": "poi_label", + "filter": [ + "all", + [ + "has", + "name" + ], + [ + "<=", + [ + "get", + "filterrank" + ], + [ + "/", + [ + "zoom" + ], + 3.5 + ] + ], + [ + "!", + [ + "all", + [ + "match", + [ + "get", + "maki" + ], + [ + "park", + "cemetery" + ], + true, + false + ], + [ + "<=", + [ + "get", + "sizerank" + ], + 10 + ] + ] + ] + ], + "layout": { + "text-optional": true, + "text-line-height": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 14, + 1, + 15, + 1.2 + ], + "text-size": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 14, + 13, + 15, + 14 + ], + "icon-offset": [ + 0, + -36 + ], + "icon-image": [ + "match", + [ + "get", + "maki" + ], + [ + "museum", + "lodging", + "theatre", + "grocery", + "restaurant" + ], + [ + "concat", + "poi_", + [ + "get", + "maki" + ] + ], + [ + "fitness-centre", + "golf", + "campsite", + "bowling-alley", + "park", + "garden", + "farm", + "picnic-site", + "zoo", + "stadium", + "dog-park", + "pitch", + "cemetery" + ], + "poi_generic_green", + [ + "bank", + "parking", + "parking-garage" + ], + "poi_generic_purple", + [ + "bar", + "cafe", + "bakery" + ], + "poi_generic_orange", + [ + "alcohol-shop", + "shop", + "shoe", + "convenience", + "clothing-store", + "jewelry-store" + ], + "poi_generic_blue", + [ + "casino", + "castle", + "art-gallery", + "attraction", + "cinema", + "music", + "monument" + ], + "poi_generic_teal", + [ + "hospital", + "doctor" + ], + "poi_generic_red", + [ + "restaurant-pizza", + "restaurant-seafood", + "restaurant-noodle", + "fast-food", + "ice-cream" + ], + "poi_restaurant", + "poi_generic" + ], + "text-font": [ + "Roboto Regular" + ], + "text-offset": [ + 0, + -2 + ], + "icon-size": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 14, + 0.25, + 15, + 0.32 + ], + "text-anchor": "bottom", + "text-field": [ + "to-string", + [ + "get", + "name" + ] + ] + }, + "paint": { + "text-color": [ + "match", + [ + "get", + "maki" + ], + [ + "museum", + "casino", + "castle", + "theatre", + "art-gallery", + "attraction", + "cinema", + "music", + "monument" + ], + "hsl(186, 78%, 44%)", + [ + "lodging" + ], + "#df7db1", + [ + "fitness-centre", + "golf", + "campsite", + "park", + "garden", + "farm", + "picnic-site", + "zoo", + "dog-park", + "stadium", + "bowling-alley", + "pitch", + "cemetery" + ], + "hsl(117, 53%, 31%)", + [ + "restaurant-pizza", + "restaurant-seafood", + "restaurant", + "restaurant-noodle", + "bar", + "cafe", + "fast-food", + "bakery", + "ice-cream" + ], + "#c77d57", + [ + "shop", + "shoe", + "alcohol-shop", + "convenience", + "grocery", + "clothing-store", + "jewelry-store" + ], + "hsl(213, 40%, 48%)", + [ + "bank", + "parking", + "parking-garage" + ], + "#737b9b", + [ + "hospital", + "doctor" + ], + "#a47172", + "#67747b" + ], + "text-halo-color": "#ffffff", + "text-halo-width": 1, + "icon-translate": [ + 0, + 0 + ], + "text-translate": [ + 0, + 0 + ], + "text-halo-blur": 1 + } + }, + { + "id": "poi_label_left", + "type": "symbol", + "metadata": { + "mapbox:group": "124a9d7a8e5226775d947c592110dfad", + "microg:gms-type-element": "labels.text", + "microg:gms-type-feature": "poi" + }, + "source": "composite", + "source-layer": "poi_label", + "filter": [ + "all", + [ + "has", + "name" + ], + [ + "<=", + [ + "get", + "filterrank" + ], + [ + "/", + [ + "zoom" + ], + 3.5 + ] + ], + [ + "!", + [ + "all", + [ + "match", + [ + "get", + "maki" + ], + [ + "park", + "cemetery" + ], + true, + false + ], + [ + "<=", + [ + "get", + "sizerank" + ], + 10 + ] + ] + ] + ], + "layout": { + "text-line-height": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 14, + 1, + 15, + 1.2 + ], + "text-size": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 14, + 13, + 15, + 14 + ], + "icon-offset": [ + 0, + -36 + ], + "icon-image": [ + "match", + [ + "get", + "maki" + ], + [ + "museum", + "lodging", + "theatre", + "grocery", + "restaurant" + ], + [ + "concat", + "poi_", + [ + "get", + "maki" + ] + ], + [ + "fitness-centre", + "golf", + "campsite", + "bowling-alley", + "park", + "garden", + "farm", + "picnic-site", + "zoo", + "stadium", + "dog-park", + "pitch", + "cemetery" + ], + "poi_generic_green", + [ + "bank", + "parking", + "parking-garage" + ], + "poi_generic_purple", + [ + "bar", + "cafe", + "bakery" + ], + "poi_generic_orange", + [ + "alcohol-shop", + "shop", + "shoe", + "convenience", + "clothing-store", + "jewelry-store" + ], + "poi_generic_blue", + [ + "casino", + "castle", + "art-gallery", + "attraction", + "cinema", + "music", + "monument" + ], + "poi_generic_teal", + [ + "hospital", + "doctor" + ], + "poi_generic_red", + [ + "restaurant-pizza", + "restaurant-seafood", + "restaurant-noodle", + "fast-food", + "ice-cream" + ], + "poi_restaurant", + "poi_generic" + ], + "text-font": [ + "Roboto Regular" + ], + "text-justify": "right", + "text-offset": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 14, + [ + "literal", + [ + -1.1, + -0.7 + ] + ], + 15, + [ + "literal", + [ + -1.1, + -0.9 + ] + ] + ], + "icon-size": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 14, + 0.25, + 15, + 0.32 + ], + "text-anchor": "right", + "text-field": [ + "to-string", + [ + "get", + "name" + ] + ] + }, + "paint": { + "text-color": [ + "match", + [ + "get", + "maki" + ], + [ + "museum", + "casino", + "castle", + "theatre", + "art-gallery", + "attraction", + "cinema", + "music", + "monument" + ], + "hsl(186, 78%, 44%)", + [ + "lodging" + ], + "#df7db1", + [ + "fitness-centre", + "golf", + "campsite", + "park", + "garden", + "farm", + "picnic-site", + "zoo", + "dog-park", + "stadium", + "bowling-alley", + "pitch", + "cemetery" + ], + "hsl(117, 53%, 31%)", + [ + "restaurant-pizza", + "restaurant-seafood", + "restaurant", + "restaurant-noodle", + "bar", + "cafe", + "fast-food", + "bakery", + "ice-cream" + ], + "#c77d57", + [ + "shop", + "shoe", + "alcohol-shop", + "convenience", + "grocery", + "clothing-store", + "jewelry-store" + ], + "hsl(213, 40%, 48%)", + [ + "bank", + "parking", + "parking-garage" + ], + "#737b9b", + [ + "hospital", + "doctor" + ], + "#a47172", + "#67747b" + ], + "text-halo-color": "#ffffff", + "text-halo-width": 1, + "icon-translate": [ + 0, + 0 + ], + "text-translate": [ + 0, + 0 + ], + "text-halo-blur": 1 + } + }, + { + "id": "poi_label_right", + "type": "symbol", + "metadata": { + "mapbox:group": "124a9d7a8e5226775d947c592110dfad", + "microg:gms-type-element": "labels.text", + "microg:gms-type-feature": "poi" + }, + "source": "composite", + "source-layer": "poi_label", + "filter": [ + "all", + [ + "has", + "name" + ], + [ + "<=", + [ + "get", + "filterrank" + ], + [ + "/", + [ + "zoom" + ], + 3.5 + ] + ], + [ + "!", + [ + "all", + [ + "match", + [ + "get", + "maki" + ], + [ + "park", + "cemetery" + ], + true, + false + ], + [ + "<=", + [ + "get", + "sizerank" + ], + 10 + ] + ] + ] + ], + "layout": { + "text-line-height": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 14, + 1, + 15, + 1.2 + ], + "text-size": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 14, + 13, + 15, + 14 + ], + "icon-offset": [ + 0, + -36 + ], + "icon-image": [ + "match", + [ + "get", + "maki" + ], + [ + "museum", + "lodging", + "theatre", + "grocery", + "restaurant" + ], + [ + "concat", + "poi_", + [ + "get", + "maki" + ] + ], + [ + "fitness-centre", + "golf", + "campsite", + "bowling-alley", + "park", + "garden", + "farm", + "picnic-site", + "zoo", + "stadium", + "dog-park", + "pitch", + "cemetery" + ], + "poi_generic_green", + [ + "bank", + "parking", + "parking-garage" + ], + "poi_generic_purple", + [ + "bar", + "cafe", + "bakery" + ], + "poi_generic_orange", + [ + "alcohol-shop", + "shop", + "shoe", + "convenience", + "clothing-store", + "jewelry-store" + ], + "poi_generic_blue", + [ + "casino", + "castle", + "art-gallery", + "attraction", + "cinema", + "music", + "monument" + ], + "poi_generic_teal", + [ + "hospital", + "doctor" + ], + "poi_generic_red", + [ + "restaurant-pizza", + "restaurant-seafood", + "restaurant-noodle", + "fast-food", + "ice-cream" + ], + "poi_restaurant", + "poi_generic" + ], + "text-font": [ + "Roboto Regular" + ], + "text-justify": "left", + "text-offset": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 14, + [ + "literal", + [ + 1.1, + -0.7 + ] + ], + 15, + [ + "literal", + [ + 1.1, + -0.9 + ] + ] + ], + "icon-size": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 14, + 0.25, + 15, + 0.32 + ], + "text-anchor": "left", + "text-field": [ + "to-string", + [ + "get", + "name" + ] + ] + }, + "paint": { + "text-color": [ + "match", + [ + "get", + "maki" + ], + [ + "museum", + "casino", + "castle", + "theatre", + "art-gallery", + "attraction", + "cinema", + "music", + "monument" + ], + "hsl(186, 78%, 44%)", + [ + "lodging" + ], + "#df7db1", + [ + "fitness-centre", + "golf", + "campsite", + "park", + "garden", + "farm", + "picnic-site", + "zoo", + "dog-park", + "stadium", + "bowling-alley", + "pitch", + "cemetery" + ], + "hsl(117, 53%, 31%)", + [ + "restaurant-pizza", + "restaurant-seafood", + "restaurant", + "restaurant-noodle", + "bar", + "cafe", + "fast-food", + "bakery", + "ice-cream" + ], + "#c77d57", + [ + "shop", + "shoe", + "alcohol-shop", + "convenience", + "grocery", + "clothing-store", + "jewelry-store" + ], + "hsl(213, 40%, 48%)", + [ + "bank", + "parking", + "parking-garage" + ], + "#737b9b", + [ + "hospital", + "doctor" + ], + "#a47172", + "#67747b" + ], + "text-halo-color": "#ffffff", + "text-halo-width": 1, + "icon-translate": [ + 0, + 0 + ], + "text-translate": [ + 0, + 0 + ], + "text-halo-blur": 1 + } + } + ], + "created": "2019-04-15T08:41:40.148Z", + "modified": "2020-09-05T19:42:03.856Z", + "id": "cjui4020201oo1fmca7yuwbor", + "owner": "microg", + "visibility": "public", + "protected": false, + "draft": false +} diff --git a/play-services-maps-core-mapbox/src/main/assets/style-microg-satellite.json b/play-services-maps-core-mapbox/src/main/assets/style-microg-satellite.json new file mode 100644 index 0000000000..accb62735b --- /dev/null +++ b/play-services-maps-core-mapbox/src/main/assets/style-microg-satellite.json @@ -0,0 +1,3489 @@ +{ + "version": 8, + "name": "Mountain View Satellite", + "metadata": { + "mapbox:origin": "basic-template", + "mapbox:autocomposite": true, + "mapbox:type": "template", + "mapbox:sdk-support": { + "js": "0.50.0", + "android": "6.7.0", + "ios": "4.6.0" + }, + "mapbox:trackposition": false, + "mapbox:groups": { + "f51b507d2a17e572c70a5db74b0fec7e": { + "name": "Base", + "collapsed": true + }, + "3f48b8dc54ff2e6544b9ef9cedbf2990": { + "name": "Streets", + "collapsed": false + }, + "29bb589e8d1b9b402583363648b70302": { + "name": "Buildings", + "collapsed": true + }, + "3c26e9cbc75335c6f0ba8de5439cf1fa": { + "name": "Country borders", + "collapsed": true + }, + "7b44201d7f1682d99f7140188aff23ce": { + "name": "Labels", + "collapsed": true + }, + "24306bdccbff03e2ee08d5d1a4ca7312": { + "name": "Street name", + "collapsed": false + }, + "124a9d7a8e5226775d947c592110dfad": { + "name": "POI", + "collapsed": true + } + }, + "mapbox:uiParadigm": "layers" + }, + "center": [ + 12.819420849458652, + 50.03325860617235 + ], + "zoom": 3.315829104862067, + "bearing": 0, + "pitch": 1.5, + "light": { + "intensity": 0.5, + "color": "hsl(0, 0%, 0%)" + }, + "sources": { + "mapbox://mapbox.satellite": { + "url": "mapbox://mapbox.satellite", + "type": "raster", + "tileSize": 256 + }, + "composite": { + "url": "mapbox://mapbox.mapbox-streets-v8,mapbox.mapbox-terrain-v2", + "type": "vector" + } + }, + "sprite": "mapbox://sprites/microg/cjxgloted25ap1ct4uex7m6hi/8fkcj5fgn4mftlzuak3guz1f9", + "glyphs": "mapbox://fonts/microg/{fontstack}/{range}.pbf", + "layers": [ + { + "id": "background", + "type": "raster", + "source": "mapbox://mapbox.satellite", + "layout": {}, + "paint": {}, + "metadata": { + "microg:gms-type-feature": "landscape.natural.landcover", + "microg:gms-type-element": "geometry.fill" + } + }, + { + "id": "path", + "type": "line", + "metadata": { + "mapbox:group": "3f48b8dc54ff2e6544b9ef9cedbf2990", + "microg:gms-type-feature": "road.local", + "microg:gms-type-element": "geometry.fill" + }, + "source": "composite", + "source-layer": "road", + "filter": [ + "all", + [ + "match", + [ + "get", + "class" + ], + [ + "path" + ], + true, + false + ], + [ + "match", + [ + "get", + "type" + ], + [ + "platform", + "steps" + ], + false, + true + ] + ], + "layout": {}, + "paint": { + "line-color": "hsl(118, 34%, 66%)", + "line-dasharray": [ + 4, + 2 + ], + "line-opacity": 0.3 + } + }, + { + "id": "steps", + "type": "line", + "metadata": { + "mapbox:group": "3f48b8dc54ff2e6544b9ef9cedbf2990", + "microg:gms-type-feature": "road.local", + "microg:gms-type-element": "geometry.fill" + }, + "source": "composite", + "source-layer": "road", + "filter": [ + "all", + [ + "match", + [ + "get", + "class" + ], + [ + "path" + ], + true, + false + ], + [ + "match", + [ + "get", + "type" + ], + [ + "steps" + ], + true, + false + ] + ], + "layout": {}, + "paint": { + "line-color": "hsl(118, 5%, 66%)", + "line-dasharray": [ + 1, + 1 + ], + "line-gap-width": 1, + "line-opacity": 0.3 + } + }, + { + "id": "platform", + "type": "fill", + "metadata": { + "mapbox:group": "3f48b8dc54ff2e6544b9ef9cedbf2990", + "microg:gms-type-feature": "transit.station.rail", + "microg:gms-type-element": "geometry.fill" + }, + "source": "composite", + "source-layer": "road", + "filter": [ + "match", + [ + "get", + "type" + ], + [ + "platform" + ], + true, + false + ], + "layout": {}, + "paint": { + "fill-color": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 15, + "hsl(2, 20%, 92%)", + 16, + "hsl(2, 95%, 92%)" + ], + "fill-outline-color": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 15, + "hsl(1, 10%, 76%)", + 16, + "hsl(1, 74%, 76%)" + ], + "fill-opacity": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 14, + 0, + 16, + 0.3 + ] + } + }, + { + "id": "primary_tunnel", + "type": "line", + "metadata": { + "mapbox:group": "3f48b8dc54ff2e6544b9ef9cedbf2990", + "microg:gms-type-feature": "road.highway", + "microg:gms-type-element": "geometry.fill" + }, + "source": "composite", + "source-layer": "road", + "filter": [ + "all", + [ + "match", + [ + "get", + "class" + ], + [ + "primary_link", + "primary", + "motorway_link" + ], + true, + false + ], + [ + "match", + [ + "get", + "structure" + ], + [ + "tunnel" + ], + true, + false + ] + ], + "layout": {}, + "paint": { + "line-width": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 8, + 1, + 12, + 4, + 14, + 6, + 16, + 10, + 22, + 64 + ], + "line-color": "hsl(0, 0%, 89%)", + "line-opacity": 0.3 + } + }, + { + "id": "aeroway", + "type": "line", + "metadata": { + "mapbox:group": "3f48b8dc54ff2e6544b9ef9cedbf2990", + "microg:gms-type-feature": "transit.station.airport", + "microg:gms-type-element": "geometry.fill" + }, + "source": "composite", + "source-layer": "aeroway", + "layout": {}, + "paint": { + "line-color": "hsla(0, 0%, 0%, 0.1)", + "line-opacity": 0.3 + } + }, + { + "id": "service_road", + "type": "line", + "metadata": { + "mapbox:group": "3f48b8dc54ff2e6544b9ef9cedbf2990", + "microg:gms-type-feature": "road.local", + "microg:gms-type-element": "geometry.fill" + }, + "source": "composite", + "source-layer": "road", + "filter": [ + "match", + [ + "get", + "class" + ], + [ + "service" + ], + true, + false + ], + "layout": {}, + "paint": { + "line-color": "hsla(0, 0%, 0%, 0.1)", + "line-opacity": 0.3 + } + }, + { + "id": "railway", + "type": "line", + "metadata": { + "mapbox:group": "3f48b8dc54ff2e6544b9ef9cedbf2990", + "microg:gms-type-feature": "transit.line", + "microg:gms-type-element": "geometry.fill" + }, + "source": "composite", + "source-layer": "road", + "filter": [ + "match", + [ + "get", + "class" + ], + [ + "major_rail", + "minor_rail", + "service_rail" + ], + true, + false + ], + "layout": {}, + "paint": { + "line-color": "hsl(220, 4%, 85%)", + "line-opacity": 0.3 + } + }, + { + "id": "pedestrian", + "type": "line", + "metadata": { + "mapbox:group": "3f48b8dc54ff2e6544b9ef9cedbf2990", + "microg:gms-type-feature": "road.local", + "microg:gms-type-element": "geometry.fill" + }, + "source": "composite", + "source-layer": "road", + "filter": [ + "all", + [ + "match", + [ + "get", + "class" + ], + [ + "pedestrian" + ], + true, + false + ], + [ + "has", + "name" + ], + [ + "match", + [ + "get", + "type" + ], + [ + "platform" + ], + false, + true + ] + ], + "layout": {}, + "paint": { + "line-color": "hsl(0, 0%, 100%)", + "line-width": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 13, + 1, + 16, + 4, + 22, + 32 + ], + "line-opacity": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 12.5, + 0, + 13.5, + 0.3 + ] + } + }, + { + "id": "street", + "type": "line", + "metadata": { + "mapbox:group": "3f48b8dc54ff2e6544b9ef9cedbf2990", + "microg:gms-type-feature": "road.local", + "microg:gms-type-element": "geometry.fill" + }, + "source": "composite", + "source-layer": "road", + "filter": [ + "match", + [ + "get", + "class" + ], + [ + "street" + ], + true, + false + ], + "layout": {}, + "paint": { + "line-color": "hsl(0, 0%, 100%)", + "line-width": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 12, + 1, + 16, + 6, + 22, + 40 + ], + "line-opacity": 0.3 + } + }, + { + "id": "secondary", + "type": "line", + "metadata": { + "mapbox:group": "3f48b8dc54ff2e6544b9ef9cedbf2990", + "microg:gms-type-feature": "road.local", + "microg:gms-type-element": "geometry.fill" + }, + "source": "composite", + "source-layer": "road", + "filter": [ + "match", + [ + "get", + "class" + ], + [ + "secondary", + "secondary_link", + "trunk_link", + "tertiary", + "tertiary_link", + "trunk" + ], + true, + false + ], + "layout": {}, + "paint": { + "line-color": "hsl(0, 0%, 100%)", + "line-width": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 10, + 1, + 17, + 10, + 22, + 48 + ], + "line-opacity": 0.3 + } + }, + { + "id": "primary", + "type": "line", + "metadata": { + "mapbox:group": "3f48b8dc54ff2e6544b9ef9cedbf2990", + "microg:gms-type-feature": "road.highway", + "microg:gms-type-element": "geometry.fill" + }, + "source": "composite", + "source-layer": "road", + "filter": [ + "all", + [ + "match", + [ + "get", + "class" + ], + [ + "primary_link", + "primary", + "motorway_link" + ], + true, + false + ], + [ + "match", + [ + "get", + "structure" + ], + [ + "tunnel" + ], + false, + true + ] + ], + "layout": { + "line-cap": "round" + }, + "paint": { + "line-color": [ + "step", + [ + "zoom" + ], + "hsl(50, 100%, 75%)", + 7, + "hsl(50, 100%, 85%)" + ], + "line-width": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 9, + 1, + 16, + 8, + 22, + 64 + ], + "line-opacity": 0.3 + } + }, + { + "id": "motorway", + "type": "line", + "metadata": { + "mapbox:group": "3f48b8dc54ff2e6544b9ef9cedbf2990", + "microg:gms-type-feature": "road.arterial", + "microg:gms-type-element": "geometry.fill" + }, + "source": "composite", + "source-layer": "road", + "filter": [ + "match", + [ + "get", + "class" + ], + [ + "motorway" + ], + true, + false + ], + "layout": {}, + "paint": { + "line-color": "hsl(47, 100%, 82%)", + "line-width": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 8, + 1, + 15.5, + 8, + 22, + 78 + ], + "line-opacity": 0.3 + } + }, + { + "id": "admin_0", + "type": "line", + "metadata": { + "mapbox:group": "3c26e9cbc75335c6f0ba8de5439cf1fa", + "microg:gms-type-feature": "administrative.country", + "microg:gms-type-element": "geometry.stroke" + }, + "source": "composite", + "source-layer": "admin", + "filter": [ + "all", + [ + "match", + [ + "get", + "maritime" + ], + [ + "false" + ], + true, + false + ], + [ + "match", + [ + "get", + "admin_level" + ], + [ + 0 + ], + true, + false + ] + ], + "layout": {}, + "paint": { + "line-color": [ + "case", + [ + "==", + [ + "get", + "disputed" + ], + true + ], + "hsl(0, 24%, 85%)", + "hsl(200, 1%, 85%)" + ] + } + }, + { + "id": "admin_1", + "type": "line", + "metadata": { + "mapbox:group": "3c26e9cbc75335c6f0ba8de5439cf1fa", + "microg:gms-type-feature": "administrative.province", + "microg:gms-type-element": "geometry.stroke" + }, + "source": "composite", + "source-layer": "admin", + "filter": [ + "all", + [ + "match", + [ + "get", + "maritime" + ], + [ + "false" + ], + true, + false + ], + [ + "match", + [ + "get", + "admin_level" + ], + [ + 1 + ], + true, + false + ] + ], + "layout": {}, + "paint": { + "line-color": [ + "case", + [ + "==", + [ + "get", + "disputed" + ], + true + ], + "hsl(0, 24%, 85%)", + "hsl(200, 1%, 85%)" + ], + "line-dasharray": [ + 1, + 1 + ] + } + }, + { + "id": "river_name", + "type": "symbol", + "metadata": { + "mapbox:group": "7b44201d7f1682d99f7140188aff23ce", + "microg:gms-type-feature": "water", + "microg:gms-type-element": "labels.text" + }, + "source": "composite", + "source-layer": "natural_label", + "filter": [ + "match", + [ + "get", + "class" + ], + [ + "river" + ], + true, + false + ], + "layout": { + "text-field": [ + "to-string", + [ + "get", + "name" + ] + ], + "symbol-placement": "line", + "symbol-spacing": 500, + "text-font": [ + "Roboto Regular", + "Arial Unicode MS Regular" + ] + }, + "paint": { + "text-color": "#5083c1", + "text-halo-color": "#5083c1", + "text-halo-blur": 1 + } + }, + { + "id": "city_label_right", + "type": "symbol", + "metadata": { + "mapbox:group": "7b44201d7f1682d99f7140188aff23ce", + "microg:gms-type-feature": "administrative.locality", + "microg:gms-type-element": "labels.text" + }, + "source": "composite", + "source-layer": "place_label", + "maxzoom": 8, + "filter": [ + "all", + [ + "<=", + [ + "get", + "filterrank" + ], + [ + "/", + [ + "zoom" + ], + 4 + ] + ], + [ + "match", + [ + "get", + "class" + ], + [ + "settlement", + "settlement_subdivision" + ], + true, + false + ] + ], + "layout": { + "text-optional": true, + "text-size": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 0, + [ + "-", + 14, + [ + "max", + 0, + [ + "-", + [ + "get", + "symbolrank" + ], + 8 + ] + ] + ], + 22, + [ + "-", + 20, + [ + "/", + [ + "get", + "symbolrank" + ], + 4 + ] + ] + ], + "icon-image": [ + "match", + [ + "get", + "capital" + ], + [ + 0, + 2 + ], + "capital", + "city" + ], + "text-font": [ + "step", + [ + "get", + "symbolrank" + ], + [ + "literal", + [ + "Roboto Medium", + "Arial Unicode MS Regular" + ] + ], + 10, + [ + "literal", + [ + "Roboto Regular", + "Arial Unicode MS Regular" + ] + ] + ], + "text-justify": "left", + "text-offset": [ + 0.5, + 0.1 + ], + "icon-size": [ + "/", + 5, + [ + "get", + "symbolrank" + ] + ], + "text-anchor": "left", + "text-field": [ + "coalesce", + [ + "get", + "name_en" + ], + [ + "get", + "name" + ] + ] + }, + "paint": { + "text-color": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 0, + [ + "concat", + "hsl(213, 11%, ", + [ + "-", + 100, + [ + "*", + [ + "get", + "symbolrank" + ], + 2 + ] + ], + "%)" + ], + 22, + [ + "concat", + "hsl(213, 11%, ", + [ + "+", + [ + "-", + 100, + [ + "get", + "symbolrank" + ] + ], + 5 + ], + "%)" + ] + ], + "text-halo-width": 1, + "text-halo-blur": 1, + "icon-opacity": 0.8, + "text-halo-color": "hsl(0, 1%, 0%)" + } + }, + { + "id": "city_label_left", + "type": "symbol", + "metadata": { + "mapbox:group": "7b44201d7f1682d99f7140188aff23ce", + "microg:gms-type-feature": "administrative.locality", + "microg:gms-type-element": "labels.text" + }, + "source": "composite", + "source-layer": "place_label", + "maxzoom": 8, + "filter": [ + "all", + [ + "<=", + [ + "get", + "filterrank" + ], + [ + "/", + [ + "zoom" + ], + 4 + ] + ], + [ + "match", + [ + "get", + "class" + ], + [ + "settlement", + "settlement_subdivision" + ], + true, + false + ] + ], + "layout": { + "text-size": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 0, + [ + "-", + 14, + [ + "max", + 0, + [ + "-", + [ + "get", + "symbolrank" + ], + 8 + ] + ] + ], + 22, + [ + "-", + 20, + [ + "/", + [ + "get", + "symbolrank" + ], + 4 + ] + ] + ], + "icon-image": [ + "match", + [ + "get", + "capital" + ], + [ + 0, + 2 + ], + "capital", + "city" + ], + "text-font": [ + "step", + [ + "get", + "symbolrank" + ], + [ + "literal", + [ + "Roboto Medium", + "Arial Unicode MS Regular" + ] + ], + 10, + [ + "literal", + [ + "Roboto Regular", + "Arial Unicode MS Regular" + ] + ] + ], + "text-justify": "right", + "text-offset": [ + -0.5, + 0.1 + ], + "icon-size": [ + "/", + 5, + [ + "get", + "symbolrank" + ] + ], + "text-anchor": "right", + "text-field": [ + "coalesce", + [ + "get", + "name_en" + ], + [ + "get", + "name" + ] + ] + }, + "paint": { + "text-color": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 0, + [ + "concat", + "hsl(213, 11%, ", + [ + "-", + 100, + [ + "*", + [ + "get", + "symbolrank" + ], + 2 + ] + ], + "%)" + ], + 22, + [ + "concat", + "hsl(213, 11%, ", + [ + "+", + [ + "-", + 100, + [ + "get", + "symbolrank" + ] + ], + 5 + ], + "%)" + ] + ], + "text-halo-width": 1, + "text-halo-blur": 1, + "icon-opacity": 0.8, + "text-halo-color": "hsl(0, 1%, 0%)" + } + }, + { + "id": "city_label_below", + "type": "symbol", + "metadata": { + "mapbox:group": "7b44201d7f1682d99f7140188aff23ce", + "microg:gms-type-feature": "administrative.locality", + "microg:gms-type-element": "labels.text" + }, + "source": "composite", + "source-layer": "place_label", + "maxzoom": 8, + "filter": [ + "all", + [ + "<=", + [ + "get", + "filterrank" + ], + [ + "/", + [ + "zoom" + ], + 4 + ] + ], + [ + "match", + [ + "get", + "class" + ], + [ + "settlement", + "settlement_subdivision" + ], + true, + false + ] + ], + "layout": { + "text-size": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 0, + [ + "-", + 14, + [ + "max", + 0, + [ + "-", + [ + "get", + "symbolrank" + ], + 8 + ] + ] + ], + 22, + [ + "-", + 20, + [ + "/", + [ + "get", + "symbolrank" + ], + 4 + ] + ] + ], + "icon-image": [ + "match", + [ + "get", + "capital" + ], + [ + 0, + 2 + ], + "capital", + "city" + ], + "text-font": [ + "step", + [ + "get", + "symbolrank" + ], + [ + "literal", + [ + "Roboto Medium", + "Arial Unicode MS Regular" + ] + ], + 10, + [ + "literal", + [ + "Roboto Regular", + "Arial Unicode MS Regular" + ] + ] + ], + "text-offset": [ + 0, + 0.4 + ], + "icon-size": [ + "/", + 5, + [ + "get", + "symbolrank" + ] + ], + "text-anchor": "top", + "text-field": [ + "coalesce", + [ + "get", + "name_en" + ], + [ + "get", + "name" + ] + ] + }, + "paint": { + "text-color": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 0, + [ + "concat", + "hsl(213, 11%, ", + [ + "-", + 100, + [ + "*", + [ + "get", + "symbolrank" + ], + 2 + ] + ], + "%)" + ], + 22, + [ + "concat", + "hsl(213, 11%, ", + [ + "+", + [ + "-", + 100, + [ + "get", + "symbolrank" + ] + ], + 5 + ], + "%)" + ] + ], + "text-halo-width": 1, + "text-halo-blur": 1, + "icon-opacity": 0.8, + "text-halo-color": "hsl(0, 1%, 0%)" + } + }, + { + "id": "city_name", + "type": "symbol", + "metadata": { + "mapbox:group": "7b44201d7f1682d99f7140188aff23ce", + "microg:gms-type-feature": "administrative.locality", + "microg:gms-type-element": "labels.text" + }, + "source": "composite", + "source-layer": "place_label", + "filter": [ + "all", + [ + "<=", + [ + "get", + "filterrank" + ], + [ + "/", + [ + "zoom" + ], + 4 + ] + ], + [ + "match", + [ + "get", + "class" + ], + [ + "settlement", + "settlement_subdivision" + ], + true, + false + ] + ], + "layout": { + "text-size": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 0, + [ + "-", + 14, + [ + "max", + 0, + [ + "-", + [ + "get", + "symbolrank" + ], + 8 + ] + ] + ], + 22, + [ + "-", + 20, + [ + "/", + [ + "get", + "symbolrank" + ], + 4 + ] + ] + ], + "icon-image": [ + "step", + [ + "zoom" + ], + [ + "match", + [ + "get", + "capital" + ], + [ + 0, + 2 + ], + "capital", + "city" + ], + 8, + "" + ], + "text-transform": [ + "step", + [ + "get", + "symbolrank" + ], + "none", + 15, + "uppercase" + ], + "text-font": [ + "step", + [ + "get", + "symbolrank" + ], + [ + "literal", + [ + "Roboto Medium", + "Arial Unicode MS Regular" + ] + ], + 10, + [ + "literal", + [ + "Roboto Regular", + "Arial Unicode MS Regular" + ] + ] + ], + "text-offset": [ + "step", + [ + "zoom" + ], + [ + "literal", + [ + 0, + -0.2 + ] + ], + 8, + [ + "literal", + [ + 0, + 0 + ] + ] + ], + "icon-size": [ + "/", + 5, + [ + "get", + "symbolrank" + ] + ], + "text-anchor": [ + "step", + [ + "zoom" + ], + "bottom", + 8, + "center" + ], + "text-field": [ + "coalesce", + [ + "get", + "name_en" + ], + [ + "get", + "name" + ] + ], + "text-letter-spacing": [ + "interpolate", + [ + "linear" + ], + [ + "get", + "symbolrank" + ], + 0, + 0, + 8, + 0, + 12, + 0.1, + 16, + 0.2 + ] + }, + "paint": { + "text-color": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 0, + [ + "concat", + "hsl(213, 11%, ", + [ + "-", + 100, + [ + "*", + [ + "get", + "symbolrank" + ], + 2 + ] + ], + "%)" + ], + 22, + [ + "concat", + "hsl(213, 11%, ", + [ + "+", + [ + "-", + 100, + [ + "get", + "symbolrank" + ] + ], + 5 + ], + "%)" + ] + ], + "text-halo-width": 1, + "text-halo-blur": 1, + "icon-opacity": 0.8, + "text-halo-color": "hsl(0, 1%, 0%)" + } + }, + { + "id": "park_name", + "type": "symbol", + "metadata": { + "mapbox:group": "7b44201d7f1682d99f7140188aff23ce", + "microg:gms-type-feature": "poi.park", + "microg:gms-type-element": "labels.text" + }, + "source": "composite", + "source-layer": "poi_label", + "filter": [ + "all", + [ + "has", + "name" + ], + [ + "match", + [ + "get", + "maki" + ], + [ + "park", + "cemetery" + ], + true, + false + ], + [ + "<=", + [ + "get", + "sizerank" + ], + 10 + ] + ], + "layout": { + "text-field": [ + "to-string", + [ + "get", + "name" + ] + ], + "text-font": [ + "Roboto Regular", + "Arial Unicode MS Regular" + ], + "text-size": 14 + }, + "paint": { + "text-color": "#297925", + "text-halo-color": "hsl(0, 0%, 100%)", + "text-halo-width": 1, + "text-halo-blur": 1 + } + }, + { + "id": "road-number-shield", + "type": "symbol", + "metadata": { + "mapbox:group": "24306bdccbff03e2ee08d5d1a4ca7312", + "microg:gms-type-feature": "road.highway", + "microg:gms-type-element": "labels.icon" + }, + "source": "composite", + "source-layer": "road", + "filter": [ + "all", + [ + "has", + "ref" + ], + [ + "<=", + [ + "get", + "reflen" + ], + 6 + ] + ], + "layout": { + "text-size": 9, + "icon-image": [ + "case", + [ + "match", + [ + "get", + "shield" + ], + [ + "de-motorway", + "rectangle-green", + "rectangle-yellow", + "rectangle-white", + "rectangle-blue", + "rectangle-red", + "us-interstate" + ], + true, + false + ], + [ + "concat", + "shield_", + [ + "get", + "shield" + ], + "_", + [ + "get", + "reflen" + ] + ], + [ + "match", + [ + "get", + "shield_text_color" + ], + [ + "white" + ], + true, + false + ], + [ + "concat", + "shield_rectangle-blue_", + [ + "get", + "reflen" + ] + ], + [ + "concat", + "shield_rectangle-white_", + [ + "get", + "reflen" + ] + ] + ], + "icon-rotation-alignment": "viewport", + "text-font": [ + "match", + [ + "get", + "shield_text_color" + ], + [ + "white" + ], + [ + "literal", + [ + "DIN Offc Pro Bold", + "Arial Unicode MS Regular" + ] + ], + [ + "black" + ], + [ + "literal", + [ + "DIN Offc Pro Medium", + "Arial Unicode MS Regular" + ] + ], + [ + "literal", + [ + "DIN Offc Pro Bold", + "Arial Unicode MS Regular" + ] + ] + ], + "symbol-placement": [ + "step", + [ + "zoom" + ], + "point", + 11, + "line" + ], + "text-rotation-alignment": "viewport", + "icon-size": 0.75, + "text-field": [ + "get", + "ref" + ], + "text-letter-spacing": 0.05 + }, + "paint": { + "text-color": [ + "case", + [ + "match", + [ + "get", + "shield_text_color" + ], + [ + "white" + ], + true, + false + ], + "#ffffff", + [ + "match", + [ + "get", + "shield_text_color" + ], + [ + "black" + ], + true, + false + ], + "hsl(0, 0%, 7%)", + [ + "match", + [ + "get", + "shield_text_color" + ], + [ + "yellow" + ], + true, + false + ], + "hsl(50, 100%, 70%)", + [ + "match", + [ + "get", + "shield_text_color" + ], + [ + "orange" + ], + true, + false + ], + "hsl(25, 100%, 75%)", + [ + "match", + [ + "get", + "shield_text_color" + ], + [ + "blue" + ], + true, + false + ], + "hsl(230, 48%, 34%)", + "#ffffff" + ] + } + }, + { + "id": "country_name", + "type": "symbol", + "metadata": { + "mapbox:group": "24306bdccbff03e2ee08d5d1a4ca7312", + "microg:gms-type-feature": "administrative.country", + "microg:gms-type-element": "labels.text" + }, + "source": "composite", + "source-layer": "place_label", + "filter": [ + "match", + [ + "get", + "class" + ], + [ + "country" + ], + true, + false + ], + "layout": { + "text-letter-spacing": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 1, + 0, + 3, + 0.15 + ], + "text-font": [ + "Roboto Medium", + "Arial Unicode MS Regular" + ], + "text-size": [ + "interpolate", + [ + "exponential", + 1.2 + ], + [ + "zoom" + ], + 1, + 12, + 7, + [ + "/", + 100, + [ + "get", + "symbolrank" + ] + ] + ], + "text-field": [ + "coalesce", + [ + "get", + "name_en" + ], + [ + "get", + "name" + ] + ] + }, + "paint": { + "text-opacity": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 6, + 1, + 7, + 0 + ], + "text-color": "hsl(0, 0%, 90%)", + "text-halo-width": 1, + "text-halo-blur": 1, + "text-halo-color": "hsl(0, 1%, 0%)" + } + }, + { + "id": "pedestrian_name", + "type": "symbol", + "metadata": { + "mapbox:group": "24306bdccbff03e2ee08d5d1a4ca7312", + "microg:gms-type-feature": "road.local", + "microg:gms-type-element": "labels.text" + }, + "source": "composite", + "source-layer": "road", + "minzoom": 16, + "filter": [ + "all", + [ + "has", + "name" + ], + [ + "match", + [ + "get", + "class" + ], + [ + "pedestrian" + ], + true, + false + ], + [ + "match", + [ + "get", + "type" + ], + [ + "platform" + ], + false, + true + ] + ], + "layout": { + "text-field": [ + "get", + "name" + ], + "symbol-placement": "line", + "text-font": [ + "Roboto Regular", + "Arial Unicode MS Regular" + ], + "text-size": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 17, + 10, + 22, + 14 + ], + "text-padding": 5 + }, + "paint": { + "text-color": "hsl(0, 0%, 86%)", + "text-halo-color": "hsl(0, 1%, 0%)", + "text-halo-width": 1, + "text-halo-blur": 1 + } + }, + { + "id": "street_name", + "type": "symbol", + "metadata": { + "mapbox:group": "24306bdccbff03e2ee08d5d1a4ca7312", + "microg:gms-type-feature": "road.local", + "microg:gms-type-element": "labels.text" + }, + "source": "composite", + "source-layer": "road", + "filter": [ + "all", + [ + "has", + "name" + ], + [ + "match", + [ + "get", + "class" + ], + [ + "street" + ], + true, + false + ] + ], + "layout": { + "text-field": [ + "get", + "name" + ], + "symbol-placement": "line", + "text-font": [ + "Roboto Regular", + "Arial Unicode MS Regular" + ], + "text-size": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 8, + 6, + 16, + 10 + ], + "text-padding": 5 + }, + "paint": { + "text-color": "hsl(0, 0%, 86%)", + "text-halo-color": "hsl(0, 1%, 0%)", + "text-halo-width": 1, + "text-halo-blur": 1 + } + }, + { + "id": "secondary_name", + "type": "symbol", + "metadata": { + "mapbox:group": "24306bdccbff03e2ee08d5d1a4ca7312", + "microg:gms-type-feature": "road.local", + "microg:gms-type-element": "labels.text" + }, + "source": "composite", + "source-layer": "road", + "filter": [ + "all", + [ + "has", + "name" + ], + [ + "match", + [ + "get", + "class" + ], + [ + "secondary", + "secondary_link", + "tertiary_link", + "tertiary", + "trunk_link", + "trunk" + ], + true, + false + ] + ], + "layout": { + "text-field": [ + "get", + "name" + ], + "symbol-placement": "line", + "text-font": [ + "Roboto Regular", + "Arial Unicode MS Regular" + ], + "text-size": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 8, + 8, + 16, + 13 + ], + "symbol-spacing": 300, + "text-padding": 25 + }, + "paint": { + "text-color": "hsl(196, 0%, 86%)", + "text-halo-width": 1, + "text-halo-color": "hsl(0, 1%, 0%)", + "text-halo-blur": 1 + } + }, + { + "id": "primary_name", + "type": "symbol", + "metadata": { + "mapbox:group": "24306bdccbff03e2ee08d5d1a4ca7312", + "microg:gms-type-feature": "road.highway", + "microg:gms-type-element": "labels.text" + }, + "source": "composite", + "source-layer": "road", + "filter": [ + "all", + [ + "has", + "name" + ], + [ + "match", + [ + "get", + "class" + ], + [ + "primary", + "primary_link" + ], + true, + false + ] + ], + "layout": { + "text-field": [ + "get", + "name" + ], + "symbol-placement": "line", + "text-font": [ + "Roboto Regular", + "Arial Unicode MS Regular" + ], + "text-size": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 8, + 10, + 18, + 14 + ], + "symbol-spacing": 800, + "text-padding": 50 + }, + "paint": { + "text-color": "hsl(32, 58%, 93%)", + "text-halo-width": 1, + "text-halo-color": "hsl(0, 1%, 0%)", + "text-halo-blur": 1 + } + }, + { + "id": "poi_label_below", + "type": "symbol", + "metadata": { + "mapbox:group": "124a9d7a8e5226775d947c592110dfad", + "microg:gms-type-feature": "poi", + "microg:gms-type-element": "labels.text" + }, + "source": "composite", + "source-layer": "poi_label", + "filter": [ + "all", + [ + "has", + "name" + ], + [ + "<=", + [ + "get", + "filterrank" + ], + [ + "/", + [ + "zoom" + ], + 4 + ] + ], + [ + "!", + [ + "all", + [ + "match", + [ + "get", + "maki" + ], + [ + "park", + "cemetery" + ], + true, + false + ], + [ + "<=", + [ + "get", + "sizerank" + ], + 10 + ] + ] + ] + ], + "layout": { + "text-optional": true, + "text-line-height": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 14, + 1, + 15, + 1.2 + ], + "text-size": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 14, + 13, + 15, + 14 + ], + "icon-offset": [ + 0, + -36 + ], + "icon-image": [ + "match", + [ + "get", + "maki" + ], + [ + "museum", + "lodging", + "theatre", + "grocery", + "restaurant" + ], + [ + "concat", + "poi_", + [ + "get", + "maki" + ] + ], + [ + "fitness-centre", + "golf", + "campsite", + "bowling-alley", + "park", + "garden", + "farm", + "picnic-site", + "zoo", + "stadium", + "dog-park", + "pitch", + "cemetery" + ], + "poi_generic_green", + [ + "bank", + "parking", + "parking-garage" + ], + "poi_generic_purple", + [ + "bar", + "cafe", + "bakery" + ], + "poi_generic_orange", + [ + "alcohol-shop", + "shop", + "shoe", + "convenience", + "clothing-store", + "jewelry-store" + ], + "poi_generic_blue", + [ + "casino", + "castle", + "art-gallery", + "attraction", + "cinema", + "music", + "monument" + ], + "poi_generic_teal", + [ + "hospital", + "doctor" + ], + "poi_generic_red", + [ + "restaurant-pizza", + "restaurant-seafood", + "restaurant-noodle", + "fast-food", + "ice-cream" + ], + "poi_restaurant", + "poi_generic" + ], + "text-font": [ + "Roboto Regular", + "Arial Unicode MS Regular" + ], + "text-offset": [ + 0, + 0.5 + ], + "icon-size": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 14, + 0.25, + 15, + 0.32 + ], + "text-anchor": "top", + "text-field": [ + "to-string", + [ + "get", + "name" + ] + ] + }, + "paint": { + "text-color": [ + "match", + [ + "get", + "maki" + ], + [ + "museum", + "casino", + "castle", + "theatre", + "art-gallery", + "attraction", + "cinema", + "music", + "monument" + ], + "hsl(186, 78%, 65%)", + [ + "lodging" + ], + "#df7db1", + [ + "fitness-centre", + "golf", + "campsite", + "park", + "garden", + "farm", + "picnic-site", + "zoo", + "dog-park", + "stadium", + "bowling-alley", + "pitch", + "cemetery" + ], + "hsl(117, 53%, 65%)", + [ + "restaurant-pizza", + "restaurant-seafood", + "restaurant", + "restaurant-noodle", + "bar", + "cafe", + "fast-food", + "bakery", + "ice-cream" + ], + "hsl(20, 50%, 65%)", + [ + "shop", + "shoe", + "alcohol-shop", + "convenience", + "grocery", + "clothing-store", + "jewelry-store" + ], + "hsl(213, 40%, 65%)", + [ + "bank", + "parking", + "parking-garage" + ], + "hsl(228, 17%, 65%)", + [ + "hospital", + "doctor" + ], + "hsl(359, 22%, 65%)", + "hsl(201, 9%, 80%)" + ], + "text-halo-color": "hsl(0, 1%, 0%)", + "text-halo-width": 1, + "icon-translate": [ + 0, + 0 + ], + "text-translate": [ + 0, + 0 + ], + "text-halo-blur": 1 + } + }, + { + "id": "poi_label_above", + "type": "symbol", + "metadata": { + "mapbox:group": "124a9d7a8e5226775d947c592110dfad", + "microg:gms-type-feature": "poi", + "microg:gms-type-element": "labels.text" + }, + "source": "composite", + "source-layer": "poi_label", + "filter": [ + "all", + [ + "has", + "name" + ], + [ + "<=", + [ + "get", + "filterrank" + ], + [ + "/", + [ + "zoom" + ], + 4 + ] + ], + [ + "!", + [ + "all", + [ + "match", + [ + "get", + "maki" + ], + [ + "park", + "cemetery" + ], + true, + false + ], + [ + "<=", + [ + "get", + "sizerank" + ], + 10 + ] + ] + ] + ], + "layout": { + "text-optional": true, + "text-line-height": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 14, + 1, + 15, + 1.2 + ], + "text-size": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 14, + 13, + 15, + 14 + ], + "icon-offset": [ + 0, + -36 + ], + "icon-image": [ + "match", + [ + "get", + "maki" + ], + [ + "museum", + "lodging", + "theatre", + "grocery", + "restaurant" + ], + [ + "concat", + "poi_", + [ + "get", + "maki" + ] + ], + [ + "fitness-centre", + "golf", + "campsite", + "bowling-alley", + "park", + "garden", + "farm", + "picnic-site", + "zoo", + "stadium", + "dog-park", + "pitch", + "cemetery" + ], + "poi_generic_green", + [ + "bank", + "parking", + "parking-garage" + ], + "poi_generic_purple", + [ + "bar", + "cafe", + "bakery" + ], + "poi_generic_orange", + [ + "alcohol-shop", + "shop", + "shoe", + "convenience", + "clothing-store", + "jewelry-store" + ], + "poi_generic_blue", + [ + "casino", + "castle", + "art-gallery", + "attraction", + "cinema", + "music", + "monument" + ], + "poi_generic_teal", + [ + "hospital", + "doctor" + ], + "poi_generic_red", + [ + "restaurant-pizza", + "restaurant-seafood", + "restaurant-noodle", + "fast-food", + "ice-cream" + ], + "poi_restaurant", + "poi_generic" + ], + "text-font": [ + "Roboto Regular", + "Arial Unicode MS Regular" + ], + "text-offset": [ + 0, + -2 + ], + "icon-size": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 14, + 0.25, + 15, + 0.32 + ], + "text-anchor": "bottom", + "text-field": [ + "to-string", + [ + "get", + "name" + ] + ] + }, + "paint": { + "text-color": [ + "match", + [ + "get", + "maki" + ], + [ + "museum", + "casino", + "castle", + "theatre", + "art-gallery", + "attraction", + "cinema", + "music", + "monument" + ], + "hsl(186, 78%, 65%)", + [ + "lodging" + ], + "#df7db1", + [ + "fitness-centre", + "golf", + "campsite", + "park", + "garden", + "farm", + "picnic-site", + "zoo", + "dog-park", + "stadium", + "bowling-alley", + "pitch", + "cemetery" + ], + "hsl(117, 53%, 65%)", + [ + "restaurant-pizza", + "restaurant-seafood", + "restaurant", + "restaurant-noodle", + "bar", + "cafe", + "fast-food", + "bakery", + "ice-cream" + ], + "hsl(20, 50%, 65%)", + [ + "shop", + "shoe", + "alcohol-shop", + "convenience", + "grocery", + "clothing-store", + "jewelry-store" + ], + "hsl(213, 40%, 65%)", + [ + "bank", + "parking", + "parking-garage" + ], + "hsl(228, 17%, 65%)", + [ + "hospital", + "doctor" + ], + "hsl(359, 22%, 65%)", + "hsl(201, 9%, 80%)" + ], + "text-halo-color": "hsl(0, 1%, 0%)", + "text-halo-width": 1, + "icon-translate": [ + 0, + 0 + ], + "text-translate": [ + 0, + 0 + ], + "text-halo-blur": 1 + } + }, + { + "id": "poi_label_left", + "type": "symbol", + "metadata": { + "mapbox:group": "124a9d7a8e5226775d947c592110dfad", + "microg:gms-type-feature": "poi", + "microg:gms-type-element": "labels.text" + }, + "source": "composite", + "source-layer": "poi_label", + "filter": [ + "all", + [ + "has", + "name" + ], + [ + "<=", + [ + "get", + "filterrank" + ], + [ + "/", + [ + "zoom" + ], + 4 + ] + ], + [ + "!", + [ + "all", + [ + "match", + [ + "get", + "maki" + ], + [ + "park", + "cemetery" + ], + true, + false + ], + [ + "<=", + [ + "get", + "sizerank" + ], + 10 + ] + ] + ] + ], + "layout": { + "text-line-height": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 14, + 1, + 15, + 1.2 + ], + "text-size": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 14, + 13, + 15, + 14 + ], + "icon-offset": [ + 0, + -36 + ], + "icon-image": [ + "match", + [ + "get", + "maki" + ], + [ + "museum", + "lodging", + "theatre", + "grocery", + "restaurant" + ], + [ + "concat", + "poi_", + [ + "get", + "maki" + ] + ], + [ + "fitness-centre", + "golf", + "campsite", + "bowling-alley", + "park", + "garden", + "farm", + "picnic-site", + "zoo", + "stadium", + "dog-park", + "pitch", + "cemetery" + ], + "poi_generic_green", + [ + "bank", + "parking", + "parking-garage" + ], + "poi_generic_purple", + [ + "bar", + "cafe", + "bakery" + ], + "poi_generic_orange", + [ + "alcohol-shop", + "shop", + "shoe", + "convenience", + "clothing-store", + "jewelry-store" + ], + "poi_generic_blue", + [ + "casino", + "castle", + "art-gallery", + "attraction", + "cinema", + "music", + "monument" + ], + "poi_generic_teal", + [ + "hospital", + "doctor" + ], + "poi_generic_red", + [ + "restaurant-pizza", + "restaurant-seafood", + "restaurant-noodle", + "fast-food", + "ice-cream" + ], + "poi_restaurant", + "poi_generic" + ], + "text-font": [ + "Roboto Regular", + "Arial Unicode MS Regular" + ], + "text-justify": "right", + "text-offset": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 14, + [ + "literal", + [ + -1.1, + -0.7 + ] + ], + 15, + [ + "literal", + [ + -1.1, + -0.9 + ] + ] + ], + "icon-size": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 14, + 0.25, + 15, + 0.32 + ], + "text-anchor": "right", + "text-field": [ + "to-string", + [ + "get", + "name" + ] + ] + }, + "paint": { + "text-color": [ + "match", + [ + "get", + "maki" + ], + [ + "museum", + "casino", + "castle", + "theatre", + "art-gallery", + "attraction", + "cinema", + "music", + "monument" + ], + "hsl(186, 78%, 65%)", + [ + "lodging" + ], + "#df7db1", + [ + "fitness-centre", + "golf", + "campsite", + "park", + "garden", + "farm", + "picnic-site", + "zoo", + "dog-park", + "stadium", + "bowling-alley", + "pitch", + "cemetery" + ], + "hsl(117, 53%, 65%)", + [ + "restaurant-pizza", + "restaurant-seafood", + "restaurant", + "restaurant-noodle", + "bar", + "cafe", + "fast-food", + "bakery", + "ice-cream" + ], + "hsl(20, 50%, 65%)", + [ + "shop", + "shoe", + "alcohol-shop", + "convenience", + "grocery", + "clothing-store", + "jewelry-store" + ], + "hsl(213, 40%, 65%)", + [ + "bank", + "parking", + "parking-garage" + ], + "hsl(228, 17%, 65%)", + [ + "hospital", + "doctor" + ], + "hsl(359, 22%, 65%)", + "hsl(201, 9%, 80%)" + ], + "text-halo-color": "hsl(0, 1%, 0%)", + "text-halo-width": 1, + "icon-translate": [ + 0, + 0 + ], + "text-translate": [ + 0, + 0 + ], + "text-halo-blur": 1 + } + }, + { + "id": "poi_label_right", + "type": "symbol", + "metadata": { + "mapbox:group": "124a9d7a8e5226775d947c592110dfad", + "microg:gms-type-feature": "poi", + "microg:gms-type-element": "labels.text" + }, + "source": "composite", + "source-layer": "poi_label", + "filter": [ + "all", + [ + "has", + "name" + ], + [ + "<=", + [ + "get", + "filterrank" + ], + [ + "/", + [ + "zoom" + ], + 4 + ] + ], + [ + "!", + [ + "all", + [ + "match", + [ + "get", + "maki" + ], + [ + "park", + "cemetery" + ], + true, + false + ], + [ + "<=", + [ + "get", + "sizerank" + ], + 10 + ] + ] + ] + ], + "layout": { + "text-line-height": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 14, + 1, + 15, + 1.2 + ], + "text-size": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 14, + 13, + 15, + 14 + ], + "icon-offset": [ + 0, + -36 + ], + "icon-image": [ + "match", + [ + "get", + "maki" + ], + [ + "museum", + "lodging", + "theatre", + "grocery", + "restaurant" + ], + [ + "concat", + "poi_", + [ + "get", + "maki" + ] + ], + [ + "fitness-centre", + "golf", + "campsite", + "bowling-alley", + "park", + "garden", + "farm", + "picnic-site", + "zoo", + "stadium", + "dog-park", + "pitch", + "cemetery" + ], + "poi_generic_green", + [ + "bank", + "parking", + "parking-garage" + ], + "poi_generic_purple", + [ + "bar", + "cafe", + "bakery" + ], + "poi_generic_orange", + [ + "alcohol-shop", + "shop", + "shoe", + "convenience", + "clothing-store", + "jewelry-store" + ], + "poi_generic_blue", + [ + "casino", + "castle", + "art-gallery", + "attraction", + "cinema", + "music", + "monument" + ], + "poi_generic_teal", + [ + "hospital", + "doctor" + ], + "poi_generic_red", + [ + "restaurant-pizza", + "restaurant-seafood", + "restaurant-noodle", + "fast-food", + "ice-cream" + ], + "poi_restaurant", + "poi_generic" + ], + "text-font": [ + "Roboto Regular", + "Arial Unicode MS Regular" + ], + "text-justify": "left", + "text-offset": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 14, + [ + "literal", + [ + 1.1, + -0.7 + ] + ], + 15, + [ + "literal", + [ + 1.1, + -0.9 + ] + ] + ], + "icon-size": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 14, + 0.25, + 15, + 0.32 + ], + "text-anchor": "left", + "text-field": [ + "to-string", + [ + "get", + "name" + ] + ] + }, + "paint": { + "text-color": [ + "match", + [ + "get", + "maki" + ], + [ + "museum", + "casino", + "castle", + "theatre", + "art-gallery", + "attraction", + "cinema", + "music", + "monument" + ], + "hsl(186, 78%, 65%)", + [ + "lodging" + ], + "#df7db1", + [ + "fitness-centre", + "golf", + "campsite", + "park", + "garden", + "farm", + "picnic-site", + "zoo", + "dog-park", + "stadium", + "bowling-alley", + "pitch", + "cemetery" + ], + "hsl(117, 53%, 65%)", + [ + "restaurant-pizza", + "restaurant-seafood", + "restaurant", + "restaurant-noodle", + "bar", + "cafe", + "fast-food", + "bakery", + "ice-cream" + ], + "hsl(20, 50%, 65%)", + [ + "shop", + "shoe", + "alcohol-shop", + "convenience", + "grocery", + "clothing-store", + "jewelry-store" + ], + "hsl(213, 40%, 65%)", + [ + "bank", + "parking", + "parking-garage" + ], + "hsl(228, 17%, 65%)", + [ + "hospital", + "doctor" + ], + "hsl(359, 22%, 65%)", + "hsl(201, 9%, 80%)" + ], + "text-halo-color": "hsl(0, 1%, 0%)", + "text-halo-width": 1, + "icon-translate": [ + 0, + 0 + ], + "text-translate": [ + 0, + 0 + ], + "text-halo-blur": 1 + } + } + ], + "created": "2019-06-28T21:20:23.628Z", + "modified": "2020-09-05T20:08:11.990Z", + "id": "cjxgloted25ap1ct4uex7m6hi", + "owner": "microg", + "visibility": "public", + "protected": false, + "draft": false +} diff --git a/play-services-maps-core-mapbox/src/main/java/com/google/android/gms/dynamite/descriptors/com/google/android/gms/maps_dynamite/ModuleDescriptor.java b/play-services-maps-core-mapbox/src/main/java/com/google/android/gms/dynamite/descriptors/com/google/android/gms/maps_dynamite/ModuleDescriptor.java new file mode 100644 index 0000000000..edfbe6c36a --- /dev/null +++ b/play-services-maps-core-mapbox/src/main/java/com/google/android/gms/dynamite/descriptors/com/google/android/gms/maps_dynamite/ModuleDescriptor.java @@ -0,0 +1,14 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.dynamite.descriptors.com.google.android.gms.maps_dynamite; + +import androidx.annotation.Keep; + +@Keep +public class ModuleDescriptor { + public static final String MODULE_ID = "com.google.android.gms.maps_dynamite"; + public static final int MODULE_VERSION = 1; +} diff --git a/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/AbstractGoogleMap.kt b/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/AbstractGoogleMap.kt new file mode 100644 index 0000000000..6c874d80bd --- /dev/null +++ b/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/AbstractGoogleMap.kt @@ -0,0 +1,171 @@ +package org.microg.gms.maps.mapbox + +import android.content.Context +import android.location.Location +import android.os.Bundle +import android.util.DisplayMetrics +import android.util.Log +import com.google.android.gms.dynamic.IObjectWrapper +import com.google.android.gms.dynamic.ObjectWrapper +import com.google.android.gms.maps.internal.* +import com.mapbox.mapboxsdk.location.engine.LocationEngineCallback +import com.mapbox.mapboxsdk.location.engine.LocationEngineResult +import org.microg.gms.maps.mapbox.model.AbstractMarker +import org.microg.gms.maps.mapbox.model.DefaultInfoWindowAdapter +import org.microg.gms.maps.mapbox.model.InfoWindow +import org.microg.gms.maps.mapbox.utils.MapContext + +abstract class AbstractGoogleMap(context: Context) : IGoogleMapDelegate.Stub() { + + internal val mapContext = MapContext(context) + + val dpiFactor: Float + get() = mapContext.resources.displayMetrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT + + internal var currentInfoWindow: InfoWindow? = null + internal var infoWindowAdapter: IInfoWindowAdapter = DefaultInfoWindowAdapter(mapContext) + internal var onInfoWindowClickListener: IOnInfoWindowClickListener? = null + internal var onInfoWindowLongClickListener: IOnInfoWindowLongClickListener? = null + internal var onInfoWindowCloseListener: IOnInfoWindowCloseListener? = null + + internal var mapClickListener: IOnMapClickListener? = null + internal var mapLongClickListener: IOnMapLongClickListener? = null + internal var markerClickListener: IOnMarkerClickListener? = null + internal var circleClickListener: IOnCircleClickListener? = null + + internal var myLocationChangeListener: IOnMyLocationChangeListener? = null + + internal val locationEngineCallback = object : LocationEngineCallback { + override fun onSuccess(result: LocationEngineResult?) { + result?.lastLocation?.let { location -> + Log.d(TAG, "myLocationChanged: $location") + myLocationChangeListener?.onMyLocationChanged(ObjectWrapper.wrap(location)) + + onLocationUpdate(location) + } + } + override fun onFailure(e: Exception) { + Log.e(TAG, "Failed to obtain location update", e) + } + } + + + internal abstract fun showInfoWindow(marker: AbstractMarker): Boolean + + internal abstract fun onLocationUpdate(location: Location) + + override fun setOnInfoWindowClickListener(listener: IOnInfoWindowClickListener?) { + onInfoWindowClickListener = listener + } + + override fun setOnInfoWindowLongClickListener(listener: IOnInfoWindowLongClickListener) { + onInfoWindowLongClickListener = listener + } + + override fun setOnInfoWindowCloseListener(listener: IOnInfoWindowCloseListener) { + onInfoWindowCloseListener = listener + } + + override fun setInfoWindowAdapter(adapter: IInfoWindowAdapter?) { + infoWindowAdapter = adapter ?: DefaultInfoWindowAdapter(mapContext) + } + + override fun setOnMapClickListener(listener: IOnMapClickListener?) { + mapClickListener = listener + } + + override fun setOnMapLongClickListener(listener: IOnMapLongClickListener?) { + mapLongClickListener = listener + } + + override fun setOnMarkerClickListener(listener: IOnMarkerClickListener?) { + markerClickListener = listener + } + + override fun setOnCircleClickListener(listener: IOnCircleClickListener?) { + circleClickListener = listener + } + + override fun setOnPolygonClickListener(listener: IOnPolygonClickListener?) { + Log.d(TAG, "Not yet implemented: setOnPolygonClickListener") + } + + override fun setOnPolylineClickListener(listener: IOnPolylineClickListener?) { + Log.d(TAG, "Not yet implemented: setOnPolylineClickListener") + } + + override fun setOnGroundOverlayClickListener(listener: IOnGroundOverlayClickListener?) { + Log.d(TAG, "Not yet implemented: setOnGroundOverlayClickListener") + } + + override fun setOnMyLocationClickListener(listener: IOnMyLocationClickListener?) { + Log.d(TAG, "Not yet implemented: setOnMyLocationClickListener") + } + + override fun getMyLocation(): Location? { + Log.d(TAG, "unimplemented Method: getMyLocation") + return null + } + + override fun setLocationSource(locationSource: ILocationSourceDelegate?) { + Log.d(TAG, "unimplemented Method: setLocationSource") + } + + override fun setOnMyLocationChangeListener(listener: IOnMyLocationChangeListener?) { + myLocationChangeListener = listener + } + + override fun setOnMyLocationButtonClickListener(listener: IOnMyLocationButtonClickListener?) { + Log.d(TAG, "unimplemented Method: setOnMyLocationButtonClickListener") + } + + override fun getTestingHelper(): IObjectWrapper { + Log.d(TAG, "unimplemented Method: getTestingHelper") + return ObjectWrapper.wrap(null) + } + + override fun isBuildingsEnabled(): Boolean { + Log.d(TAG, "unimplemented Method: isBuildingsEnabled") + return false + } + + override fun setBuildingsEnabled(buildings: Boolean) { + Log.d(TAG, "unimplemented Method: setBuildingsEnabled") + } + + override fun useViewLifecycleWhenInFragment(): Boolean { + Log.d(TAG, "unimplemented Method: useViewLifecycleWhenInFragment") + return false + } + + override fun onEnterAmbient(bundle: Bundle?) { + Log.d(TAG, "unimplemented Method: onEnterAmbient") + } + + override fun onExitAmbient() { + Log.d(TAG, "unimplemented Method: onExitAmbient") + } + + override fun isTrafficEnabled(): Boolean { + Log.d(TAG, "unimplemented Method: isTrafficEnabled") + return false + } + + override fun setTrafficEnabled(traffic: Boolean) { + Log.d(TAG, "unimplemented Method: setTrafficEnabled") + + } + + override fun isIndoorEnabled(): Boolean { + Log.d(TAG, "unimplemented Method: isIndoorEnabled") + return false + } + + override fun setIndoorEnabled(indoor: Boolean) { + Log.d(TAG, "unimplemented Method: setIndoorEnabled") + } + + companion object { + val TAG = "GmsMapAbstract" + } +} \ No newline at end of file diff --git a/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/CameraBoundsWithSizeUpdate.kt b/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/CameraBoundsWithSizeUpdate.kt index eea4304f56..6d97b262f9 100644 --- a/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/CameraBoundsWithSizeUpdate.kt +++ b/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/CameraBoundsWithSizeUpdate.kt @@ -17,26 +17,48 @@ package org.microg.gms.maps.mapbox import android.util.Log +import com.google.android.gms.maps.internal.IGoogleMapDelegate import com.mapbox.mapboxsdk.camera.CameraPosition import com.mapbox.mapboxsdk.camera.CameraUpdate import com.mapbox.mapboxsdk.geometry.LatLngBounds import com.mapbox.mapboxsdk.maps.MapboxMap import java.util.* -internal class CameraBoundsWithSizeUpdate(val bounds: LatLngBounds, val width: Int, val height: Int, val padding: IntArray) : CameraUpdate { +internal class CameraBoundsWithSizeUpdate(val bounds: LatLngBounds, val width: Int, val height: Int, val padding: IntArray) : LiteModeCameraUpdate, CameraUpdate { constructor(bounds: LatLngBounds, width: Int, height: Int, paddingLeft: Int, paddingTop: Int = paddingLeft, paddingRight: Int = paddingLeft, paddingBottom: Int = paddingTop) : this(bounds, width, height, intArrayOf(paddingLeft, paddingTop, paddingRight, paddingBottom)) {} + override fun getLiteModeCameraPosition(map: IGoogleMapDelegate) = null + + override fun getLiteModeCameraBounds() = bounds + override fun getCameraPosition(map: MapboxMap): CameraPosition? { val padding = this.padding.clone() - val widthPad = ((map.width + map.padding[0] + map.padding[2] - width) / 2).toInt() - val heightPad = ((map.height + map.padding[1] + map.padding[3] - height) / 2).toInt() - padding[0] += widthPad - padding[1] += heightPad - padding[2] += widthPad - padding[3] += heightPad - Log.d(TAG, "map ${map.width} ${map.height}, set $width $height -> ${padding.map { it.toString() }.reduce { a, b -> "$a,$b"}}") - return map.getCameraForLatLngBounds(bounds, padding) + + val mapPadding = map.cameraPosition.padding + mapPadding?.let { + for (i in 0..3) { + padding[i] += it[i].toInt() + } + } + + val widthPadding = ((map.width - width) / 2).toInt() + val heightPadding = ((map.height - height) / 2).toInt() + padding[0] += widthPadding + padding[1] += heightPadding + padding[2] += widthPadding + padding[3] += heightPadding + + Log.d(TAG, "map ${map.width} ${map.height}, set $width $height -> ${Arrays.toString(padding)}") + return map.getCameraForLatLngBounds(bounds, padding)?.let { + CameraPosition.Builder(it) + .apply { + mapPadding?.let { + padding(it) + } + }.build() + } + } override fun equals(other: Any?): Boolean { diff --git a/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/CameraUpdateFactory.kt b/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/CameraUpdateFactory.kt index 675429b90e..d5cd86d49b 100644 --- a/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/CameraUpdateFactory.kt +++ b/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/CameraUpdateFactory.kt @@ -17,11 +17,13 @@ package org.microg.gms.maps.mapbox import android.graphics.Point +import android.graphics.PointF import android.os.Parcel import android.util.Log import com.google.android.gms.dynamic.IObjectWrapper import com.google.android.gms.dynamic.ObjectWrapper import com.google.android.gms.maps.internal.ICameraUpdateFactoryDelegate +import com.google.android.gms.maps.internal.IGoogleMapDelegate import com.google.android.gms.maps.model.CameraPosition import com.google.android.gms.maps.model.LatLng import com.google.android.gms.maps.model.LatLngBounds @@ -32,29 +34,40 @@ import org.microg.gms.maps.mapbox.utils.toMapbox class CameraUpdateFactoryImpl : ICameraUpdateFactoryDelegate.Stub() { - override fun zoomIn(): IObjectWrapper = ObjectWrapper.wrap(CameraUpdateFactory.zoomIn()) - override fun zoomOut(): IObjectWrapper = ObjectWrapper.wrap(CameraUpdateFactory.zoomOut()) + override fun zoomIn(): IObjectWrapper = ObjectWrapper.wrap(ZoomByCameraUpdate(1f)) + override fun zoomOut(): IObjectWrapper = ObjectWrapper.wrap(ZoomByCameraUpdate(-1f)) - override fun zoomTo(zoom: Float): IObjectWrapper = - ObjectWrapper.wrap(CameraUpdateFactory.zoomTo(zoom.toDouble() - 1.0)) + override fun zoomTo(zoom: Float): IObjectWrapper = ObjectWrapper.wrap(ZoomToCameraUpdate(zoom)) override fun zoomBy(zoomDelta: Float): IObjectWrapper = - ObjectWrapper.wrap(CameraUpdateFactory.zoomBy(zoomDelta.toDouble())) + ObjectWrapper.wrap(ZoomByCameraUpdate(zoomDelta)).also { + Log.d(TAG, "zoomBy") + } override fun zoomByWithFocus(zoomDelta: Float, x: Int, y: Int): IObjectWrapper = - ObjectWrapper.wrap(CameraUpdateFactory.zoomBy(zoomDelta.toDouble(), Point(x, y))) + ObjectWrapper.wrap(ZoomByWithFocusCameraUpdate(zoomDelta, x, y)).also { + Log.d(TAG, "zoomByWithFocus") + } override fun newCameraPosition(cameraPosition: CameraPosition): IObjectWrapper = - ObjectWrapper.wrap(CameraUpdateFactory.newCameraPosition(cameraPosition.toMapbox())) + ObjectWrapper.wrap(NewCameraPositionCameraUpdate(cameraPosition)).also { + Log.d(TAG, "newCameraPosition") + } override fun newLatLng(latLng: LatLng): IObjectWrapper = - ObjectWrapper.wrap(CameraUpdateFactory.newLatLng(latLng.toMapbox())) + ObjectWrapper.wrap(NewLatLngCameraUpdate(latLng)).also { + Log.d(TAG, "newLatLng") + } override fun newLatLngZoom(latLng: LatLng, zoom: Float): IObjectWrapper = - ObjectWrapper.wrap(CameraUpdateFactory.newLatLngZoom(latLng.toMapbox(), zoom.toDouble() - 1.0)) + ObjectWrapper.wrap(NewLatLngZoomCameraUpdate(latLng, zoom)).also { + Log.d(TAG, "newLatLngZoom") + } override fun newLatLngBounds(bounds: LatLngBounds, padding: Int): IObjectWrapper = - ObjectWrapper.wrap(CameraUpdateFactory.newLatLngBounds(bounds.toMapbox(), padding)) + ObjectWrapper.wrap(NewLatLngBoundsCameraUpdate(bounds, padding)).also { + Log.d(TAG, "newLatLngBounds") + } override fun scrollBy(x: Float, y: Float): IObjectWrapper { Log.d(TAG, "unimplemented Method: scrollBy") @@ -62,7 +75,9 @@ class CameraUpdateFactoryImpl : ICameraUpdateFactoryDelegate.Stub() { } override fun newLatLngBoundsWithSize(bounds: LatLngBounds, width: Int, height: Int, padding: Int): IObjectWrapper = - ObjectWrapper.wrap(CameraBoundsWithSizeUpdate(bounds.toMapbox(), width, height, padding)) + ObjectWrapper.wrap(CameraBoundsWithSizeUpdate(bounds.toMapbox(), width, height, padding)).also { + Log.d(TAG, "newLatLngBoundsWithSize") + } override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean = if (super.onTransact(code, data, reply, flags)) { @@ -71,9 +86,11 @@ class CameraUpdateFactoryImpl : ICameraUpdateFactoryDelegate.Stub() { Log.d(TAG, "onTransact [unknown]: $code, $data, $flags"); false } - private inner class NoCameraUpdate : CameraUpdate { + private inner class NoCameraUpdate : CameraUpdate, LiteModeCameraUpdate { override fun getCameraPosition(mapboxMap: MapboxMap): com.mapbox.mapboxsdk.camera.CameraPosition? = mapboxMap.cameraPosition + + override fun getLiteModeCameraPosition(map: IGoogleMapDelegate): CameraPosition = map.cameraPosition } companion object { @@ -81,4 +98,70 @@ class CameraUpdateFactoryImpl : ICameraUpdateFactoryDelegate.Stub() { } } +interface LiteModeCameraUpdate { + fun getLiteModeCameraPosition(map: IGoogleMapDelegate): CameraPosition? + + fun getLiteModeCameraBounds(): com.mapbox.mapboxsdk.geometry.LatLngBounds? = null +} + +class ZoomToCameraUpdate(private val zoom: Float) : LiteModeCameraUpdate, CameraUpdate { + override fun getLiteModeCameraPosition(map: IGoogleMapDelegate): CameraPosition = + CameraPosition.Builder(map.cameraPosition).zoom(zoom).build() + + override fun getCameraPosition(mapboxMap: MapboxMap): com.mapbox.mapboxsdk.camera.CameraPosition? = + CameraUpdateFactory.zoomTo(zoom.toDouble() - 1.0).getCameraPosition(mapboxMap) + +} + +class ZoomByCameraUpdate(private val delta: Float) : LiteModeCameraUpdate, CameraUpdate { + override fun getLiteModeCameraPosition(map: IGoogleMapDelegate): CameraPosition = + CameraPosition.Builder(map.cameraPosition).zoom(map.cameraPosition.zoom + delta).build() + + override fun getCameraPosition(mapboxMap: MapboxMap): com.mapbox.mapboxsdk.camera.CameraPosition? = + CameraUpdateFactory.zoomBy(delta.toDouble()).getCameraPosition(mapboxMap) + +} + +class ZoomByWithFocusCameraUpdate(private val delta: Float, private val x: Int, private val y: Int) : LiteModeCameraUpdate, + CameraUpdate { + override fun getLiteModeCameraPosition(map: IGoogleMapDelegate): CameraPosition = + CameraPosition.Builder(map.cameraPosition).zoom(map.cameraPosition.zoom + delta) + .target(map.projection.fromScreenLocation(ObjectWrapper.wrap(PointF(x.toFloat(), y.toFloat())))).build() + + override fun getCameraPosition(mapboxMap: MapboxMap): com.mapbox.mapboxsdk.camera.CameraPosition? = + CameraUpdateFactory.zoomBy(delta.toDouble(), Point(x, y)).getCameraPosition(mapboxMap) +} + +class NewCameraPositionCameraUpdate(private val cameraPosition: CameraPosition) : LiteModeCameraUpdate, CameraUpdate { + override fun getLiteModeCameraPosition(map: IGoogleMapDelegate): CameraPosition = this.cameraPosition + + override fun getCameraPosition(mapboxMap: MapboxMap): com.mapbox.mapboxsdk.camera.CameraPosition = + this.cameraPosition.toMapbox() +} + +class NewLatLngCameraUpdate(private val latLng: LatLng) : LiteModeCameraUpdate, CameraUpdate { + override fun getLiteModeCameraPosition(map: IGoogleMapDelegate): CameraPosition = + CameraPosition.Builder(map.cameraPosition).target(latLng).build() + + override fun getCameraPosition(mapboxMap: MapboxMap): com.mapbox.mapboxsdk.camera.CameraPosition? = + CameraUpdateFactory.newLatLng(latLng.toMapbox()).getCameraPosition(mapboxMap) +} + +class NewLatLngZoomCameraUpdate(private val latLng: LatLng, private val zoom: Float) : LiteModeCameraUpdate, CameraUpdate { + override fun getLiteModeCameraPosition(map: IGoogleMapDelegate): CameraPosition = + CameraPosition.Builder(map.cameraPosition).target(latLng).zoom(zoom).build() + + override fun getCameraPosition(mapboxMap: MapboxMap): com.mapbox.mapboxsdk.camera.CameraPosition? = + CameraUpdateFactory.newLatLngZoom(latLng.toMapbox(), zoom - 1.0).getCameraPosition(mapboxMap) +} + +class NewLatLngBoundsCameraUpdate(private val bounds: LatLngBounds, internal val padding: Int) : LiteModeCameraUpdate, + CameraUpdate { + + override fun getLiteModeCameraPosition(map: IGoogleMapDelegate): CameraPosition? = null + + override fun getLiteModeCameraBounds() = bounds.toMapbox() + override fun getCameraPosition(mapboxMap: MapboxMap): com.mapbox.mapboxsdk.camera.CameraPosition? = + CameraUpdateFactory.newLatLngBounds(bounds.toMapbox(), padding).getCameraPosition(mapboxMap) +} \ No newline at end of file diff --git a/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/GoogleLocationEngine.kt b/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/GoogleLocationEngine.kt new file mode 100644 index 0000000000..654a1407ba --- /dev/null +++ b/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/GoogleLocationEngine.kt @@ -0,0 +1,63 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.maps.mapbox + +import android.app.PendingIntent +import android.content.Context +import android.os.Looper +import com.google.android.gms.location.LocationListener +import com.google.android.gms.location.LocationRequest +import com.google.android.gms.location.LocationServices +import com.google.android.gms.location.Priority +import com.mapbox.mapboxsdk.location.engine.LocationEngine +import com.mapbox.mapboxsdk.location.engine.LocationEngineCallback +import com.mapbox.mapboxsdk.location.engine.LocationEngineRequest +import com.mapbox.mapboxsdk.location.engine.LocationEngineResult + +class GoogleLocationEngine(context: Context) : LocationEngine { + private val listenerMap: MutableMap, LocationListener> = hashMapOf() + private val client = LocationServices.getFusedLocationProviderClient(context) + + override fun getLastLocation(callback: LocationEngineCallback) { + client.lastLocation.addOnCompleteListener { + if (it.isSuccessful) callback.onSuccess(LocationEngineResult.create(it.result)) + else callback.onFailure(it.exception) + } + } + + override fun requestLocationUpdates(request: LocationEngineRequest, callback: LocationEngineCallback, looper: Looper?) { + listenerMap[callback] = listenerMap[callback] ?: LocationListener { callback.onSuccess(LocationEngineResult.create(it)) } + client.requestLocationUpdates( + LocationRequest.Builder(request.interval) + .setPriority( + when (request.priority) { + LocationEngineRequest.PRIORITY_HIGH_ACCURACY -> Priority.PRIORITY_HIGH_ACCURACY + LocationEngineRequest.PRIORITY_BALANCED_POWER_ACCURACY -> Priority.PRIORITY_BALANCED_POWER_ACCURACY + LocationEngineRequest.PRIORITY_LOW_POWER -> Priority.PRIORITY_LOW_POWER + LocationEngineRequest.PRIORITY_NO_POWER -> Priority.PRIORITY_PASSIVE + else -> Priority.PRIORITY_BALANCED_POWER_ACCURACY + } + ) + .setMinUpdateDistanceMeters(request.displacement) + .setMinUpdateIntervalMillis(request.fastestInterval) + .setMaxUpdateDelayMillis(request.maxWaitTime) + .build(), listenerMap[callback], looper + ) + } + + override fun requestLocationUpdates(request: LocationEngineRequest, pendingIntent: PendingIntent?) { + throw UnsupportedOperationException() + } + + override fun removeLocationUpdates(callback: LocationEngineCallback) { + listenerMap[callback]?.let { client.removeLocationUpdates(it) } + listenerMap.remove(callback) + } + + override fun removeLocationUpdates(pendingIntent: PendingIntent?) { + throw UnsupportedOperationException() + } +} \ No newline at end of file diff --git a/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/GoogleMap.kt b/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/GoogleMap.kt index b6feb4a007..8140370a7d 100644 --- a/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/GoogleMap.kt +++ b/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/GoogleMap.kt @@ -18,11 +18,11 @@ package org.microg.gms.maps.mapbox import android.annotation.SuppressLint import android.content.Context +import android.graphics.Bitmap import android.location.Location import android.os.* import androidx.annotation.IdRes import androidx.annotation.Keep -import android.util.DisplayMetrics import android.util.Log import android.view.Gravity import android.view.View @@ -52,20 +52,23 @@ import com.mapbox.mapboxsdk.plugins.annotation.* import com.mapbox.mapboxsdk.plugins.annotation.Annotation import com.mapbox.mapboxsdk.style.layers.Property.LINE_CAP_ROUND import com.google.android.gms.dynamic.unwrap +import com.google.android.gms.maps.GoogleMap import com.mapbox.mapboxsdk.WellKnownTileServer -import com.mapbox.mapboxsdk.location.engine.LocationEngineCallback -import com.mapbox.mapboxsdk.location.engine.LocationEngineResult -import org.microg.gms.maps.MapsConstants.* +import org.microg.gms.maps.mapbox.model.InfoWindow +import org.microg.gms.maps.mapbox.model.getInfoWindowViewFor +import com.mapbox.mapboxsdk.camera.CameraUpdateFactory +import com.mapbox.mapboxsdk.location.engine.* +import com.mapbox.mapboxsdk.maps.OnMapReadyCallback import org.microg.gms.maps.mapbox.model.* -import org.microg.gms.maps.mapbox.utils.MapContext import org.microg.gms.maps.mapbox.utils.MultiArchLoader import org.microg.gms.maps.mapbox.utils.toGms import org.microg.gms.maps.mapbox.utils.toMapbox +import java.util.concurrent.atomic.AtomicBoolean private fun LongSparseArray.values() = (0 until size()).mapNotNull { valueAt(it) } -fun runOnMainLooper(method: () -> Unit) { - if (Looper.myLooper() == Looper.getMainLooper()) { +fun runOnMainLooper(forceQueue: Boolean = false, method: () -> Unit) { + if (!forceQueue && Looper.myLooper() == Looper.getMainLooper()) { method() } else { Handler(Looper.getMainLooper()).post { @@ -74,13 +77,11 @@ fun runOnMainLooper(method: () -> Unit) { } } -class GoogleMapImpl(private val context: Context, var options: GoogleMapOptions) : IGoogleMapDelegate.Stub() { +class GoogleMapImpl(context: Context, var options: GoogleMapOptions) : AbstractGoogleMap(context) { val view: FrameLayout var map: MapboxMap? = null private set - val dpiFactor: Float - get() = context.resources.displayMetrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT private var mapView: MapView? = null private var created = false @@ -88,57 +89,46 @@ class GoogleMapImpl(private val context: Context, var options: GoogleMapOptions) private var loaded = false private val mapLock = Object() - private val initializedCallbackList = mutableListOf() + private val internalOnInitializedCallbackList = mutableListOf() + private val userOnInitializedCallbackList = mutableListOf() private var loadedCallback: IOnMapLoadedCallback? = null private var cameraChangeListener: IOnCameraChangeListener? = null private var cameraMoveListener: IOnCameraMoveListener? = null private var cameraMoveCanceledListener: IOnCameraMoveCanceledListener? = null private var cameraMoveStartedListener: IOnCameraMoveStartedListener? = null private var cameraIdleListener: IOnCameraIdleListener? = null - private var mapClickListener: IOnMapClickListener? = null - private var mapLongClickListener: IOnMapLongClickListener? = null - private var markerClickListener: IOnMarkerClickListener? = null private var markerDragListener: IOnMarkerDragListener? = null - private var myLocationChangeListener: IOnMyLocationChangeListener? = null - - private val locationEngineCallback = object : LocationEngineCallback { - override fun onSuccess(result: LocationEngineResult?) { - result?.lastLocation?.let { location -> - Log.d(TAG, "myLocationChanged: $location") - myLocationChangeListener?.onMyLocationChanged(ObjectWrapper.wrap(location)) - } - } - override fun onFailure(e: Exception) { - Log.w(TAG, e) - } - } var lineManager: LineManager? = null - val pendingLines = mutableSetOf() + val pendingLines = mutableSetOf>() var lineId = 0L var fillManager: FillManager? = null - val pendingFills = mutableSetOf() + val pendingFills = mutableSetOf>() + val circles = mutableMapOf() var fillId = 0L - var circleManager: CircleManager? = null - val pendingCircles = mutableSetOf() - var circleId = 0L - var symbolManager: SymbolManager? = null val pendingMarkers = mutableSetOf() val markers = mutableMapOf() var markerId = 0L + val pendingBitmaps = mutableMapOf() + var groundId = 0L var tileId = 0L var storedMapType: Int = options.mapType + var mapStyle: MapStyleOptions? = null val waitingCameraUpdates = mutableListOf() var locationEnabled: Boolean = false + val defaultLocationEngine = GoogleLocationEngine(context) + var locationEngine: LocationEngine = defaultLocationEngine + + var isStarted = false + init { - val mapContext = MapContext(context) BitmapDescriptorFactoryImpl.initialize(mapContext.resources, context.resources) LibraryLoader.setLibraryLoader(MultiArchLoader(mapContext, context)) runOnMainLooper { @@ -184,7 +174,9 @@ class GoogleMapImpl(private val context: Context, var options: GoogleMapOptions) } } - override fun getCameraPosition(): CameraPosition? = map?.cameraPosition?.toGms() + override fun getCameraPosition(): CameraPosition = + map?.cameraPosition?.toGms() ?: CameraPosition(LatLng(0.0, 0.0), 0f, 0f, 0f) + override fun getMaxZoomLevel(): Float = (map?.maxZoomLevel?.toFloat() ?: 20f) + 1f override fun getMinZoomLevel(): Float = (map?.minZoomLevel?.toFloat() ?: 0f) + 1f @@ -210,14 +202,16 @@ class GoogleMapImpl(private val context: Context, var options: GoogleMapOptions) } } - fun afterInitialized(runnable: () -> Unit) { - initializedCallbackList.add(object : IOnMapReadyCallback { - override fun onMapReady(map: IGoogleMapDelegate?) { - runnable() + fun afterInitialize(runnable: (MapboxMap) -> Unit) { + synchronized(mapLock) { + if (initialized) { + runnable(map!!) + } else { + internalOnInitializedCallbackList.add(OnMapReadyCallback { + runnable(it) + }) } - - override fun asBinder(): IBinder? = null - }) + } } override fun animateCameraWithCallback(cameraUpdate: IObjectWrapper?, callback: ICancelableCallback?) { @@ -227,7 +221,7 @@ class GoogleMapImpl(private val context: Context, var options: GoogleMapOptions) this.map?.animateCamera(update, callback?.toMapbox()) } else { waitingCameraUpdates.add(update) - afterInitialized { callback?.onFinish() } + afterInitialize { callback?.onFinish() } } } } @@ -239,7 +233,7 @@ class GoogleMapImpl(private val context: Context, var options: GoogleMapOptions) this.map?.animateCamera(update, duration, callback?.toMapbox()) } else { waitingCameraUpdates.add(update) - afterInitialized { callback?.onFinish() } + afterInitialize { callback?.onFinish() } } } } @@ -248,24 +242,25 @@ class GoogleMapImpl(private val context: Context, var options: GoogleMapOptions) override fun setMapStyle(options: MapStyleOptions?): Boolean { Log.d(TAG, "setMapStyle options: " + options?.getJson()) + mapStyle = options return true } - override fun setMinZoomPreference(minZoom: Float) { - map?.setMinZoomPreference(minZoom.toDouble() - 1) + override fun setMinZoomPreference(minZoom: Float) = afterInitialize { + it.setMinZoomPreference(minZoom.toDouble() - 1) } - override fun setMaxZoomPreference(maxZoom: Float) { - map?.setMaxZoomPreference(maxZoom.toDouble() - 1) + override fun setMaxZoomPreference(maxZoom: Float) = afterInitialize { + it.setMaxZoomPreference(maxZoom.toDouble() - 1) } - override fun resetMinMaxZoomPreference() { - map?.setMinZoomPreference(MapboxConstants.MINIMUM_ZOOM.toDouble()) - map?.setMaxZoomPreference(MapboxConstants.MAXIMUM_ZOOM.toDouble()) + override fun resetMinMaxZoomPreference() = afterInitialize { + it.setMinZoomPreference(MapboxConstants.MINIMUM_ZOOM.toDouble()) + it.setMaxZoomPreference(MapboxConstants.MAXIMUM_ZOOM.toDouble()) } - override fun setLatLngBoundsForCameraTarget(bounds: LatLngBounds?) { - map?.setLatLngBoundsForCameraTarget(bounds?.toMapbox()) + override fun setLatLngBoundsForCameraTarget(bounds: LatLngBounds?) = afterInitialize { + it.setLatLngBoundsForCameraTarget(bounds?.toMapbox()) } override fun addPolyline(options: PolylineOptions): IPolylineDelegate? { @@ -291,6 +286,13 @@ class GoogleMapImpl(private val context: Context, var options: GoogleMapOptions) } else { fill.update(fillManager) } + + val lineManager = lineManager + if (lineManager == null) { + pendingLines.addAll(fill.strokes) + } else { + for (stroke in fill.strokes) stroke.update(lineManager) + } } return fill } @@ -318,21 +320,32 @@ class GoogleMapImpl(private val context: Context, var options: GoogleMapOptions) return TileOverlayImpl(this, "t${tileId++}", options) } - override fun addCircle(options: CircleOptions): ICircleDelegate? { - val circle = CircleImpl(this, "c${circleId++}", options) + override fun addCircle(options: CircleOptions): ICircleDelegate { + val circle = CircleImpl(this, "c${fillId++}", options) synchronized(this) { - val circleManager = circleManager - if (circleManager == null) { - pendingCircles.add(circle) + val fillManager = fillManager + if (fillManager == null) { + pendingFills.add(circle) } else { - circle.update(circleManager) + circle.update(fillManager) + } + val lineManager = lineManager + if (lineManager == null) { + pendingLines.add(circle.line) + } else { + circle.line.update(lineManager) + } + circle.strokePattern?.let { + addBitmap( + it.getName(circle.strokeColor, circle.strokeWidth), + it.makeBitmap(circle.strokeColor, circle.strokeWidth) + ) } } return circle } override fun clear() { - circleManager?.let { clear(it) } lineManager?.let { clear(it) } fillManager?.let { clear(it) } symbolManager?.let { clear(it) } @@ -355,56 +368,30 @@ class GoogleMapImpl(private val context: Context, var options: GoogleMapOptions) override fun setMapType(type: Int) { storedMapType = type - applyMapType() + applyMapStyle() } - fun applyMapType() { - val circles = circleManager?.annotations?.values() + fun applyMapStyle() { val lines = lineManager?.annotations?.values() val fills = fillManager?.annotations?.values() val symbols = symbolManager?.annotations?.values() val update: (Style) -> Unit = { - circles?.let { runCatching { circleManager?.update(it) } } lines?.let { runCatching { lineManager?.update(it) } } fills?.let { runCatching { fillManager?.update(it) } } symbols?.let { runCatching { symbolManager?.update(it) } } } - // TODO: Serve map styles locally - when (storedMapType) { - MAP_TYPE_SATELLITE -> map?.setStyle(Style.Builder().fromUri("mapbox://styles/microg/cjxgloted25ap1ct4uex7m6hi"), update) - MAP_TYPE_TERRAIN -> map?.setStyle(Style.Builder().fromUri("mapbox://styles/mapbox/outdoors-v12"), update) - MAP_TYPE_HYBRID -> map?.setStyle(Style.Builder().fromUri("mapbox://styles/microg/cjxgloted25ap1ct4uex7m6hi"), update) - //MAP_TYPE_NONE, MAP_TYPE_NORMAL, - else -> map?.setStyle(Style.Builder().fromUri("mapbox://styles/microg/cjui4020201oo1fmca7yuwbor"), update) - } + map?.setStyle( + getStyle(mapContext, storedMapType, mapStyle), + update + ) map?.let { BitmapDescriptorFactoryImpl.registerMap(it) } } - override fun setWatermarkEnabled(watermark: Boolean) { - map?.uiSettings?.isLogoEnabled = watermark - } - - override fun isTrafficEnabled(): Boolean { - Log.d(TAG, "unimplemented Method: isTrafficEnabled") - return false - } - - override fun setTrafficEnabled(traffic: Boolean) { - Log.d(TAG, "unimplemented Method: setTrafficEnabled") - - } - - override fun isIndoorEnabled(): Boolean { - Log.d(TAG, "unimplemented Method: isIndoorEnabled") - return false - } - - override fun setIndoorEnabled(indoor: Boolean) { - Log.d(TAG, "unimplemented Method: setIndoorEnabled") - + override fun setWatermarkEnabled(watermark: Boolean) = afterInitialize { + it.uiSettings.isLogoEnabled = watermark } override fun isMyLocationEnabled(): Boolean { @@ -415,25 +402,40 @@ class GoogleMapImpl(private val context: Context, var options: GoogleMapOptions) synchronized(mapLock) { locationEnabled = myLocation if (!loaded) return - val locationComponent = map?.locationComponent ?: return try { - if (locationComponent.isLocationComponentActivated) { - locationComponent.isLocationComponentEnabled = myLocation - if (myLocation) { - locationComponent.locationEngine?.requestLocationUpdates( - locationComponent.locationEngineRequest, - locationEngineCallback, - null - ) - } else { - locationComponent.locationEngine?.removeLocationUpdates(locationEngineCallback) - } - } + updateLocationEngineListener(myLocation) } catch (e: SecurityException) { Log.w(TAG, e) locationEnabled = false } - Unit + } + } + + private fun updateLocationEngineListener(myLocation: Boolean) { + val locationComponent = map?.locationComponent ?: return + if (locationComponent.isLocationComponentActivated) { + locationComponent.isLocationComponentEnabled = myLocation + if (myLocation) { + locationComponent.locationEngine?.requestLocationUpdates( + locationComponent.locationEngineRequest, + locationEngineCallback, + null + ) + } else { + locationComponent.locationEngine?.removeLocationUpdates(locationEngineCallback) + } + } + } + + override fun setLocationSource(locationSource: ILocationSourceDelegate?) { + synchronized(mapLock) { + updateLocationEngineListener(false) + locationEngine = locationSource?.let { SourceLocationEngine(it) } ?: defaultLocationEngine + if (!loaded) return + if (map?.locationComponent?.isLocationComponentActivated == true) { + map?.locationComponent?.locationEngine = locationEngine + } + updateLocationEngineListener(locationEnabled) } } @@ -443,96 +445,82 @@ class GoogleMapImpl(private val context: Context, var options: GoogleMapOptions) } } - override fun setLocationSource(locationSource: ILocationSourceDelegate?) { - Log.d(TAG, "unimplemented Method: setLocationSource") + override fun onLocationUpdate(location: Location) { + // no action necessary, as the location component will automatically place a marker on the map } override fun setContentDescription(desc: String?) { mapView?.contentDescription = desc } - override fun getUiSettings(): IUiSettingsDelegate? = map?.uiSettings?.let { UiSettingsImpl(it) } + override fun getUiSettings(): IUiSettingsDelegate = + map?.uiSettings?.let { UiSettingsImpl(it) } ?: UiSettingsCache().also { + // Apply cached UI settings after map is initialized + internalOnInitializedCallbackList.add(it.getMapReadyCallback()) + } - override fun getProjection(): IProjectionDelegate? = map?.projection?.let { + override fun getProjection(): IProjectionDelegate = map?.projection?.let { val experiment = try { map?.cameraPosition?.tilt == 0.0 && map?.cameraPosition?.bearing == 0.0 } catch (e: Exception) { Log.w(TAG, e); false } ProjectionImpl(it, experiment) - } + } ?: DummyProjection() override fun setOnCameraChangeListener(listener: IOnCameraChangeListener?) { cameraChangeListener = listener } - override fun setOnMapClickListener(listener: IOnMapClickListener?) { - mapClickListener = listener - } - - override fun setOnMapLongClickListener(listener: IOnMapLongClickListener?) { - mapLongClickListener = listener - } - - override fun setOnMarkerClickListener(listener: IOnMarkerClickListener?) { - markerClickListener = listener - } - override fun setOnMarkerDragListener(listener: IOnMarkerDragListener?) { markerDragListener = listener } - override fun setOnInfoWindowClickListener(listener: IOnInfoWindowClickListener?) { - Log.d(TAG, "unimplemented Method: setOnInfoWindowClickListener") - - } - - override fun setInfoWindowAdapter(adapter: IInfoWindowAdapter?) { - Log.d(TAG, "unimplemented Method: setInfoWindowAdapter") - - } - - override fun getTestingHelper(): IObjectWrapper? { - Log.d(TAG, "unimplemented Method: getTestingHelper") - return null - } - - override fun setOnMyLocationChangeListener(listener: IOnMyLocationChangeListener?) { - myLocationChangeListener = listener - } - - override fun setOnMyLocationButtonClickListener(listener: IOnMyLocationButtonClickListener?) { - Log.d(TAG, "unimplemented Method: setOnMyLocationButtonClickListener") - - } - override fun snapshot(callback: ISnapshotReadyCallback, bitmap: IObjectWrapper?) { - Log.d(TAG, "unimplemented Method: snapshot") + val map = map + if (map == null) { + // Snapshot cannot be taken + Log.e(TAG, "snapshot could not be taken because map is null") + runOnMainLooper { callback.onBitmapWrappedReady(ObjectWrapper.wrap(null)) } + } else { + if (!isStarted) { + Log.w(TAG, "Caller did not call onStart() before taking snapshot. Calling onStart() now, for snapshot not to fail.") + // Snapshots fail silently if onStart had not been called. This is the case with Signal. + onStart() + isStarted = true + } - } + Log.d(TAG, "taking snapshot now") - override fun setPadding(left: Int, top: Int, right: Int, bottom: Int) { - Log.d(TAG, "setPadding: $left $top $right $bottom") - map?.let { map -> - map.setPadding(left, top, right, bottom) - val fourDp = mapView?.context?.resources?.getDimension(R.dimen.mapbox_four_dp)?.toInt() - ?: 0 - val ninetyTwoDp = mapView?.context?.resources?.getDimension(R.dimen.mapbox_ninety_two_dp)?.toInt() - ?: 0 - map.uiSettings.setLogoMargins(left + fourDp, top + fourDp, right + fourDp, bottom + fourDp) - map.uiSettings.setCompassMargins(left + fourDp, top + fourDp, right + fourDp, bottom + fourDp) - map.uiSettings.setAttributionMargins(left + ninetyTwoDp, top + fourDp, right + fourDp, bottom + fourDp) + map.snapshot { + runOnMainLooper { + Log.d(TAG, "snapshot ready, providing to application") + callback.onBitmapWrappedReady(ObjectWrapper.wrap(it)) + } + } } } - override fun isBuildingsEnabled(): Boolean { - Log.d(TAG, "unimplemented Method: isBuildingsEnabled") - return false + override fun snapshotForTest(callback: ISnapshotReadyCallback?) { + Log.d(TAG, "Not yet implemented: snapshotForTest") } - override fun setBuildingsEnabled(buildings: Boolean) { - Log.d(TAG, "unimplemented Method: setBuildingsEnabled") + override fun setPadding(left: Int, top: Int, right: Int, bottom: Int) = afterInitialize { map -> + Log.d(TAG, "setPadding: $left $top $right $bottom") + val padding = map.cameraPosition.padding + if (padding == null || padding[0] != left.toDouble() || padding[1] != top.toDouble() || padding[2] != right.toDouble() || padding[3] != bottom.toDouble()) { + // Don't send camera update if we already got these paddings + CameraUpdateFactory.paddingTo(left.toDouble(), top.toDouble(), right.toDouble(), bottom.toDouble()) + .let { map.moveCamera(it) } + } + val fourDp = mapView?.context?.resources?.getDimension(R.dimen.maplibre_four_dp)?.toInt() + ?: 0 + val ninetyTwoDp = mapView?.context?.resources?.getDimension(R.dimen.maplibre_ninety_two_dp)?.toInt() + ?: 0 + map.uiSettings.setLogoMargins(left + fourDp, top + fourDp, right + fourDp, bottom + fourDp) + map.uiSettings.setCompassMargins(left + fourDp, top + fourDp, right + fourDp, bottom + fourDp) + map.uiSettings.setAttributionMargins(left + ninetyTwoDp, top + fourDp, right + fourDp, bottom + fourDp) } override fun setOnMapLoadedCallback(callback: IOnMapLoadedCallback?) { @@ -574,12 +562,13 @@ class GoogleMapImpl(private val context: Context, var options: GoogleMapOptions) override fun onCreate(savedInstanceState: Bundle?) { if (!created) { Log.d(TAG, "create"); - val mapView = MapView(MapContext(context)) + val mapView = MapView(mapContext) this.mapView = mapView view.addView(mapView) mapView.onCreate(savedInstanceState?.toMapbox()) mapView.getMapAsync(this::initMap) created = true + runOnMainLooper(forceQueue = true) { tryRunUserInitializedCallbacks("onCreate") } } } @@ -600,8 +589,6 @@ class GoogleMapImpl(private val context: Context, var options: GoogleMapOptions) } catch (e: Exception) { Log.w(TAG, e) } - } - map.addOnCameraIdleListener { try { cameraIdleListener?.onCameraIdle() } catch (e: Exception) { @@ -614,10 +601,17 @@ class GoogleMapImpl(private val context: Context, var options: GoogleMapOptions) } catch (e: Exception) { Log.w(TAG, e) } + currentInfoWindow?.update() } map.addOnCameraMoveStartedListener { try { - cameraMoveStartedListener?.onCameraMoveStarted(it) + val reason = when (it) { + MapboxMap.OnCameraMoveStartedListener.REASON_API_GESTURE -> GoogleMap.OnCameraMoveStartedListener.REASON_GESTURE + MapboxMap.OnCameraMoveStartedListener.REASON_API_ANIMATION -> GoogleMap.OnCameraMoveStartedListener.REASON_API_ANIMATION + MapboxMap.OnCameraMoveStartedListener.REASON_DEVELOPER_ANIMATION -> GoogleMap.OnCameraMoveStartedListener.REASON_DEVELOPER_ANIMATION + else -> 0 + } + cameraMoveStartedListener?.onCameraMoveStarted(reason) } catch (e: Exception) { Log.w(TAG, e) } @@ -631,7 +625,11 @@ class GoogleMapImpl(private val context: Context, var options: GoogleMapOptions) } map.addOnMapClickListener { latlng -> try { - mapClickListener?.let { if (!hasSymbolAt(latlng)) it.onMapClick(latlng.toGms()); } + if (!hasSymbolAt(latlng)) { + mapClickListener?.onMapClick(latlng.toGms()) + currentInfoWindow?.close() + currentInfoWindow = null + } } catch (e: Exception) { Log.w(TAG, e) } @@ -646,7 +644,7 @@ class GoogleMapImpl(private val context: Context, var options: GoogleMapOptions) false } - applyMapType() + applyMapStyle() options.minZoomPreference?.let { if (it != 0f) map.setMinZoomPreference(it.toDouble()) } options.maxZoomPreference?.let { if (it != 0f) map.setMaxZoomPreference(it.toDouble()) } options.latLngBoundsForCameraTarget?.let { map.setLatLngBoundsForCameraTarget(it.toMapbox()) } @@ -659,27 +657,24 @@ class GoogleMapImpl(private val context: Context, var options: GoogleMapOptions) synchronized(mapLock) { initialized = true waitingCameraUpdates.forEach { map.moveCamera(it) } - val initializedCallbackList = ArrayList(initializedCallbackList) - Log.d(TAG, "Invoking ${initializedCallbackList.size} callbacks delayed, as map is initialized") + val initializedCallbackList = ArrayList(internalOnInitializedCallbackList) + Log.d(TAG, "Invoking ${initializedCallbackList.size} internal callbacks now that the true map is initialized") for (callback in initializedCallbackList) { - try { - callback.onMapReady(this) - } catch (e: Exception) { - Log.w(TAG, e) - } + callback.onMapReady(map) } } + // No effect if no initialized callbacks are present. + tryRunUserInitializedCallbacks(tag = "initMap") + map.getStyle { mapView?.let { view -> if (loaded) return@let val symbolManager: SymbolManager val lineManager: LineManager - val circleManager: CircleManager val fillManager: FillManager synchronized(mapLock) { - circleManager = CircleManager(view, map, it) fillManager = FillManager(view, map, it) symbolManager = SymbolManager(view, map, it) lineManager = LineManager(view, map, it) @@ -687,17 +682,21 @@ class GoogleMapImpl(private val context: Context, var options: GoogleMapOptions) this.symbolManager = symbolManager this.lineManager = lineManager - this.circleManager = circleManager this.fillManager = fillManager } symbolManager.iconAllowOverlap = true symbolManager.addClickListener { + val marker = markers[it.id] try { - markers[it.id]?.let { markerClickListener?.onMarkerClick(it) } == true + if (markers[it.id]?.let { markerClickListener?.onMarkerClick(it) } == true) { + return@addClickListener true + } } catch (e: Exception) { Log.w(TAG, e) - false + return@addClickListener false } + + marker?.let { showInfoWindow(it) } == true } symbolManager.addDragListener(object : OnSymbolDragListener { override fun onAnnotationDragStarted(annotation: Symbol?) { @@ -710,22 +709,42 @@ class GoogleMapImpl(private val context: Context, var options: GoogleMapOptions) override fun onAnnotationDrag(annotation: Symbol?) { try { - markers[annotation?.id]?.let { markerDragListener?.onMarkerDrag(it) } + annotation?.let { symbol -> + markers[symbol.id]?.let { marker -> + marker.setPositionWhileDragging(symbol.latLng.toGms()) + markerDragListener?.onMarkerDrag(marker) + } + } } catch (e: Exception) { Log.w(TAG, e) } } override fun onAnnotationDragFinished(annotation: Symbol?) { + mapView?.post { try { markers[annotation?.id]?.let { markerDragListener?.onMarkerDragEnd(it) } } catch (e: Exception) { Log.w(TAG, e) } + } } }) - pendingCircles.forEach { it.update(circleManager) } - pendingCircles.clear() + fillManager.addClickListener { fill -> + try { + circles[fill.id]?.let { circle -> + if (circle.isClickable) { + circleClickListener?.let { + it.onCircleClick(circle) + return@addClickListener true + } + } + } + } catch (e: Exception) { + Log.w(TAG, e) + } + false + } pendingFills.forEach { it.update(fillManager) } pendingFills.clear() pendingLines.forEach { it.update(lineManager) } @@ -733,18 +752,20 @@ class GoogleMapImpl(private val context: Context, var options: GoogleMapOptions) pendingMarkers.forEach { it.update(symbolManager) } pendingMarkers.clear() - val mapContext = MapContext(context) + pendingBitmaps.forEach { map -> it.addImage(map.key, map.value) } + pendingBitmaps.clear() + map.locationComponent.apply { activateLocationComponent(LocationComponentActivationOptions.builder(mapContext, it) + .locationEngine(this@GoogleMapImpl.locationEngine) .useSpecializedLocationLayer(true) .locationComponentOptions(LocationComponentOptions.builder(mapContext).pulseEnabled(true).build()) .build()) - cameraMode = CameraMode.TRACKING + cameraMode = CameraMode.NONE renderMode = RenderMode.COMPASS + setMaxAnimationFps(2) } - setMyLocationEnabled(locationEnabled) - synchronized(mapLock) { loaded = true if (loadedCallback != null) { @@ -752,27 +773,47 @@ class GoogleMapImpl(private val context: Context, var options: GoogleMapOptions) loadedCallback?.onMapLoaded() } } + + isMyLocationEnabled = locationEnabled } } } - override fun useViewLifecycleWhenInFragment(): Boolean { - Log.d(TAG, "unimplemented Method: useViewLifecycleWhenInFragment") + override fun showInfoWindow(marker: AbstractMarker): Boolean { + infoWindowAdapter.getInfoWindowViewFor(marker, mapContext)?.let { infoView -> + currentInfoWindow?.close() + currentInfoWindow = InfoWindow(infoView, this, marker).also { infoWindow -> + mapView?.let { infoWindow.open(it) } + } + return true + } return false } + internal fun addBitmap(name: String, bitmap: Bitmap) { + val map = map + if (map != null) { + map.getStyle { + it.addImage(name, bitmap) + } + } else { + pendingBitmaps[name] = bitmap + } + } + override fun onResume() = mapView?.onResume() ?: Unit override fun onPause() = mapView?.onPause() ?: Unit override fun onDestroy() { Log.d(TAG, "destroy"); - circleManager?.onDestroy() - circleManager = null + userOnInitializedCallbackList.clear() lineManager?.onDestroy() lineManager = null fillManager?.onDestroy() fillManager = null + circles.clear() symbolManager?.onDestroy() symbolManager = null + currentInfoWindow?.close() pendingMarkers.clear() markers.clear() BitmapDescriptorFactoryImpl.unregisterMap(map) @@ -781,8 +822,7 @@ class GoogleMapImpl(private val context: Context, var options: GoogleMapOptions) mapView?.onDestroy() mapView = null - // Don't make it null; this object is not deleted immediately, and it may want to access map.* stuff - //map = null + map = null created = false initialized = false @@ -790,22 +830,17 @@ class GoogleMapImpl(private val context: Context, var options: GoogleMapOptions) } override fun onStart() { + isStarted = true mapView?.onStart() } override fun onStop() { + isStarted = false mapView?.onStop() } - override fun onEnterAmbient(bundle: Bundle?) { - Log.d(TAG, "unimplemented Method: onEnterAmbient") - } - - override fun onExitAmbient() { - Log.d(TAG, "unimplemented Method: onExitAmbient") - } - override fun onLowMemory() = mapView?.onLowMemory() ?: Unit + override fun onSaveInstanceState(outState: Bundle) { val newBundle = Bundle() mapView?.onSaveInstanceState(newBundle) @@ -814,18 +849,56 @@ class GoogleMapImpl(private val context: Context, var options: GoogleMapOptions) fun getMapAsync(callback: IOnMapReadyCallback) { synchronized(mapLock) { - if (initialized) { - Log.d(TAG, "Invoking callback instantly, as map is initialized") - try { - callback.onMapReady(this) - } catch (e: Exception) { - Log.w(TAG, e) + userOnInitializedCallbackList.add(callback) + } + tryRunUserInitializedCallbacks("getMapAsync") + } + + private var isInvokingInitializedCallbacks = AtomicBoolean(false) + fun tryRunUserInitializedCallbacks(tag: String = "") { + + synchronized(mapLock) { + if (userOnInitializedCallbackList.isEmpty()) return + } + + val runCallbacks = { + synchronized(mapLock) { + userOnInitializedCallbackList.forEach { + try { + it.onMapReady(this) + } catch (e: Exception) { + Log.w(TAG, e) + } + }.also { + userOnInitializedCallbackList.clear() } - } else { - Log.d(TAG, "Delay callback invocation, as map is not yet initialized") - initializedCallbackList.add(callback) } } + + val map = map + if (initialized && map != null) { + // Call all callbacks immediately, as map is ready + Log.d("$TAG:$tag", "Invoking callback now, as map is initialized") + val wasCallbackActive = isInvokingInitializedCallbacks.getAndSet(true) + runOnMainLooper(forceQueue = wasCallbackActive) { + runCallbacks() + } + if (!wasCallbackActive) isInvokingInitializedCallbacks.set(false) + } else if (mapView?.isShown == false) { + /* If map is hidden, an app (e.g. Dott) may expect it to initialize anyway and + * will not show the map until it is initialized. However, we should not call + * the callback before onCreate is started (we know this is the case if mapView is + * null), otherwise that results in other problems (e.g. Gas Now app not + * initializing). + */ + runOnMainLooper(forceQueue = true) { + Log.d("$TAG:$tag", "Invoking callback now: map cannot be initialized because it is not shown (yet)") + runCallbacks() + } + } else { + Log.d("$TAG:$tag", "Initialized callbacks could not be run at this point, as the map view has not been created yet.") + // Will be retried after initialization. + } } override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean = diff --git a/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/LiteGoogleMap.kt b/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/LiteGoogleMap.kt new file mode 100644 index 0000000000..cea552b9b5 --- /dev/null +++ b/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/LiteGoogleMap.kt @@ -0,0 +1,677 @@ +package org.microg.gms.maps.mapbox + +import android.Manifest +import android.content.ActivityNotFoundException +import android.content.Context +import android.content.Intent +import android.content.Intent.ACTION_VIEW +import android.content.pm.PackageManager +import android.graphics.PointF +import android.location.Location +import android.net.Uri +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.util.Log +import android.view.View +import android.view.ViewGroup.LayoutParams.MATCH_PARENT +import android.widget.FrameLayout +import android.widget.ImageView +import androidx.annotation.UiThread +import androidx.core.app.ActivityCompat +import androidx.core.content.ContextCompat +import com.google.android.gms.dynamic.IObjectWrapper +import com.google.android.gms.dynamic.ObjectWrapper +import com.google.android.gms.dynamic.unwrap +import com.google.android.gms.maps.GoogleMapOptions +import com.google.android.gms.maps.internal.* +import com.google.android.gms.maps.model.* +import com.google.android.gms.maps.model.internal.* +import com.mapbox.mapboxsdk.Mapbox +import com.mapbox.mapboxsdk.WellKnownTileServer +import com.mapbox.mapboxsdk.location.engine.* +import com.mapbox.mapboxsdk.plugins.annotation.SymbolOptions +import com.mapbox.mapboxsdk.snapshotter.MapSnapshot +import com.mapbox.mapboxsdk.snapshotter.MapSnapshotter +import com.mapbox.mapboxsdk.style.layers.* +import com.mapbox.mapboxsdk.style.sources.GeoJsonSource +import com.mapbox.turf.TurfConstants.UNIT_METERS +import com.mapbox.turf.TurfMeasurement +import org.microg.gms.maps.mapbox.model.* +import org.microg.gms.maps.mapbox.utils.toGms +import org.microg.gms.maps.mapbox.utils.toMapbox +import org.microg.gms.maps.mapbox.utils.toPoint +import java.util.concurrent.atomic.AtomicBoolean +import kotlin.math.max +import kotlin.math.roundToInt + +// From com.mapbox.mapboxsdk.location.LocationComponent +const val DEFAULT_INTERVAL_MILLIS = 1000L +const val DEFAULT_FASTEST_INTERVAL_MILLIS = 1000L + +class MetaSnapshot( + val snapshot: MapSnapshot, + val cameraPosition: CameraPosition, + val cameraBounds: com.mapbox.mapboxsdk.geometry.LatLngBounds?, + val width: Int, + val height: Int, + val paddingRight: Int, + val paddingTop: Int, + val dpi: Float +) { + fun latLngForPixelFixed(point: PointF) = snapshot.latLngForPixel( + PointF( + point.x / dpi, point.y / dpi + ) + ) +} + +class LiteGoogleMapImpl(context: Context, var options: GoogleMapOptions) : AbstractGoogleMap(context) { + + internal val view: FrameLayout = FrameLayout(mapContext) + val map: ImageView + + private var created = false + + private var cameraPosition: CameraPosition = options.camera ?: CameraPosition.fromLatLngZoom(LatLng(0.0, 0.0), 2f) + private var cameraBounds: com.mapbox.mapboxsdk.geometry.LatLngBounds? = null + + private var mapType: Int = options.mapType + private var mapStyle: MapStyleOptions? = null + + private var currentSnapshotter: MapSnapshotter? = null + + private var lastSnapshot: MetaSnapshot? = null + + private var lastTouchPosition = PointF(0f, 0f) + + private val afterNextDrawCallback = mutableListOf<() -> Unit>() + private var cameraChangeListener: IOnCameraChangeListener? = null + + private var myLocationEnabled = false + private var myLocation: Location? = null + private val defaultLocationEngine = GoogleLocationEngine(context) + private var locationEngine: LocationEngine = defaultLocationEngine + + internal val markers: MutableList = mutableListOf() + internal val polygons: MutableList = mutableListOf() + internal val polylines: MutableList = mutableListOf() + internal val circles: MutableList = mutableListOf() + + private var nextObjectId = 0 + + private var showWatermark = true + + private val updatePosted = AtomicBoolean(false) + + init { + map = ImageView(mapContext).apply { + layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT) + } + + view.addView(map) + + view.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ -> + postUpdateSnapshot() + currentInfoWindow?.update() + } + + BitmapDescriptorFactoryImpl.initialize(mapContext.resources, context.resources) + + // noinspection ClickableViewAccessibility; touch listener only has side effects + map.setOnTouchListener { _, event -> + lastTouchPosition = PointF(event.x + map.paddingLeft, event.y + map.paddingTop) + false + } + + map.setOnClickListener { + + // Test if clickable + if ((view.parent as View?)?.isClickable == false) return@setOnClickListener + + lastSnapshot?.let { meta -> + // Calculate marker hitboxes + for (marker in markers.filter { it.isVisible }) { + + marker.getIconDimensions()?.let { iconDimensions -> // consider only markers with icon + val anchorPoint = meta.snapshot.pixelForLatLng(marker.position.toMapbox()) + + val leftX = anchorPoint.x - marker.anchor[0] * iconDimensions[0] + val topY = anchorPoint.y - marker.anchor[1] * iconDimensions[1] + + if (lastTouchPosition.x >= leftX && lastTouchPosition.x <= leftX + iconDimensions[0] + && lastTouchPosition.y >= topY && lastTouchPosition.y <= topY + iconDimensions[1]) { + // Marker was clicked + if (markerClickListener?.onMarkerClick(marker) == true) { + currentInfoWindow?.close() + currentInfoWindow = null + return@setOnClickListener + } else if (showInfoWindow(marker)) { + return@setOnClickListener + } + } + } + } + + currentInfoWindow?.close() + currentInfoWindow = null + + // Test if circle was clicked + for (circle in circles.filter { it.isVisible && it.isClickable }) { + Log.d(TAG, "last touch ${lastTouchPosition.x}, ${lastTouchPosition.y}, turf ${TurfMeasurement.distance( + circle.center.toPoint(), + meta.latLngForPixelFixed(lastTouchPosition).toPoint(), + UNIT_METERS + )}, radius ${circle.radiusInMeters}") + if (TurfMeasurement.distance( + circle.center.toPoint(), + meta.latLngForPixelFixed(lastTouchPosition).toPoint(), + UNIT_METERS + ) <= circle.radiusInMeters) { + // Circle was clicked + circleClickListener?.onCircleClick(circle) + return@setOnClickListener + } + } + + val clickedPosition = meta.latLngForPixelFixed(lastTouchPosition) + val clickListenerConsumedClick = mapClickListener?.let { + it.onMapClick(clickedPosition.toGms()) + true + } ?: false + + if (clickListenerConsumedClick) return@setOnClickListener + + // else open external map at clicked location + val intent = + Intent(ACTION_VIEW, Uri.parse("geo:${clickedPosition.latitude},${clickedPosition.longitude}")) + + try { + context.startActivity(intent) + } catch (e: ActivityNotFoundException) { + Log.e(TAG, "No compatible mapping application installed. Not handling click.") + } + } + + + } + map.setOnLongClickListener { + mapLongClickListener?.onMapLongClick( + lastSnapshot?.latLngForPixelFixed(lastTouchPosition)?.toGms() ?: LatLng(0.0, 0.0) + ) + mapLongClickListener != null + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + if (!created) { + + Mapbox.getInstance(mapContext, BuildConfig.MAPBOX_KEY, WellKnownTileServer.Mapbox) + + if (savedInstanceState?.containsKey(BUNDLE_CAMERA_POSITION) == true) { + cameraPosition = savedInstanceState.getParcelable(BUNDLE_CAMERA_POSITION)!! + cameraBounds = savedInstanceState.getParcelable(BUNDLE_CAMERA_BOUNDS) + } + + postUpdateSnapshot() + + created = true + } + } + + internal fun postUpdateSnapshot() { + if (updatePosted.compareAndSet(false, true)) { + Handler(Looper.getMainLooper()).post { + updatePosted.set(false) + updateSnapshot() + } + } + } + + @UiThread + private fun updateSnapshot() { + + val cameraPosition = cameraPosition + val dpi = dpiFactor + + val cameraBounds = cameraBounds + + val pixelWidth = map.width + val pixelHeight = map.height + + val styleBuilder = getStyle(mapContext, mapType, mapStyle, styleFromFileWorkaround = true) + + // Add visible polygons (before polylines, so that they are drawn below their strokes) + for (polygon in polygons.filter { it.isVisible }) { + styleBuilder.withLayer( + FillLayer("l${polygon.id}", polygon.id).withProperties( + PropertyFactory.fillColor(polygon.fillColor) + ) + ).withSource( + GeoJsonSource(polygon.id, polygon.annotationOptions.geometry) + ) + } + + // Add visible polylines + for (polyline in polylines.filter { it.isVisible }) { + styleBuilder.withLayer( + LineLayer("l${polyline.id}", polyline.id).withProperties( + PropertyFactory.lineWidth(polyline.width), + PropertyFactory.lineColor(polyline.color), + PropertyFactory.lineCap(Property.LINE_CAP_ROUND) + ) + ).withSource( + GeoJsonSource(polyline.id, polyline.annotationOptions.geometry) + ) + } + + // Add circles + for (circle in circles.filter { it.isVisible }) { + styleBuilder.withLayer(FillLayer("l${circle.id}c", circle.id).withProperties( + PropertyFactory.fillColor(circle.fillColor) + )).withSource(GeoJsonSource(circle.id, circle.annotationOptions.geometry)) + + styleBuilder.withLayer(LineLayer("l${circle.id}s", "${circle.id}s").withProperties( + PropertyFactory.lineWidth(circle.strokeWidth), + PropertyFactory.lineColor(circle.strokeColor), + PropertyFactory.lineCap(Property.LINE_CAP_ROUND), + ).apply { + circle.strokePattern?.let { + val name = it.getName(circle.strokeColor, circle.strokeWidth, dpi) + withProperties(PropertyFactory.linePattern(name)) + styleBuilder.withImage(name, it.makeBitmap(circle.strokeColor, circle.strokeWidth, dpi)) + } + }).withSource(GeoJsonSource("${circle.id}s", circle.line.annotationOptions.geometry)) + } + + // Add markers + BitmapDescriptorFactoryImpl.put(styleBuilder) + for (marker in markers.filter { it.isVisible }) { + val layer = SymbolLayer("l${marker.id}", marker.id).withProperties( + PropertyFactory.symbolSortKey(marker.zIndex), + PropertyFactory.iconAllowOverlap(true) + ) + marker.icon?.applyTo(layer, marker.anchor, dpi) + styleBuilder.withLayer(layer).withSource( + GeoJsonSource(marker.id, marker.annotationOptions.geometry) + ) + } + + // Add location overlay + if (myLocationEnabled) myLocation?.let { + val indicator = ContextCompat.getDrawable(mapContext, R.drawable.location_dot)!! + styleBuilder.withImage("locationIndicator", indicator) + val layer = SymbolLayer("location", "locationSource").withProperties( + PropertyFactory.iconAllowOverlap(true), + PropertyFactory.iconImage("locationIndicator"), + PropertyFactory.iconAnchor(Property.ICON_ANCHOR_TOP_LEFT), + PropertyFactory.iconOffset(arrayOf( + 0.5f * indicator.minimumWidth / dpi, 0.5f * indicator.minimumHeight / dpi + )) + ) + styleBuilder.withLayer(layer).withSource( + GeoJsonSource( + "locationSource", + SymbolOptions().withLatLng(com.mapbox.mapboxsdk.geometry.LatLng(it.latitude, it.longitude)).geometry + ) + ) + } + + val dpiWidth = max(pixelWidth / dpi, 1f).roundToInt() + val dpiHeight = max(pixelHeight / dpi, 1f).roundToInt() + + val snapshotter = MapSnapshotter( + mapContext, MapSnapshotter.Options(dpiWidth, dpiHeight) + .withCameraPosition(this@LiteGoogleMapImpl.cameraPosition.toMapbox()) + .apply { + // if camera bounds are set, overwrite camera position + cameraBounds?.let { withRegion(it) } + } + .withStyleBuilder(styleBuilder) + .withLogo(showWatermark) + .withPixelRatio(dpi) + ) + + synchronized(this) { + this.currentSnapshotter?.cancel() + this.currentSnapshotter = snapshotter + } + + snapshotter.start({ + + val cameraPositionChanged = cameraPosition != lastSnapshot?.cameraPosition || (cameraBounds != lastSnapshot?.cameraBounds) + + lastSnapshot = MetaSnapshot( + it, cameraPosition, cameraBounds, pixelWidth, pixelHeight, view.paddingRight, view.paddingTop, dpi + ) + map.setImageBitmap(it.bitmap) + + for (callback in afterNextDrawCallback) callback() + afterNextDrawCallback.clear() + + if (cameraPositionChanged) { + // Notify apps that new projection is now available + cameraChangeListener?.onCameraChange(cameraPosition) + } + + currentInfoWindow?.update() + + synchronized(this) { + this.currentSnapshotter = null + } + + }, null) + } + + fun getMapAsync(callback: IOnMapReadyCallback) { + if (lastSnapshot == null) { + Log.d(TAG, "Invoking callback instantly, as a snapshot is ready") + callback.onMapReady(this) + } else { + Log.d(TAG, "Delay callback invocation, as snapshot has not been rendered yet") + afterNextDrawCallback.add { callback.onMapReady(this) } + } + } + + override fun getCameraPosition(): CameraPosition = cameraPosition + + override fun getMaxZoomLevel() = 21f + + override fun getMinZoomLevel() = 1f + + override fun moveCamera(cameraUpdate: IObjectWrapper?): Unit = cameraUpdate.unwrap()?.let { + cameraPosition = it.getLiteModeCameraPosition(this) ?: cameraPosition + cameraBounds = it.getLiteModeCameraBounds() + + postUpdateSnapshot() + } ?: Unit + + override fun animateCamera(cameraUpdate: IObjectWrapper?) = moveCamera(cameraUpdate) + + override fun animateCameraWithCallback(cameraUpdate: IObjectWrapper?, callback: ICancelableCallback?) { + moveCamera(cameraUpdate) + Log.d(TAG, "animateCameraWithCallback: animation not possible in lite mode, invoking callback instantly") + callback?.onFinish() + } + + override fun animateCameraWithDurationAndCallback( + cameraUpdate: IObjectWrapper?, duration: Int, callback: ICancelableCallback? + ) = animateCameraWithCallback(cameraUpdate, callback) + + override fun stopAnimation() { + Log.d(TAG, "stopAnimation: animation not possible in lite mode") + } + + override fun addPolyline(options: PolylineOptions): IPolylineDelegate { + return LitePolylineImpl(this, "polyline${nextObjectId++}", options).also { polylines.add(it) } + } + + override fun addPolygon(options: PolygonOptions): IPolygonDelegate { + return LitePolygonImpl( + "polygon${nextObjectId++}", options, this + ).also { + polygons.add(it) + polylines.addAll(it.strokes) + postUpdateSnapshot() + } + } + + override fun addMarker(options: MarkerOptions): IMarkerDelegate { + return LiteMarkerImpl("marker${nextObjectId++}", options, this).also { + markers.add(it) + postUpdateSnapshot() + } + } + + override fun addGroundOverlay(options: GroundOverlayOptions?): IGroundOverlayDelegate? { + Log.d(TAG, "addGroundOverlay: not supported in lite mode") + return null + } + + override fun addTileOverlay(options: TileOverlayOptions?): ITileOverlayDelegate? { + Log.d(TAG, "addTileOverlay: not supported in lite mode") + return null + } + + override fun clear() { + polylines.clear() + polygons.clear() + markers.clear() + circles.clear() + postUpdateSnapshot() + } + + override fun getMapType(): Int { + return mapType + } + + override fun setMapType(type: Int) { + mapType = type + postUpdateSnapshot() + } + + override fun isTrafficEnabled(): Boolean { + Log.d(TAG, "isTrafficEnabled: traffic not supported in lite mode") + return false + } + + override fun setTrafficEnabled(traffic: Boolean) { + Log.d(TAG, "setTrafficEnabled: traffic not supported in lite mode") + } + + override fun isIndoorEnabled(): Boolean { + Log.d(TAG, "isIndoorEnabled: indoor not supported in lite mode") + return false + } + + override fun setIndoorEnabled(indoor: Boolean) { + Log.d(TAG, "setIndoorEnabled: indoor not supported in lite mode") + } + + override fun isMyLocationEnabled(): Boolean = myLocationEnabled + + override fun setMyLocationEnabled(myLocation: Boolean) { + if (!myLocationEnabled && myLocation) { + activateLocationProvider() + } else if (myLocationEnabled && !myLocation) { + deactivateLocationProvider() + } // else situation is unchanged + myLocationEnabled = myLocation + } + + private fun activateLocationProvider() { + // Activate only if sufficient permissions + if (ActivityCompat.checkSelfPermission( + mapContext, Manifest.permission.ACCESS_FINE_LOCATION + ) == PackageManager.PERMISSION_GRANTED || ActivityCompat.checkSelfPermission( + mapContext, Manifest.permission.ACCESS_COARSE_LOCATION + ) == PackageManager.PERMISSION_GRANTED + ) { + locationEngine.requestLocationUpdates( + LocationEngineRequest.Builder(DEFAULT_INTERVAL_MILLIS) + .setFastestInterval(DEFAULT_FASTEST_INTERVAL_MILLIS) + .setPriority(LocationEngineRequest.PRIORITY_HIGH_ACCURACY) + .build(), locationEngineCallback, Looper.getMainLooper() + ) + + } else { + Log.w(TAG, "Called setMyLocationEnabled(true) without sufficient permissions. Not showing location.") + } + } + + private fun deactivateLocationProvider() { + locationEngine.removeLocationUpdates(locationEngineCallback) + } + + override fun setLocationSource(locationSource: ILocationSourceDelegate?) { + if (myLocationEnabled) deactivateLocationProvider() + locationEngine = locationSource?.let { SourceLocationEngine(it) } ?: defaultLocationEngine + if (myLocationEnabled) activateLocationProvider() + } + + override fun onLocationUpdate(location: Location) { + this@LiteGoogleMapImpl.myLocation = location + postUpdateSnapshot() + } + + override fun getUiSettings(): IUiSettingsDelegate { + Log.d(TAG, "UI settings have no effect") + return UiSettingsCache() + } + + /** + * Gets a projection snapshot. This means that, in accordance to the docs, the projection object + * will represent the map as it is seen at the point in time that the projection is queried, and + * not updated later on. + */ + override fun getProjection(): IProjectionDelegate = lastSnapshot?.let { LiteProjection(it) } ?: DummyProjection() + + override fun setOnCameraChangeListener(listener: IOnCameraChangeListener?) { + cameraChangeListener = listener + } + + override fun setOnMarkerDragListener(listener: IOnMarkerDragListener?) { + Log.d(TAG, "setOnMarkerDragListener: marker drag is not supported in lite mode") + } + + override fun addCircle(options: CircleOptions): ICircleDelegate { + return LiteCircleImpl(this, "circle${nextObjectId++}", options).also { circles.add(it) } + } + + override fun snapshot(callback: ISnapshotReadyCallback?, bitmap: IObjectWrapper?) { + val lastSnapshot = lastSnapshot + if (lastSnapshot == null) { + afterNextDrawCallback.add { + callback?.onBitmapWrappedReady(ObjectWrapper.wrap(this@LiteGoogleMapImpl.lastSnapshot!!.snapshot.bitmap)) + } + } else { + callback?.onBitmapWrappedReady(ObjectWrapper.wrap(lastSnapshot.snapshot.bitmap)) + } + } + + override fun snapshotForTest(callback: ISnapshotReadyCallback?) { + Log.d(TAG, "Not yet implemented: snapshotForTest") + } + + override fun setPadding(left: Int, top: Int, right: Int, bottom: Int) { + view.setPadding(left, top, right, bottom) + postUpdateSnapshot() + } + + override fun isBuildingsEnabled(): Boolean { + Log.d(TAG, "isBuildingsEnabled: never enabled in light mode") + return false + } + + override fun setBuildingsEnabled(buildings: Boolean) { + Log.d(TAG, "setBuildingsEnabled: cannot be enabled in light mode") + } + + override fun setOnMapLoadedCallback(callback: IOnMapLoadedCallback?) = callback?.let { onMapLoadedCallback -> + if (lastSnapshot != null) { + Log.d(TAG, "Invoking map loaded callback instantly, as a snapshot is ready") + onMapLoadedCallback.onMapLoaded() + } + else { + Log.d(TAG, "Delaying map loaded callback, as snapshot has not been taken yet") + afterNextDrawCallback.add { onMapLoadedCallback.onMapLoaded() } + } + Unit + } ?: Unit + + override fun setWatermarkEnabled(watermark: Boolean) { + showWatermark = watermark + } + + override fun showInfoWindow(marker: AbstractMarker): Boolean { + infoWindowAdapter.getInfoWindowViewFor(marker, mapContext)?.let { infoView -> + currentInfoWindow?.close() + currentInfoWindow = InfoWindow(infoView, this, marker).also { infoWindow -> + infoWindow.open(view) + } + return true + } + return false + } + + override fun onResume() { + if (myLocationEnabled) activateLocationProvider() + } + + override fun onPause() { + synchronized(this) { + currentSnapshotter?.cancel() + currentSnapshotter = null + } + deactivateLocationProvider() + } + + override fun onDestroy() { + view.removeView(map) + } + + override fun onLowMemory() { + } + + override fun onSaveInstanceState(outState: Bundle) { + outState.putParcelable(BUNDLE_CAMERA_POSITION, cameraPosition) + outState.putParcelable(BUNDLE_CAMERA_BOUNDS, cameraBounds) + } + + override fun setContentDescription(desc: String?) { + view.contentDescription = desc + } + + override fun setMapStyle(options: MapStyleOptions?): Boolean { + Log.d(TAG, "setMapStyle options: " + options?.getJson()) + mapStyle = options + + return true + } + + override fun setMinZoomPreference(minZoom: Float) { + Log.d(TAG, "setMinZoomPreference: no interactivity in lite mode") + } + + override fun setMaxZoomPreference(maxZoom: Float) { + Log.d(TAG, "setMaxZoomPreference: no interactivity in lite mode") + } + + override fun resetMinMaxZoomPreference() { + Log.d(TAG, "resetMinMaxZoomPreference: no interactivity in lite mode") + } + + override fun setLatLngBoundsForCameraTarget(bounds: LatLngBounds?) { + Log.d(TAG, "setLatLngBoundsForCameraTarget: no interactivity in lite mode") + } + + override fun setCameraMoveStartedListener(listener: IOnCameraMoveStartedListener?) { + Log.d(TAG, "setCameraMoveStartedListener: event not supported in lite mode") + } + + override fun setCameraMoveListener(listener: IOnCameraMoveListener?) { + Log.d(TAG, "setCameraMoveListener: event not supported in lite mode") + + } + + override fun setCameraMoveCanceledListener(listener: IOnCameraMoveCanceledListener?) { + Log.d(TAG, "setCameraMoveCanceledListener: event not supported in lite mode") + } + + override fun setCameraIdleListener(listener: IOnCameraIdleListener?) { + Log.d(TAG, "setCameraIdleListener: event not supported in lite mode") + } + + override fun onStart() { + } + + override fun onStop() { + } + + companion object { + private val TAG = "GmsMapLite" + private val BUNDLE_CAMERA_POSITION = "camera" + private val BUNDLE_CAMERA_BOUNDS = "cameraBounds" + } +} + diff --git a/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/MapFragment.kt b/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/MapFragment.kt index daf4bfb92e..69135ab299 100644 --- a/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/MapFragment.kt +++ b/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/MapFragment.kt @@ -32,12 +32,18 @@ import com.google.android.gms.maps.internal.IOnMapReadyCallback class MapFragmentImpl(private val activity: Activity) : IMapFragmentDelegate.Stub() { - private var map: GoogleMapImpl? = null + private var map: IGoogleMapDelegate? = null private var options: GoogleMapOptions? = null override fun onInflate(activity: IObjectWrapper, options: GoogleMapOptions, savedInstanceState: Bundle?) { this.options = options - map?.options = options + map?.apply { + if (this is GoogleMapImpl) { + this.options = options + } else if (this is LiteGoogleMapImpl) { + this.options = options + } + } } override fun onCreate(savedInstanceState: Bundle?) { @@ -47,7 +53,11 @@ class MapFragmentImpl(private val activity: Activity) : IMapFragmentDelegate.Stu if (options == null) { options = GoogleMapOptions() } - map = GoogleMapImpl(activity, options ?: GoogleMapOptions()) + if (options?.liteMode == true) { + map = LiteGoogleMapImpl(activity, options ?: GoogleMapOptions()) + } else { + map = GoogleMapImpl(activity, options ?: GoogleMapOptions()) + } } override fun onCreateView(layoutInflater: IObjectWrapper, container: IObjectWrapper, savedInstanceState: Bundle?): IObjectWrapper { @@ -56,13 +66,25 @@ class MapFragmentImpl(private val activity: Activity) : IMapFragmentDelegate.Stu } Log.d(TAG, "onCreateView: ${options?.camera?.target}") if (map == null) { - map = GoogleMapImpl(activity, options ?: GoogleMapOptions()) + map = if (options?.liteMode == true) { + LiteGoogleMapImpl(activity, options ?: GoogleMapOptions()) + } else { + GoogleMapImpl(activity, options ?: GoogleMapOptions()) + } + } + map!!.apply { + onCreate(savedInstanceState) + + val view = when (this) { + is GoogleMapImpl -> this.view + is LiteGoogleMapImpl -> this.view + else -> null + } + + val parent = view?.parent as ViewGroup? + parent?.removeView(view) + return ObjectWrapper.wrap(view) } - map!!.onCreate(savedInstanceState) - val view = map!!.view - val parent = view.parent as ViewGroup? - parent?.removeView(view) - return ObjectWrapper.wrap(view) } override fun getMap(): IGoogleMapDelegate? = map @@ -74,7 +96,10 @@ class MapFragmentImpl(private val activity: Activity) : IMapFragmentDelegate.Stu override fun onPause() = map?.onPause() ?: Unit override fun onLowMemory() = map?.onLowMemory() ?: Unit override fun isReady(): Boolean = this.map != null - override fun getMapAsync(callback: IOnMapReadyCallback) = map?.getMapAsync(callback) ?: Unit + override fun getMapAsync(callback: IOnMapReadyCallback) = map?.let { + if (it is GoogleMapImpl) it.getMapAsync(callback) + else if (it is LiteGoogleMapImpl) it.getMapAsync(callback) + } ?: Unit override fun onDestroyView() { map?.onDestroy() diff --git a/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/MapView.kt b/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/MapView.kt index 8951aaf150..ec3231a6fd 100644 --- a/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/MapView.kt +++ b/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/MapView.kt @@ -30,12 +30,17 @@ import com.google.android.gms.maps.internal.IOnMapReadyCallback class MapViewImpl(private val context: Context, options: GoogleMapOptions?) : IMapViewDelegate.Stub() { private val options: GoogleMapOptions = options ?: GoogleMapOptions() - private var map: GoogleMapImpl? = null + private var map: IGoogleMapDelegate? = null override fun onCreate(savedInstanceState: Bundle?) { Log.d(TAG, "onCreate: ${options?.camera?.target}") - map = GoogleMapImpl(context, options) - map!!.onCreate(savedInstanceState) + map = if (options.liteMode) { + LiteGoogleMapImpl(context, options) + } else { + GoogleMapImpl(context, options) + }.apply { + this.onCreate(savedInstanceState) + } } override fun getMap(): IGoogleMapDelegate? = map @@ -52,8 +57,23 @@ class MapViewImpl(private val context: Context, options: GoogleMapOptions?) : IM override fun onLowMemory() = map?.onLowMemory() ?: Unit override fun onSaveInstanceState(outState: Bundle) = map?.onSaveInstanceState(outState) ?: Unit - override fun getView(): IObjectWrapper = ObjectWrapper.wrap(map?.view) - override fun getMapAsync(callback: IOnMapReadyCallback) = map?.getMapAsync(callback) ?: Unit + override fun getView(): IObjectWrapper = ObjectWrapper.wrap( + map?.let { + when (it) { + is GoogleMapImpl -> it.view + is LiteGoogleMapImpl -> it.view + else -> null + } + } + ) + + override fun getMapAsync(callback: IOnMapReadyCallback) = map?.let { + when (it) { + is GoogleMapImpl -> it.getMapAsync(callback) + is LiteGoogleMapImpl -> it.getMapAsync(callback) + else -> null + } + } ?: Unit override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean = if (super.onTransact(code, data, reply, flags)) { diff --git a/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/Pattern.kt b/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/Pattern.kt new file mode 100644 index 0000000000..d96147c286 --- /dev/null +++ b/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/Pattern.kt @@ -0,0 +1,88 @@ +package org.microg.gms.maps.mapbox + +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.RectF +import com.google.android.gms.maps.model.Dash +import com.google.android.gms.maps.model.Dot +import com.google.android.gms.maps.model.Gap +import com.google.android.gms.maps.model.PatternItem +import kotlin.math.max + +fun PatternItem.getName(): String = when (this) { + is Dash -> "dash${this.length}" + is Gap -> "gap${this.length}" + is Dot -> "dot" + else -> this.javaClass.name +} + +/** + * Name of pattern, to identify it after it is added to map + */ +fun List.getName(color: Int, strokeWidth: Float, skew: Float = 1f) = if (isEmpty()) { + "solid-${color}" +} else {joinToString("-") { + it.getName() + } + "-${color}-width${strokeWidth}-skew${skew}" +} + +/** + * Gets width that a bitmap for this pattern item would have if the pattern's bitmap + * were to be drawn with respect to aspect ratio onto a canvas with height 1. + */ +fun PatternItem.getWidth(strokeWidth: Float, skew: Float): Float = when (this) { + is Dash -> this.length + is Gap -> this.length + is Dot -> strokeWidth * skew + else -> 1f +} + +/** + * Gets width that a bitmap for this pattern would have if it were to be drawn + * with respect to aspect ratio onto a canvas with height 1. + */ +fun List.getWidth(strokeWidth: Float, skew: Float) = map { it.getWidth(strokeWidth, skew) }.sum() + +fun List.makeBitmap(color: Int, strokeWidth: Float, skew: Float = 1f): Bitmap = makeBitmap(Paint().apply { + setColor(color) + style = Paint.Style.FILL +}, strokeWidth, skew) + + +fun List.makeBitmap(paint: Paint, strokeWidth: Float, skew: Float): Bitmap { + + // Pattern aspect ratio is not respected by renderer + val width = getWidth(strokeWidth, skew).toInt() + val height = (strokeWidth * skew).toInt() // avoids squished image bugs + + // For empty list or nonsensical input (zero-width items) + if (width == 0 || height == 0) { + val nonZeroHeight = max(1f, strokeWidth) + return Bitmap.createBitmap(1, nonZeroHeight.toInt(), Bitmap.Config.ARGB_8888).also { + Canvas(it).drawRect(0f, 0f, nonZeroHeight, nonZeroHeight, paint) + } + } + + val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) + val canvas = Canvas(bitmap) + + var drawCursor = 0f + for (item in this) { + val rect = RectF( + drawCursor, + 0f, + drawCursor + item.getWidth(strokeWidth, skew), + strokeWidth * skew + ) + when (item) { + is Dash -> canvas.drawRect(rect, paint) + // is Gap -> do nothing, only move cursor + is Dot -> canvas.drawOval(rect, paint) + } + + drawCursor += item.getWidth(strokeWidth, skew) + } + + return bitmap +} diff --git a/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/Projection.kt b/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/Projection.kt index ab22186dc5..75546684af 100644 --- a/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/Projection.kt +++ b/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/Projection.kt @@ -26,25 +26,30 @@ import com.google.android.gms.maps.model.LatLng import com.google.android.gms.maps.model.VisibleRegion import com.mapbox.mapboxsdk.maps.Projection import com.google.android.gms.dynamic.unwrap +import com.google.android.gms.maps.model.LatLngBounds import org.microg.gms.maps.mapbox.utils.toGms import org.microg.gms.maps.mapbox.utils.toMapbox import kotlin.math.roundToInt +val ZERO_LAT_LNG = com.mapbox.mapboxsdk.geometry.LatLng(0.0, 0.0) + // TODO: Do calculations using backed up locations instead of live (which requires UI thread) class ProjectionImpl(private val projection: Projection, private val withoutTiltOrBearing: Boolean) : IProjectionDelegate.Stub() { - private val visibleRegion = projection.visibleRegion - private val farLeft = projection.toScreenLocation(visibleRegion.farLeft) - private val farRight = projection.toScreenLocation(visibleRegion.farRight) - private val nearLeft = projection.toScreenLocation(visibleRegion.nearLeft) - private val nearRight = projection.toScreenLocation(visibleRegion.nearRight) + private val visibleRegion = projection.getVisibleRegion(false) + private val farLeft = visibleRegion.farLeft?.let { projection.toScreenLocation(it) } + private val farRight = visibleRegion.farRight?.let { projection.toScreenLocation(it) } + private val nearLeft = visibleRegion.nearLeft?.let { projection.toScreenLocation(it) } + private val nearRight = visibleRegion.nearRight?.let { projection.toScreenLocation(it) } override fun fromScreenLocation(obj: IObjectWrapper?): LatLng? = try { obj.unwrap()?.let { - if (withoutTiltOrBearing) { + if (withoutTiltOrBearing && farLeft != null && farRight != null && nearLeft != null) { val xPercent = (it.x.toFloat() - farLeft.x) / (farRight.x - farLeft.x) val yPercent = (it.y.toFloat() - farLeft.y) / (nearLeft.y - farLeft.y) - val lon = visibleRegion.farLeft.longitude + xPercent * (visibleRegion.farRight.longitude - visibleRegion.farLeft.longitude) - val lat = visibleRegion.farLeft.latitude + yPercent * (visibleRegion.nearLeft.latitude - visibleRegion.farLeft.latitude) + val lon = (visibleRegion.farLeft?.longitude ?: 0.0) + xPercent * + ((visibleRegion.farRight?.longitude ?: 0.0) - (visibleRegion.farLeft?.longitude ?: 0.0)) + val lat = (visibleRegion.farLeft?.latitude?: 0.0) + yPercent * + ((visibleRegion.nearLeft?.latitude?: 0.0) - (visibleRegion.farLeft?.latitude?: 0.0)) LatLng(lat, lon) } else { projection.fromScreenLocation(PointF(it)).toGms() @@ -57,12 +62,14 @@ class ProjectionImpl(private val projection: Projection, private val withoutTilt override fun toScreenLocation(latLng: LatLng?): IObjectWrapper = try { ObjectWrapper.wrap(latLng?.toMapbox()?.let { - if (withoutTiltOrBearing) { - val xPercent = (it.longitude - visibleRegion.farLeft.longitude) / (visibleRegion.farRight.longitude - visibleRegion.farLeft.longitude) - val yPercent = (it.latitude - visibleRegion.farLeft.latitude) / (visibleRegion.nearLeft.latitude - visibleRegion.farLeft.latitude) + if (withoutTiltOrBearing && farLeft != null && farRight != null && nearLeft != null) { + val xPercent = (it.longitude - (visibleRegion.farLeft?.longitude ?: 0.0)) / + ((visibleRegion.farRight?.longitude ?: 0.0) - (visibleRegion.farLeft?.longitude ?: 0.0)) + val yPercent = (it.latitude - (visibleRegion.farLeft?.latitude ?: 0.0)) / + ((visibleRegion.nearLeft?.latitude ?: 0.0) - (visibleRegion.farLeft?.latitude ?: 0.0)) val x = farLeft.x + xPercent * (farRight.x - farLeft.x) val y = farLeft.y + yPercent * (nearLeft.y - farLeft.y) - Point(x.roundToInt(), y.roundToInt()) + Point(x.roundToInt(), y.roundToInt()).also { p -> Log.d(TAG, "$p vs.\n${projection.toScreenLocation(it).let { Point(it.x.roundToInt(), it.y.roundToInt()) }}") } } else { projection.toScreenLocation(it).let { Point(it.x.roundToInt(), it.y.roundToInt()) } } @@ -78,3 +85,48 @@ class ProjectionImpl(private val projection: Projection, private val withoutTilt private val TAG = "GmsMapProjection" } } + +class LiteProjection(private val snapshot: MetaSnapshot) : IProjectionDelegate.Stub() { + + private fun fromScreenLocationAfterPadding(point: Point?): LatLng = + point?.let { snapshot.latLngForPixelFixed(PointF(point)).toGms() } ?: LatLng(0.0, 0.0) + + override fun fromScreenLocation(obj: IObjectWrapper?): LatLng = fromScreenLocationAfterPadding(obj.unwrap()?.let { + Point((it.x - snapshot.paddingRight), (it.y - snapshot.paddingRight)) + }) + + override fun toScreenLocation(latLng: LatLng?): IObjectWrapper = + ObjectWrapper.wrap(snapshot.snapshot.pixelForLatLng(latLng?.toMapbox()).let { + Point(it.x.roundToInt() + snapshot.paddingRight, it.y.roundToInt() + snapshot.paddingTop) + }) + + override fun getVisibleRegion(): VisibleRegion { + val nearLeft = fromScreenLocationAfterPadding(Point(0, snapshot.height)) + val nearRight = fromScreenLocationAfterPadding(Point(snapshot.width, snapshot.height)) + val farLeft = fromScreenLocationAfterPadding(Point(0, 0)) + val farRight = fromScreenLocationAfterPadding(Point(snapshot.width, 0)) + + return VisibleRegion(nearLeft, nearRight, farLeft, farRight, LatLngBounds(nearLeft, farRight)) + } +} + +class DummyProjection : IProjectionDelegate.Stub() { + override fun fromScreenLocation(obj: IObjectWrapper?): LatLng { + Log.d(TAG, "Map not initialized when calling getProjection(). Cannot calculate fromScreenLocation") + return LatLng(0.0, 0.0) + } + + override fun toScreenLocation(latLng: LatLng?): IObjectWrapper { + Log.d(TAG, "Map not initialized when calling getProjection(). Cannot calculate toScreenLocation") + return ObjectWrapper.wrap(Point(0, 0)) + } + + override fun getVisibleRegion(): VisibleRegion { + Log.d(TAG, "Map not initialized when calling getProjection(). Cannot calculate getVisibleRegion") + return VisibleRegion(LatLngBounds(LatLng(0.0, 0.0), LatLng(0.0, 0.0))) + } + + companion object { + private val TAG = "GmsMapDummyProjection" + } +} \ No newline at end of file diff --git a/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/SourceLocationEngine.kt b/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/SourceLocationEngine.kt new file mode 100644 index 0000000000..653483470f --- /dev/null +++ b/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/SourceLocationEngine.kt @@ -0,0 +1,69 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.maps.mapbox + +import android.app.PendingIntent +import android.location.Location +import android.os.Handler +import android.os.Looper +import android.util.Log +import com.google.android.gms.maps.internal.ILocationSourceDelegate +import com.google.android.gms.maps.internal.IOnLocationChangeListener +import com.mapbox.mapboxsdk.location.engine.LocationEngine +import com.mapbox.mapboxsdk.location.engine.LocationEngineCallback +import com.mapbox.mapboxsdk.location.engine.LocationEngineRequest +import com.mapbox.mapboxsdk.location.engine.LocationEngineResult + +class SourceLocationEngine(private val locationSource: ILocationSourceDelegate) : LocationEngine, IOnLocationChangeListener.Stub() { + val callbacks: MutableSet, Handler>> = hashSetOf() + var lastLocation: Location? = null + var active: Boolean = false + + override fun getLastLocation(callback: LocationEngineCallback) { + callback.onSuccess(LocationEngineResult.create(lastLocation)) + } + + override fun requestLocationUpdates(request: LocationEngineRequest, callback: LocationEngineCallback, looper: Looper?) { + callbacks.add(callback to Handler(looper ?: Looper.myLooper() ?: Looper.getMainLooper())) + if (!active) { + active = true + try { + locationSource.activate(this) + } catch (e: Exception) { + Log.w(TAG, e) + } + } + } + + override fun requestLocationUpdates(request: LocationEngineRequest, pendingIntent: PendingIntent?) { + throw UnsupportedOperationException() + } + + override fun removeLocationUpdates(callback: LocationEngineCallback) { + callbacks.removeAll { it.first == callback } + if (callbacks.isEmpty() && active) { + active = false + try { + locationSource.deactivate() + } catch (e: Exception) { + Log.w(TAG, e) + } + } + } + + override fun removeLocationUpdates(pendingIntent: PendingIntent?) { + throw UnsupportedOperationException() + } + + override fun onLocationChanged(location: Location?) { + lastLocation = location + for ((callback, handler) in callbacks) { + handler.post { + callback.onSuccess(LocationEngineResult.create(location)) + } + } + } +} \ No newline at end of file diff --git a/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/Styles.kt b/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/Styles.kt new file mode 100644 index 0000000000..ed8e6ef469 --- /dev/null +++ b/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/Styles.kt @@ -0,0 +1,425 @@ +package org.microg.gms.maps.mapbox + +import android.graphics.Color +import android.os.Build.VERSION.SDK_INT +import android.util.Log +import androidx.annotation.ColorInt +import androidx.annotation.FloatRange +import androidx.core.graphics.ColorUtils +import com.google.android.gms.maps.GoogleMap +import com.google.android.gms.maps.model.MapStyleOptions +import com.google.gson.Gson +import com.google.gson.JsonSyntaxException +import com.google.gson.annotations.SerializedName +import com.mapbox.mapboxsdk.maps.Style +import org.json.JSONArray +import org.json.JSONObject +import org.microg.gms.maps.mapbox.utils.MapContext +import java.io.File +import java.io.IOException +import java.lang.NumberFormatException +import kotlin.math.pow +import kotlin.math.roundToInt + +const val TAG = "GmsMapStyles" +const val KEY_METADATA_FEATURE_TYPE = "microg:gms-type-feature" +const val KEY_METADATA_ELEMENT_TYPE = "microg:gms-type-element" + +const val SELECTOR_ALL = "all" +const val SELECTOR_ELEMENT_LABEL_TEXT_FILL = "labels.text.fill" +const val SELECTOR_ELEMENT_LABEL_TEXT_STROKE = "labels.text.stroke" +const val SELECTOR_ELEMENT_GEOMETRY_STROKE = "geometry.stroke" +const val KEY_LAYER_METADATA = "metadata" +const val KEY_LAYER_PAINT = "paint" + + +fun getStyle( + context: MapContext, mapType: Int, styleOptions: MapStyleOptions?, styleFromFileWorkaround: Boolean = false +): Style.Builder { + + // TODO: Serve map style resources locally + val styleJson = JSONObject( + context.assets.open( + when (mapType) { + GoogleMap.MAP_TYPE_SATELLITE, GoogleMap.MAP_TYPE_HYBRID -> "style-microg-satellite.json" + GoogleMap.MAP_TYPE_TERRAIN -> "style-mapbox-outdoors-v12.json" + //MAP_TYPE_NONE, MAP_TYPE_NORMAL, + else -> "style-microg-normal.json" + } + ).bufferedReader().readText() + ) + + styleOptions?.apply(styleJson) + + return if (styleFromFileWorkaround) { + val temporaryFile = File(context.cacheDir, styleJson.hashCode().toString()) + + if (!temporaryFile.exists()) { + temporaryFile.createNewFile() + } + + try { + temporaryFile.bufferedWriter().use { + it.write(styleJson.toString()) + } + Log.d(TAG, "file:/${temporaryFile.absolutePath}") + Style.Builder().fromUri("file:/${temporaryFile.absolutePath}") + } catch (e: IOException) { + e.printStackTrace() + Style.Builder().fromUri(getFallbackStyleOnlineUri(mapType)) + } + } else { + Style.Builder().fromJson(styleJson.toString()) + } +} + + +fun getFallbackStyleOnlineUri(mapType: Int) = when (mapType) { + GoogleMap.MAP_TYPE_SATELLITE -> "mapbox://styles/microg/cjxgloted25ap1ct4uex7m6hi" + GoogleMap.MAP_TYPE_TERRAIN -> "mapbox://styles/mapbox/outdoors-v12" + GoogleMap.MAP_TYPE_HYBRID -> "mapbox://styles/microg/cjxgloted25ap1ct4uex7m6hi" + //MAP_TYPE_NONE, MAP_TYPE_NORMAL, + else -> "mapbox://styles/microg/cjui4020201oo1fmca7yuwbor" +} + +fun MapStyleOptions.apply(style: JSONObject) { + try { + Gson().fromJson(json, Array::class.java).let { styleOperations -> + + val layerArray = style.getJSONArray("layers") + + // Apply operations in order + operations@ for (operation in styleOperations.map { it.toNonNull() }) { + if (!operation.isValid()) { + Log.w(TAG, "Operating is invalid: $operation") + continue + } + + var applied = 0 + + // Reverse direction allows removing hidden layers + layers@ for (i in layerArray.length() - 1 downTo 0) { + + val layer = layerArray.getJSONObject(i) + if (layer.layerHasRequiredFields()) { + + if (operation.isValid() && layer.matchesOperation(operation)) { + applied++ + + if (layer.layerShouldBeRemoved(operation)) { + layerArray.removeCompat(i) + } else { + layer.applyOperation(operation) + } + } + } + } + + Log.v(TAG, "Operation applied to $applied layers: ${Gson().toJson(operation)}") + } + } + + + } catch (e: JsonSyntaxException) { + e.printStackTrace() + } +} + +data class StyleOperation(val featureType: String?, val elementType: String?, val stylers: Array?) + +data class NonNullStyleOperation(val featureType: String, val elementType: String, val stylers: Array) + +class Styler( + val hue: String?, + @FloatRange(from = -100.0, to = 100.0) val saturation: Float?, + @FloatRange(from = -100.0, to = 100.0) val lightness: Float?, + @FloatRange(from = 0.01, to = 10.0) val gamma: Float?, + @SerializedName("invert_lightness") val invertLightness: Boolean?, + val visibility: String?, + val color: String?, + //val weight: Int? +) + +/** + * Constructs a `NonNullStyleOperation` out of the `StyleOperation` while filling null fields with + * default values. + */ +fun StyleOperation.toNonNull() = + NonNullStyleOperation(featureType ?: SELECTOR_ALL, elementType ?: SELECTOR_ALL, stylers ?: emptyArray()) + +/** + * Returns false iff the operation is invalid. + * + * There is one invalid selector that is tested for – per docs: + * "`administrative` selects all administrative areas. Styling affects only + * the labels of administrative areas, not the geographical borders or fill." + */ +fun NonNullStyleOperation.isValid() = !(featureType.startsWith("administrative") && + elementType.startsWith("geometry")) + +/** + * True iff the layer represented by the JSON object should be modified according to the stylers in the operation. + * + * Layer metadata always has the most concrete category, while operation applies to all subcategories as well. + * Therefore, we test if the operation is a substring of the layer's metadata – i.e. the layer's metadata contains + * (more concretely: starts with) the operation's selector. + */ +fun JSONObject.matchesOperation(operation: NonNullStyleOperation) = + (getJSONObject(KEY_LAYER_METADATA).getString(KEY_METADATA_FEATURE_TYPE).startsWith(operation.featureType) + || operation.featureType == "all") + && (getJSONObject(KEY_LAYER_METADATA).getString(KEY_METADATA_ELEMENT_TYPE).startsWith(operation.elementType) + || getJSONObject(KEY_LAYER_METADATA).getString(KEY_METADATA_ELEMENT_TYPE) == "labels.text" && operation.elementType == SELECTOR_ELEMENT_LABEL_TEXT_FILL + || getJSONObject(KEY_LAYER_METADATA).getString(KEY_METADATA_ELEMENT_TYPE) == "labels.text" && operation.elementType == SELECTOR_ELEMENT_LABEL_TEXT_STROKE + || operation.elementType == "all") + + +/** + * Layer has fields that allow applying style operations. + */ +fun JSONObject.layerHasRequiredFields() = has(KEY_LAYER_PAINT) && has(KEY_LAYER_METADATA) && + getJSONObject(KEY_LAYER_METADATA).let { it.has(KEY_METADATA_FEATURE_TYPE) && it.has(KEY_METADATA_ELEMENT_TYPE) } + +/** + * True iff the layer represented by the JSON object should be removed according to the provided style operation. + */ +fun JSONObject.layerShouldBeRemoved(operation: NonNullStyleOperation) = + // A styler sets the layer to be invisible + operation.stylers.any { it.visibility == "off" } || + // A styler sets the layer to simplified and we are working with a label + (getJSONObject(KEY_LAYER_METADATA).getString(KEY_METADATA_ELEMENT_TYPE) == SELECTOR_ELEMENT_GEOMETRY_STROKE && operation.stylers.any { it.visibility == "simplified" }) + +/** + * Applies the provided style operation to the layer represented by the JSON object. + */ +fun JSONObject.applyOperation(operation: NonNullStyleOperation) = operation.stylers.forEach { styler -> + val paintJson = getJSONObject(KEY_LAYER_PAINT) + val metadataJson = getJSONObject(KEY_LAYER_METADATA) + if (styler.isColorChange()) { + when (operation.elementType) { + SELECTOR_ELEMENT_LABEL_TEXT_FILL -> styler.applyTextFill(paintJson) + SELECTOR_ELEMENT_LABEL_TEXT_STROKE -> styler.applyTextOutline(paintJson) + else -> styler.traverse(paintJson) + } + } + if (styler.visibility == "simplified" && metadataJson.getString(KEY_METADATA_ELEMENT_TYPE) == "labels.text") { + paintJson.remove("text-halo-blur") + paintJson.remove("text-halo-color") + } +} + +/** + * Returns true if string is likely to contain a color. + */ +fun String.isColor() = ((startsWith("hsl(") || startsWith("hsla(") || startsWith("rgba(")) && endsWith(")")) || startsWith("#") + +/** + * Can parse colors in the format '#rrggbb', '#aarrggbb', 'hsl(h, s, l)', and 'rgba(r, g, b, a)' + * Returns 0 and prints to log if an invalid color is provided. + */ +@ColorInt +fun String.parseColor(): Int { + if (startsWith("#") && length in listOf(7, 9)) { + return Color.parseColor(this) + } else if (startsWith("hsl(")) { + val hslArray = replace("hsl(", "").replace(")", "").split(", ") + if (hslArray.size != 3) { + Log.w(TAG, "Invalid color `$this`") + return 0 + } + + return try { + ColorUtils.HSLToColor( + floatArrayOf( + hslArray[0].toFloat(), + hslArray[1].parseFloat(), + hslArray[2].parseFloat() + ) + ) + } catch (e: NumberFormatException) { + Log.w(TAG, "Invalid color `$this`") + 0 + } + } else if (startsWith("hsla(")) { + val hslArray = replace("hsla(", "").replace(")", "").split(", ") + if (hslArray.size != 4) { + Log.w(TAG, "Invalid color `$this`") + return 0 + } + + return try { + ColorUtils.setAlphaComponent( + ColorUtils.HSLToColor( + floatArrayOf( + hslArray[0].toFloat(), hslArray[1].parseFloat(), hslArray[2].parseFloat() + ) + ), (hslArray[3].parseFloat() * 255).roundToInt() + ) + } catch (e: NumberFormatException) { + Log.w(TAG, "Invalid color `$this`") + 0 + } + + } else if (startsWith("rgba(")) { + return com.mapbox.mapboxsdk.utils.ColorUtils.rgbaToColor(this) + } + + Log.w(TAG, "Invalid color `$this`") + return 0 +} + +/** + * Formats color int in such a format that it MapLibre's rendering engine understands it. + */ +fun Int.colorToString() = com.mapbox.mapboxsdk.utils.ColorUtils.colorToRgbaString(this) + +/** + * Can parse string values that contain '%'. + */ +fun String.parseFloat(): Float { + return if (contains("%")) { + replace("%", "").toFloat() / 100f + } else { + toFloat() + } +} + +/** + * Applies operation specified by styler to the provided color int, and returns + * a new, corresponding color int. + */ +@ColorInt +fun Styler.applyColorChanges(color: Int): Int { + // There may only be one operation per styler per docs. + + hue?.let { hue -> + // Extract hue from input color + val hslResult = FloatArray(3) + ColorUtils.colorToHSL(hue.parseColor(), hslResult) + + val hueDegree = hslResult[0] + + // Apply hue to layer color + ColorUtils.colorToHSL(color, hslResult) + hslResult[0] = hueDegree + return ColorUtils.HSLToColor(hslResult) + } + + lightness?.let { lightness -> + // Apply lightness to layer color + val hsl = FloatArray(3) + ColorUtils.colorToHSL(color, hsl) + hsl[2] = if (lightness < 0) { + // Increase darkness. Percentage amount = relative reduction of is-lightness. + (lightness / 100 + 1) * hsl[2] + } else { + // Increase brightness. Percentage amount = relative reduction of difference between is-lightness and 1.0. + hsl[2] + (lightness / 100) * (1 - hsl[2]) + } + return ColorUtils.HSLToColor(hsl) + } + + saturation?.let { saturation -> + // Apply saturation to layer color + val hsl = FloatArray(3) + ColorUtils.colorToHSL(color, hsl) + hsl[1] = if (saturation < 0) { + // Reduce intensity. Percentage amount = relative reduction of is-saturation. + (saturation / 100 + 1) * hsl[1] + } else { + // Increase intensity. Percentage amount = relative reduction of difference between is-saturation and 1.0. + hsl[1] + (saturation / 100) * (1 - hsl[1]) + } + + return ColorUtils.HSLToColor(hsl) + } + + gamma?.let { gamma -> + // Apply gamma to layer color + val hsl = FloatArray(3) + ColorUtils.colorToHSL(color, hsl) + hsl[2] = hsl[2].toDouble().pow(gamma.toDouble()).toFloat() + + return ColorUtils.HSLToColor(hsl) + } + + if (invertLightness == true) { + // Invert layer color's lightness + val hsl = FloatArray(3) + ColorUtils.colorToHSL(color, hsl) + hsl[2] = 1 - hsl[2] + + return ColorUtils.HSLToColor(hsl) + } + + this.color?.let { + return it.parseColor() + } + + Log.w(TAG, "No applicable operation") + return color +} + +fun Styler.isColorChange(): Boolean = hue != null || lightness!= null || saturation != null || gamma != null || invertLightness != null || color != null + +/** + * Traverse JSON object and replace any color strings according to styler + */ +fun Styler.traverse(json: JSONObject) { + // Traverse layer and replace any color strings + json.keys().forEach { key -> + json.get(key).let { + when (it) { + is JSONObject -> traverse(it) + is JSONArray -> traverse(it) + is String -> if (it.isColor()) { + json.put(key, applyColorChanges(it.parseColor()).colorToString()) + } + } + } + } +} + +/** + * Traverse array and replace any color strings according to styler + */ +fun Styler.traverse(array: JSONArray) { + for (i in 0 until array.length()) { + array.get(i).let { + when (it) { + is JSONObject -> traverse(it) + is JSONArray -> traverse(it) + is String -> if (it.isColor()) { + array.put(i, applyColorChanges(it.parseColor()).colorToString()) + } + } + } + } +} + +fun Styler.applyTextFill(paint: JSONObject) { + if (paint.has("text-color")) when (val textColor = paint.get("text-color")) { + is JSONObject -> traverse(textColor) + is JSONArray -> traverse(textColor) + is String -> paint.put("text-color", applyColorChanges(textColor.parseColor()).colorToString()) + } +} + +fun Styler.applyTextOutline(paint: JSONObject) { + if (paint.has("text-halo-color")) when (val textOutline = paint.get("text-halo-color")) { + is JSONObject -> traverse(textOutline) + is JSONArray -> traverse(textOutline) + is String -> paint.put("text-halo-color", applyColorChanges(textOutline.parseColor()).colorToString()) + } +} + +fun JSONArray.removeCompat(index: Int) = + if (SDK_INT >= 19) { + remove(index) + this + } else { + val field = JSONArray::class.java.getDeclaredField("values") + field.isAccessible = true + val list = field.get(this) as MutableList<*> + list.removeAt(index) + this + } \ No newline at end of file diff --git a/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/UiSettings.kt b/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/UiSettings.kt index 32320eb448..e6ab10cf5f 100644 --- a/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/UiSettings.kt +++ b/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/UiSettings.kt @@ -17,45 +17,22 @@ package org.microg.gms.maps.mapbox import android.os.Parcel -import android.os.RemoteException import android.util.Log import com.google.android.gms.maps.internal.IUiSettingsDelegate +import com.mapbox.mapboxsdk.maps.OnMapReadyCallback import com.mapbox.mapboxsdk.maps.UiSettings -class UiSettingsImpl(private val uiSettings: UiSettings) : IUiSettingsDelegate.Stub() { - +/** + * This class "implements" unimplemented methods to avoid duplication in subclasses + */ +abstract class AbstractUiSettings : IUiSettingsDelegate.Stub() { override fun setZoomControlsEnabled(zoom: Boolean) { Log.d(TAG, "unimplemented Method: setZoomControlsEnabled") } - override fun setCompassEnabled(compass: Boolean) { - uiSettings.isCompassEnabled = compass - } - override fun setMyLocationButtonEnabled(locationButton: Boolean) { Log.d(TAG, "unimplemented Method: setMyLocationButtonEnabled") - - } - - override fun setScrollGesturesEnabled(scrollGestures: Boolean) { - uiSettings.isScrollGesturesEnabled = scrollGestures - } - - override fun setZoomGesturesEnabled(zoomGestures: Boolean) { - uiSettings.isZoomGesturesEnabled = zoomGestures - } - - override fun setTiltGesturesEnabled(tiltGestures: Boolean) { - uiSettings.isTiltGesturesEnabled = tiltGestures - } - - override fun setRotateGesturesEnabled(rotateGestures: Boolean) { - uiSettings.isRotateGesturesEnabled = rotateGestures - } - - override fun setAllGesturesEnabled(gestures: Boolean) { - uiSettings.setAllGesturesEnabled(gestures) } override fun isZoomControlsEnabled(): Boolean { @@ -63,21 +40,11 @@ class UiSettingsImpl(private val uiSettings: UiSettings) : IUiSettingsDelegate.S return false } - override fun isCompassEnabled(): Boolean = uiSettings.isCompassEnabled - override fun isMyLocationButtonEnabled(): Boolean { Log.d(TAG, "unimplemented Method: isMyLocationButtonEnabled") return false } - override fun isScrollGesturesEnabled(): Boolean = uiSettings.isScrollGesturesEnabled - - override fun isZoomGesturesEnabled(): Boolean = uiSettings.isZoomGesturesEnabled - - override fun isTiltGesturesEnabled(): Boolean = uiSettings.isTiltGesturesEnabled - - override fun isRotateGesturesEnabled(): Boolean = uiSettings.isRotateGesturesEnabled - override fun setIndoorLevelPickerEnabled(indoorLevelPicker: Boolean) { Log.d(TAG, "unimplemented Method: setIndoorLevelPickerEnabled") } @@ -105,6 +72,48 @@ class UiSettingsImpl(private val uiSettings: UiSettings) : IUiSettingsDelegate.S return true } + companion object { + private val TAG = "GmsMapsUi" + } +} + +class UiSettingsImpl(private val uiSettings: UiSettings) : AbstractUiSettings() { + + + override fun setCompassEnabled(compass: Boolean) { + uiSettings.isCompassEnabled = compass + } + + override fun setScrollGesturesEnabled(scrollGestures: Boolean) { + uiSettings.isScrollGesturesEnabled = scrollGestures + } + + override fun setZoomGesturesEnabled(zoomGestures: Boolean) { + uiSettings.isZoomGesturesEnabled = zoomGestures + } + + override fun setTiltGesturesEnabled(tiltGestures: Boolean) { + uiSettings.isTiltGesturesEnabled = tiltGestures + } + + override fun setRotateGesturesEnabled(rotateGestures: Boolean) { + uiSettings.isRotateGesturesEnabled = rotateGestures + } + + override fun setAllGesturesEnabled(gestures: Boolean) { + uiSettings.setAllGesturesEnabled(gestures) + } + + override fun isCompassEnabled(): Boolean = uiSettings.isCompassEnabled + + override fun isScrollGesturesEnabled(): Boolean = uiSettings.isScrollGesturesEnabled + + override fun isZoomGesturesEnabled(): Boolean = uiSettings.isZoomGesturesEnabled + + override fun isTiltGesturesEnabled(): Boolean = uiSettings.isTiltGesturesEnabled + + override fun isRotateGesturesEnabled(): Boolean = uiSettings.isRotateGesturesEnabled + override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean = if (super.onTransact(code, data, reply, flags)) { true @@ -113,6 +122,80 @@ class UiSettingsImpl(private val uiSettings: UiSettings) : IUiSettingsDelegate.S } companion object { - private val TAG = "GmsMapsUi" + private val TAG = "GmsMapsUiImpl" } } + +class UiSettingsCache : AbstractUiSettings() { + + private var compass: Boolean? = null + private var scrollGestures: Boolean? = null + private var zoomGestures: Boolean? = null + private var tiltGestures: Boolean? = null + private var rotateGestures: Boolean? = null + private var otherGestures: Boolean? = null + + override fun setCompassEnabled(compass: Boolean) { + this.compass = compass + } + + override fun setScrollGesturesEnabled(scrollGestures: Boolean) { + this.scrollGestures = scrollGestures + } + + override fun setZoomGesturesEnabled(zoomGestures: Boolean) { + this.zoomGestures = zoomGestures + } + + override fun setTiltGesturesEnabled(tiltGestures: Boolean) { + this.tiltGestures = tiltGestures + } + + override fun setRotateGesturesEnabled(rotateGestures: Boolean) { + this.rotateGestures = rotateGestures + } + + override fun setAllGesturesEnabled(gestures: Boolean) { + // Simulate MapLibre's UiSettings behavior + isScrollGesturesEnabled = gestures + isRotateGesturesEnabled = gestures + isTiltGesturesEnabled = gestures + isZoomGesturesEnabled = gestures + + // Other gestures toggles double tap and quick zoom gestures + otherGestures = gestures + } + + override fun isCompassEnabled(): Boolean { + return compass ?: true + } + + override fun isScrollGesturesEnabled(): Boolean { + return scrollGestures ?: true + } + + override fun isZoomGesturesEnabled(): Boolean { + return zoomGestures ?: true + } + + override fun isTiltGesturesEnabled(): Boolean { + return tiltGestures ?: true + } + + override fun isRotateGesturesEnabled(): Boolean { + return rotateGestures ?: true + } + + fun getMapReadyCallback(): OnMapReadyCallback = OnMapReadyCallback { map -> + val uiSettings = map.uiSettings + compass?.let { uiSettings.isCompassEnabled = it } + scrollGestures?.let { uiSettings.isScrollGesturesEnabled = it } + zoomGestures?.let { uiSettings.isZoomGesturesEnabled = it } + tiltGestures?.let { uiSettings.isTiltGesturesEnabled = it } + rotateGestures?.let { uiSettings.isRotateGesturesEnabled = it } + otherGestures?.let { + uiSettings.isDoubleTapGesturesEnabled = it + uiSettings.isQuickZoomGesturesEnabled = it + } + } +} \ No newline at end of file diff --git a/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/BitmapDescriptor.kt b/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/BitmapDescriptor.kt index e228387da7..0fa9a720c7 100644 --- a/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/BitmapDescriptor.kt +++ b/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/BitmapDescriptor.kt @@ -22,9 +22,11 @@ import android.util.Log import com.mapbox.mapboxsdk.plugins.annotation.Symbol import com.mapbox.mapboxsdk.plugins.annotation.SymbolOptions import com.mapbox.mapboxsdk.style.layers.Property.ICON_ANCHOR_TOP_LEFT +import com.mapbox.mapboxsdk.style.layers.PropertyFactory +import com.mapbox.mapboxsdk.style.layers.SymbolLayer import com.mapbox.mapboxsdk.utils.ColorUtils -open class BitmapDescriptorImpl(private val id: String, private val size: FloatArray) { +open class BitmapDescriptorImpl(val id: String, internal val size: FloatArray) { open fun applyTo(options: SymbolOptions, anchor: FloatArray, dpiFactor: Float): SymbolOptions { return options.withIconImage(id).withIconAnchor(ICON_ANCHOR_TOP_LEFT).withIconOffset(arrayOf(-anchor[0] * size[0] / dpiFactor, -anchor[1] * size[1] / dpiFactor)) } @@ -34,6 +36,22 @@ open class BitmapDescriptorImpl(private val id: String, private val size: FloatA symbol.iconOffset = PointF(-anchor[0] * size[0] / dpiFactor, -anchor[1] * size[1] / dpiFactor) symbol.iconImage = id } + + open fun applyTo(symbolLayer: SymbolLayer, anchor: FloatArray, dpiFactor: Float) { + symbolLayer.withProperties( + PropertyFactory.iconAnchor(ICON_ANCHOR_TOP_LEFT), + PropertyFactory.iconOffset(arrayOf(-anchor[0] * size[0] / dpiFactor, -anchor[1] * size[1] / dpiFactor)), + PropertyFactory.iconImage(id) + ) + } + + protected fun finalize() { + BitmapDescriptorFactoryImpl.disposeDescriptor(id) + } + + override fun toString(): String { + return "[BitmapDescriptor $id]" + } } class ColorBitmapDescriptorImpl(id: String, size: FloatArray, val hue: Float) : BitmapDescriptorImpl(id, size) { diff --git a/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/BitmapDescriptorFactory.kt b/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/BitmapDescriptorFactory.kt index f4f5174814..6e46de389f 100644 --- a/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/BitmapDescriptorFactory.kt +++ b/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/BitmapDescriptorFactory.kt @@ -18,14 +18,13 @@ package org.microg.gms.maps.mapbox.model import android.content.res.Resources import android.graphics.* -import android.os.Handler -import android.os.Looper import android.os.Parcel import android.util.Log import com.google.android.gms.dynamic.IObjectWrapper import com.google.android.gms.dynamic.ObjectWrapper import com.google.android.gms.maps.model.internal.IBitmapDescriptorFactoryDelegate import com.mapbox.mapboxsdk.maps.MapboxMap +import com.mapbox.mapboxsdk.maps.Style import org.microg.gms.maps.mapbox.R import org.microg.gms.maps.mapbox.runOnMainLooper @@ -36,6 +35,7 @@ object BitmapDescriptorFactoryImpl : IBitmapDescriptorFactoryDelegate.Stub() { private var mapResources: Resources? = null private val maps = hashSetOf() private val bitmaps = hashMapOf() + private val refCount = hashMapOf() fun initialize(mapResources: Resources?, resources: Resources?) { BitmapDescriptorFactoryImpl.mapResources = mapResources ?: resources @@ -54,65 +54,105 @@ object BitmapDescriptorFactoryImpl : IBitmapDescriptorFactoryDelegate.Stub() { fun unregisterMap(map: MapboxMap?) { maps.remove(map) - // TODO: cleanup bitmaps? + } + + fun put(style: Style.Builder) { + synchronized(bitmaps) { + for (bitmap in bitmaps) { + style.withImage(bitmap.key, bitmap.value) + } + } } fun bitmapSize(id: String): FloatArray = - bitmaps[id]?.let { floatArrayOf(it.width.toFloat(), it.height.toFloat()) } - ?: floatArrayOf(0f, 0f) + bitmaps[id]?.let { floatArrayOf(it.width.toFloat(), it.height.toFloat()) } + ?: floatArrayOf(0f, 0f) + + fun disposeDescriptor(id: String) { + synchronized(refCount) { + if (refCount.containsKey(id)) { + val old = refCount[id]!! + if (old > 1) { + refCount[id] = old - 1; + return + } + } + } + unregisterBitmap(id) + } + + private fun unregisterBitmap(id: String) { + synchronized(bitmaps) { + if (!bitmaps.containsKey(id)) return + bitmaps.remove(id) + } + + for (map in maps) { + map.getStyle { + runOnMainLooper { + try { + it.removeImage(id) + } catch (e: Exception) { + Log.w(TAG, e) + } + } + } + } - private fun registerBitmap(id: String, bitmapCreator: () -> Bitmap?) { - val bitmap: Bitmap = synchronized(bitmaps) { - if (bitmaps.contains(id)) return + refCount.remove(id) + } + + private fun registerBitmap(id: String, descriptorCreator: (id: String, size: FloatArray) -> BitmapDescriptorImpl = { id, size -> BitmapDescriptorImpl(id, size) }, bitmapCreator: () -> Bitmap?): IObjectWrapper { + val bitmap: Bitmap? = synchronized(bitmaps) { + if (bitmaps.contains(id)) return@synchronized null val bitmap = bitmapCreator() if (bitmap == null) { Log.w(TAG, "Failed to register bitmap $id, creator returned null") - return + return@synchronized null } bitmaps[id] = bitmap bitmap } - for (map in maps) { - map.getStyle { - runOnMainLooper { - it.addImage(id, bitmap) + + if (bitmap != null) { + for (map in maps) { + map.getStyle { + runOnMainLooper { + it.addImage(id, bitmap) + } } } } + + synchronized(refCount) { + refCount[id] = (refCount[id] ?: 0) + 1 + } + + return ObjectWrapper.wrap(descriptorCreator(id, bitmapSize(id))) } - override fun fromResource(resourceId: Int): IObjectWrapper? { - val id = "resource-$resourceId" - registerBitmap(id) { - val bitmap = BitmapFactory.decodeResource(resources, resourceId) - if (bitmap == null) { - try { - Log.d(TAG, "Resource $resourceId not found in $resources (${resources?.getResourceName(resourceId)})") - } catch (e: Resources.NotFoundException) { - Log.d(TAG, "Resource $resourceId not found in $resources") - } + override fun fromResource(resourceId: Int): IObjectWrapper = registerBitmap("resource-$resourceId") { + val bitmap = BitmapFactory.decodeResource(resources, resourceId) + if (bitmap == null) { + try { + Log.d(TAG, "Resource $resourceId not found in $resources (${resources?.getResourceName(resourceId)})") + } catch (e: Resources.NotFoundException) { + Log.d(TAG, "Resource $resourceId not found in $resources") } - bitmap } - return ObjectWrapper.wrap(BitmapDescriptorImpl(id, bitmapSize(id))) + bitmap } - override fun fromAsset(assetName: String): IObjectWrapper? { - val id = "asset-$assetName" - registerBitmap(id) { resources?.assets?.open(assetName)?.let { BitmapFactory.decodeStream(it) } } - return ObjectWrapper.wrap(BitmapDescriptorImpl(id, bitmapSize(id))) + override fun fromAsset(assetName: String): IObjectWrapper = registerBitmap("asset-$assetName") { + resources?.assets?.open(assetName)?.let { BitmapFactory.decodeStream(it) } } - override fun fromFile(fileName: String): IObjectWrapper? { - val id = "file-$fileName" - registerBitmap(id) { BitmapFactory.decodeFile(fileName) } - return ObjectWrapper.wrap(BitmapDescriptorImpl(id, bitmapSize(id))) + override fun fromFile(fileName: String): IObjectWrapper = registerBitmap("file-$fileName") { + BitmapFactory.decodeFile(fileName) } - override fun defaultMarker(): IObjectWrapper? { - val id = "marker" - registerBitmap(id) { BitmapFactory.decodeResource(mapResources, R.drawable.maps_default_marker) } - return ObjectWrapper.wrap(BitmapDescriptorImpl(id, bitmapSize(id))) + override fun defaultMarker(): IObjectWrapper = registerBitmap("marker") { + BitmapFactory.decodeResource(mapResources, R.drawable.maps_default_marker) } private fun adjustHue(cm: ColorMatrix, value: Float) { @@ -135,8 +175,7 @@ object BitmapDescriptorFactoryImpl : IBitmapDescriptorFactoryDelegate.Stub() { } override fun defaultMarkerWithHue(hue: Float): IObjectWrapper? { - val id = "marker-${hue.toInt()}" - registerBitmap(id) { + return registerBitmap("marker-${hue.toInt()}", { id, size -> ColorBitmapDescriptorImpl(id, size, hue) }) { val bitmap = BitmapFactory.decodeResource(mapResources, R.drawable.maps_default_marker).copy(Bitmap.Config.ARGB_8888, true) val paint = Paint() val matrix = ColorMatrix() @@ -148,25 +187,16 @@ object BitmapDescriptorFactoryImpl : IBitmapDescriptorFactoryDelegate.Stub() { canvas.drawBitmap(bitmap, 0f, 0f, paint) bitmap } - return ObjectWrapper.wrap(ColorBitmapDescriptorImpl(id, bitmapSize(id), hue)) } - override fun fromBitmap(bitmap: Bitmap): IObjectWrapper? { - val id = "bitmap-${bitmap.hashCode()}" - registerBitmap(id) { bitmap } - return ObjectWrapper.wrap(BitmapDescriptorImpl(id, bitmapSize(id))) - } + override fun fromBitmap(bitmap: Bitmap): IObjectWrapper = registerBitmap("bitmap-${bitmap.hashCode()}") { bitmap } - override fun fromPath(absolutePath: String): IObjectWrapper? { - val id = "path-$absolutePath" - registerBitmap(id) { BitmapFactory.decodeFile(absolutePath) } - return ObjectWrapper.wrap(BitmapDescriptorImpl(id, bitmapSize(id))) - } + override fun fromPath(absolutePath: String): IObjectWrapper = registerBitmap("path-$absolutePath") { BitmapFactory.decodeFile(absolutePath) } override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean = - if (super.onTransact(code, data, reply, flags)) { - true - } else { - Log.d(TAG, "onTransact [unknown]: $code, $data, $flags"); false - } + if (super.onTransact(code, data, reply, flags)) { + true + } else { + Log.d(TAG, "onTransact [unknown]: $code, $data, $flags"); false + } } diff --git a/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/Circle.kt b/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/Circle.kt index aa016ea800..5cc551212d 100644 --- a/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/Circle.kt +++ b/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/Circle.kt @@ -18,78 +18,172 @@ package org.microg.gms.maps.mapbox.model import android.os.Parcel import android.util.Log +import com.google.android.gms.dynamic.IObjectWrapper +import com.google.android.gms.dynamic.ObjectWrapper +import com.google.android.gms.dynamic.unwrap +import com.google.android.gms.maps.model.Dash import com.google.android.gms.maps.model.LatLng +import com.google.android.gms.maps.model.PatternItem import com.google.android.gms.maps.model.internal.ICircleDelegate -import com.mapbox.mapboxsdk.plugins.annotation.Circle -import com.mapbox.mapboxsdk.plugins.annotation.CircleOptions +import com.mapbox.geojson.LineString +import com.mapbox.geojson.Point +import com.mapbox.mapboxsdk.plugins.annotation.* import com.mapbox.mapboxsdk.utils.ColorUtils +import com.mapbox.turf.TurfConstants +import com.mapbox.turf.TurfConstants.UNIT_METERS +import com.mapbox.turf.TurfMeasurement +import com.mapbox.turf.TurfMeta +import com.mapbox.turf.TurfTransformation import org.microg.gms.maps.mapbox.GoogleMapImpl -import org.microg.gms.maps.mapbox.utils.toMapbox +import org.microg.gms.maps.mapbox.LiteGoogleMapImpl +import org.microg.gms.maps.mapbox.utils.toPoint +import org.microg.gms.maps.mapbox.getName +import org.microg.gms.maps.mapbox.makeBitmap import com.google.android.gms.maps.model.CircleOptions as GmsCircleOptions -class CircleImpl(private val map: GoogleMapImpl, private val id: String, options: GmsCircleOptions) : ICircleDelegate.Stub(), Markup { - private var center: LatLng = options.center - private var radius: Double = options.radius - private var strokeWidth: Float = options.strokeWidth - private var strokeColor: Int = options.strokeColor - private var fillColor: Int = options.fillColor - private var visible: Boolean = options.isVisible +val NORTH_POLE: Point = Point.fromLngLat(0.0, 90.0) +val SOUTH_POLE: Point = Point.fromLngLat(0.0, -90.0) - override var annotation: Circle? = null - override var removed: Boolean = false - override val annotationOptions: CircleOptions - get() = CircleOptions() - .withLatLng(center.toMapbox()) - .withCircleColor(ColorUtils.colorToRgbaString(fillColor)) - .withCircleRadius(radius.toFloat()) - .withCircleStrokeColor(ColorUtils.colorToRgbaString(strokeColor)) - .withCircleStrokeWidth(strokeWidth / map.dpiFactor) - .withCircleOpacity(if (visible) 1f else 0f) - .withCircleStrokeOpacity(if (visible) 1f else 0f) +/** + * Amount of points to be used in the polygon that approximates the circle. + */ +const val CIRCLE_POLYGON_STEPS = 256 - override fun remove() { - removed = true - map.circleManager?.let { update(it) } +abstract class AbstractCircle( + private val id: String, options: GmsCircleOptions, private val dpiFactor: Function0 +) : ICircleDelegate.Stub() { + + internal var center: LatLng = options.center + internal var radiusInMeters: Double = options.radius // unlike MapLibre's circles, which only work with pixel radii + internal var strokeWidth: Float = options.strokeWidth + internal var strokeColor: Int = options.strokeColor + internal var fillColor: Int = options.fillColor + internal var visible: Boolean = options.isVisible + internal var clickable: Boolean = options.isClickable + internal var strokePattern: MutableList? = options.strokePattern + internal var tag: Any? = null + + internal val line: Markup = object : Markup { + override var annotation: Line? = null + override val annotationOptions: LineOptions + get() = LineOptions() + .withGeometry( + LineString.fromLngLats( + makeOutlineLatLngs() + ) + ).withLineWidth(strokeWidth / dpiFactor()) + .withLineColor(ColorUtils.colorToRgbaString(strokeColor)) + .withLineOpacity(if (visible) 1f else 0f) + .apply { + strokePattern?.let { + withLinePattern(it.getName(strokeColor, strokeWidth)) + } + } + + override var removed: Boolean = false + } + + val annotationOptions: FillOptions + get() = + FillOptions() + .withGeometry(makePolygon()) + .withFillColor(ColorUtils.colorToRgbaString(fillColor)) + .withFillOpacity(if (visible && !wrapsAroundPoles()) 1f else 0f) + + internal abstract fun update() + + internal fun makePolygon() = TurfTransformation.circle( + Point.fromLngLat(center.longitude, center.latitude), radiusInMeters, CIRCLE_POLYGON_STEPS, TurfConstants.UNIT_METERS + ) + + /** + * Google's "map renderer is unable to draw the circle fill if the circle encompasses + * either the North or South pole" (though it does so incorrectly anyway) + */ + internal fun wrapsAroundPoles() = center.toPoint().let { + TurfMeasurement.distance( + it, NORTH_POLE, UNIT_METERS + ) < radiusInMeters || TurfMeasurement.distance( + it, SOUTH_POLE, UNIT_METERS + ) < radiusInMeters + } + + internal fun makeOutlineLatLngs(): MutableList { + val pointList = TurfMeta.coordAll( + makePolygon(), wrapsAroundPoles() + ) + // Circles around the poles are tricky to draw (https://github.com/mapbox/mapbox-gl-js/issues/11235). + // We modify our lines such to match the way Mapbox / MapLibre draws them. + // This results in a small gap somewhere in the line, but avoids an incorrect horizontal line. + + val centerPoint = center.toPoint() + + if (!centerPoint.equals(NORTH_POLE) && TurfMeasurement.distance(centerPoint, NORTH_POLE, UNIT_METERS) < radiusInMeters) { + // Wraps around North Pole + for (i in 0 until pointList.size) { + // We want to have the north-most points at the start and end + if (pointList[0].latitude() > pointList[1].latitude() && pointList[pointList.size - 1].latitude() > pointList[pointList.size - 2].latitude()) { + return pointList + } else { + // Cycle point list + val zero = pointList.removeFirst() + pointList.add(zero) + } + } + } + + if (!centerPoint.equals(SOUTH_POLE) && TurfMeasurement.distance(centerPoint, SOUTH_POLE, UNIT_METERS) < radiusInMeters) { + // Wraps around South Pole + for (i in 0 until pointList.size) { + // We want to have the south-most points at the start and end + if (pointList[0].latitude() < pointList[1].latitude() && pointList[pointList.size - 1].latitude() < pointList[pointList.size - 2].latitude()) { + return pointList + } else { + // Cycle point list + val last = pointList.removeAt(pointList.size - 1) + pointList.add(0, last) + } + } + } + + // In this case no changes were made + return pointList } override fun getId(): String = id override fun setCenter(center: LatLng) { this.center = center - annotation?.latLng = center.toMapbox() - map.circleManager?.let { update(it) } + update() + } override fun getCenter(): LatLng = center override fun setRadius(radius: Double) { - this.radius = radius - annotation?.circleRadius = radius.toFloat() - map.circleManager?.let { update(it) } + this.radiusInMeters = radius + update() } - override fun getRadius(): Double = radius + override fun getRadius(): Double = radiusInMeters override fun setStrokeWidth(width: Float) { this.strokeWidth = width - annotation?.circleStrokeWidth = width / map.dpiFactor - map.circleManager?.let { update(it) } + update() } override fun getStrokeWidth(): Float = strokeWidth override fun setStrokeColor(color: Int) { this.strokeColor = color - annotation?.setCircleStrokeColor(color) - map.circleManager?.let { update(it) } + update() } override fun getStrokeColor(): Int = strokeColor override fun setFillColor(color: Int) { this.fillColor = color - annotation?.setCircleColor(color) - map.circleManager?.let { update(it) } + update() } override fun getFillColor(): Int = fillColor @@ -105,9 +199,7 @@ class CircleImpl(private val map: GoogleMapImpl, private val id: String, options override fun setVisible(visible: Boolean) { this.visible = visible - annotation?.circleOpacity = if (visible) 1f else 0f - annotation?.circleStrokeOpacity = if (visible) 1f else 0f - map.circleManager?.let { update(it) } + update() } override fun isVisible(): Boolean = visible @@ -116,6 +208,30 @@ class CircleImpl(private val map: GoogleMapImpl, private val id: String, options override fun hashCodeRemote(): Int = hashCode() + override fun setClickable(clickable: Boolean) { + this.clickable = clickable + } + + override fun isClickable(): Boolean { + return clickable + } + + override fun setStrokePattern(pattern: MutableList?) { + this.strokePattern = pattern + update() + } + + + override fun getStrokePattern(): MutableList? { + return strokePattern + } + + override fun setTag(o: IObjectWrapper) { + this.tag = o.unwrap() + } + + override fun getTag(): IObjectWrapper = ObjectWrapper.wrap(tag) + override fun hashCode(): Int { return id.hashCode() } @@ -132,13 +248,92 @@ class CircleImpl(private val map: GoogleMapImpl, private val id: String, options } override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean = - if (super.onTransact(code, data, reply, flags)) { - true - } else { - Log.d(TAG, "onTransact [unknown]: $code, $data, $flags"); false + if (super.onTransact(code, data, reply, flags)) { + true + } else { + Log.d(TAG, "onTransact [unknown]: $code, $data, $flags"); false + } + + companion object { + val TAG = "GmsMapAbstractCircle" + } +} + +class CircleImpl(private val map: GoogleMapImpl, private val id: String, options: GmsCircleOptions) : + AbstractCircle(id, options, { map.dpiFactor }), Markup { + + override var annotation: Fill? = null + override var removed: Boolean = false + + override fun update() { + val polygon = makePolygon() + + // Extracts points from generated polygon in expected format + annotation?.let { + it.latLngs = FillOptions().withGeometry(polygon).latLngs + it.setFillColor(fillColor) + it.fillOpacity = if (visible && !wrapsAroundPoles()) 1f else 0f + } + + line.annotation?.let { + it.latLngs = makeOutlineLatLngs().map { point -> + com.mapbox.mapboxsdk.geometry.LatLng( + point.latitude(), + point.longitude() + ) + } + + it.lineWidth = strokeWidth / map.dpiFactor + + (strokePattern ?: emptyList()).let { pattern -> + val bitmapName = pattern.getName(strokeColor, strokeWidth) + map.addBitmap(bitmapName, pattern.makeBitmap(strokeColor, strokeWidth)) + line.annotation?.linePattern = bitmapName + } + map.lineManager?.let { line.update(it) } + + it.setLineColor(strokeColor) + } + + map.fillManager?.let { update(it) } + map.lineManager?.let { line.update(it) } + } + + override fun remove() { + removed = true + line.removed = true + map.fillManager?.let { update(it) } + map.lineManager?.let { line.update(it) } + } + + + override fun update(manager: AnnotationManager<*, Fill, FillOptions, *, *, *>) { + synchronized(this) { + val id = annotation?.id + if (removed && id != null) { + map.circles.remove(id) + } + super.update(manager) + val annotation = annotation + if (annotation != null && id == null) { + map.circles[annotation.id] = this } + } + } companion object { val TAG = "GmsMapCircle" } +} + +class LiteCircleImpl(private val map: LiteGoogleMapImpl, id: String, options: GmsCircleOptions) : + AbstractCircle(id, options, { map.dpiFactor }) { + override fun update() { + map.postUpdateSnapshot() + } + + override fun remove() { + map.circles.remove(this) + } + } \ No newline at end of file diff --git a/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/GroundOverlay.kt b/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/GroundOverlay.kt index 1dbab9512c..0c99d4dd4a 100644 --- a/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/GroundOverlay.kt +++ b/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/GroundOverlay.kt @@ -8,11 +8,14 @@ package org.microg.gms.maps.mapbox.model import android.os.Parcel import android.util.Log import com.google.android.gms.dynamic.IObjectWrapper +import com.google.android.gms.dynamic.ObjectWrapper +import com.google.android.gms.dynamic.unwrap import com.google.android.gms.maps.model.GroundOverlayOptions import com.google.android.gms.maps.model.LatLng import com.google.android.gms.maps.model.LatLngBounds import com.google.android.gms.maps.model.internal.IGroundOverlayDelegate import org.microg.gms.maps.mapbox.GoogleMapImpl +import org.microg.gms.utils.warnOnTransactionIssues class GroundOverlayImpl(private val map: GoogleMapImpl, private val id: String, options: GroundOverlayOptions) : IGroundOverlayDelegate.Stub() { private var location: LatLng? = options.location @@ -23,6 +26,8 @@ class GroundOverlayImpl(private val map: GoogleMapImpl, private val id: String, private var zIndex: Float = options.zIndex private var visible: Boolean = options.isVisible private var transparency: Float = options.transparency + private var clickable: Boolean = options.isClickable + private var tag: Any? = null override fun getId(): String { return id @@ -93,6 +98,24 @@ class GroundOverlayImpl(private val map: GoogleMapImpl, private val id: String, this.bounds = bounds } + override fun setClickable(clickable: Boolean) { + this.clickable = clickable + } + + override fun isClickable(): Boolean { + return clickable + } + + override fun setImage(img: IObjectWrapper?) { + Log.d(TAG, "Not yet implemented: setImage") + } + + override fun setTag(o: IObjectWrapper?) { + this.tag = o.unwrap() + } + + override fun getTag(): IObjectWrapper = ObjectWrapper.wrap(tag) + override fun equalsRemote(other: IGroundOverlayDelegate?): Boolean { return this.equals(other) } @@ -109,18 +132,9 @@ class GroundOverlayImpl(private val map: GoogleMapImpl, private val id: String, Log.w(TAG, "unimplemented Method: remove") } - override fun todo(obj: IObjectWrapper?) { - Log.w(TAG, "unimplemented Method: todo") - } - - override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean = - if (super.onTransact(code, data, reply, flags)) { - true - } else { - Log.d(TAG, "onTransact [unknown]: $code, $data, $flags"); false - } + override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean = warnOnTransactionIssues(code, reply, flags, TAG) { super.onTransact(code, data, reply, flags) } companion object { - private val TAG = "GmsMapMarker" + private const val TAG = "GroundOverlay" } } diff --git a/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/InfoWindow.kt b/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/InfoWindow.kt new file mode 100644 index 0000000000..16f0471d8b --- /dev/null +++ b/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/InfoWindow.kt @@ -0,0 +1,166 @@ +package org.microg.gms.maps.mapbox.model + +import android.graphics.Point +import android.graphics.PointF +import android.view.LayoutInflater +import android.view.View +import android.view.View.VISIBLE +import android.view.ViewGroup +import android.view.ViewManager +import android.widget.FrameLayout +import android.widget.TextView +import androidx.core.content.ContextCompat +import androidx.core.view.ViewCompat +import com.google.android.gms.dynamic.ObjectWrapper +import com.google.android.gms.dynamic.unwrap +import com.google.android.gms.maps.internal.IInfoWindowAdapter +import com.google.android.gms.maps.model.internal.IMarkerDelegate +import com.mapbox.android.gestures.Utils +import org.microg.gms.maps.mapbox.AbstractGoogleMap +import org.microg.gms.maps.mapbox.GoogleMapImpl +import org.microg.gms.maps.mapbox.R +import org.microg.gms.maps.mapbox.utils.MapContext +import org.microg.gms.maps.mapbox.utils.toMapbox +import kotlin.math.* + +/** + * `InfoWindow` is a tooltip shown when a [MarkerImpl] is tapped. Only + * one info window is displayed at a time. When the user clicks on a marker, the currently open info + * window will be closed and the new info window will be displayed. If the user clicks the same + * marker while its info window is currently open, the info window will be reopened. + * + * The info window is drawn oriented against the device's screen, centered above its associated + * marker, unless a different info window anchor is set. The default info window contains the title + * in bold and snippet text below the title. + * If neither is set, no default info window is shown. + * + * Based on Mapbox's / MapLibre's [com.mapbox.mapboxsdk.annotations.InfoWindow]. + * + */ + +fun IInfoWindowAdapter.getInfoWindowViewFor(marker: IMarkerDelegate, mapContext: MapContext): View? { + getInfoWindow(marker).unwrap()?.let { return it } + + getInfoContents(marker).unwrap()?.let { view -> + // Detach from previous BubbleLayout parent, if exists + view.parent?.let { (it as ViewManager).removeView(view) } + + return FrameLayout(view.context).apply { + ViewCompat.setBackground(this, ContextCompat.getDrawable(mapContext, R.drawable.maps_default_bubble)) + val fourDp = Utils.dpToPx(4f) + ViewCompat.setElevation(this, fourDp) + setPadding(fourDp.toInt(), fourDp.toInt(), fourDp.toInt(), fourDp.toInt() * 3) + addView(view) + } + } + + // When a custom adapter is used, but both methods return null, the default adapter must be used + if (this !is DefaultInfoWindowAdapter) { + return DefaultInfoWindowAdapter(mapContext).getInfoWindowViewFor(marker, mapContext) + } + + return null +} + +class InfoWindow internal constructor( + private val view: View, private val map: AbstractGoogleMap, internal val marker: AbstractMarker +) { + private var coordinates: PointF = PointF(0f, 0f) + var isVisible = false + + init { + view.setOnClickListener { + map.onInfoWindowClickListener?.onInfoWindowClick(marker) + } + view.setOnLongClickListener { + map.onInfoWindowLongClickListener?.onInfoWindowLongClick(marker) + true + } + } + + fun open(mapView: FrameLayout) { + val layoutParams: FrameLayout.LayoutParams = FrameLayout.LayoutParams( + FrameLayout.LayoutParams.WRAP_CONTENT, FrameLayout.LayoutParams.WRAP_CONTENT + ) + view.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED) + + close(true) // if it was already opened + mapView.addView(view, layoutParams) + isVisible = true + + // Set correct position + update() + } + + /** + * Close this [InfoWindow] if it is visible, otherwise calling this will do nothing. + * + * @param silent `OnInfoWindowCloseListener` is only called if `silent` is not `false` + */ + fun close(silent: Boolean = false) { + if (isVisible) { + isVisible = false + (view.parent as ViewGroup?)?.removeView(view) + if (!silent) { + map.onInfoWindowCloseListener?.onInfoWindowClose(marker) + } + } + } + + /** + * Updates the position of the displayed view. + */ + fun update() { + + if (map is GoogleMapImpl) { + map.map?.projection?.toScreenLocation(marker.position.toMapbox())?.let { + coordinates = it + } + } else { + map.projection.toScreenLocation(marker.position)?.let { + coordinates = PointF(it.unwrap()!!) + } + } + + val iconDimensions = marker.getIconDimensions() + val width = iconDimensions?.get(0) ?: 0f + val height = iconDimensions?.get(1) ?: 0f + + view.x = + coordinates.x - view.measuredWidth / 2f + sin(Math.toRadians(marker.rotation.toDouble())).toFloat() * width * marker.infoWindowAnchor[0] + view.y = coordinates.y - view.measuredHeight - max( + height * cos(Math.toRadians(marker.rotation.toDouble())).toFloat() * marker.infoWindowAnchor[1], 0f + ) + } +} + +class DefaultInfoWindowAdapter(val context: MapContext) : IInfoWindowAdapter { + override fun asBinder() = null + + override fun getInfoWindow(marker: IMarkerDelegate?): ObjectWrapper { + + if (marker == null) return ObjectWrapper.wrap(null) + + val showDefaultMarker = (marker.title != null) || (marker.snippet != null) + + return if (!showDefaultMarker) ObjectWrapper.wrap(null) + else ObjectWrapper.wrap( + LayoutInflater.from(context).inflate(R.layout.maps_default_bubble_layout, null, false).apply { + + marker.title?.let { + val titleTextView = findViewById(R.id.title) + titleTextView.text = it + titleTextView.visibility = VISIBLE + } + + marker.snippet?.let { + val snippetTextView = findViewById(R.id.snippet) + snippetTextView.text = it + snippetTextView.visibility = VISIBLE + } + } + ) + } + + override fun getInfoContents(marker: IMarkerDelegate?) = null +} \ No newline at end of file diff --git a/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/Marker.kt b/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/Marker.kt index 06da365780..a1423b6cae 100644 --- a/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/Marker.kt +++ b/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/Marker.kt @@ -27,44 +27,167 @@ import com.mapbox.mapboxsdk.plugins.annotation.AnnotationManager import com.mapbox.mapboxsdk.plugins.annotation.Symbol import com.mapbox.mapboxsdk.plugins.annotation.SymbolOptions import com.google.android.gms.dynamic.unwrap +import org.microg.gms.maps.mapbox.AbstractGoogleMap import org.microg.gms.maps.mapbox.GoogleMapImpl +import org.microg.gms.maps.mapbox.LiteGoogleMapImpl import org.microg.gms.maps.mapbox.utils.toMapbox -class MarkerImpl(private val map: GoogleMapImpl, private val id: String, options: MarkerOptions) : IMarkerDelegate.Stub(), Markup { - private var position: LatLng = options.position - private var visible: Boolean = options.isVisible - private var rotation: Float = options.rotation - private var anchor: FloatArray = floatArrayOf(options.anchorU, options.anchorV) - private var icon: BitmapDescriptorImpl? = options.icon?.remoteObject.unwrap() - private var alpha: Float = options.alpha - private var title: String? = options.title - private var snippet: String? = options.snippet - private var zIndex: Float = options.zIndex - private var draggable: Boolean = options.isDraggable - private var tag: IObjectWrapper? = null - - private var infoWindowShown = false - - override var annotation: Symbol? = null - override var removed: Boolean = false - override val annotationOptions: SymbolOptions +abstract class AbstractMarker( + private val id: String, options: MarkerOptions, private val map: AbstractGoogleMap +) : IMarkerDelegate.Stub() { + + internal var position: LatLng = options.position + internal var visible: Boolean = options.isVisible + internal var anchor: FloatArray = floatArrayOf(options.anchorU, options.anchorV) + internal var infoWindowAnchor: FloatArray = floatArrayOf(0.5f, 1f) + internal var icon: BitmapDescriptorImpl? = options.icon?.remoteObject.unwrap() + internal var alpha: Float = options.alpha + internal var title: String? = options.title + internal var snippet: String? = options.snippet + internal var zIndex: Float = options.zIndex + internal var tag: IObjectWrapper? = null + internal open var draggable = false + + val annotationOptions: SymbolOptions get() { val symbolOptions = SymbolOptions() - .withIconOpacity(if (visible) alpha else 0f) - .withIconRotate(rotation) - .withSymbolSortKey(zIndex) - .withDraggable(draggable) + .withIconOpacity(if (visible) alpha else 0f) + .withIconRotate(rotation) + .withSymbolSortKey(zIndex) + .withDraggable(draggable) position.let { symbolOptions.withLatLng(it.toMapbox()) } icon?.applyTo(symbolOptions, anchor, map.dpiFactor) return symbolOptions } + internal abstract fun update() + + override fun getId(): String = id + + override fun setPosition(position: LatLng?) { + this.position = position ?: return + update() + } + + override fun getPosition(): LatLng = position + + override fun setIcon(obj: IObjectWrapper?) { + obj.unwrap()?.let { icon -> + this.icon = icon + update() + } + } + + override fun setVisible(visible: Boolean) { + this.visible = visible + update() + } + + override fun setTitle(title: String?) { + this.title = title + update() + } + + override fun getTitle(): String? = title + + override fun getSnippet(): String? = snippet + + override fun isVisible(): Boolean = visible + + override fun setAnchor(x: Float, y: Float) { + anchor = floatArrayOf(x, y) + update() + } + + override fun setAlpha(alpha: Float) { + this.alpha = alpha + update() + } + + override fun getAlpha(): Float = alpha + + override fun setZIndex(zIndex: Float) { + this.zIndex = zIndex + update() + } + + override fun getZIndex(): Float = zIndex + + fun getIconDimensions(): FloatArray? { + return icon?.size + } + + override fun showInfoWindow() { + if (isInfoWindowShown) { + // Per docs, don't call `onWindowClose` if info window is re-opened programmatically + map.currentInfoWindow?.close(silent = true) + } + map.showInfoWindow(this) + } + + override fun hideInfoWindow() { + if (isInfoWindowShown) { + map.currentInfoWindow?.close() + map.currentInfoWindow = null + } + } + + override fun isInfoWindowShown(): Boolean { + return map.currentInfoWindow?.marker == this + } + + override fun setTag(obj: IObjectWrapper?) { + this.tag = obj + } + + override fun getTag(): IObjectWrapper? = tag ?: ObjectWrapper.wrap(null) + + override fun setSnippet(snippet: String?) { + this.snippet = snippet + } + + override fun equalsRemote(other: IMarkerDelegate?): Boolean = equals(other) + + override fun hashCodeRemote(): Int = hashCode() + + override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean = + if (super.onTransact(code, data, reply, flags)) { + true + } else { + Log.d(TAG, "onTransact [unknown]: $code, $data, $flags"); false + } + + companion object { + private val TAG = "GmsMapAbstractMarker" + } +} + +class MarkerImpl(private val map: GoogleMapImpl, private val id: String, options: MarkerOptions) : + AbstractMarker(id, options, map), Markup { + + internal var rotation: Float = options.rotation + override var draggable: Boolean = options.isDraggable + + override var annotation: Symbol? = null + override var removed: Boolean = false + override fun remove() { removed = true map.symbolManager?.let { update(it) } } + override fun update() { + annotation?.let { + it.latLng = position.toMapbox() + it.isDraggable = draggable + it.iconOpacity = if (visible) alpha else 0f + it.symbolSortKey = zIndex + icon?.applyTo(it, anchor, map.dpiFactor) + } + map.symbolManager?.let { update(it) } + } + override fun update(manager: AnnotationManager<*, Symbol, SymbolOptions, *, *, *>) { synchronized(this) { val id = annotation?.id @@ -79,67 +202,47 @@ class MarkerImpl(private val map: GoogleMapImpl, private val id: String, options } } - override fun getId(): String = id - override fun setPosition(position: LatLng?) { - this.position = position ?: return - annotation?.latLng = position.toMapbox() - map.symbolManager?.let { update(it) } + super.setPosition(position) + map.currentInfoWindow?.update() } - override fun getPosition(): LatLng = position + /** + * New position is already reflected on map while if drag is in progress. Calling + * `symbolManager.update` would interrupt the drag. + */ + internal fun setPositionWhileDragging(position: LatLng) { + this.position = position + map.currentInfoWindow?.update() + } override fun setTitle(title: String?) { - this.title = title + super.setTitle(title) + map.currentInfoWindow?.let { + if (it.marker == this) it.close() + } } - override fun getTitle(): String? = title - override fun setSnippet(snippet: String?) { - this.snippet = snippet + super.setSnippet(snippet) + map.currentInfoWindow?.let { + if (it.marker == this) it.close() + } } - override fun getSnippet(): String? = snippet - override fun setDraggable(draggable: Boolean) { this.draggable = draggable - annotation?.isDraggable = draggable map.symbolManager?.let { update(it) } } override fun isDraggable(): Boolean = draggable - override fun showInfoWindow() { - Log.d(TAG, "unimplemented Method: showInfoWindow") - infoWindowShown = true - } - - override fun hideInfoWindow() { - Log.d(TAG, "unimplemented Method: hideInfoWindow") - infoWindowShown = false - } - - override fun isInfoWindowShown(): Boolean { - Log.d(TAG, "unimplemented Method: isInfoWindowShow") - return infoWindowShown - } - - override fun setVisible(visible: Boolean) { - this.visible = visible - annotation?.iconOpacity = if (visible) alpha else 0f - map.symbolManager?.let { update(it) } - } - - override fun isVisible(): Boolean = visible - override fun equals(other: Any?): Boolean { if (this === other) return true if (other is IMarkerDelegate) return other.id == id return false } - override fun equalsRemote(other: IMarkerDelegate?): Boolean = equals(other) - override fun hashCode(): Int { return id.hashCode() } @@ -148,20 +251,9 @@ class MarkerImpl(private val map: GoogleMapImpl, private val id: String, options return "$id ($title)" } - override fun hashCodeRemote(): Int = hashCode() - - override fun setIcon(obj: IObjectWrapper?) { - obj.unwrap()?.let { icon -> - this.icon = icon - annotation?.let { icon.applyTo(it, anchor, map.dpiFactor) } - } - map.symbolManager?.let { update(it) } - } - override fun setAnchor(x: Float, y: Float) { - anchor = floatArrayOf(x, y) - annotation?.let { icon?.applyTo(it, anchor, map.dpiFactor) } - map.symbolManager?.let { update(it) } + super.setAnchor(x, y) + map.currentInfoWindow?.update() } override fun setFlat(flat: Boolean) { @@ -177,44 +269,65 @@ class MarkerImpl(private val map: GoogleMapImpl, private val id: String, options this.rotation = rotation annotation?.iconRotate = rotation map.symbolManager?.let { update(it) } + map.currentInfoWindow?.update() } override fun getRotation(): Float = rotation override fun setInfoWindowAnchor(x: Float, y: Float) { - Log.d(TAG, "unimplemented Method: setInfoWindowAnchor") + infoWindowAnchor = floatArrayOf(x, y) + map.currentInfoWindow?.update() } - override fun setAlpha(alpha: Float) { - this.alpha = alpha - annotation?.iconOpacity = if (visible) alpha else 0f - map.symbolManager?.let { update(it) } + companion object { + private val TAG = "GmsMapMarker" } +} - override fun getAlpha(): Float = alpha +class LiteMarkerImpl(id: String, options: MarkerOptions, private val map: LiteGoogleMapImpl) : + AbstractMarker(id, options, map) { + override fun remove() { + map.markers.remove(this) + map.postUpdateSnapshot() + } - override fun setZIndex(zIndex: Float) { - this.zIndex = zIndex - annotation?.symbolSortKey = zIndex - map.symbolManager?.let { update(it) } + override fun update() { + map.postUpdateSnapshot() } - override fun getZIndex(): Float = zIndex + override fun setDraggable(drag: Boolean) { + Log.d(TAG, "setDraggable: not available in lite mode") + } - override fun setTag(obj: IObjectWrapper?) { - this.tag = obj + override fun isDraggable(): Boolean { + Log.d(TAG, "isDraggable: markers are never draggable in lite mode") + return false } - override fun getTag(): IObjectWrapper = tag ?: ObjectWrapper.wrap(null) + override fun setFlat(flat: Boolean) { + Log.d(TAG, "setFlat: not available in lite mode") + } - override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean = - if (super.onTransact(code, data, reply, flags)) { - true - } else { - Log.d(TAG, "onTransact [unknown]: $code, $data, $flags"); false - } + override fun isFlat(): Boolean { + Log.d(TAG, "isFlat: markers in lite mode can never be flat") + return false + } + + override fun setRotation(rotation: Float) { + Log.d(TAG, "setRotation: not available in lite mode") + } + + override fun getRotation(): Float { + Log.d(TAG, "setRotation: markers in lite mode can never be rotated") + return 0f + } + + override fun setInfoWindowAnchor(x: Float, y: Float) { + infoWindowAnchor = floatArrayOf(x, y) + map.currentInfoWindow?.update() + } companion object { - private val TAG = "GmsMapMarker" + private val TAG = "GmsMapMarkerLite" } -} +} \ No newline at end of file diff --git a/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/Markup.kt b/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/Markup.kt index c737afccd4..179c5a0b07 100644 --- a/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/Markup.kt +++ b/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/Markup.kt @@ -24,7 +24,7 @@ import com.mapbox.mapboxsdk.plugins.annotation.Options interface Markup, S : Options> { var annotation: T? val annotationOptions: S - val removed: Boolean + var removed: Boolean fun update(manager: AnnotationManager<*, T, S, *, *, *>) { synchronized(this) { diff --git a/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/Polygon.kt b/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/Polygon.kt index 2c10af4a70..f67d21fe67 100644 --- a/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/Polygon.kt +++ b/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/Polygon.kt @@ -7,6 +7,7 @@ package org.microg.gms.maps.mapbox.model import android.os.Parcel import android.util.Log +import androidx.annotation.CallSuper import com.google.android.gms.dynamic.IObjectWrapper import com.google.android.gms.dynamic.ObjectWrapper import com.google.android.gms.maps.model.LatLng @@ -14,62 +15,55 @@ import com.google.android.gms.maps.model.PatternItem import com.google.android.gms.maps.model.PolygonOptions import com.google.android.gms.maps.model.PolylineOptions import com.google.android.gms.maps.model.internal.IPolygonDelegate -import com.mapbox.mapboxsdk.plugins.annotation.AnnotationManager import com.mapbox.mapboxsdk.plugins.annotation.Fill import com.mapbox.mapboxsdk.plugins.annotation.FillOptions import com.mapbox.mapboxsdk.utils.ColorUtils import org.microg.gms.maps.mapbox.GoogleMapImpl +import org.microg.gms.maps.mapbox.LiteGoogleMapImpl import org.microg.gms.maps.mapbox.utils.toMapbox import org.microg.gms.utils.warnOnTransactionIssues -class PolygonImpl(private val map: GoogleMapImpl, private val id: String, options: PolygonOptions) : IPolygonDelegate.Stub(), Markup { - private var points = ArrayList(options.points.orEmpty()) - private var holes: List> = ArrayList(options.holes.map { ArrayList(it.orEmpty()) }) - private var fillColor = options.fillColor - private var strokeColor = options.strokeColor - private var strokeWidth = options.strokeWidth - private var strokeJointType = options.strokeJointType - private var strokePattern = ArrayList(options.strokePattern.orEmpty()) - private var visible: Boolean = options.isVisible - private var clickable: Boolean = options.isClickable - private var tag: IObjectWrapper? = null - - private var strokes = (listOf(PolylineImpl(map, "$id-stroke-main", PolylineOptions().color(strokeColor).width(strokeWidth).addAll(points))) - + holes.mapIndexed { idx, it -> PolylineImpl(map, "$id-stroke-hole-$idx", PolylineOptions().color(strokeColor).width(strokeWidth).addAll(it)) }).toMutableList() - - override var annotation: Fill? = null - override var removed: Boolean = false - override val annotationOptions: FillOptions +abstract class AbstractPolygon(private val id: String, options: PolygonOptions) : IPolygonDelegate.Stub() { + internal var points = ArrayList(options.points.orEmpty()) + internal var holes: List> = ArrayList(options.holes.map { ArrayList(it.orEmpty()) }) + internal var fillColor = options.fillColor + internal var strokeColor = options.strokeColor + internal var strokeWidth = options.strokeWidth + internal var strokeJointType = options.strokeJointType + internal var strokePattern = ArrayList(options.strokePattern.orEmpty()) + internal var visible: Boolean = options.isVisible + internal var clickable: Boolean = options.isClickable + internal var tag: IObjectWrapper? = null + + val annotationOptions: FillOptions get() = FillOptions() - .withLatLngs(mutableListOf(points.map { it.toMapbox() }).plus(holes.map { it.map { it.toMapbox() } })) - .withFillColor(ColorUtils.colorToRgbaString(fillColor)) - .withFillOpacity(if (visible) 1f else 0f) + .withLatLngs(mutableListOf(points.map { it.toMapbox() }).plus(holes.map { it.map { it.toMapbox() } })) + .withFillColor(ColorUtils.colorToRgbaString(fillColor)) + .withFillOpacity(if (visible) 1f else 0f) - override fun remove() { - removed = true - map.fillManager?.let { update(it) } - strokes.forEach { it.remove() } - } + internal abstract val strokes: MutableList + + internal abstract fun update() - override fun update(manager: AnnotationManager<*, Fill, FillOptions, *, *, *>) { - super.update(manager) - map.lineManager?.let { lineManager -> strokes.forEach { it.update(lineManager) } } + @CallSuper + override fun remove() { + for (stroke in strokes) stroke.remove() } override fun getId(): String = id override fun setPoints(points: List) { this.points = ArrayList(points) - annotation?.latLngs = mutableListOf(points.map { it.toMapbox() }).plus(holes.map { it.map { it.toMapbox() } }) - map.fillManager?.let { update(it) } - strokes[0].points = points + strokes[0].setPoints(points) + update() } override fun getPoints(): List = points + internal abstract fun addPolyline(id: String, options: PolylineOptions) + override fun setHoles(holes: List?) { this.holes = if (holes == null) emptyList() else ArrayList(holes.mapNotNull { if (it is List<*>) it.mapNotNull { if (it is LatLng) it else null }.let { if (it.isNotEmpty()) it else null } else null }) - annotation?.latLngs = mutableListOf(points.map { it.toMapbox() }).plus(this.holes.map { it.map { it.toMapbox() } }) while (strokes.size > this.holes.size + 1) { val last = strokes.last() last.remove() @@ -78,34 +72,40 @@ class PolygonImpl(private val map: GoogleMapImpl, private val id: String, option strokes.forEachIndexed { idx, it -> if (idx > 0) it.points = this.holes[idx - 1] } if (this.holes.size + 1 > strokes.size) { try { - strokes.addAll(this.holes.subList(strokes.size, this.holes.size - 1).mapIndexed { idx, it -> PolylineImpl(map, "$id-stroke-hole-${strokes.size + idx}", PolylineOptions().color(strokeColor).width(strokeWidth).addAll(it)) }) + this.holes.subList(strokes.size, this.holes.size - 1).mapIndexed { idx, it -> + addPolyline( + "$id-stroke-hole-${strokes.size + idx}", + PolylineOptions().color(strokeColor).width(strokeWidth).addAll(it) + ) + } } catch (e: Exception) { Log.w(TAG, e) } } - map.fillManager?.let { update(it) } } - override fun getHoles(): List = holes + override fun getHoles(): List> = holes + override fun setStrokeWidth(width: Float) { - this.strokeWidth = width - strokes.forEach { it.width = width } + strokeWidth = width + strokes.forEach { it.setWidth(width) } + update() } override fun getStrokeWidth(): Float = strokeWidth override fun setStrokeColor(color: Int) { - this.strokeColor = color - strokes.forEach { it.color = color } + strokeColor = color + strokes.forEach { it.setColor(color) } + update() } override fun getStrokeColor(): Int = strokeColor override fun setFillColor(color: Int) { - this.fillColor = color - annotation?.setFillColor(color) - map.fillManager?.let { update(it) } + fillColor = color + update() } override fun getFillColor(): Int = fillColor @@ -121,8 +121,7 @@ class PolygonImpl(private val map: GoogleMapImpl, private val id: String, option override fun setVisible(visible: Boolean) { this.visible = visible - annotation?.fillOpacity = if (visible) 1f else 0f - map.fillManager?.let { update(it) } + update() } override fun isVisible(): Boolean = visible @@ -136,10 +135,6 @@ class PolygonImpl(private val map: GoogleMapImpl, private val id: String, option return false } - override fun equalsRemote(other: IPolygonDelegate?): Boolean = equals(other) - - override fun hashCodeRemote(): Int = hashCode() - override fun setClickable(click: Boolean) { clickable = click } @@ -148,12 +143,14 @@ class PolygonImpl(private val map: GoogleMapImpl, private val id: String, option override fun setStrokeJointType(type: Int) { strokeJointType = type + update() } override fun getStrokeJointType(): Int = strokeJointType override fun setStrokePattern(items: MutableList?) { strokePattern = ArrayList(items.orEmpty()) + update() } override fun getStrokePattern(): MutableList = strokePattern @@ -164,19 +161,65 @@ class PolygonImpl(private val map: GoogleMapImpl, private val id: String, option override fun getTag(): IObjectWrapper = tag ?: ObjectWrapper.wrap(null) + override fun equalsRemote(other: IPolygonDelegate?): Boolean = equals(other) + + override fun hashCodeRemote(): Int = hashCode() + override fun hashCode(): Int { return id.hashCode() } + override fun equals(other: Any?): Boolean { + if (other is AbstractPolygon) { + return other.id == id + } + return false + } + override fun toString(): String { return id } - override fun equals(other: Any?): Boolean { - if (other is PolygonImpl) { - return other.id == id + companion object { + private val TAG = "GmsMapAbstractPolygon" + } +} + +class PolygonImpl(private val map: GoogleMapImpl, id: String, options: PolygonOptions) : + AbstractPolygon(id, options), Markup { + + + override val strokes = (listOf( + PolylineImpl( + map, "$id-stroke-main", PolylineOptions().color(strokeColor).width(strokeWidth).addAll(points) + ) + ) + holes.mapIndexed { idx, it -> + PolylineImpl( + map, "$id-stroke-hole-$idx", PolylineOptions().color(strokeColor).width(strokeWidth).addAll(it) + ) + }).toMutableList() + + override var annotation: Fill? = null + override var removed: Boolean = false + + override fun remove() { + removed = true + map.fillManager?.let { update(it) } + super.remove() + } + + override fun update() { + annotation?.let { + it.latLngs = mutableListOf(points.map { it.toMapbox() }).plus(holes.map { it.map { it.toMapbox() } }) + it.setFillColor(fillColor) + it.fillOpacity = if (visible) 1f else 0f + it.latLngs = mutableListOf(points.map { it.toMapbox() }).plus(this.holes.map { it.map { it.toMapbox() } }) } - return false + map.fillManager?.let { update(it) } + } + + override fun addPolyline(id: String, options: PolylineOptions) { + strokes.add(PolylineImpl(map, id, options)) } override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean = warnOnTransactionIssues(code, reply, flags) { super.onTransact(code, data, reply, flags) } @@ -185,3 +228,31 @@ class PolygonImpl(private val map: GoogleMapImpl, private val id: String, option private val TAG = "GmsMapPolygon" } } + +class LitePolygonImpl(id: String, options: PolygonOptions, private val map: LiteGoogleMapImpl) : AbstractPolygon(id, options) { + + override val strokes: MutableList = (listOf( + LitePolylineImpl( + map, "$id-stroke-main", PolylineOptions().color(strokeColor).width(strokeWidth).addAll(points) + ) + ) + holes.mapIndexed { idx, it -> + LitePolylineImpl( + map, "$id-stroke-hole-$idx", PolylineOptions().color(strokeColor).width(strokeWidth).addAll(it) + ) + }).toMutableList() + + + override fun remove() { + super.remove() + map.polygons.remove(this) + map.postUpdateSnapshot() + } + + override fun update() { + map.postUpdateSnapshot() + } + + override fun addPolyline(id: String, options: PolylineOptions) { + strokes.add(LitePolylineImpl(map, id, options)) + } +} \ No newline at end of file diff --git a/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/Polyline.kt b/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/Polyline.kt index 15751f906f..4405c21c77 100644 --- a/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/Polyline.kt +++ b/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/Polyline.kt @@ -1,25 +1,14 @@ /* - * Copyright (C) 2019 microG Project Team - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * SPDX-FileCopyrightText: 2019 microG Project Team + * SPDX-License-Identifier: Apache-2.0 */ package org.microg.gms.maps.mapbox.model import android.os.Parcel -import android.util.Log import com.google.android.gms.dynamic.IObjectWrapper import com.google.android.gms.dynamic.ObjectWrapper +import com.google.android.gms.maps.model.Cap import com.google.android.gms.maps.model.LatLng import com.google.android.gms.maps.model.PatternItem import com.google.android.gms.maps.model.internal.IPolylineDelegate @@ -27,85 +16,88 @@ import com.mapbox.mapboxsdk.plugins.annotation.Line import com.mapbox.mapboxsdk.plugins.annotation.LineOptions import com.mapbox.mapboxsdk.utils.ColorUtils import org.microg.gms.maps.mapbox.GoogleMapImpl +import org.microg.gms.maps.mapbox.LiteGoogleMapImpl import org.microg.gms.maps.mapbox.utils.toMapbox +import org.microg.gms.utils.warnOnTransactionIssues import com.google.android.gms.maps.model.PolylineOptions as GmsLineOptions -class PolylineImpl(private val map: GoogleMapImpl, private val id: String, options: GmsLineOptions) : IPolylineDelegate.Stub(), Markup { - private var points = ArrayList(options.points) - private var width = options.width - private var jointType = options.jointType - private var pattern = ArrayList(options.pattern.orEmpty()) - private var color = options.color - private var visible: Boolean = options.isVisible - private var clickable: Boolean = options.isClickable - private var tag: IObjectWrapper? = null - - override var annotation: Line? = null - override var removed: Boolean = false - override val annotationOptions: LineOptions +abstract class AbstractPolylineImpl(private val id: String, options: GmsLineOptions, private val dpiFactor: Function0) : IPolylineDelegate.Stub() { + internal var points: List = ArrayList(options.points) + internal var width = options.width + internal var jointType = options.jointType + internal var pattern = ArrayList(options.pattern.orEmpty()) + internal var color = options.color + internal var visible: Boolean = options.isVisible + internal var clickable: Boolean = options.isClickable + internal var tag: IObjectWrapper? = null + internal var startCap: Cap = options.startCap + internal var endCap: Cap = options.endCap + internal var geodesic = options.isGeodesic + internal var zIndex = options.zIndex + + val annotationOptions: LineOptions get() = LineOptions() - .withLatLngs(points.map { it.toMapbox() }) - .withLineWidth(width / map.dpiFactor) - .withLineColor(ColorUtils.colorToRgbaString(color)) - .withLineOpacity(if (visible) 1f else 0f) + .withLatLngs(points.map { it.toMapbox() }) + .withLineWidth(width / dpiFactor.invoke()) + .withLineColor(ColorUtils.colorToRgbaString(color)) + .withLineOpacity(if (visible) 1f else 0f) - override fun remove() { - removed = true - map.lineManager?.let { update(it) } - } + internal abstract fun update() override fun getId(): String = id override fun setPoints(points: List) { this.points = ArrayList(points) - annotation?.latLngs = points.map { it.toMapbox() } - map.lineManager?.let { update(it) } + update() } override fun getPoints(): List = points override fun setWidth(width: Float) { this.width = width - annotation?.lineWidth = width / map.dpiFactor - map.lineManager?.let { update(it) } + update() } override fun getWidth(): Float = width override fun setColor(color: Int) { this.color = color - annotation?.setLineColor(color) - map.lineManager?.let { update(it) } + update() } override fun getColor(): Int = color override fun setZIndex(zIndex: Float) { - Log.d(TAG, "unimplemented Method: setZIndex") + this.zIndex = zIndex } - override fun getZIndex(): Float { - Log.d(TAG, "unimplemented Method: getZIndex") - return 0f - } + override fun getZIndex(): Float = zIndex override fun setVisible(visible: Boolean) { this.visible = visible - annotation?.lineOpacity = if (visible) 1f else 0f - map.lineManager?.let { update(it) } + update() } override fun isVisible(): Boolean = visible override fun setGeodesic(geod: Boolean) { - Log.d(TAG, "unimplemented Method: setGeodesic") + this.geodesic = geod } - override fun isGeodesic(): Boolean { - Log.d(TAG, "unimplemented Method: isGeodesic") - return false + override fun isGeodesic(): Boolean = geodesic + + override fun setStartCap(startCap: Cap) { + this.startCap = startCap + } + + override fun getStartCap(): Cap = startCap + + override fun setEndCap(endCap: Cap) { + this.endCap = endCap } + override fun getEndCap(): Cap = endCap + override fun equalsRemote(other: IPolylineDelegate?): Boolean = equals(other) override fun hashCodeRemote(): Int = hashCode() @@ -138,25 +130,58 @@ class PolylineImpl(private val map: GoogleMapImpl, private val id: String, optio return id.hashCode() } - override fun toString(): String { - return id - } - override fun equals(other: Any?): Boolean { - if (other is PolylineImpl) { + if (other is AbstractPolylineImpl) { return other.id == id } return false } - override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean = - if (super.onTransact(code, data, reply, flags)) { - true - } else { - Log.d(TAG, "onTransact [unknown]: $code, $data, $flags"); false - } + override fun toString(): String { + return id + } + + override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean = warnOnTransactionIssues(code, reply, flags, TAG) { super.onTransact(code, data, reply, flags) } + + companion object { + const val TAG = "GmsPolylineAbstract" + } +} + +class PolylineImpl(private val map: GoogleMapImpl, id: String, options: GmsLineOptions) : + AbstractPolylineImpl(id, options, { map.dpiFactor }), Markup { + + override var annotation: Line? = null + override var removed: Boolean = false + + override fun remove() { + removed = true + map.lineManager?.let { update(it) } + } + + override fun update() { + annotation?.apply { + latLngs = points.map { it.toMapbox() } + lineWidth = width / map.dpiFactor + setLineColor(color) + lineOpacity = if (visible) 1f else 0f + } + map.lineManager?.let { update(it) } + } companion object { private val TAG = "GmsMapPolyline" } } + +class LitePolylineImpl(private val map: LiteGoogleMapImpl, id: String, options: GmsLineOptions) : + AbstractPolylineImpl(id, options, { map.dpiFactor }) { + override fun remove() { + map.polylines.remove(this) + map.postUpdateSnapshot() + } + + override fun update() { + map.postUpdateSnapshot() + } +} diff --git a/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/TileOverlay.kt b/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/TileOverlay.kt index dfa4cb4994..0c8f5c68cf 100644 --- a/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/TileOverlay.kt +++ b/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/TileOverlay.kt @@ -1,14 +1,64 @@ /* - * SPDX-FileCopyrightText: 2020, microG Project Team + * SPDX-FileCopyrightText: 2020 microG Project Team * SPDX-License-Identifier: Apache-2.0 */ package org.microg.gms.maps.mapbox.model +import android.os.Parcel +import android.util.Log import com.google.android.gms.maps.model.TileOverlayOptions import com.google.android.gms.maps.model.internal.ITileOverlayDelegate import org.microg.gms.maps.mapbox.GoogleMapImpl +import org.microg.gms.utils.warnOnTransactionIssues class TileOverlayImpl(private val map: GoogleMapImpl, private val id: String, options: TileOverlayOptions) : ITileOverlayDelegate.Stub() { + private var zIndex = options.zIndex + private var visible = options.isVisible + private var fadeIn = options.fadeIn + private var transparency = options.transparency + override fun remove() { + Log.d(TAG, "Not yet implemented: remove") + } + + override fun clearTileCache() { + Log.d(TAG, "Not yet implemented: clearTileCache") + } + + override fun getId(): String = id + + override fun setZIndex(zIndex: Float) { + this.zIndex = zIndex + } + + override fun getZIndex(): Float = zIndex + + override fun setVisible(visible: Boolean) { + this.visible = visible + } + + override fun isVisible(): Boolean = visible + + override fun equalsRemote(other: ITileOverlayDelegate?): Boolean = this == other + + override fun hashCodeRemote(): Int = hashCode() + + override fun setFadeIn(fadeIn: Boolean) { + this.fadeIn = fadeIn + } + + override fun getFadeIn(): Boolean = fadeIn + + override fun setTransparency(transparency: Float) { + this.transparency = transparency + } + + override fun getTransparency(): Float = transparency + + override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean = warnOnTransactionIssues(code, reply, flags, TAG) { super.onTransact(code, data, reply, flags) } + + companion object { + private const val TAG = "TileOverlay" + } } diff --git a/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/utils/typeConverter.kt b/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/utils/typeConverter.kt index 9664aea6f0..2a4e9d714f 100644 --- a/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/utils/typeConverter.kt +++ b/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/utils/typeConverter.kt @@ -17,12 +17,15 @@ package org.microg.gms.maps.mapbox.utils import android.os.Bundle +import android.util.Log import com.google.android.gms.maps.internal.ICancelableCallback +import com.mapbox.geojson.Point import com.mapbox.mapboxsdk.camera.CameraPosition import com.mapbox.mapboxsdk.geometry.LatLng import com.mapbox.mapboxsdk.geometry.LatLngBounds import com.mapbox.mapboxsdk.geometry.VisibleRegion import com.mapbox.mapboxsdk.maps.MapboxMap +import org.microg.gms.maps.mapbox.TAG import com.google.android.gms.maps.model.CameraPosition as GmsCameraPosition import com.google.android.gms.maps.model.LatLng as GmsLatLng import com.google.android.gms.maps.model.LatLngBounds as GmsLatLngBounds @@ -31,8 +34,10 @@ import com.google.android.gms.maps.model.VisibleRegion as GmsVisibleRegion fun GmsLatLng.toMapbox(): LatLng = LatLng(latitude, longitude) +fun GmsLatLng.toPoint() = Point.fromLngLat(latitude, longitude) + fun GmsLatLngBounds.toMapbox(): LatLngBounds = - LatLngBounds.from(this.northeast.latitude, this.northeast.longitude, this.southwest.latitude, this.southwest.longitude) + LatLngBounds.from(this.northeast.latitude, this.northeast.longitude + if (this.northeast.longitude < this.southwest.longitude) 360.0 else 0.0, this.southwest.latitude, this.southwest.longitude) fun GmsCameraPosition.toMapbox(): CameraPosition = CameraPosition.Builder() @@ -68,10 +73,12 @@ fun Bundle.toMapbox(): Bundle { fun LatLng.toGms(): GmsLatLng = GmsLatLng(latitude, longitude) +fun LatLng.toPoint(): Point = Point.fromLngLat(latitude, longitude) + fun LatLngBounds.toGms(): GmsLatLngBounds = GmsLatLngBounds(southWest.toGms(), northEast.toGms()) fun CameraPosition.toGms(): GmsCameraPosition = - GmsCameraPosition(target.toGms(), zoom.toFloat() + 1.0f, tilt.toFloat(), bearing.toFloat()) + GmsCameraPosition(target?.toGms(), zoom.toFloat() + 1.0f, tilt.toFloat(), bearing.toFloat()) fun Bundle.toGms(): Bundle { val newBundle = Bundle(this) @@ -91,4 +98,4 @@ fun Bundle.toGms(): Bundle { } fun VisibleRegion.toGms(): GmsVisibleRegion = - GmsVisibleRegion(nearLeft.toGms(), nearRight.toGms(), farLeft.toGms(), farRight.toGms(), latLngBounds.toGms()) + GmsVisibleRegion(nearLeft?.toGms(), nearRight?.toGms(), farLeft?.toGms(), farRight?.toGms(), latLngBounds.toGms()) diff --git a/play-services-maps-core-mapbox/src/main/res/drawable/location_dot.xml b/play-services-maps-core-mapbox/src/main/res/drawable/location_dot.xml new file mode 100644 index 0000000000..af953b62bb --- /dev/null +++ b/play-services-maps-core-mapbox/src/main/res/drawable/location_dot.xml @@ -0,0 +1,5 @@ + + + + diff --git a/play-services-maps-core-mapbox/src/main/res/drawable/maps_default_bubble.xml b/play-services-maps-core-mapbox/src/main/res/drawable/maps_default_bubble.xml new file mode 100644 index 0000000000..c172be81e9 --- /dev/null +++ b/play-services-maps-core-mapbox/src/main/res/drawable/maps_default_bubble.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/play-services-maps-core-mapbox/src/main/res/layout/maps_default_bubble_layout.xml b/play-services-maps-core-mapbox/src/main/res/layout/maps_default_bubble_layout.xml new file mode 100644 index 0000000000..6f175407c2 --- /dev/null +++ b/play-services-maps-core-mapbox/src/main/res/layout/maps_default_bubble_layout.xml @@ -0,0 +1,34 @@ + + + + + + + + \ No newline at end of file diff --git a/play-services-maps-core-vtm/src/main/java/org/microg/gms/maps/vtm/GoogleMapImpl.java b/play-services-maps-core-vtm/src/main/java/org/microg/gms/maps/vtm/GoogleMapImpl.java index 87a512091f..82eee41fae 100644 --- a/play-services-maps-core-vtm/src/main/java/org/microg/gms/maps/vtm/GoogleMapImpl.java +++ b/play-services-maps-core-vtm/src/main/java/org/microg/gms/maps/vtm/GoogleMapImpl.java @@ -35,26 +35,7 @@ import com.google.android.gms.dynamic.IObjectWrapper; import com.google.android.gms.dynamic.ObjectWrapper; import com.google.android.gms.maps.GoogleMapOptions; -import com.google.android.gms.maps.internal.ICancelableCallback; -import com.google.android.gms.maps.internal.IGoogleMapDelegate; -import com.google.android.gms.maps.internal.IInfoWindowAdapter; -import com.google.android.gms.maps.internal.ILocationSourceDelegate; -import com.google.android.gms.maps.internal.IOnCameraChangeListener; -import com.google.android.gms.maps.internal.IOnCameraIdleListener; -import com.google.android.gms.maps.internal.IOnCameraMoveCanceledListener; -import com.google.android.gms.maps.internal.IOnCameraMoveListener; -import com.google.android.gms.maps.internal.IOnCameraMoveStartedListener; -import com.google.android.gms.maps.internal.IOnInfoWindowClickListener; -import com.google.android.gms.maps.internal.IOnMapClickListener; -import com.google.android.gms.maps.internal.IOnMapLoadedCallback; -import com.google.android.gms.maps.internal.IOnMapLongClickListener; -import com.google.android.gms.maps.internal.IOnMarkerClickListener; -import com.google.android.gms.maps.internal.IOnMarkerDragListener; -import com.google.android.gms.maps.internal.IOnMyLocationButtonClickListener; -import com.google.android.gms.maps.internal.IOnMyLocationChangeListener; -import com.google.android.gms.maps.internal.IProjectionDelegate; -import com.google.android.gms.maps.internal.ISnapshotReadyCallback; -import com.google.android.gms.maps.internal.IUiSettingsDelegate; +import com.google.android.gms.maps.internal.*; import com.google.android.gms.maps.model.CameraPosition; import com.google.android.gms.maps.model.CircleOptions; import com.google.android.gms.maps.model.GroundOverlayOptions; @@ -200,6 +181,11 @@ public void setContentDescription(String desc) throws RemoteException { } + @Override + public void snapshotForTest(ISnapshotReadyCallback callback) throws RemoteException { + + } + @Override public void onEnterAmbient(Bundle bundle) throws RemoteException { Log.d(TAG, "unimplemented Method: onEnterAmbient"); @@ -212,6 +198,26 @@ public void onExitAmbient() throws RemoteException { } + @Override + public void setOnGroundOverlayClickListener(IOnGroundOverlayClickListener listener) throws RemoteException { + Log.d(TAG, "unimplemented Method: setOnGroundOverlayClickListener"); + } + + @Override + public void setOnPolygonClickListener(IOnPolygonClickListener listener) throws RemoteException { + Log.d(TAG, "unimplemented Method: setOnPolygonClickListener"); + } + + @Override + public void setOnPolylineClickListener(IOnPolylineClickListener listener) throws RemoteException { + Log.d(TAG, "unimplemented Method: setOnPolylineClickListener"); + } + + @Override + public void setOnCircleClickListener(IOnCircleClickListener listener) throws RemoteException { + Log.d(TAG, "unimplemented Method: setCircleClickListener"); + } + @Override public boolean setMapStyle(MapStyleOptions options) throws RemoteException { Log.d(TAG, "unimplemented Method: setMapStyle"); @@ -661,6 +667,16 @@ public void setCameraIdleListener(IOnCameraIdleListener listener) throws RemoteE } + @Override + public void setOnInfoWindowLongClickListener(IOnInfoWindowLongClickListener listener) throws RemoteException { + Log.d(TAG, "unimplemented Method: setOnInfoWindowLongClickListener"); + } + + @Override + public void setOnInfoWindowCloseListener(IOnInfoWindowCloseListener listener) throws RemoteException { + Log.d(TAG, "unimplemented Method: setOnInfoWindowCloseListener"); + } + @Override public void onStart() throws RemoteException { Log.d(TAG, "unimplemented Method: onStart"); @@ -672,6 +688,11 @@ public void onStop() throws RemoteException { Log.d(TAG, "unimplemented Method: onStop"); } + + @Override + public void setOnMyLocationClickListener(IOnMyLocationClickListener listener) throws RemoteException { + Log.d(TAG, "unimplemented Method: setOnMyLocationClickListener"); + } /* Misc diff --git a/play-services-maps-core-vtm/src/main/java/org/microg/gms/maps/vtm/markup/CircleImpl.java b/play-services-maps-core-vtm/src/main/java/org/microg/gms/maps/vtm/markup/CircleImpl.java index 18b4a99945..8a30cd21ec 100644 --- a/play-services-maps-core-vtm/src/main/java/org/microg/gms/maps/vtm/markup/CircleImpl.java +++ b/play-services-maps-core-vtm/src/main/java/org/microg/gms/maps/vtm/markup/CircleImpl.java @@ -18,8 +18,12 @@ import android.os.RemoteException; +import android.util.Log; +import com.google.android.gms.dynamic.IObjectWrapper; +import com.google.android.gms.dynamic.ObjectWrapper; import com.google.android.gms.maps.model.CircleOptions; import com.google.android.gms.maps.model.LatLng; +import com.google.android.gms.maps.model.PatternItem; import com.google.android.gms.maps.model.internal.ICircleDelegate; import org.microg.gms.maps.vtm.GmsMapsTypeHelper; @@ -28,8 +32,12 @@ import org.oscim.layers.vector.geometries.Style; import org.oscim.map.Map; +import java.util.List; + public class CircleImpl extends ICircleDelegate.Stub implements DrawableMarkup { + private static final String TAG = "GmsMapCircle"; + private final String id; private final CircleOptions options; private final MarkupListener listener; @@ -139,6 +147,38 @@ public int hashCodeRemote() throws RemoteException { return id.hashCode(); } + @Override + public void setClickable(boolean clickable) throws RemoteException { + Log.d(TAG, "unimplemented method: setClickable"); + } + + @Override + public boolean isClickable() throws RemoteException { + return false; + } + + @Override + public void setStrokePattern(List object) throws RemoteException { + Log.d(TAG, "unimplemented method: setStrokePattern"); + } + + @Override + public List getStrokePattern() throws RemoteException { + Log.d(TAG, "unimplemented method: getStrokePattern"); + return null; + } + + @Override + public void setTag(IObjectWrapper object) throws RemoteException { + Log.d(TAG, "unimplemented method: setTag"); + } + + @Override + public IObjectWrapper getTag() throws RemoteException { + Log.d(TAG, "unimplemented method: getTag"); + return ObjectWrapper.wrap(null); + } + @Override public boolean onClick() { return listener.onClick(this); diff --git a/play-services-maps-core-vtm/src/main/java/org/microg/gms/maps/vtm/markup/GroundOverlayImpl.java b/play-services-maps-core-vtm/src/main/java/org/microg/gms/maps/vtm/markup/GroundOverlayImpl.java index a577bd2e3c..9329ed0933 100644 --- a/play-services-maps-core-vtm/src/main/java/org/microg/gms/maps/vtm/markup/GroundOverlayImpl.java +++ b/play-services-maps-core-vtm/src/main/java/org/microg/gms/maps/vtm/markup/GroundOverlayImpl.java @@ -139,7 +139,27 @@ public int hashCodeRemote() throws RemoteException { } @Override - public void todo(IObjectWrapper obj) throws RemoteException { + public void setImage(IObjectWrapper img) throws RemoteException { } + + @Override + public void setClickable(boolean clickable) throws RemoteException { + + } + + @Override + public boolean isClickable() throws RemoteException { + return false; + } + + @Override + public void setTag(IObjectWrapper obj) throws RemoteException { + + } + + @Override + public IObjectWrapper getTag() throws RemoteException { + return null; + } } diff --git a/play-services-maps-core-vtm/src/main/java/org/microg/gms/maps/vtm/markup/PolylineImpl.java b/play-services-maps-core-vtm/src/main/java/org/microg/gms/maps/vtm/markup/PolylineImpl.java index 7e098596e4..6af82b5c81 100644 --- a/play-services-maps-core-vtm/src/main/java/org/microg/gms/maps/vtm/markup/PolylineImpl.java +++ b/play-services-maps-core-vtm/src/main/java/org/microg/gms/maps/vtm/markup/PolylineImpl.java @@ -20,6 +20,7 @@ import android.util.Log; import com.google.android.gms.dynamic.IObjectWrapper; +import com.google.android.gms.maps.model.Cap; import com.google.android.gms.maps.model.LatLng; import com.google.android.gms.maps.model.PatternItem; import com.google.android.gms.maps.model.PolylineOptions; @@ -170,6 +171,26 @@ public boolean isClickable() throws RemoteException { return false; } + @Override + public void setStartCap(Cap startCap) throws RemoteException { + + } + + @Override + public Cap getStartCap() throws RemoteException { + return null; + } + + @Override + public void setEndCap(Cap endCap) throws RemoteException { + + } + + @Override + public Cap getEndCap() throws RemoteException { + return null; + } + @Override public void setJointType(int jointType) throws RemoteException { diff --git a/play-services-maps-core-vtm/src/main/java/org/microg/gms/maps/vtm/markup/TileOverlayImpl.java b/play-services-maps-core-vtm/src/main/java/org/microg/gms/maps/vtm/markup/TileOverlayImpl.java index c13c0f6d61..c4de167a84 100644 --- a/play-services-maps-core-vtm/src/main/java/org/microg/gms/maps/vtm/markup/TileOverlayImpl.java +++ b/play-services-maps-core-vtm/src/main/java/org/microg/gms/maps/vtm/markup/TileOverlayImpl.java @@ -16,7 +16,72 @@ package org.microg.gms.maps.vtm.markup; +import android.os.RemoteException; import com.google.android.gms.maps.model.internal.ITileOverlayDelegate; public class TileOverlayImpl extends ITileOverlayDelegate.Stub { + @Override + public void remove() throws RemoteException { + + } + + @Override + public void clearTileCache() throws RemoteException { + + } + + @Override + public String getId() throws RemoteException { + return null; + } + + @Override + public void setZIndex(float zIndex) throws RemoteException { + + } + + @Override + public float getZIndex() throws RemoteException { + return 0; + } + + @Override + public void setVisible(boolean visible) throws RemoteException { + + } + + @Override + public boolean isVisible() throws RemoteException { + return false; + } + + @Override + public boolean equalsRemote(ITileOverlayDelegate other) throws RemoteException { + return false; + } + + @Override + public int hashCodeRemote() throws RemoteException { + return 0; + } + + @Override + public void setFadeIn(boolean fadeIn) throws RemoteException { + + } + + @Override + public boolean getFadeIn() throws RemoteException { + return false; + } + + @Override + public void setTransparency(float transparency) throws RemoteException { + + } + + @Override + public float getTransparency() throws RemoteException { + return 0; + } } diff --git a/play-services-maps/src/main/aidl/com/google/android/gms/maps/internal/IGoogleMapDelegate.aidl b/play-services-maps/src/main/aidl/com/google/android/gms/maps/internal/IGoogleMapDelegate.aidl index 0e4434451d..f0e30d5bc4 100644 --- a/play-services-maps/src/main/aidl/com/google/android/gms/maps/internal/IGoogleMapDelegate.aidl +++ b/play-services-maps/src/main/aidl/com/google/android/gms/maps/internal/IGoogleMapDelegate.aidl @@ -13,15 +13,22 @@ import com.google.android.gms.maps.internal.IOnCameraIdleListener; import com.google.android.gms.maps.internal.IOnCameraMoveCanceledListener; import com.google.android.gms.maps.internal.IOnCameraMoveListener; import com.google.android.gms.maps.internal.IOnCameraMoveStartedListener; +import com.google.android.gms.maps.internal.IOnCircleClickListener; import com.google.android.gms.maps.internal.IOnMapClickListener; import com.google.android.gms.maps.internal.IOnMapLongClickListener; import com.google.android.gms.maps.internal.IOnMarkerClickListener; import com.google.android.gms.maps.internal.IOnMarkerDragListener; import com.google.android.gms.maps.internal.IOnInfoWindowClickListener; +import com.google.android.gms.maps.internal.IOnInfoWindowCloseListener; +import com.google.android.gms.maps.internal.IOnInfoWindowLongClickListener; import com.google.android.gms.maps.internal.IInfoWindowAdapter; import com.google.android.gms.maps.internal.IOnMapLoadedCallback; import com.google.android.gms.maps.internal.IOnMyLocationChangeListener; +import com.google.android.gms.maps.internal.IOnMyLocationClickListener; import com.google.android.gms.maps.internal.IOnMyLocationButtonClickListener; +import com.google.android.gms.maps.internal.IOnGroundOverlayClickListener; +import com.google.android.gms.maps.internal.IOnPolygonClickListener; +import com.google.android.gms.maps.internal.IOnPolylineClickListener; import com.google.android.gms.maps.internal.ISnapshotReadyCallback; import com.google.android.gms.maps.model.CircleOptions; import com.google.android.gms.maps.model.GroundOverlayOptions; @@ -114,18 +121,18 @@ interface IGoogleMapDelegate { void setContentDescription(String desc) = 60; - //void snapshotForTest(ISnapshotReadyCallback callback) = 70; + void snapshotForTest(ISnapshotReadyCallback callback) = 70; //void setPoiClickListener(IOnPoiClickListener listener) = 79; void onEnterAmbient(in Bundle bundle) = 80; void onExitAmbient() = 81; - //void setOnGroundOverlayClickListener(IOnGroundOverlayClickListener listener) = 82; - //void setInfoWindowLongClickListener(IOnInfoWindowLongClickListener listener) = 83; - //void setPolygonClickListener(IOnPolygonClickListener listener) = 84; - //void setInfoWindowCloseListener(IOnInfoWindowCloseListener listener) = 85; - //void setPolylineClickListener(IOnPolylineClickListener listener) = 86; - //void setCircleClickListener(IOnCircleClickListener listener) = 88; + void setOnGroundOverlayClickListener(IOnGroundOverlayClickListener listener) = 82; + void setOnInfoWindowLongClickListener(IOnInfoWindowLongClickListener listener) = 83; + void setOnPolygonClickListener(IOnPolygonClickListener listener) = 84; + void setOnInfoWindowCloseListener(IOnInfoWindowCloseListener listener) = 85; + void setOnPolylineClickListener(IOnPolylineClickListener listener) = 86; + void setOnCircleClickListener(IOnCircleClickListener listener) = 88; boolean setMapStyle(in MapStyleOptions options) = 90; void setMinZoomPreference(float minZoom) = 91; @@ -141,5 +148,5 @@ interface IGoogleMapDelegate { void onStart() = 100; void onStop() = 101; - //void setOnMyLocationClickListener(IOnMyLocationClickListener listener) = 106; + void setOnMyLocationClickListener(IOnMyLocationClickListener listener) = 106; } diff --git a/play-services-maps/src/main/aidl/com/google/android/gms/maps/internal/IOnCircleClickListener.aidl b/play-services-maps/src/main/aidl/com/google/android/gms/maps/internal/IOnCircleClickListener.aidl new file mode 100644 index 0000000000..cd7ad60e6c --- /dev/null +++ b/play-services-maps/src/main/aidl/com/google/android/gms/maps/internal/IOnCircleClickListener.aidl @@ -0,0 +1,7 @@ +package com.google.android.gms.maps.internal; + +import com.google.android.gms.maps.model.internal.ICircleDelegate; + +interface IOnCircleClickListener { + void onCircleClick(ICircleDelegate circle); +} diff --git a/play-services-maps/src/main/aidl/com/google/android/gms/maps/internal/IOnGroundOverlayClickListener.aidl b/play-services-maps/src/main/aidl/com/google/android/gms/maps/internal/IOnGroundOverlayClickListener.aidl new file mode 100644 index 0000000000..41d23be060 --- /dev/null +++ b/play-services-maps/src/main/aidl/com/google/android/gms/maps/internal/IOnGroundOverlayClickListener.aidl @@ -0,0 +1,7 @@ +package com.google.android.gms.maps.internal; + +import com.google.android.gms.maps.model.internal.IGroundOverlayDelegate; + +interface IOnGroundOverlayClickListener { + void onGroundOverlayClick(IGroundOverlayDelegate groundOverlay); +} \ No newline at end of file diff --git a/play-services-maps/src/main/aidl/com/google/android/gms/maps/internal/IOnInfoWindowCloseListener.aidl b/play-services-maps/src/main/aidl/com/google/android/gms/maps/internal/IOnInfoWindowCloseListener.aidl new file mode 100644 index 0000000000..1ddae39c1b --- /dev/null +++ b/play-services-maps/src/main/aidl/com/google/android/gms/maps/internal/IOnInfoWindowCloseListener.aidl @@ -0,0 +1,7 @@ +package com.google.android.gms.maps.internal; + +import com.google.android.gms.maps.model.internal.IMarkerDelegate; + +interface IOnInfoWindowCloseListener { + void onInfoWindowClose(IMarkerDelegate marker); +} diff --git a/play-services-maps/src/main/aidl/com/google/android/gms/maps/internal/IOnInfoWindowLongClickListener.aidl b/play-services-maps/src/main/aidl/com/google/android/gms/maps/internal/IOnInfoWindowLongClickListener.aidl new file mode 100644 index 0000000000..536e55e994 --- /dev/null +++ b/play-services-maps/src/main/aidl/com/google/android/gms/maps/internal/IOnInfoWindowLongClickListener.aidl @@ -0,0 +1,7 @@ +package com.google.android.gms.maps.internal; + +import com.google.android.gms.maps.model.internal.IMarkerDelegate; + +interface IOnInfoWindowLongClickListener { + void onInfoWindowLongClick(IMarkerDelegate marker); +} diff --git a/play-services-maps/src/main/aidl/com/google/android/gms/maps/internal/IOnMyLocationButtonClickListener.aidl b/play-services-maps/src/main/aidl/com/google/android/gms/maps/internal/IOnMyLocationButtonClickListener.aidl index b10880dd02..00a08314b6 100644 --- a/play-services-maps/src/main/aidl/com/google/android/gms/maps/internal/IOnMyLocationButtonClickListener.aidl +++ b/play-services-maps/src/main/aidl/com/google/android/gms/maps/internal/IOnMyLocationButtonClickListener.aidl @@ -1,4 +1,5 @@ package com.google.android.gms.maps.internal; interface IOnMyLocationButtonClickListener { + boolean onMyLocationButtonClick(); } diff --git a/play-services-maps/src/main/aidl/com/google/android/gms/maps/internal/IOnMyLocationClickListener.aidl b/play-services-maps/src/main/aidl/com/google/android/gms/maps/internal/IOnMyLocationClickListener.aidl new file mode 100644 index 0000000000..07972a4a9b --- /dev/null +++ b/play-services-maps/src/main/aidl/com/google/android/gms/maps/internal/IOnMyLocationClickListener.aidl @@ -0,0 +1,7 @@ +package com.google.android.gms.maps.internal; + +import android.location.Location; + +interface IOnMyLocationClickListener { + void onMyLocationClick(in Location location); +} \ No newline at end of file diff --git a/play-services-maps/src/main/aidl/com/google/android/gms/maps/internal/IOnPolygonClickListener.aidl b/play-services-maps/src/main/aidl/com/google/android/gms/maps/internal/IOnPolygonClickListener.aidl new file mode 100644 index 0000000000..facaf421a4 --- /dev/null +++ b/play-services-maps/src/main/aidl/com/google/android/gms/maps/internal/IOnPolygonClickListener.aidl @@ -0,0 +1,7 @@ +package com.google.android.gms.maps.internal; + +import com.google.android.gms.maps.model.internal.IPolygonDelegate; + +interface IOnPolygonClickListener { + void onPolygonClick(IPolygonDelegate polygon); +} \ No newline at end of file diff --git a/play-services-maps/src/main/aidl/com/google/android/gms/maps/internal/IOnPolylineClickListener.aidl b/play-services-maps/src/main/aidl/com/google/android/gms/maps/internal/IOnPolylineClickListener.aidl new file mode 100644 index 0000000000..71520bddee --- /dev/null +++ b/play-services-maps/src/main/aidl/com/google/android/gms/maps/internal/IOnPolylineClickListener.aidl @@ -0,0 +1,7 @@ +package com.google.android.gms.maps.internal; + +import com.google.android.gms.maps.model.internal.IPolylineDelegate; + +interface IOnPolylineClickListener { + void onPolylineClick(IPolylineDelegate polyline); +} \ No newline at end of file diff --git a/play-services-maps/src/main/aidl/com/google/android/gms/maps/model/Dash.aidl b/play-services-maps/src/main/aidl/com/google/android/gms/maps/model/Dash.aidl new file mode 100644 index 0000000000..256aedac9d --- /dev/null +++ b/play-services-maps/src/main/aidl/com/google/android/gms/maps/model/Dash.aidl @@ -0,0 +1,3 @@ +package com.google.android.gms.maps.model; + +parcelable Dash; diff --git a/play-services-maps/src/main/aidl/com/google/android/gms/maps/model/Dot.aidl b/play-services-maps/src/main/aidl/com/google/android/gms/maps/model/Dot.aidl new file mode 100644 index 0000000000..c8aa8afb0d --- /dev/null +++ b/play-services-maps/src/main/aidl/com/google/android/gms/maps/model/Dot.aidl @@ -0,0 +1,3 @@ +package com.google.android.gms.maps.model; + +parcelable Dot; diff --git a/play-services-maps/src/main/aidl/com/google/android/gms/maps/model/Gap.aidl b/play-services-maps/src/main/aidl/com/google/android/gms/maps/model/Gap.aidl new file mode 100644 index 0000000000..fd4fde3ae3 --- /dev/null +++ b/play-services-maps/src/main/aidl/com/google/android/gms/maps/model/Gap.aidl @@ -0,0 +1,3 @@ +package com.google.android.gms.maps.model; + +parcelable Gap; diff --git a/play-services-maps/src/main/aidl/com/google/android/gms/maps/model/internal/IBitmapDescriptorFactoryDelegate.aidl b/play-services-maps/src/main/aidl/com/google/android/gms/maps/model/internal/IBitmapDescriptorFactoryDelegate.aidl index dca49b6028..b2facea429 100644 --- a/play-services-maps/src/main/aidl/com/google/android/gms/maps/model/internal/IBitmapDescriptorFactoryDelegate.aidl +++ b/play-services-maps/src/main/aidl/com/google/android/gms/maps/model/internal/IBitmapDescriptorFactoryDelegate.aidl @@ -4,11 +4,11 @@ import android.graphics.Bitmap; import com.google.android.gms.dynamic.IObjectWrapper; interface IBitmapDescriptorFactoryDelegate { - IObjectWrapper fromResource(int resourceId); - IObjectWrapper fromAsset(String assetName); - IObjectWrapper fromFile(String fileName); - IObjectWrapper defaultMarker(); - IObjectWrapper defaultMarkerWithHue(float hue); - IObjectWrapper fromBitmap(in Bitmap bitmap); - IObjectWrapper fromPath(String absolutePath); + IObjectWrapper fromResource(int resourceId); + IObjectWrapper fromAsset(String assetName); + IObjectWrapper fromFile(String fileName); + IObjectWrapper defaultMarker(); + IObjectWrapper defaultMarkerWithHue(float hue); + IObjectWrapper fromBitmap(in Bitmap bitmap); + IObjectWrapper fromPath(String absolutePath); } diff --git a/play-services-maps/src/main/aidl/com/google/android/gms/maps/model/internal/ICircleDelegate.aidl b/play-services-maps/src/main/aidl/com/google/android/gms/maps/model/internal/ICircleDelegate.aidl index 719d8eab6b..1258687250 100644 --- a/play-services-maps/src/main/aidl/com/google/android/gms/maps/model/internal/ICircleDelegate.aidl +++ b/play-services-maps/src/main/aidl/com/google/android/gms/maps/model/internal/ICircleDelegate.aidl @@ -1,6 +1,8 @@ package com.google.android.gms.maps.model.internal; +import com.google.android.gms.dynamic.IObjectWrapper; import com.google.android.gms.maps.model.LatLng; +import com.google.android.gms.maps.model.PatternItem; interface ICircleDelegate { void remove(); @@ -9,16 +11,22 @@ interface ICircleDelegate { LatLng getCenter(); void setRadius(double radius); double getRadius(); - void setStrokeWidth(float width); - float getStrokeWidth(); - void setStrokeColor(int color); - int getStrokeColor(); - void setFillColor(int color); + void setStrokeWidth(float width); + float getStrokeWidth(); + void setStrokeColor(int color); + int getStrokeColor(); + void setFillColor(int color); int getFillColor(); void setZIndex(float zIndex); float getZIndex(); void setVisible(boolean visible); boolean isVisible(); - boolean equalsRemote(ICircleDelegate other); - int hashCodeRemote(); + boolean equalsRemote(ICircleDelegate other); + int hashCodeRemote(); + void setClickable(boolean clickable); + boolean isClickable(); + void setStrokePattern(in List items); + List getStrokePattern(); + void setTag(IObjectWrapper object); + IObjectWrapper getTag(); } diff --git a/play-services-maps/src/main/aidl/com/google/android/gms/maps/model/internal/IGroundOverlayDelegate.aidl b/play-services-maps/src/main/aidl/com/google/android/gms/maps/model/internal/IGroundOverlayDelegate.aidl index 0a1a063281..ccf64bc4f3 100644 --- a/play-services-maps/src/main/aidl/com/google/android/gms/maps/model/internal/IGroundOverlayDelegate.aidl +++ b/play-services-maps/src/main/aidl/com/google/android/gms/maps/model/internal/IGroundOverlayDelegate.aidl @@ -5,25 +5,29 @@ import com.google.android.gms.maps.model.LatLng; import com.google.android.gms.maps.model.LatLngBounds; interface IGroundOverlayDelegate { - void remove(); - String getId(); - void setPosition(in LatLng pos); - LatLng getPosition(); - void setDimension(float dimension); - void setDimensions(float width, float height); - float getWidth(); - float getHeight(); - void setPositionFromBounds(in LatLngBounds bounds); - LatLngBounds getBounds(); - void setBearing(float bearing); - float getBearing(); - void setZIndex(float zIndex); - float getZIndex(); - void setVisible(boolean visible); - boolean isVisible(); - void setTransparency(float transparency); - float getTransparency(); - boolean equalsRemote(IGroundOverlayDelegate other); - int hashCodeRemote(); - void todo(IObjectWrapper obj); + void remove() = 0; + String getId() = 1; + void setPosition(in LatLng pos) = 2; + LatLng getPosition() = 3; + void setDimension(float dimension) = 4; + void setDimensions(float width, float height) = 5; + float getWidth() = 6; + float getHeight() = 7; + void setPositionFromBounds(in LatLngBounds bounds) = 8; + LatLngBounds getBounds() = 9; + void setBearing(float bearing) = 10; + float getBearing() = 11; + void setZIndex(float zIndex) = 12; + float getZIndex() = 13; + void setVisible(boolean visible) = 14; + boolean isVisible() = 15; + void setTransparency(float transparency) = 16; + float getTransparency() = 17; + boolean equalsRemote(IGroundOverlayDelegate other) = 18; + int hashCodeRemote() = 19; + void setImage(IObjectWrapper img) = 20; + void setClickable(boolean clickable) = 21; + boolean isClickable() = 22; + void setTag(IObjectWrapper obj) = 23; + IObjectWrapper getTag() = 24; } diff --git a/play-services-maps/src/main/aidl/com/google/android/gms/maps/model/internal/IMarkerDelegate.aidl b/play-services-maps/src/main/aidl/com/google/android/gms/maps/model/internal/IMarkerDelegate.aidl index ca2d260379..86c9e9a996 100644 --- a/play-services-maps/src/main/aidl/com/google/android/gms/maps/model/internal/IMarkerDelegate.aidl +++ b/play-services-maps/src/main/aidl/com/google/android/gms/maps/model/internal/IMarkerDelegate.aidl @@ -17,21 +17,21 @@ interface IMarkerDelegate { void showInfoWindow(); void hideInfoWindow(); boolean isInfoWindowShown(); - void setVisible(boolean visible); - boolean isVisible(); - boolean equalsRemote(IMarkerDelegate other); - int hashCodeRemote(); - void setIcon(IObjectWrapper obj); - void setAnchor(float x, float y); - void setFlat(boolean flat); - boolean isFlat(); - void setRotation(float rotation); - float getRotation(); - void setInfoWindowAnchor(float x, float y); - void setAlpha(float alpha); - float getAlpha(); - void setZIndex(float zIndex); - float getZIndex(); - void setTag(IObjectWrapper obj); - IObjectWrapper getTag(); + void setVisible(boolean visible); + boolean isVisible(); + boolean equalsRemote(IMarkerDelegate other); + int hashCodeRemote(); + void setIcon(IObjectWrapper obj); + void setAnchor(float x, float y); + void setFlat(boolean flat); + boolean isFlat(); + void setRotation(float rotation); + float getRotation(); + void setInfoWindowAnchor(float x, float y); + void setAlpha(float alpha); + float getAlpha(); + void setZIndex(float zIndex); + float getZIndex(); + void setTag(IObjectWrapper obj); + IObjectWrapper getTag(); } diff --git a/play-services-maps/src/main/aidl/com/google/android/gms/maps/model/internal/IPolygonDelegate.aidl b/play-services-maps/src/main/aidl/com/google/android/gms/maps/model/internal/IPolygonDelegate.aidl index ef1d273abd..e0fe97f8e3 100644 --- a/play-services-maps/src/main/aidl/com/google/android/gms/maps/model/internal/IPolygonDelegate.aidl +++ b/play-services-maps/src/main/aidl/com/google/android/gms/maps/model/internal/IPolygonDelegate.aidl @@ -12,30 +12,30 @@ import com.google.android.gms.maps.model.PatternItem; interface IPolygonDelegate { void remove() = 0; String getId() = 1; - void setPoints(in List points) = 2; - List getPoints() = 3; - void setHoles(in List holes) = 4; - List getHoles() = 5; - void setStrokeWidth(float width) = 6; - float getStrokeWidth() = 7; - void setStrokeColor(int color) = 8; - int getStrokeColor() = 9; - void setFillColor(int color) = 10; - int getFillColor() = 11; - void setZIndex(float zIndex) = 12; - float getZIndex() = 13; - void setVisible(boolean visible) = 14; - boolean isVisible() = 15; - void setGeodesic(boolean geod) = 16; - boolean isGeodesic() = 17; - boolean equalsRemote(IPolygonDelegate other) = 18; - int hashCodeRemote() = 19; - void setClickable(boolean click) = 20; - boolean isClickable() = 21; - void setStrokeJointType(int type) = 22; - int getStrokeJointType() = 23; - void setStrokePattern(in List items) = 24; - List getStrokePattern() = 25; - void setTag(IObjectWrapper obj) = 26; - IObjectWrapper getTag() = 27; + void setPoints(in List points) = 2; + List getPoints() = 3; + void setHoles(in List holes) = 4; + List getHoles() = 5; + void setStrokeWidth(float width) = 6; + float getStrokeWidth() = 7; + void setStrokeColor(int color) = 8; + int getStrokeColor() = 9; + void setFillColor(int color) = 10; + int getFillColor() = 11; + void setZIndex(float zIndex) = 12; + float getZIndex() = 13; + void setVisible(boolean visible) = 14; + boolean isVisible() = 15; + void setGeodesic(boolean geod) = 16; + boolean isGeodesic() = 17; + boolean equalsRemote(IPolygonDelegate other) = 18; + int hashCodeRemote() = 19; + void setClickable(boolean click) = 20; + boolean isClickable() = 21; + void setStrokeJointType(int type) = 22; + int getStrokeJointType() = 23; + void setStrokePattern(in List items) = 24; + List getStrokePattern() = 25; + void setTag(IObjectWrapper obj) = 26; + IObjectWrapper getTag() = 27; } diff --git a/play-services-maps/src/main/aidl/com/google/android/gms/maps/model/internal/IPolylineDelegate.aidl b/play-services-maps/src/main/aidl/com/google/android/gms/maps/model/internal/IPolylineDelegate.aidl index 0e957b2cab..39c472a6f4 100644 --- a/play-services-maps/src/main/aidl/com/google/android/gms/maps/model/internal/IPolylineDelegate.aidl +++ b/play-services-maps/src/main/aidl/com/google/android/gms/maps/model/internal/IPolylineDelegate.aidl @@ -1,39 +1,40 @@ package com.google.android.gms.maps.model.internal; import com.google.android.gms.dynamic.IObjectWrapper; +import com.google.android.gms.maps.model.Cap; import com.google.android.gms.maps.model.LatLng; import com.google.android.gms.maps.model.PatternItem; import com.google.android.gms.maps.model.StyleSpan; interface IPolylineDelegate { - void remove() = 0; - String getId() = 1; - void setPoints(in List points) = 2; - List getPoints() = 3; - void setWidth(float width) = 4; - float getWidth() = 5; - void setColor(int color) = 6; - int getColor() = 7; - void setZIndex(float zIndex) = 8; - float getZIndex() = 9; - void setVisible(boolean visible) = 10; - boolean isVisible() = 11; - void setGeodesic(boolean geod) = 12; - boolean isGeodesic() = 13; - boolean equalsRemote(IPolylineDelegate other) = 14; - int hashCodeRemote() = 15; - void setClickable(boolean clickable) = 16; + void remove() = 0; + String getId() = 1; + void setPoints(in List points) = 2; + List getPoints() = 3; + void setWidth(float width) = 4; + float getWidth() = 5; + void setColor(int color) = 6; + int getColor() = 7; + void setZIndex(float zIndex) = 8; + float getZIndex() = 9; + void setVisible(boolean visible) = 10; + boolean isVisible() = 11; + void setGeodesic(boolean geod) = 12; + boolean isGeodesic() = 13; + boolean equalsRemote(IPolylineDelegate other) = 14; + int hashCodeRemote() = 15; + void setClickable(boolean clickable) = 16; boolean isClickable() = 17; - //void setStartCap(Cap startCap) = 18; - //Cap getStartCap() = 19; - //void setEndCap(Cap endCap) = 20; - //Cap getEndCap() = 21; - void setJointType(int jointType) = 22; - int getJointType() = 23; - void setPattern(in List pattern) = 24; - List getPattern() = 25; - void setTag(IObjectWrapper tag) = 26; - IObjectWrapper getTag() = 27; - //void setSpans(in List spans) = 28; - //List getSpans() = 29 + void setStartCap(in Cap startCap) = 18; + Cap getStartCap() = 19; + void setEndCap(in Cap endCap) = 20; + Cap getEndCap() = 21; + void setJointType(int jointType) = 22; + int getJointType() = 23; + void setPattern(in List pattern) = 24; + List getPattern() = 25; + void setTag(IObjectWrapper tag) = 26; + IObjectWrapper getTag() = 27; + //void setSpans(in List spans) = 28; + //List getSpans() = 29 } diff --git a/play-services-maps/src/main/aidl/com/google/android/gms/maps/model/internal/ITileOverlayDelegate.aidl b/play-services-maps/src/main/aidl/com/google/android/gms/maps/model/internal/ITileOverlayDelegate.aidl index 417cb02409..89f9d72292 100644 --- a/play-services-maps/src/main/aidl/com/google/android/gms/maps/model/internal/ITileOverlayDelegate.aidl +++ b/play-services-maps/src/main/aidl/com/google/android/gms/maps/model/internal/ITileOverlayDelegate.aidl @@ -1,4 +1,17 @@ package com.google.android.gms.maps.model.internal; interface ITileOverlayDelegate { + void remove() = 0; + void clearTileCache() = 1; + String getId() = 2; + void setZIndex(float zIndex) = 3; + float getZIndex() = 4; + void setVisible(boolean visible) = 5; + boolean isVisible() = 6; + boolean equalsRemote(ITileOverlayDelegate other) = 7; + int hashCodeRemote() = 8; + void setFadeIn(boolean fadeIn) = 9; + boolean getFadeIn() = 10; + void setTransparency(float transparency) = 11; + float getTransparency() = 12; } diff --git a/play-services-maps/src/main/java/com/google/android/gms/maps/CameraUpdate.java b/play-services-maps/src/main/java/com/google/android/gms/maps/CameraUpdate.java new file mode 100644 index 0000000000..99b8be4052 --- /dev/null +++ b/play-services-maps/src/main/java/com/google/android/gms/maps/CameraUpdate.java @@ -0,0 +1,27 @@ +/* + * 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.maps; + +import com.google.android.gms.dynamic.IObjectWrapper; +import org.microg.gms.common.PublicApi; + +/** + * Defines a camera move. An object of this type can be used to modify a map's camera by calling {@link GoogleMap#animateCamera(CameraUpdate)}, + * {@link GoogleMap#animateCamera(CameraUpdate, GoogleMap.CancelableCallback)} or {@link GoogleMap#moveCamera(CameraUpdate)}. + *

+ * To obtain a {@link CameraUpdate} use the factory class {@link CameraUpdateFactory}. + */ +@PublicApi +public class CameraUpdate { + IObjectWrapper wrapped; + + CameraUpdate(IObjectWrapper wrapped) { + this.wrapped = wrapped; + } +} diff --git a/play-services-maps/src/main/java/com/google/android/gms/maps/CameraUpdateFactory.java b/play-services-maps/src/main/java/com/google/android/gms/maps/CameraUpdateFactory.java new file mode 100644 index 0000000000..7f331b7aba --- /dev/null +++ b/play-services-maps/src/main/java/com/google/android/gms/maps/CameraUpdateFactory.java @@ -0,0 +1,48 @@ +/* + * 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.maps; + +import android.os.RemoteException; +import androidx.annotation.NonNull; +import com.google.android.gms.maps.internal.ICameraUpdateFactoryDelegate; +import com.google.android.gms.maps.model.CameraPosition; +import org.microg.gms.common.Hide; + +/** + * A class containing methods for creating {@link CameraUpdate} objects that change a map's camera. To modify the map's camera, call + * {@link GoogleMap#animateCamera(CameraUpdate)}, {@link GoogleMap#animateCamera(CameraUpdate, GoogleMap.CancelableCallback)} or + * {@link GoogleMap#moveCamera(CameraUpdate)}, using a {@link CameraUpdate} object created with this class. + */ +public class CameraUpdateFactory { + private static ICameraUpdateFactoryDelegate delegate; + @Hide + public static void setDelegate(@NonNull ICameraUpdateFactoryDelegate delegate) { + CameraUpdateFactory.delegate = delegate; + } + private static ICameraUpdateFactoryDelegate getDelegate() { + if (delegate == null) throw new IllegalStateException("CameraUpdateFactory is not initialized"); + return delegate; + } + + /** + * Returns a {@link CameraUpdate} that moves the camera to a specified {@link CameraPosition}. In effect, this creates a transformation from the + * {@link CameraPosition} object's latitude, longitude, zoom level, bearing and tilt. + * + * @param cameraPosition The requested position. Must not be {@code null}. + * @return a {@link CameraUpdate} containing the transformation. + */ + public static CameraUpdate newCameraPosition(@NonNull CameraPosition cameraPosition) { + try { + return new CameraUpdate(getDelegate().newCameraPosition(cameraPosition)); + } catch (RemoteException e) { + throw new RuntimeException(e); + } + } + +} diff --git a/play-services-maps/src/main/java/com/google/android/gms/maps/GoogleMap.java b/play-services-maps/src/main/java/com/google/android/gms/maps/GoogleMap.java new file mode 100644 index 0000000000..4adb946191 --- /dev/null +++ b/play-services-maps/src/main/java/com/google/android/gms/maps/GoogleMap.java @@ -0,0 +1,199 @@ +/* + * 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.maps; + +import android.os.RemoteException; +import android.view.View; +import androidx.annotation.Nullable; +import com.google.android.gms.maps.internal.ICancelableCallback; +import com.google.android.gms.maps.internal.IGoogleMapDelegate; +import com.google.android.gms.maps.model.CameraPosition; +import com.google.android.gms.maps.model.Circle; +import com.google.android.gms.maps.model.CircleOptions; +import com.google.android.gms.maps.model.RuntimeRemoteException; +import org.microg.gms.common.Hide; + +/** + * This is the main class of the Google Maps SDK for Android and is the entry point for all methods related to the map. You cannot instantiate a + * {@link GoogleMap} object directly, rather, you must obtain one from the {@code getMapAsync()} method on a {@link MapFragment} or {@link MapView} that you + * have added to your application. + *

+ * Note: Similar to a {@link View} object, a {@link GoogleMap} can only be read and modified from the Android UI thread. Calling {@link GoogleMap} methods from + * another thread will result in an exception. + *

+ * You can adjust the viewpoint of a map by changing the position of the camera (as opposed to moving the map). You can use the map's camera to set parameters + * such as location, zoom level, tilt angle, and bearing. + */ +public class GoogleMap { + /** + * No base map tiles. + */ + public static final int MAP_TYPE_NONE = 0; + /** + * Basic maps. + */ + public static final int MAP_TYPE_NORMAL = 1; + /** + * Satellite maps with no labels. + */ + public static final int MAP_TYPE_SATELLITE = 2; + /** + * Terrain maps. + */ + public static final int MAP_TYPE_TERRAIN = 3; + /** + * Satellite maps with a transparent layer of major streets. + */ + public static final int MAP_TYPE_HYBRID = 4; + + private final IGoogleMapDelegate delegate; + + @Hide + public GoogleMap(IGoogleMapDelegate delegate) { + this.delegate = delegate; + } + + private IGoogleMapDelegate getDelegate() { + return delegate; + } + + public Circle addCircle(CircleOptions options) { + try { + return new Circle(getDelegate().addCircle(options)); + } catch (RemoteException e) { + throw new RuntimeRemoteException(e); + } + } + + /** + * Animates the movement of the camera from the current position to the position defined in the update. During the animation, a call to + * {@link #getCameraPosition()} returns an intermediate location of the camera. + *

+ * See CameraUpdateFactory for a set of updates. + * + * @param update The change that should be applied to the camera. + */ + public void animateCamera(CameraUpdate update) { + try { + getDelegate().animateCamera(update.wrapped); + } catch (RemoteException e) { + throw new RuntimeRemoteException(e); + } + } + + /** + * Animates the movement of the camera from the current position to the position defined in the update and calls an optional callback on completion. See + * {@link CameraUpdateFactory} for a set of updates. + *

+ * During the animation, a call to {@link #getCameraPosition()} returns an intermediate location of the camera. + * + * @param update The change that should be applied to the camera. + * @param callback The callback to invoke from the Android UI thread when the animation stops. If the animation completes normally, + * {@link GoogleMap.CancelableCallback#onFinish()} is called; otherwise, {@link GoogleMap.CancelableCallback#onCancel()} is called. Do not + * update or animate the camera from within {@link GoogleMap.CancelableCallback#onCancel()}. + */ + public void animateCamera(CameraUpdate update, @Nullable GoogleMap.CancelableCallback callback) { + try { + getDelegate().animateCameraWithCallback(update.wrapped, new ICancelableCallback.Stub() { + @Override + public void onFinish() throws RemoteException { + if (callback != null) { + callback.onFinish(); + } + } + + @Override + public void onCancel() throws RemoteException { + if (callback != null) { + callback.onCancel(); + } + } + }); + } catch (RemoteException e) { + throw new RuntimeRemoteException(e); + } + } + + /** + * Gets the current position of the camera. + *

+ * The {@link CameraPosition} returned is a snapshot of the current position, and will not automatically update when the camera moves. + * + * @return The current position of the Camera. + */ + public CameraPosition getCameraPosition() { + try { + return getDelegate().getCameraPosition(); + } catch (RemoteException e) { + throw new RuntimeRemoteException(e); + } + } + + /** + * Repositions the camera according to the instructions defined in the update. The move is instantaneous, and a subsequent {@link #getCameraPosition()} will + * reflect the new position. See {@link CameraUpdateFactory} for a set of updates. + * + * @param update The change that should be applied to the camera. + */ + public void moveCamera(CameraUpdate update) { + try { + getDelegate().moveCamera(update.wrapped); + } catch (RemoteException e) { + throw new RuntimeRemoteException(e); + } + } + + /** + * A callback interface for reporting when a task is complete or canceled. + */ + public interface CancelableCallback { + /** + * Invoked when a task is canceled. + */ + void onCancel(); + + /** + * Invoked when a task is complete. + */ + void onFinish(); + } + + /** + * Callback interface for when the camera motion starts. + */ + public interface OnCameraMoveStartedListener { + /** + * Camera motion initiated in response to user gestures on the map. For example: pan, tilt, pinch to zoom, or rotate. + */ + int REASON_GESTURE = 1; + /** + * Non-gesture animation initiated in response to user actions. For example: zoom buttons, my location button, or marker clicks. + */ + int REASON_API_ANIMATION = 2; + /** + * Developer initiated animation. + */ + int REASON_DEVELOPER_ANIMATION = 3; + + /** + * Called when the camera starts moving after it has been idle or when the reason for camera motion has changed. + * Do not update or animate the camera from within this method. + *

+ * This is called on the Android UI thread. + * + * @param reason The reason for the camera change. Possible values: + *

    + *
  • {@link #REASON_GESTURE}: User gestures on the map.
  • + *
  • {@link #REASON_API_ANIMATION}: Default animations resulting from user interaction.
  • + *
  • {@link #REASON_DEVELOPER_ANIMATION}: Developer animations.
  • + *
+ */ + void onCameraMoveStarted(int reason); + } +} diff --git a/play-services-maps/src/main/java/com/google/android/gms/maps/GoogleMapOptions.java b/play-services-maps/src/main/java/com/google/android/gms/maps/GoogleMapOptions.java index 405f31d117..448c078f92 100644 --- a/play-services-maps/src/main/java/com/google/android/gms/maps/GoogleMapOptions.java +++ b/play-services-maps/src/main/java/com/google/android/gms/maps/GoogleMapOptions.java @@ -1,132 +1,445 @@ /* - * Copyright (C) 2013-2017 microG Project Team - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * SPDX-FileCopyrightText: 2015 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.maps; +import android.content.Context; +import android.content.res.TypedArray; +import android.util.AttributeSet; +import android.view.SurfaceView; +import androidx.annotation.ColorInt; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import com.google.android.gms.maps.model.CameraPosition; import com.google.android.gms.maps.model.LatLngBounds; - import org.microg.safeparcel.AutoSafeParcelable; -import org.microg.safeparcel.SafeParceled; +/** + * Defines configuration GoogleMapOptions for a {@link GoogleMap}. These options can be used when adding a map to your application programmatically + * (as opposed to via XML). If you are using a {@link MapFragment}, you can pass these options in using the static factory method + * {@link MapFragment#newInstance(GoogleMapOptions)}. If you are using a {@link MapView}, you can pass these options in using the constructor + * {@link MapView#MapView(Context, GoogleMapOptions)}. + *

+ * If you add a map using XML, then you can apply these options using custom XML tags. + */ public final class GoogleMapOptions extends AutoSafeParcelable { - @SafeParceled(1) + @Field(1) private int versionCode; - @SafeParceled(2) + @Field(2) private int zOrderOnTop; - @SafeParceled(3) + @Field(3) private boolean useViewLifecycleInFragment; - @SafeParceled(4) + @Field(4) private int mapType; - @SafeParceled(5) + @Field(5) + @Nullable private CameraPosition camera; - @SafeParceled(6) + @Field(6) private boolean zoomControlsEnabled; - @SafeParceled(7) + @Field(7) private boolean compassEnabled; - @SafeParceled(8) + @Field(8) private boolean scrollGesturesEnabled = true; - @SafeParceled(9) + @Field(9) private boolean zoomGesturesEnabled = true; - @SafeParceled(10) + @Field(10) private boolean tiltGesturesEnabled = true; - @SafeParceled(11) + @Field(11) private boolean rotateGesturesEnabled = true; - @SafeParceled(12) - private boolean liteMode = false; - @SafeParceled(14) + @Field(12) + private int liteMode = 0; + @Field(14) private boolean mapToobarEnabled = false; - @SafeParceled(15) + @Field(15) private boolean ambientEnabled = false; - @SafeParceled(16) + @Field(16) private float minZoom; - @SafeParceled(17) + @Field(17) private float maxZoom; - @SafeParceled(18) + @Field(18) + @Nullable private LatLngBounds boundsForCamera; - @SafeParceled(19) + @Field(19) private boolean scrollGesturesEnabledDuringRotateOrZoom = true; + @Field(20) + @ColorInt + @Nullable + private Integer backgroundColor; + @Field(21) + @Nullable + private String mapId; + /** + * Creates a new GoogleMapOptions object. + */ public GoogleMapOptions() { } + /** + * Creates a {@code GoogleMapsOptions} from the {@link AttributeSet}. + */ + public static @Nullable GoogleMapOptions createFromAttributes(@Nullable Context context, @Nullable AttributeSet attrs) { + if (context == null || attrs == null) return null; + GoogleMapOptions options = new GoogleMapOptions(); + TypedArray obtainAttributes = context.getResources().obtainAttributes(attrs, R.styleable.MapAttrs); + // TODO: Handle attributes + if (obtainAttributes.hasValue(R.styleable.MapAttrs_mapType)) { +// options.mapType(obtainAttributes.getInt(R.styleable.MapAttrs_mapType, -1)); + } + obtainAttributes.recycle(); + return options; + } + + /** + * Specifies whether ambient-mode styling should be enabled. The default value is {@code false}. + * When enabled, ambient-styled maps can be displayed when an Ambiactive device enters ambient mode. + */ + @NonNull + public GoogleMapOptions ambientEnabled(boolean enabled) { + this.ambientEnabled = enabled; + return this; + } + + /** + * Sets the map background color. This is the color that shows underneath map tiles and displays whenever the renderer does not have a tile available for + * a portion of the viewport. + * + * @param backgroundColor the color to show in the background of the map. If {@code null} is supplied then the map uses the default renderer background color. + */ + @NonNull + public GoogleMapOptions backgroundColor(@Nullable @ColorInt Integer backgroundColor) { + this.backgroundColor = backgroundColor; + return this; + } + + /** + * Specifies the initial camera position for the map (specify null to use the default camera position). + */ + @NonNull + public GoogleMapOptions camera(@Nullable CameraPosition camera) { + this.camera = camera; + return this; + } + + /** + * Specifies whether the compass should be enabled. See {@link UiSettings#setCompassEnabled(boolean)} for more details. The default value is {@code true}. + */ + @NonNull + public GoogleMapOptions compassEnabled(boolean enabled) { + this.compassEnabled = enabled; + return this; + } + + /** + * Specifies a LatLngBounds to constrain the camera target, so that when users scroll and pan the map, the camera target does not move outside these bounds. + *

+ * See {@link GoogleMap#setLatLngBoundsForCameraTarget(LatLngBounds)} for details. + */ + @NonNull + public GoogleMapOptions latLngBoundsForCameraTarget(@Nullable LatLngBounds llbounds) { + this.boundsForCamera = llbounds; + return this; + } + + /** + * Specifies whether the map should be created in lite mode. The default value is {@code false}. If lite mode is enabled, maps will load as static images. + * This improves performance in the case where a lot of maps need to be displayed at the same time, for example in a scrolling list, however lite-mode maps + * cannot be panned or zoomed by the user, or tilted or rotated at all. + */ + @NonNull + public GoogleMapOptions liteMode(boolean enabled) { + this.liteMode = enabled ? 1 : 0; + return this; + } + + /** + * Specifies the map's ID. + */ + @NonNull + public GoogleMapOptions mapId(@NonNull String mapId) { + this.mapId = mapId; + return this; + } + + /** + * Specifies whether the mapToolbar should be enabled. See {@link UiSettings#setMapToolbarEnabled(boolean)} for more details. + * The default value is {@code true}. + */ + @NonNull + public GoogleMapOptions mapToolbarEnabled(boolean enabled) { + this.mapToobarEnabled = enabled; + return this; + } + + /** + * Specifies a change to the initial map type. + */ + @NonNull + public GoogleMapOptions mapType(int mapType) { + this.mapType = mapType; + return this; + } + + /** + * Specifies a preferred upper bound for camera zoom. + *

+ * See {@link GoogleMap#setMaxZoomPreference(float)} for details. + */ + @NonNull + public GoogleMapOptions maxZoomPreference(float maxZoomPreference) { + this.maxZoom = maxZoomPreference; + return this; + } + + /** + * Specifies a preferred lower bound for camera zoom. + *

+ * See {@link GoogleMap#setMinZoomPreference(float)} for details. + */ + @NonNull + public GoogleMapOptions minZoomPreference(float minZoomPreference) { + this.minZoom = minZoomPreference; + return this; + } + + /** + * Specifies whether rotate gestures should be enabled. See {@link UiSettings#setRotateGesturesEnabled(boolean)} for more details. + * The default value is {@code true}. + */ + @NonNull + public GoogleMapOptions rotateGesturesEnabled(boolean enabled) { + this.rotateGesturesEnabled = enabled; + return this; + } + + /** + * Specifies whether scroll gestures should be enabled. See {@link UiSettings#setScrollGesturesEnabled(boolean)} for more details. + * The default value is {@code true}. + */ + @NonNull + public GoogleMapOptions scrollGesturesEnabled(boolean enabled) { + this.scrollGesturesEnabled = enabled; + return this; + } + + /** + * Specifies whether scroll gestures should be enabled during rotate and zoom gestures. + * See {@link UiSettings#setScrollGesturesEnabledDuringRotateOrZoom(boolean)} for more details. The default value is {@code true}. + */ + @NonNull + public GoogleMapOptions scrollGesturesEnabledDuringRotateOrZoom(boolean enabled) { + this.scrollGesturesEnabledDuringRotateOrZoom = enabled; + return this; + } + + /** + * Specifies whether tilt gestures should be enabled. See {@link UiSettings#setTiltGesturesEnabled(boolean)} for more details. + * The default value is {@code true}. + */ + @NonNull + public GoogleMapOptions tiltGesturesEnabled(boolean enabled) { + this.tiltGesturesEnabled = enabled; + return this; + } + + /** + * When using a {@link MapFragment}, this flag specifies whether the lifecycle of the map should be tied to the fragment's view or the fragment itself. + * The default value is {@code false}, tying the lifecycle of the map to the fragment. + *

+ * Using the lifecycle of the fragment allows faster rendering of the map when the fragment is detached and reattached, because the underlying GL context + * is preserved. This has the cost that detaching the fragment, but not destroying it, will not release memory used by the map. + *

+ * Using the lifecycle of a fragment's view means that a map is not reused when the fragment is detached and reattached. This will cause the map to + * re-render from scratch, which can take a few seconds. It also means that while a fragment is detached, and therefore has no view, all {@link GoogleMap} + * methods will throw {@link NullPointerException}. + */ + @NonNull + public GoogleMapOptions useViewLifecycleInFragment(boolean useViewLifecycleInFragment) { + this.useViewLifecycleInFragment = useViewLifecycleInFragment; + return this; + } + + /** + * Control whether the map view's surface is placed on top of its window. See {@link SurfaceView#setZOrderOnTop(boolean)} for more details. + * Note that this will cover all other views that could appear on the map (e.g., the zoom controls, the my location button). + */ + @NonNull + public GoogleMapOptions zOrderOnTop(boolean zOrderOnTop) { + this.zOrderOnTop = zOrderOnTop ? 1 : 0; + return this; + } + + /** + * Specifies whether the zoom controls should be enabled. See {@link UiSettings#setZoomControlsEnabled(boolean)} for more details. + * The default value is {@code true}. + */ + @NonNull + public GoogleMapOptions zoomControlsEnabled(boolean enabled) { + this.zoomControlsEnabled = enabled; + return this; + } + + /** + * Specifies whether zoom gestures should be enabled. See {@link UiSettings#setZoomGesturesEnabled(boolean)} for more details. + * The default value is {@code true}. + */ + @NonNull + public GoogleMapOptions zoomGesturesEnabled(boolean enabled) { + this.zoomGesturesEnabled = enabled; + return this; + } + + /** + * @return the {@code ambientEnabled} option, or {@code null} if unspecified. + */ public Boolean getAmbientEnabled() { return ambientEnabled; } + /** + * @return the current backgroundColor for the map, or null if unspecified. + */ + public Integer getBackgroundColor() { + return backgroundColor; + } + + /** + * @return the camera option, or {@code null} if unspecified. + */ + @Nullable public CameraPosition getCamera() { return camera; } + /** + * @return the {@code compassEnabled} option, or {@code null} if unspecified. + */ + @Nullable public Boolean getCompassEnabled() { return compassEnabled; } + /** + * @return the {@code LatLngBounds} used to constrain the camera target, or {@code null} if unspecified. + */ + @Nullable public LatLngBounds getLatLngBoundsForCameraTarget() { return boundsForCamera; } - public Boolean getLiteMode() { - return liteMode; + /** + * @return the {@code liteMode} option, or {@code null} if unspecified. + */ + @Nullable + public boolean getLiteMode() { + // Is encoded as `-1` if null, `0` if false, `1` if true. The default is false. + return liteMode == 1; + } + + /** + * @return the {@code mapId}, or {@code null} if unspecified. + */ + @Nullable + public String getMapId() { + return mapId; } + /** + * @return the {@code mapToolbarEnabled} option, or {@code null} if unspecified. + */ + @Nullable public Boolean getMapToolbarEnabled() { return mapToobarEnabled; } + /** + * @return the {@code mapType} option, or -1 if unspecified. + */ public int getMapType() { return mapType; } + /** + * @return the maximum zoom level preference, or {@code null} if unspecified. + */ + @Nullable public Float getMaxZoomPreference() { return maxZoom; } + /** + * @return the minimum zoom level preference, or {@code null} if unspecified. + */ + @Nullable public Float getMinZoomPreference() { return minZoom; } + /** + * @return the {@code rotateGesturesEnabled} option, or {@code null} if unspecified. + */ + @Nullable public Boolean getRotateGesturesEnabled() { return rotateGesturesEnabled; } + /** + * @return the {@code scrollGesturesEnabled} option, or {@code null} if unspecified. + */ + @Nullable public Boolean getScrollGesturesEnabled() { return scrollGesturesEnabled; } + /** + * @return the {@code scrollGesturesEnabledDuringRotateOrZoom} option, or {@code null} if unspecified. + */ + @Nullable public Boolean getScrollGesturesEnabledDuringRotateOrZoom() { return scrollGesturesEnabledDuringRotateOrZoom; } + /** + * @return the {@code tiltGesturesEnabled} option, or {@code null} if unspecified. + */ + @Nullable public Boolean getTiltGesturesEnabled() { return tiltGesturesEnabled; } + /** + * @return the {@code useViewLifecycleInFragment} option, or {@code null} if unspecified. + */ + @Nullable public Boolean getUseViewLifecycleInFragment() { return useViewLifecycleInFragment; } + /** + * @return the {@code zOrderOnTop} option, or {@code null} if unspecified. + */ + @Nullable public Boolean getZOrderOnTop() { return zOrderOnTop == 1; // TODO } + /** + * @return the {@code zoomControlsEnabled} option, or {@code null} if unspecified. + */ + @Nullable public Boolean getZoomControlsEnabled() { return zoomControlsEnabled; } + /** + * @return the {@code zoomGesturesEnabled} option, or {@code null} if unspecified. + */ + @Nullable public Boolean getZoomGesturesEnabled() { return zoomGesturesEnabled; } diff --git a/play-services-maps/src/main/java/com/google/android/gms/maps/MapFragment.java b/play-services-maps/src/main/java/com/google/android/gms/maps/MapFragment.java new file mode 100644 index 0000000000..f1999798df --- /dev/null +++ b/play-services-maps/src/main/java/com/google/android/gms/maps/MapFragment.java @@ -0,0 +1,11 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.maps; + +import android.app.Fragment; + +public class MapFragment extends Fragment { +} diff --git a/play-services-maps/src/main/java/com/google/android/gms/maps/MapView.java b/play-services-maps/src/main/java/com/google/android/gms/maps/MapView.java new file mode 100644 index 0000000000..890a1ea0fb --- /dev/null +++ b/play-services-maps/src/main/java/com/google/android/gms/maps/MapView.java @@ -0,0 +1,155 @@ +/* + * 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.maps; + +import android.app.Activity; +import android.app.Fragment; +import android.content.Context; +import android.os.Bundle; +import android.util.AttributeSet; +import android.widget.FrameLayout; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import org.microg.gms.maps.MapViewLifecycleHelper; + +/** + * A View which displays a map. When focused, it will capture keypresses and touch gestures to move the map. + *

+ * Users of this class must forward all the life cycle methods from the {@link Activity} or {@link Fragment} containing this view to the corresponding ones in + * this class. + *

+ * A {@link GoogleMap} must be acquired using {@link #getMapAsync(OnMapReadyCallback)}. + * The {@link MapView} automatically initializes the maps system and the view. + */ +public class MapView extends FrameLayout { + private final MapViewLifecycleHelper helper; + + public MapView(@NonNull Context context) { + super(context); + helper = new MapViewLifecycleHelper(this, context, null); + setClickable(true); + } + + public MapView(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + helper = new MapViewLifecycleHelper(this, context, GoogleMapOptions.createFromAttributes(context, attrs)); + setClickable(true); + } + + public MapView(@NonNull Context context, @NonNull AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + helper = new MapViewLifecycleHelper(this, context, GoogleMapOptions.createFromAttributes(context, attrs)); + setClickable(true); + } + + /** + * Constructs MapView with {@link GoogleMapOptions}. + * + * @param options configuration GoogleMapOptions for a {@link GoogleMap}, or {@code null} to use the default options. + */ + public MapView(@NonNull Context context, @Nullable GoogleMapOptions options) { + super(context); + helper = new MapViewLifecycleHelper(this, context, options); + setClickable(true); + } + + /** + * Returns a instance of the {@link GoogleMap} through the callback, ready to be used. + *

+ * Note that: + *

    + *
  • This method must be called from the main thread.
  • + *
  • The callback will be executed in the main thread.
  • + *
  • In the case where Google Play services is not installed on the user's device, the callback will not be triggered until the user installs it.
  • + *
  • The GoogleMap object provided by the callback is never null.
  • + *
+ * + * @param callback The callback object that will be triggered when the map is ready to be used. + */ + public void getMapAsync(OnMapReadyCallback callback) { + helper.getMapAsync(callback); + } + + /** + * You must call this method from the parent Activity/Fragment's corresponding method. + */ + public void onCreate(Bundle savedInstanceState) { + helper.onCreate(savedInstanceState); + } + + /** + * You must call this method from the parent Activity/Fragment's corresponding method. + */ + public void onDestroy() { + helper.onDestroy(); + } + + /** + * You must call this method from the parent WearableActivity's corresponding method. + */ + public void onEnterAmbient(Bundle ambientDetails) { + if (helper.getDelegate() != null) { + helper.getDelegate().onEnterAmbient(ambientDetails); + } + } + + /** + * You must call this method from the parent WearableActivity's corresponding method. + */ + public void onExitAmbient() { + if (helper.getDelegate() != null) { + helper.getDelegate().onExitAmbient(); + } + } + + /** + * You must call this method from the parent Activity/Fragment's corresponding method. + */ + public void onLowMemory() { + helper.onLowMemory(); + } + + /** + * You must call this method from the parent Activity/Fragment's corresponding method. + */ + public void onPause() { + helper.onPause(); + } + + /** + * You must call this method from the parent Activity/Fragment's corresponding method. + */ + public void onResume() { + helper.onResume(); + } + + /** + * You must call this method from the parent Activity/Fragment's corresponding method. + *

+ * Provides a {@link Bundle} to store the state of the View before it gets destroyed. + * It can later be retrieved when {@link #onCreate(Bundle)} is called again. + */ + public void onSaveInstanceState(Bundle outState) { + helper.onSaveInstanceState(outState); + } + + /** + * You must call this method from the parent Activity/Fragment's corresponding method. + */ + public void onStart() { + helper.onStart(); + } + + /** + * You must call this method from the parent Activity/Fragment's corresponding method. + */ + public void onStop() { + helper.onStop(); + } +} diff --git a/play-services-maps/src/main/java/com/google/android/gms/maps/MapsInitializer.java b/play-services-maps/src/main/java/com/google/android/gms/maps/MapsInitializer.java new file mode 100644 index 0000000000..499a4ebe22 --- /dev/null +++ b/play-services-maps/src/main/java/com/google/android/gms/maps/MapsInitializer.java @@ -0,0 +1,120 @@ +/* + * 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.maps; + +import android.app.Application; +import android.content.Context; +import android.os.Bundle; +import android.os.RemoteException; +import android.util.Log; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.android.gms.common.api.CommonStatusCodes; +import com.google.android.gms.dynamic.ObjectWrapper; +import com.google.android.gms.maps.internal.ICreator; +import com.google.android.gms.maps.model.BitmapDescriptorFactory; +import com.google.android.gms.maps.model.RuntimeRemoteException; +import org.microg.gms.maps.MapsContextLoader; + +/** + * Use this class to initialize the Maps SDK for Android if features need to be used before obtaining a map. + * It must be called because some classes such as BitmapDescriptorFactory and CameraUpdateFactory need to be initialized. + *

+ * If you are using {@link MapFragment} or {@link MapView} and have already obtained a (non-null) {@link GoogleMap} by calling {@code getMapAsync()} on either + * of these classes and waiting for the {@code onMapReady(GoogleMap map)} callback, then you do not need to worry about this class. + */ +public class MapsInitializer { + private static final String TAG = "MapsInitializer"; + private static boolean initialized = false; + private static Renderer renderer = Renderer.LEGACY; + + /** + * Initializes the Maps SDK for Android so that its classes are ready for use. If you are using {@link MapFragment} or {@link MapView} and have + * already obtained a (non-null) {@link GoogleMap} by calling {@code getMapAsync()} on either of these classes, then it is not necessary to call this. + * + * @param context Required to fetch the necessary SDK resources and code. Must not be {@code null}. + * @return A ConnectionResult error code. + */ + public static synchronized int initialize(@NonNull Context context) { + return initialize(context, null, null); + } + + /** + * Specifies which {@link MapsInitializer.Renderer} type you prefer to use to initialize the Maps SDK for Android, and provides a callback to receive the + * actual {@link MapsInitializer.Renderer} type. This call will initialize the Maps SDK for Android, so that its classes are ready for use. The callback + * will be triggered when the Maps SDK is initialized. + *

+ * The Maps SDK only initializes once per Application lifecycle. Only the first call of this method or {@link #initialize(Context)} takes effect. + * If you are using {@link MapFragment} or {@link MapView} and have already obtained a (non-null) {@link GoogleMap} by calling {@code getMapAsync()} on + * either of these classes, then this call will have no effect other than triggering the callback for the initialized {@link MapsInitializer.Renderer}. + * To make renderer preference meaningful, you must call this method before {@link #initialize(Context)}, and before {@link MapFragment#onCreate(Bundle)} + * and {@link MapView#onCreate(Bundle)}. It's recommended to do this in {@link Application#onCreate()}. + *

+ * Note the following: + *

    + *
  • Use {@code LATEST} to request the new renderer. No action is necessary if you prefer to use the legacy renderer.
  • + *
  • The latest renderer may not always be returned due to various reasons, including not enough memory, unsupported Android version, or routine downtime.
  • + *
  • The new renderer will eventually become the default renderer through a progressive rollout. At that time, you will need to request {@code LEGACY} in + * order to continue using the legacy renderer.
  • + *
+ * + * @param context Required to fetch the necessary SDK resources and code. Must not be {@code null}. + * @param preferredRenderer Which {@link MapsInitializer.Renderer} type you prefer to use for your application. + * If {@code null} is provided, the default preference is taken. + * @param callback The callback that the Maps SDK triggers when it informs you about which renderer type was actually loaded. + * You can define what you want to do differently according to the maps renderer that is loaded. + * @return A ConnectionResult error code. + */ + public static synchronized int initialize(@NonNull Context context, @Nullable MapsInitializer.Renderer preferredRenderer, @Nullable OnMapsSdkInitializedCallback callback) { + Log.d(TAG, "preferredRenderer: " + preferredRenderer); + if (initialized) { + if (callback != null) { + callback.onMapsSdkInitialized(renderer); + } + return CommonStatusCodes.SUCCESS; + } + try { + ICreator creator = MapsContextLoader.getCreator(context, preferredRenderer); + try { + CameraUpdateFactory.setDelegate(creator.newCameraUpdateFactoryDelegate()); + BitmapDescriptorFactory.setDelegate(creator.newBitmapDescriptorFactoryDelegate()); + int preferredRendererInt = 0; + if (preferredRenderer != null) { + if (preferredRenderer == Renderer.LEGACY) preferredRendererInt = 1; + else if (preferredRenderer == Renderer.LATEST) preferredRendererInt = 2; + } + try { + if (creator.getRendererType() == 2) { + renderer = Renderer.LATEST; + } + creator.logInitialization(ObjectWrapper.wrap(context), preferredRendererInt); + } catch (RemoteException e) { + Log.e(TAG, "Failed to retrieve renderer type or log initialization.", e); + } + Log.d(TAG, "loadedRenderer: " + renderer); + if (callback != null) { + callback.onMapsSdkInitialized(renderer); + } + return CommonStatusCodes.SUCCESS; + } catch (RemoteException e) { + throw new RuntimeRemoteException(e); + } + } catch (Exception e) { + return CommonStatusCodes.INTERNAL_ERROR; + } + } + + /** + * Enables you to specify which {@link MapsInitializer.Renderer} you prefer to use for your application {@code LATEST} or {@code LEGACY}. + * It also informs you which maps {@link MapsInitializer.Renderer} is actually used for your application. + */ + public enum Renderer { + LEGACY, LATEST + } +} diff --git a/play-services-maps/src/main/java/com/google/android/gms/maps/OnMapReadyCallback.java b/play-services-maps/src/main/java/com/google/android/gms/maps/OnMapReadyCallback.java new file mode 100644 index 0000000000..1c60e12eaf --- /dev/null +++ b/play-services-maps/src/main/java/com/google/android/gms/maps/OnMapReadyCallback.java @@ -0,0 +1,42 @@ +/* + * 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.maps; + +import android.view.View; +import android.view.ViewTreeObserver; +import com.google.android.gms.maps.model.LatLngBounds; + +/** + * Callback interface for when the map is ready to be used. + *

+ * Once an instance of this interface is set on a {@link MapFragment} or {@link MapView} object, the {@link #onMapReady(GoogleMap)} method is triggered when + * the map is ready to be used and provides a non-null instance of {@link GoogleMap}. + *

+ * If required services are not installed on the device, the user will be prompted to install it, and the {@link #onMapReady(GoogleMap)} method will only be + * triggered when the user has installed it and returned to the app. + */ +public interface OnMapReadyCallback { + /** + * Called when the map is ready to be used. + *

+ * Note that this does not guarantee that the map has undergone layout. Therefore, the map's size may not have been determined by the time the callback + * method is called. If you need to know the dimensions or call a method in the API that needs to know the dimensions, get the map's {@link View} and + * register an {@link ViewTreeObserver.OnGlobalLayoutListener} as well. + *

+ * Do not chain the {@code OnMapReadyCallback} and {@code OnGlobalLayoutListener} listeners, but instead register and wait for both callbacks independently, + * since the callbacks can be fired in any order. + *

+ * As an example, if you want to update the map's camera using a {@link LatLngBounds} without dimensions, you should wait until both + * {@code OnMapReadyCallback} and {@code OnGlobalLayoutListener} have completed. Otherwise there is a race condition that could trigger an + * {@link IllegalStateException}. + * + * @param googleMap A non-null instance of a GoogleMap associated with the {@link MapFragment} or {@link MapView} that defines the callback. + */ + void onMapReady(GoogleMap googleMap); +} diff --git a/play-services-maps/src/main/java/com/google/android/gms/maps/OnMapsSdkInitializedCallback.java b/play-services-maps/src/main/java/com/google/android/gms/maps/OnMapsSdkInitializedCallback.java new file mode 100644 index 0000000000..9860921170 --- /dev/null +++ b/play-services-maps/src/main/java/com/google/android/gms/maps/OnMapsSdkInitializedCallback.java @@ -0,0 +1,23 @@ +/* + * 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.maps; + +/** + * Callback interface used by the Maps SDK to inform you which maps {@link MapsInitializer.Renderer} type has been loaded for your application. + */ +public interface OnMapsSdkInitializedCallback { + /** + * The Maps SDK calls this method to inform you which maps {@link MapsInitializer.Renderer} has been loaded for your application. + *

+ * You can implement this method to define configurations or operations that are specific to each {@link MapsInitializer.Renderer} type. + * + * @param renderer The actual maps {@link MapsInitializer.Renderer} the maps SDK has loaded for your application. + */ + void onMapsSdkInitialized(MapsInitializer.Renderer renderer); +} diff --git a/play-services-maps/src/main/java/com/google/android/gms/maps/internal/MapLifecycleDelegate.java b/play-services-maps/src/main/java/com/google/android/gms/maps/internal/MapLifecycleDelegate.java new file mode 100644 index 0000000000..676d51f920 --- /dev/null +++ b/play-services-maps/src/main/java/com/google/android/gms/maps/internal/MapLifecycleDelegate.java @@ -0,0 +1,14 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.maps.internal; + +import androidx.annotation.NonNull; +import com.google.android.gms.dynamic.LifecycleDelegate; +import com.google.android.gms.maps.OnMapReadyCallback; + +public interface MapLifecycleDelegate extends LifecycleDelegate { + void getMapAsync(@NonNull OnMapReadyCallback callback); +} diff --git a/play-services-maps/src/main/java/com/google/android/gms/maps/model/BitmapDescriptorFactory.java b/play-services-maps/src/main/java/com/google/android/gms/maps/model/BitmapDescriptorFactory.java new file mode 100644 index 0000000000..edbeb4ee48 --- /dev/null +++ b/play-services-maps/src/main/java/com/google/android/gms/maps/model/BitmapDescriptorFactory.java @@ -0,0 +1,24 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.maps.model; + +import androidx.annotation.NonNull; +import com.google.android.gms.maps.CameraUpdateFactory; +import com.google.android.gms.maps.internal.ICameraUpdateFactoryDelegate; +import com.google.android.gms.maps.model.internal.IBitmapDescriptorFactoryDelegate; +import org.microg.gms.common.Hide; + +public class BitmapDescriptorFactory { + private static IBitmapDescriptorFactoryDelegate delegate; + @Hide + public static void setDelegate(@NonNull IBitmapDescriptorFactoryDelegate delegate) { + BitmapDescriptorFactory.delegate = delegate; + } + private static IBitmapDescriptorFactoryDelegate getDelegate() { + if (delegate == null) throw new IllegalStateException("CameraUpdateFactory is not initialized"); + return delegate; + } +} diff --git a/play-services-maps/src/main/java/com/google/android/gms/maps/model/ButtCap.java b/play-services-maps/src/main/java/com/google/android/gms/maps/model/ButtCap.java new file mode 100644 index 0000000000..999f4aac70 --- /dev/null +++ b/play-services-maps/src/main/java/com/google/android/gms/maps/model/ButtCap.java @@ -0,0 +1,31 @@ +/* + * 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.maps.model; + +import androidx.annotation.NonNull; + +/** + * Cap that is squared off exactly at the start or end vertex of a {@link Polyline} with solid stroke pattern, equivalent to having no + * additional cap beyond the start or end vertex. This is the default cap type at start and end vertices of {@link Polyline}s with + * solid stroke pattern. + */ +public class ButtCap extends Cap { + /** + * Constructs a {@code ButtCap}. + */ + public ButtCap() { + super(0, null, null); + } + + @NonNull + @Override + public String toString() { + return "[ButtCap]"; + } +} diff --git a/play-services-maps/src/main/java/com/google/android/gms/maps/model/Cap.java b/play-services-maps/src/main/java/com/google/android/gms/maps/model/Cap.java index d5d38d1f77..7d6b80078f 100644 --- a/play-services-maps/src/main/java/com/google/android/gms/maps/model/Cap.java +++ b/play-services-maps/src/main/java/com/google/android/gms/maps/model/Cap.java @@ -1,21 +1,104 @@ /* * SPDX-FileCopyrightText: 2022 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.maps.model; import android.os.IBinder; +import android.os.Parcel; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import org.microg.safeparcel.AutoSafeParcelable; +import java.util.Objects; + +/** + * Immutable cap that can be applied at the start or end vertex of a {@link Polyline}. + */ public class Cap extends AutoSafeParcelable { @Field(2) - private int type; + public final int type; @Field(3) - private IBinder bitmap; - private BitmapDescriptor bitmapDescriptor; + @Nullable + private final IBinder bitmap; + /** + * Descriptor of the bitmap to be overlaid at the start or end vertex. + */ + @Nullable + private final BitmapDescriptor bitmapDescriptor; + /** + * Reference stroke width (in pixels) - the stroke width for which the cap bitmap at its native dimension is designed. + * The default reference stroke width is 10 pixels. + */ @Field(4) - private float bitmapRefWidth; - public static final Creator CREATOR = new AutoCreator<>(Cap.class); + @Nullable + private final Float refWidth; + + private Cap() { + type = 0; + bitmap = null; + bitmapDescriptor = null; + refWidth = 0.0f; + } + + protected Cap(int type, @Nullable BitmapDescriptor bitmapDescriptor, @Nullable Float refWidth) { + this.type = type; + this.bitmap = bitmapDescriptor == null ? null : bitmapDescriptor.getRemoteObject().asBinder(); + this.bitmapDescriptor = bitmapDescriptor; + this.refWidth = refWidth; + } + + @NonNull + @Override + public String toString() { + return "[Cap: type=" + type + "]"; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Cap)) return false; + + Cap cap = (Cap) o; + + if (type != cap.type) return false; + if (!Objects.equals(bitmapDescriptor, cap.bitmapDescriptor)) return false; + return Objects.equals(refWidth, cap.refWidth); + } + + @Override + public int hashCode() { + int result = type; + result = 31 * result + (bitmapDescriptor != null ? bitmapDescriptor.hashCode() : 0); + result = 31 * result + (refWidth != null ? refWidth.hashCode() : 0); + return result; + } + + public static final Creator CREATOR = new AutoCreator(Cap.class) { + @Override + public Cap createFromParcel(Parcel parcel) { + Cap item = super.createFromParcel(parcel); + switch (item.type) { + case 0: + return new ButtCap(); + case 1: + return new SquareCap(); + case 2: + return new RoundCap(); + case 3: + if (item.refWidth != null) { + return new CustomCap(item.bitmapDescriptor, item.refWidth); + } else { + return new CustomCap(item.bitmapDescriptor); + } + default: + return item; + } + } + }; } diff --git a/play-services-maps/src/main/java/com/google/android/gms/maps/model/Circle.java b/play-services-maps/src/main/java/com/google/android/gms/maps/model/Circle.java new file mode 100644 index 0000000000..fc6796fe73 --- /dev/null +++ b/play-services-maps/src/main/java/com/google/android/gms/maps/model/Circle.java @@ -0,0 +1,185 @@ +/* + * 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.maps.model; + +import android.os.RemoteException; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.android.gms.maps.model.internal.ICircleDelegate; +import org.microg.gms.common.Hide; + +import java.util.List; + +/** + * A circle on the earth's surface (spherical cap). + */ +public class Circle { + private final ICircleDelegate delegate; + + @Hide + public Circle(ICircleDelegate delegate) { + this.delegate = delegate; + } + + @NonNull + public LatLng getCenter() { + try { + return this.delegate.getCenter(); + } catch (RemoteException e) { + throw new RuntimeRemoteException(e); + } + } + + public int getFillColor() { + try { + return this.delegate.getFillColor(); + } catch (RemoteException e) { + throw new RuntimeRemoteException(e); + } + } + + @NonNull + public String getId() { + try { + return this.delegate.getId(); + } catch (RemoteException e) { + throw new RuntimeRemoteException(e); + } + } + + public double getRadius() { + try { + return this.delegate.getRadius(); + } catch (RemoteException e) { + throw new RuntimeRemoteException(e); + } + } + + @Nullable + public List getStrokePattern() { + throw new UnsupportedOperationException(); + } + + public int getStrokeColor() { + try { + return this.delegate.getStrokeColor(); + } catch (RemoteException e) { + throw new RuntimeRemoteException(e); + } + } + + public float getStrokeWidth() { + try { + return this.delegate.getStrokeWidth(); + } catch (RemoteException e) { + throw new RuntimeRemoteException(e); + } + } + + @Nullable + public Object getTag() { + throw new UnsupportedOperationException(); + } + + public float getZIndex() { + try { + return this.delegate.getZIndex(); + } catch (RemoteException e) { + throw new RuntimeRemoteException(e); + } + } + + public boolean isClickable() { + throw new UnsupportedOperationException(); + } + + public boolean isVisible() { + try { + return this.delegate.isVisible(); + } catch (RemoteException e) { + throw new RuntimeRemoteException(e); + } + } + + public void remove() { + try { + this.delegate.remove(); + } catch (RemoteException e) { + throw new RuntimeRemoteException(e); + } + } + + public void setCenter(@NonNull LatLng center) { + try { + this.delegate.setCenter(center); + } catch (RemoteException e) { + throw new RuntimeRemoteException(e); + } + } + + public void setClickable(boolean clickable) { + throw new UnsupportedOperationException(); + } + + public void setFillColor(int color) { + try { + this.delegate.setFillColor(color); + } catch (RemoteException e) { + throw new RuntimeRemoteException(e); + } + } + + public void setRadius(double radius) { + try { + this.delegate.setRadius(radius); + } catch (RemoteException e) { + throw new RuntimeRemoteException(e); + } + } + + public void setStrokeColor(int color) { + try { + this.delegate.setStrokeColor(color); + } catch (RemoteException e) { + throw new RuntimeRemoteException(e); + } + } + + public void setStrokePattern(@Nullable List pattern) { + throw new UnsupportedOperationException(); + } + + public void setStrokeWidth(float width) { + try { + this.delegate.setStrokeWidth(width); + } catch (RemoteException e) { + throw new RuntimeRemoteException(e); + } + } + + public void setTag(@Nullable Object tag) { + throw new UnsupportedOperationException(); + } + + public void setVisible(boolean visible) { + try { + this.delegate.setVisible(visible); + } catch (RemoteException e) { + throw new RuntimeRemoteException(e); + } + } + + public void setZIndex(float zIndex) { + try { + this.delegate.setZIndex(zIndex); + } catch (RemoteException e) { + throw new RuntimeRemoteException(e); + } + } +} diff --git a/play-services-maps/src/main/java/com/google/android/gms/maps/model/CircleOptions.java b/play-services-maps/src/main/java/com/google/android/gms/maps/model/CircleOptions.java index 9f61106023..f58094c445 100644 --- a/play-services-maps/src/main/java/com/google/android/gms/maps/model/CircleOptions.java +++ b/play-services-maps/src/main/java/com/google/android/gms/maps/model/CircleOptions.java @@ -22,27 +22,35 @@ import org.microg.safeparcel.AutoSafeParcelable; import org.microg.safeparcel.SafeParceled; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + /** * Defines options for a Circle. */ @PublicApi public class CircleOptions extends AutoSafeParcelable { - @SafeParceled(1) + @Field(1) private int versionCode; - @SafeParceled(2) + @Field(2) private LatLng center; - @SafeParceled(3) - private double radius = 0; - @SafeParceled(4) - private float strokeWidth = 10; - @SafeParceled(5) + @Field(3) + private double radius = 0.0d; + @Field(4) + private float strokeWidth = 10.0f; + @Field(5) private int strokeColor = Color.BLACK; - @SafeParceled(6) + @Field(6) private int fillColor = Color.TRANSPARENT; - @SafeParceled(7) - private float zIndex = 0; - @SafeParceled(8) + @Field(7) + private float zIndex = 0.0f; + @Field(8) private boolean visible = true; + @Field(9) + private boolean clickable = false; + @Field(10) + private List strokePattern = null; /** * Creates circle options. @@ -144,6 +152,15 @@ public boolean isVisible() { return visible; } + /** + * Gets the clickability setting for the circle. + * + * @return {@code true} if the circle is clickable; {@code false} if it is not. + */ + public boolean isClickable() { + return clickable; + } + /** * Sets the radius in meters. *

@@ -217,5 +234,35 @@ public CircleOptions zIndex(float zIndex) { return this; } + /** + * Specifies whether this circle is clickable. The default setting is {@code false}. + * + * @param clickable + * @return this {@code CircleOptions} object with a new clickability setting. + */ + public CircleOptions clickable(boolean clickable) { + this.clickable = clickable; + return this; + } + + /** + * Specifies a stroke pattern for the circle's outline. The default stroke pattern is solid, represented by {@code null}. + * + * @return this {@link CircleOptions} object with a new stroke pattern set. + */ + public CircleOptions strokePattern(List pattern) { + this.strokePattern = pattern; + return this; + } + + /** + * Gets the stroke pattern set in this {@link CircleOptions} object for the circle's outline. + * + * @return the stroke pattern of the circle's outline. + */ + public List getStrokePattern() { + return strokePattern; + } + public static Creator CREATOR = new AutoCreator(CircleOptions.class); } diff --git a/play-services-maps/src/main/java/com/google/android/gms/maps/model/CustomCap.java b/play-services-maps/src/main/java/com/google/android/gms/maps/model/CustomCap.java new file mode 100644 index 0000000000..8d3f4dff18 --- /dev/null +++ b/play-services-maps/src/main/java/com/google/android/gms/maps/model/CustomCap.java @@ -0,0 +1,51 @@ +/* + * 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.maps.model; + +import androidx.annotation.NonNull; + +/** + * Bitmap overlay centered at the start or end vertex of a {@link Polyline}, orientated according to the direction of the line's first + * or last edge and scaled with respect to the line's stroke width. {@code CustomCap} can be applied to {@link Polyline} with any stroke pattern. + */ +public class CustomCap extends Cap { + @NonNull + public final BitmapDescriptor bitmapDescriptor; + public final Float refWidth; + + /** + * Constructs a new {@code CustomCap}. + * + * @param bitmapDescriptor Descriptor of the bitmap to be used. Must not be {@code null}. + * @param refWidth Stroke width, in pixels, for which the cap bitmap at its native dimension is designed. Must be positive. + */ + public CustomCap(@NonNull BitmapDescriptor bitmapDescriptor, float refWidth) { + super(3, bitmapDescriptor, refWidth); + this.bitmapDescriptor = bitmapDescriptor; + this.refWidth = refWidth; + } + + /** + * Constructs a new {@code CustomCap} with default reference stroke width of 10 pixels (equal to the default stroke width, see + * {@link PolylineOptions#width(float)}). + * + * @param bitmapDescriptor Descriptor of the bitmap to be used. Must not be {@code null}. + */ + public CustomCap(@NonNull BitmapDescriptor bitmapDescriptor) { + super(3, bitmapDescriptor, null); + this.bitmapDescriptor = bitmapDescriptor; + this.refWidth = null; + } + + @NonNull + @Override + public String toString() { + return "[CustomCap bitmapDescriptor=" + bitmapDescriptor + " refWidth=" + refWidth + "]"; + } +} diff --git a/play-services-maps/src/main/java/com/google/android/gms/maps/model/GroundOverlayOptions.java b/play-services-maps/src/main/java/com/google/android/gms/maps/model/GroundOverlayOptions.java index b29afdf75e..9cf169d225 100644 --- a/play-services-maps/src/main/java/com/google/android/gms/maps/model/GroundOverlayOptions.java +++ b/play-services-maps/src/main/java/com/google/android/gms/maps/model/GroundOverlayOptions.java @@ -1,17 +1,9 @@ /* - * Copyright (C) 2013-2017 microG Project Team - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * SPDX-FileCopyrightText: 2015 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.maps.model; @@ -22,7 +14,6 @@ import org.microg.gms.common.PublicApi; import org.microg.safeparcel.AutoSafeParcelable; -import org.microg.safeparcel.SafeParceled; /** * Defines options for a ground overlay. @@ -34,31 +25,33 @@ public class GroundOverlayOptions extends AutoSafeParcelable { */ public static final float NO_DIMENSION = -1; - @SafeParceled(1) + @Field(1) private int versionCode; - @SafeParceled(2) + @Field(2) private IBinder image; private BitmapDescriptor imageDescriptor; - @SafeParceled(3) + @Field(3) private LatLng location; - @SafeParceled(4) + @Field(4) private float width; - @SafeParceled(5) + @Field(5) private float height; - @SafeParceled(6) + @Field(6) private LatLngBounds bounds; - @SafeParceled(7) + @Field(7) private float bearing; - @SafeParceled(8) + @Field(8) private float zIndex; - @SafeParceled(9) - private boolean visible; - @SafeParceled(10) - private float transparency = 0; - @SafeParceled(11) - private float anchorU; - @SafeParceled(12) - private float anchorV; + @Field(9) + private boolean visible = true; + @Field(10) + private float transparency = 0.0f; + @Field(11) + private float anchorU = 0.5f; + @Field(12) + private float anchorV = 0.5f; + @Field(13) + private boolean clickable = false; /** * Creates a new set of ground overlay options. @@ -102,6 +95,17 @@ public GroundOverlayOptions bearing(float bearing) { return this; } + /** + * Specifies whether the ground overlay is clickable. The default clickability is {@code false}. + * + * @param clickable The new clickability setting. + * @return this {@link GroundOverlayOptions} object with a new clickability setting. + */ + public GroundOverlayOptions clickable(boolean clickable) { + this.clickable = clickable; + return this; + } + /** * Horizontal distance, normalized to [0, 1], of the anchor from the left edge. * @@ -216,6 +220,15 @@ public GroundOverlayOptions image(BitmapDescriptor image) { return this; } + /** + * Gets the clickability setting for this {@link GroundOverlayOptions} object. + * + * @return {@code true} if the ground overlay is clickable; {@code false} if it is not. + */ + public boolean isClickable() { + return clickable; + } + /** * Gets the visibility setting for this options object. * diff --git a/play-services-maps/src/main/java/com/google/android/gms/maps/model/PatternItem.java b/play-services-maps/src/main/java/com/google/android/gms/maps/model/PatternItem.java index 6f8e3f7ab6..1278df94d9 100644 --- a/play-services-maps/src/main/java/com/google/android/gms/maps/model/PatternItem.java +++ b/play-services-maps/src/main/java/com/google/android/gms/maps/model/PatternItem.java @@ -21,7 +21,7 @@ public class PatternItem extends AutoSafeParcelable { @Field(2) private int type; @Field(3) - private float length; + private Float length; private PatternItem() { } diff --git a/play-services-maps/src/main/java/com/google/android/gms/maps/model/PolylineOptions.java b/play-services-maps/src/main/java/com/google/android/gms/maps/model/PolylineOptions.java index 5e2cda9b1c..6991a70208 100644 --- a/play-services-maps/src/main/java/com/google/android/gms/maps/model/PolylineOptions.java +++ b/play-services-maps/src/main/java/com/google/android/gms/maps/model/PolylineOptions.java @@ -1,30 +1,37 @@ /* * SPDX-FileCopyrightText: 2015 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.maps.model; import android.graphics.Color; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import org.microg.gms.common.Hide; import org.microg.gms.common.PublicApi; import org.microg.safeparcel.AutoSafeParcelable; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; /** * Defines options for a polyline. - * TODO */ @PublicApi public class PolylineOptions extends AutoSafeParcelable { @Field(1) private int versionCode = 1; @Field(value = 2, subClass = LatLng.class) - private List points = new ArrayList(); + @NonNull + private List points = new ArrayList<>(); @Field(3) - private float width = 10; + private float width = 10.0f; @Field(4) private int color = Color.BLACK; @Field(5) @@ -36,99 +43,299 @@ public class PolylineOptions extends AutoSafeParcelable { @Field(8) private boolean clickable = false; @Field(9) - private Cap startCap; + @NonNull + private Cap startCap = new ButtCap(); @Field(10) - private Cap endCap; + @NonNull + private Cap endCap = new ButtCap(); @Field(11) private int jointType = JointType.DEFAULT; @Field(value = 12, subClass = PatternItem.class) + @Nullable private List pattern = null; @Field(value = 13, subClass = StyleSpan.class) - private List spans = null; + @NonNull + private List spans = new ArrayList<>(); + /** + * Creates polyline options. + */ public PolylineOptions() { } + /** + * Adds vertices to the end of the polyline being built. + * + * @param points an array of {@link LatLng}s that are added to the end of the polyline. Must not be {@code null}. + * @return this {@link PolylineOptions} object with the given points on the end. + */ + public PolylineOptions add(LatLng... points) { + this.points.addAll(Arrays.asList(points)); + return this; + } + + /** + * Adds a vertex to the end of the polyline being built. + * + * @param point a {@link LatLng} that is added to the end of the polyline. Must not be {@code null}. + * @return this {@link PolylineOptions} object with the given point on the end. + */ public PolylineOptions add(LatLng point) { points.add(point); return this; } - public PolylineOptions add(LatLng... points) { + /** + * Adds vertices to the end of the polyline being built. + * + * @param points a list of {@link LatLng}s that are added to the end of the polyline. Must not be {@code null}. + * @return this {@link PolylineOptions} object with the given points on the end. + */ + public PolylineOptions addAll(Iterable points) { for (LatLng point : points) { this.points.add(point); } return this; } - public PolylineOptions addAll(Iterable points) { - for (LatLng point : points) { - this.points.add(point); + /** + * Adds new style spans to the polyline being built. + * + * @param spans the style spans that will be added to the polyline. + * @return this {@link PolylineOptions} object with new style spans added. + */ + public PolylineOptions addAllSpans(Iterable spans) { + for (StyleSpan span : spans) { + this.spans.add(span); } return this; } + /** + * Adds a new style span to the polyline being built. + * + * @param span the style span that will be added to the polyline. + * @return this {@link PolylineOptions} object with new style span added. + */ + public PolylineOptions addSpan(StyleSpan span) { + this.spans.add(span); + return this; + } + + /** + * Adds new style spans to the polyline being built. + * + * @param spans the style spans that will be added to the polyline. + * @return this {@link PolylineOptions} object with new style spans added. + */ + public PolylineOptions addSpan(StyleSpan... spans) { + this.spans.addAll(Arrays.asList(spans)); + return this; + } + + /** + * Specifies whether this polyline is clickable. The default setting is {@code false} + * + * @return this {@link PolylineOptions} object with a new clickability setting. + */ public PolylineOptions clickable(boolean clickable) { this.clickable = clickable; return this; } + /** + * Sets the color of the polyline as a 32-bit ARGB color. The default color is black ({@code 0xff000000}). + * + * @return this {@link PolylineOptions} object with a new color set. + */ public PolylineOptions color(int color) { this.color = color; return this; } + /** + * Sets the cap at the end vertex of the polyline. The default end cap is {@link ButtCap}. + * + * @return this {@link PolylineOptions} object with a new end cap set. + */ + public PolylineOptions endCap(@NonNull Cap endCap) { + this.endCap = endCap; + return this; + } + + /** + * Specifies whether to draw each segment of this polyline as a geodesic. The default setting is {@code false} + * + * @return this {@link PolylineOptions} object with a new geodesic setting. + */ public PolylineOptions geodesic(boolean geodesic) { this.geodesic = geodesic; return this; } + /** + * Gets the color set for this {@link PolylineOptions} object. + * + * @return the color of the polyline in ARGB format. + */ public int getColor() { return color; } + /** + * Gets the cap set for the end vertex in this {@link PolylineOptions} object. + * + * @return the end cap of the polyline. + */ + public Cap getEndCap() { + return endCap; + } + + /** + * Gets the joint type set in this {@link PolylineOptions} object for all vertices except the start and end vertices. + * See {@link JointType} for possible values. + * + * @return the joint type of the polyline. + */ public int getJointType() { return jointType; } + /** + * Gets the stroke pattern set in this {@link PolylineOptions} object for the polyline. + * + * @return the stroke pattern of the polyline. + */ public List getPattern() { return pattern; } + /** + * Gets the points set for this {@link PolylineOptions} object. + * + * @return the list of {@link LatLng}s specifying the vertices of the polyline. + */ public List getPoints() { return points; } + @Hide + public List getSpans() { + return spans; + } + + /** + * Gets the cap set for the start vertex in this {@link PolylineOptions} object. + * + * @return the start cap of the polyline. + */ + public Cap getStartCap() { + return startCap; + } + + /** + * Gets the width set for this {@link PolylineOptions} object. + * + * @return the width of the polyline in screen pixels. + */ public float getWidth() { return width; } + /** + * Gets the zIndex set for this {@link PolylineOptions} object. + * + * @return the zIndex of the polyline. + */ public float getZIndex() { return zIndex; } + /** + * Gets the clickability setting for this {@link PolylineOptions} object. + * + * @return {@code true} if the polyline is clickable; {@code false} if it is not. + */ + public boolean isClickable() { + return clickable; + } + + /** + * Gets the geodesic setting for this {@link PolylineOptions} object. + * + * @return {@code true} if the polyline segments should be geodesics; {@code false} they should not be. + */ public boolean isGeodesic() { return geodesic; } + /** + * Gets the visibility setting for this {@link PolylineOptions} object. + * + * @return {@code true} if the polyline is visible; {@code false} if it is not. + */ public boolean isVisible() { return visible; } - public boolean isClickable() { - return clickable; + /** + * Sets the joint type for all vertices of the polyline except the start and end vertices. + *

+ * See {@link JointType} for allowed values. The default value {@link JointType#DEFAULT} will be used if joint type is undefined or is + * not one of the allowed values. + * + * @return this {@link PolylineOptions} object with a new joint type set. + */ + public PolylineOptions jointType(int jointType) { + this.jointType = jointType; + return this; + } + + /** + * Sets the stroke pattern for the polyline. The default stroke pattern is solid, represented by {@code null}. + * + * @return this {@link PolylineOptions} object with a new stroke pattern set. + */ + public PolylineOptions pattern(@Nullable List pattern) { + this.pattern = pattern; + return this; + } + + /** + * Sets the cap at the start vertex of the polyline. The default start cap is {@link ButtCap}. + * + * @return this {@link PolylineOptions} object with a new start cap set. + */ + public PolylineOptions startCap(@NonNull Cap startCap) { + this.startCap = startCap; + return this; } + /** + * Specifies the visibility for the polyline. The default visibility is {@code true}. + * + * @return this {@link PolylineOptions} object with a new visibility setting. + */ public PolylineOptions visible(boolean visible) { this.visible = visible; return this; } + /** + * Sets the width of the polyline in screen pixels. The default is {@code 10}. + * + * @return this {@link PolylineOptions} object with a new width set. + */ public PolylineOptions width(float width) { this.width = width; return this; } + /** + * Specifies the polyline's zIndex, i.e., the order in which it will be drawn. + * + * @return this {@link PolylineOptions} object with a new zIndex set. + */ public PolylineOptions zIndex(float zIndex) { this.zIndex = zIndex; return this; diff --git a/play-services-maps/src/main/java/com/google/android/gms/maps/model/RoundCap.java b/play-services-maps/src/main/java/com/google/android/gms/maps/model/RoundCap.java new file mode 100644 index 0000000000..99dc3098f7 --- /dev/null +++ b/play-services-maps/src/main/java/com/google/android/gms/maps/model/RoundCap.java @@ -0,0 +1,29 @@ +/* + * 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.maps.model; + +import androidx.annotation.NonNull; + +/** + * Cap that is a semicircle with radius equal to half the stroke width, centered at the start or end vertex of a {@link Polyline} with solid stroke pattern. + */ +public class RoundCap extends Cap { + /** + * Constructs a {@code RoundCap}. + */ + public RoundCap() { + super(2, null, null); + } + + @NonNull + @Override + public String toString() { + return "[RoundCap]"; + } +} diff --git a/play-services-maps/src/main/java/com/google/android/gms/maps/model/RuntimeRemoteException.java b/play-services-maps/src/main/java/com/google/android/gms/maps/model/RuntimeRemoteException.java new file mode 100644 index 0000000000..d0ebd4ee88 --- /dev/null +++ b/play-services-maps/src/main/java/com/google/android/gms/maps/model/RuntimeRemoteException.java @@ -0,0 +1,18 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.maps.model; + +import android.os.RemoteException; +import androidx.annotation.NonNull; + +/** + * A RuntimeException wrapper for RemoteException. Thrown when normally there is something seriously wrong and there is no way to recover. + */ +public class RuntimeRemoteException extends RuntimeException { + public RuntimeRemoteException(@NonNull RemoteException e) { + super(e); + } +} diff --git a/play-services-maps/src/main/java/com/google/android/gms/maps/model/SquareCap.java b/play-services-maps/src/main/java/com/google/android/gms/maps/model/SquareCap.java new file mode 100644 index 0000000000..7165a966a8 --- /dev/null +++ b/play-services-maps/src/main/java/com/google/android/gms/maps/model/SquareCap.java @@ -0,0 +1,29 @@ +/* + * 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.maps.model; + +import androidx.annotation.NonNull; + +/** + * Cap that is squared off after extending half the stroke width beyond the start or end vertex of a {@link Polyline} with solid stroke pattern. + */ +public class SquareCap extends Cap { + /** + * Constructs a {@code SquareCap}. + */ + public SquareCap() { + super(1, null, null); + } + + @NonNull + @Override + public String toString() { + return "[SquareCap]"; + } +} diff --git a/play-services-maps/src/main/java/com/google/android/gms/maps/model/TileOverlayOptions.java b/play-services-maps/src/main/java/com/google/android/gms/maps/model/TileOverlayOptions.java index 1fdffe3625..dbf1b2e4fe 100644 --- a/play-services-maps/src/main/java/com/google/android/gms/maps/model/TileOverlayOptions.java +++ b/play-services-maps/src/main/java/com/google/android/gms/maps/model/TileOverlayOptions.java @@ -1,17 +1,9 @@ /* - * Copyright (C) 2013-2017 microG Project Team - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * SPDX-FileCopyrightText: 2015 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.maps.model; @@ -19,11 +11,11 @@ import android.os.IBinder; import android.os.RemoteException; +import androidx.annotation.NonNull; import com.google.android.gms.maps.model.internal.ITileProviderDelegate; import org.microg.gms.common.PublicApi; import org.microg.safeparcel.AutoSafeParcelable; -import org.microg.safeparcel.SafeParceled; /** * Defines options for a TileOverlay. @@ -31,20 +23,22 @@ @PublicApi public class TileOverlayOptions extends AutoSafeParcelable { - @SafeParceled(1) + @Field(1) private int versionCode = 1; /** * This is a IBinder to the {@link #tileProvider}, built using {@link ITileProviderDelegate}. */ - @SafeParceled(2) + @Field(2) private IBinder tileProviderBinder; private TileProvider tileProvider; - @SafeParceled(3) + @Field(3) private boolean visible = true; - @SafeParceled(4) + @Field(4) private float zIndex; - @SafeParceled(5) + @Field(5) private boolean fadeIn = true; + @Field(6) + private float transparency = 0.0f; /** * Creates a new set of tile overlay options. @@ -80,6 +74,15 @@ public TileProvider getTileProvider() { return tileProvider; } + /** + * Gets the transparency set for this {@link TileOverlayOptions} object. + * + * @return the transparency of the tile overlay. + */ + public float getTransparency() { + return transparency; + } + /** * Gets the zIndex set for this {@link TileOverlayOptions} object. * @@ -104,7 +107,7 @@ public boolean isVisible() { * @param tileProvider the {@link TileProvider} to use for this tile overlay. * @return the object for which the method was called, with the new tile provider set. */ - public TileOverlayOptions tileProvider(final TileProvider tileProvider) { + public TileOverlayOptions tileProvider(@NonNull final TileProvider tileProvider) { this.tileProvider = tileProvider; this.tileProviderBinder = new ITileProviderDelegate.Stub() { @Override @@ -115,6 +118,19 @@ public Tile getTile(int x, int y, int zoom) throws RemoteException { return this; } + /** + * Specifies the transparency of the tile overlay. The default transparency is {@code 0} (opaque). + * + * @param transparency a float in the range {@code [0..1]} where {@code 0} means that the tile overlay is opaque and {@code 1} means that the tile overlay is transparent. + * @return this {@link TileOverlayOptions} object with a new transparency setting. + * @throws IllegalArgumentException if the transparency is outside the range [0..1]. + */ + public TileOverlayOptions transparency(float transparency) { + if (transparency < 0.0f || transparency > 1.0f) throw new IllegalArgumentException("Transparency must be in the range [0..1]"); + this.transparency = transparency; + return this; + } + /** * Specifies the visibility for the tile overlay. The default visibility is {@code true}. * diff --git a/play-services-maps/src/main/java/org/microg/gms/maps/MapViewDelegate.java b/play-services-maps/src/main/java/org/microg/gms/maps/MapViewDelegate.java new file mode 100644 index 0000000000..b4d224104a --- /dev/null +++ b/play-services-maps/src/main/java/org/microg/gms/maps/MapViewDelegate.java @@ -0,0 +1,163 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.maps; + +import android.app.Activity; +import android.os.Bundle; +import android.os.RemoteException; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.android.gms.dynamic.ObjectWrapper; +import com.google.android.gms.maps.GoogleMap; +import com.google.android.gms.maps.OnMapReadyCallback; +import com.google.android.gms.maps.internal.IGoogleMapDelegate; +import com.google.android.gms.maps.internal.IMapViewDelegate; +import com.google.android.gms.maps.internal.IOnMapReadyCallback; +import com.google.android.gms.maps.internal.MapLifecycleDelegate; +import com.google.android.gms.maps.model.RuntimeRemoteException; + +public class MapViewDelegate implements MapLifecycleDelegate { + private final ViewGroup container; + private final IMapViewDelegate delegate; + private View view; + + public MapViewDelegate(ViewGroup container, IMapViewDelegate delegate) { + this.container = container; + this.delegate = delegate; + } + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup parent, @Nullable Bundle savedInstanceState) { + throw new UnsupportedOperationException("onCreateView not allowed on MapViewDelegate"); + } + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + try { + Bundle temp = new Bundle(); + MapsBundleHelper.transfer(savedInstanceState, temp); + delegate.onCreate(temp); + MapsBundleHelper.transfer(temp, savedInstanceState); + view = (View) ObjectWrapper.unwrap(delegate.getView()); + container.removeAllViews(); + container.addView(view); + } catch (RemoteException e) { + throw new RuntimeRemoteException(e); + } + } + + @Override + public void onDestroy() { + try { + delegate.onDestroy(); + } catch (RemoteException e) { + throw new RuntimeRemoteException(e); + } + } + + @Override + public void onDestroyView() { + throw new UnsupportedOperationException("onDestroyView not allowed on MapViewDelegate"); + } + + public void onEnterAmbient(Bundle bundle) { + try { + Bundle temp = new Bundle(); + MapsBundleHelper.transfer(bundle, temp); + delegate.onEnterAmbient(temp); + MapsBundleHelper.transfer(temp, bundle); + } catch (RemoteException e) { + throw new RuntimeRemoteException(e); + } + } + + public void onExitAmbient() { + try { + delegate.onExitAmbient(); + } catch (RemoteException e) { + throw new RuntimeRemoteException(e); + } + } + + @Override + public void onInflate(@NonNull Activity activity, @NonNull Bundle options, @Nullable Bundle onInflate) { + throw new UnsupportedOperationException("onInflate not allowed on MapViewDelegate"); + } + + @Override + public void onLowMemory() { + try { + delegate.onLowMemory(); + } catch (RemoteException e) { + throw new RuntimeRemoteException(e); + } + } + + @Override + public void onPause() { + try { + delegate.onPause(); + } catch (RemoteException e) { + throw new RuntimeRemoteException(e); + } + } + + @Override + public void onResume() { + try { + delegate.onResume(); + } catch (RemoteException e) { + throw new RuntimeRemoteException(e); + } + } + + @Override + public void onSaveInstanceState(@NonNull Bundle outState) { + try { + Bundle temp = new Bundle(); + MapsBundleHelper.transfer(outState, temp); + delegate.onSaveInstanceState(temp); + MapsBundleHelper.transfer(temp, outState); + } catch (RemoteException e) { + throw new RuntimeRemoteException(e); + } + } + + @Override + public void onStart() { + try { + delegate.onStart(); + } catch (RemoteException e) { + throw new RuntimeRemoteException(e); + } + } + + @Override + public void onStop() { + try { + delegate.onStop(); + } catch (RemoteException e) { + throw new RuntimeRemoteException(e); + } + } + + @Override + public void getMapAsync(@NonNull OnMapReadyCallback callback) { + try { + delegate.getMapAsync(new IOnMapReadyCallback.Stub() { + @Override + public void onMapReady(IGoogleMapDelegate map) throws RemoteException { + callback.onMapReady(new GoogleMap(map)); + } + }); + } catch (RemoteException e) { + throw new RuntimeRemoteException(e); + } + } +} diff --git a/play-services-maps/src/main/java/org/microg/gms/maps/MapViewLifecycleHelper.java b/play-services-maps/src/main/java/org/microg/gms/maps/MapViewLifecycleHelper.java new file mode 100644 index 0000000000..a57b062f39 --- /dev/null +++ b/play-services-maps/src/main/java/org/microg/gms/maps/MapViewLifecycleHelper.java @@ -0,0 +1,57 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.maps; + +import android.content.Context; +import android.view.ViewGroup; +import androidx.annotation.NonNull; +import com.google.android.gms.dynamic.DeferredLifecycleHelper; +import com.google.android.gms.dynamic.ObjectWrapper; +import com.google.android.gms.dynamic.OnDelegateCreatedListener; +import com.google.android.gms.maps.GoogleMapOptions; +import com.google.android.gms.maps.MapsInitializer; +import com.google.android.gms.maps.OnMapReadyCallback; +import com.google.android.gms.maps.internal.IMapViewDelegate; + +import java.util.ArrayList; +import java.util.List; + +public class MapViewLifecycleHelper extends DeferredLifecycleHelper { + private final ViewGroup container; + private final Context context; + private final GoogleMapOptions options; + private final List pendingMapReadyCallbacks = new ArrayList<>(); + + public MapViewLifecycleHelper(ViewGroup container, Context context, GoogleMapOptions options) { + this.container = container; + this.context = context; + this.options = options; + } + + public final void getMapAsync(OnMapReadyCallback callback) { + if (getDelegate() != null) { + getDelegate().getMapAsync(callback); + } else { + this.pendingMapReadyCallbacks.add(callback); + } + } + + @Override + protected void createDelegate(@NonNull OnDelegateCreatedListener listener) { + if (getDelegate() != null) return; + try { + MapsInitializer.initialize(context); + IMapViewDelegate delegate = MapsContextLoader.getCreator(context, null).newMapViewDelegate(ObjectWrapper.wrap(context), options); + listener.onDelegateCreated(new MapViewDelegate(container, delegate)); + for (OnMapReadyCallback callback : pendingMapReadyCallbacks) { + getDelegate().getMapAsync(callback); + } + pendingMapReadyCallbacks.clear(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/play-services-maps/src/main/java/org/microg/gms/maps/MapsBundleHelper.java b/play-services-maps/src/main/java/org/microg/gms/maps/MapsBundleHelper.java new file mode 100644 index 0000000000..3e8c9d9df7 --- /dev/null +++ b/play-services-maps/src/main/java/org/microg/gms/maps/MapsBundleHelper.java @@ -0,0 +1,65 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.maps; + +import android.os.Bundle; +import android.os.Parcelable; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +public class MapsBundleHelper { + @NonNull + private static ClassLoader getClassLoader() { + return MapsBundleHelper.class.getClassLoader(); + } + + public static Parcelable getParcelableFromMapStateBundle(@Nullable Bundle bundle, String key) { + ClassLoader classLoader = getClassLoader(); + bundle.setClassLoader(classLoader); + Bundle mapStateBundle = bundle.getBundle("map_state"); + if (mapStateBundle == null) { + return null; + } + mapStateBundle.setClassLoader(classLoader); + return mapStateBundle.getParcelable(key); + } + + public static void setParcelableInMapStateBundle(Bundle bundle, String key, @Nullable Parcelable parcelable) { + ClassLoader classLoader = getClassLoader(); + bundle.setClassLoader(classLoader); + Bundle mapStateBundle = bundle.getBundle("map_state"); + if (mapStateBundle == null) { + mapStateBundle = new Bundle(); + } + mapStateBundle.setClassLoader(classLoader); + mapStateBundle.putParcelable(key, parcelable); + bundle.putBundle("map_state", mapStateBundle); + } + + public static void transfer(@Nullable Bundle src, @Nullable Bundle dest) { + if (src == null || dest == null) { + return; + } + Parcelable parcelableFromMapStateBundle = getParcelableFromMapStateBundle(src, "MapOptions"); + if (parcelableFromMapStateBundle != null) { + setParcelableInMapStateBundle(dest, "MapOptions", parcelableFromMapStateBundle); + } + Parcelable parcelableFromMapStateBundle2 = getParcelableFromMapStateBundle(src, "StreetViewPanoramaOptions"); + if (parcelableFromMapStateBundle2 != null) { + setParcelableInMapStateBundle(dest, "StreetViewPanoramaOptions", parcelableFromMapStateBundle2); + } + Parcelable parcelableFromMapStateBundle3 = getParcelableFromMapStateBundle(src, "camera"); + if (parcelableFromMapStateBundle3 != null) { + setParcelableInMapStateBundle(dest, "camera", parcelableFromMapStateBundle3); + } + if (src.containsKey("position")) { + dest.putString("position", src.getString("position")); + } + if (src.containsKey("com.google.android.wearable.compat.extra.LOWBIT_AMBIENT")) { + dest.putBoolean("com.google.android.wearable.compat.extra.LOWBIT_AMBIENT", src.getBoolean("com.google.android.wearable.compat.extra.LOWBIT_AMBIENT", false)); + } + } +} diff --git a/play-services-maps/src/main/java/org/microg/gms/maps/MapsConstants.java b/play-services-maps/src/main/java/org/microg/gms/maps/MapsConstants.java deleted file mode 100644 index 301d077119..0000000000 --- a/play-services-maps/src/main/java/org/microg/gms/maps/MapsConstants.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright (C) 2013-2017 microG Project Team - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.microg.gms.maps; - -public class MapsConstants { - /** - * No base map tiles. - */ - public static final int MAP_TYPE_NONE = 0; - /** - * Basic maps. - */ - public static final int MAP_TYPE_NORMAL = 1; - /** - * Satellite maps with no labels. - */ - public static final int MAP_TYPE_SATELLITE = 2; - /** - * Terrain maps. - */ - public static final int MAP_TYPE_TERRAIN = 3; - /** - * Satellite maps with a transparent layer of major streets. - */ - public static final int MAP_TYPE_HYBRID = 4; -} diff --git a/play-services-maps/src/main/java/org/microg/gms/maps/MapsContextLoader.java b/play-services-maps/src/main/java/org/microg/gms/maps/MapsContextLoader.java new file mode 100644 index 0000000000..4577b3d4a3 --- /dev/null +++ b/play-services-maps/src/main/java/org/microg/gms/maps/MapsContextLoader.java @@ -0,0 +1,85 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.maps; + +import android.content.Context; +import android.os.IBinder; +import android.os.RemoteException; +import android.util.Log; +import androidx.annotation.Nullable; +import com.google.android.gms.common.GooglePlayServicesUtil; +import com.google.android.gms.dynamic.ObjectWrapper; +import com.google.android.gms.dynamite.DynamiteModule; +import com.google.android.gms.maps.MapsInitializer; +import com.google.android.gms.maps.internal.ICreator; +import com.google.android.gms.maps.model.RuntimeRemoteException; +import org.microg.gms.common.Constants; + +public class MapsContextLoader { + private static final String TAG = "MapsContextLoader"; + private static final String DYNAMITE_MODULE_DEFAULT = "com.google.android.gms.maps_dynamite"; + private static final String DYNAMITE_MODULE_LEGACY = "com.google.android.gms.maps_legacy_dynamite"; + private static final String DYNAMITE_MODULE_LATEST = "com.google.android.gms.maps_core_dynamite"; + + private static Context mapsContext; + private static ICreator creator; + + private static Context getMapsContext(Context context, @Nullable MapsInitializer.Renderer preferredRenderer) { + if (mapsContext == null) { + String moduleName; + if (preferredRenderer == null) { + moduleName = DYNAMITE_MODULE_DEFAULT; + } else if (preferredRenderer == MapsInitializer.Renderer.LEGACY) { + moduleName = DYNAMITE_MODULE_LEGACY; + } else if (preferredRenderer == MapsInitializer.Renderer.LATEST) { + moduleName = DYNAMITE_MODULE_LATEST; + } else { + moduleName = DYNAMITE_MODULE_DEFAULT; + } + Context mapsContext; + try { + mapsContext = DynamiteModule.load(context, DynamiteModule.PREFER_REMOTE, moduleName).getModuleContext(); + } catch (Exception e) { + if (!moduleName.equals(DYNAMITE_MODULE_DEFAULT)) { + try { + Log.d(TAG, "Attempting to load maps_dynamite again."); + mapsContext = DynamiteModule.load(context, DynamiteModule.PREFER_REMOTE, DYNAMITE_MODULE_DEFAULT).getModuleContext(); + } catch (Exception e2) { + Log.e(TAG, "Failed to load maps module, use pre-Chimera", e2); + mapsContext = GooglePlayServicesUtil.getRemoteContext(context); + } + } else { + Log.e(TAG, "Failed to load maps module, use pre-Chimera", e); + mapsContext = GooglePlayServicesUtil.getRemoteContext(context); + } + } + MapsContextLoader.mapsContext = mapsContext; + } + return mapsContext; + } + + public static ICreator getCreator(Context context, @Nullable MapsInitializer.Renderer preferredRenderer) { + Log.d(TAG, "preferredRenderer: " + preferredRenderer); + if (creator == null) { + Log.d(TAG, "Making Creator dynamically"); + try { + Context mapsContext = getMapsContext(context, preferredRenderer); + Class clazz = mapsContext.getClassLoader().loadClass("com.google.android.gms.maps.internal.CreatorImpl"); + creator = ICreator.Stub.asInterface((IBinder) clazz.newInstance()); + creator.initV2(ObjectWrapper.wrap(mapsContext.getResources()), Constants.GMS_VERSION_CODE); + } catch (ClassNotFoundException e) { + throw new IllegalStateException("Unable to find dynamic class com.google.android.gms.maps.internal.CreatorImpl"); + } catch (IllegalAccessException e) { + throw new IllegalStateException("Unable to call the default constructor of com.google.android.gms.maps.internal.CreatorImpl"); + } catch (InstantiationException e) { + throw new IllegalStateException("Unable to instantiate the dynamic class com.google.android.gms.maps.internal.CreatorImpl"); + } catch (RemoteException e) { + throw new RuntimeRemoteException(e); + } + } + return creator; + } +} diff --git a/play-services-maps/src/main/res/values/maps_attrs.xml b/play-services-maps/src/main/res/values/maps_attrs.xml new file mode 100644 index 0000000000..3a1fad2c17 --- /dev/null +++ b/play-services-maps/src/main/res/values/maps_attrs.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/play-services-nearby/core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/AdvertiserService.kt b/play-services-nearby/core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/AdvertiserService.kt index da9228fb51..bdd3701873 100644 --- a/play-services-nearby/core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/AdvertiserService.kt +++ b/play-services-nearby/core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/AdvertiserService.kt @@ -19,7 +19,7 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter -import android.os.Build +import android.os.Build.VERSION.SDK_INT import android.os.Handler import android.os.Looper import android.os.SystemClock @@ -142,7 +142,7 @@ class AdvertiserService : LifecycleService() { } val data = AdvertiseData.Builder().addServiceUuid(SERVICE_UUID).addServiceData(SERVICE_UUID, payload).build() Log.i(TAG, "Starting advertiser") - if (Build.VERSION.SDK_INT >= 26) { + if (SDK_INT >= 26) { setCallback = SetCallback() val params = AdvertisingSetParameters.Builder() .setInterval(AdvertisingSetParameters.INTERVAL_MEDIUM) @@ -201,7 +201,7 @@ class AdvertiserService : LifecycleService() { val intent = Intent(this, AdvertiserService::class.java).apply { action = ACTION_RESTART_ADVERTISING } val pendingIntent = PendingIntent.getService(this, ACTION_RESTART_ADVERTISING.hashCode(), intent, FLAG_ONE_SHOT or FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE) when { - Build.VERSION.SDK_INT >= 23 -> + SDK_INT >= 23 -> alarmManager.setExactAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime() + nextSend, pendingIntent) else -> alarmManager.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime() + nextSend, pendingIntent) @@ -214,7 +214,7 @@ class AdvertiserService : LifecycleService() { if (!advertising) return Log.i(TAG, "Stopping advertiser") advertising = false - if (Build.VERSION.SDK_INT >= 26) { + if (SDK_INT >= 26) { wantStartAdvertising = true try { advertiser?.stopAdvertisingSet(setCallback as AdvertisingSetCallback) @@ -259,7 +259,7 @@ class AdvertiserService : LifecycleService() { val adapter = getDefaultAdapter() return when { adapter == null -> false - Build.VERSION.SDK_INT >= 26 && (adapter.isLeExtendedAdvertisingSupported || adapter.isLePeriodicAdvertisingSupported) -> true + SDK_INT >= 26 && (adapter.isLeExtendedAdvertisingSupported || adapter.isLePeriodicAdvertisingSupported) -> true adapter.state != STATE_ON -> null adapter.bluetoothLeAdvertiser != null -> true else -> false diff --git a/play-services-nearby/core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/DeviceInfo.kt b/play-services-nearby/core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/DeviceInfo.kt index cb18b8b5c3..226264f163 100644 --- a/play-services-nearby/core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/DeviceInfo.kt +++ b/play-services-nearby/core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/DeviceInfo.kt @@ -8,7 +8,9 @@ package org.microg.gms.nearby.exposurenotification -import android.os.Build +import android.os.Build.DEVICE +import android.os.Build.MANUFACTURER +import android.os.Build.MODEL import android.util.Log import com.google.android.gms.nearby.exposurenotification.CalibrationConfidence import kotlin.math.roundToInt @@ -25,10 +27,10 @@ val currentDeviceInfo: DeviceInfo var deviceInfo = knownDeviceInfo if (deviceInfo == null) { // Note: Custom ROMs sometimes have slightly different model information, so we have some flexibility for those - val byOem = allDeviceInfos.filter { it.oem.equalsIgnoreCase(Build.MANUFACTURER) } - val byDevice = allDeviceInfos.filter { it.device.equalsIgnoreCase(Build.DEVICE) } - val byModel = allDeviceInfos.filter { it.model.equalsIgnoreCase(Build.MODEL) } - val exactMatch = byOem.find { it.device.equalsIgnoreCase(Build.DEVICE) && it.model.equalsIgnoreCase(Build.MODEL) } + val byOem = allDeviceInfos.filter { it.oem.equalsIgnoreCase(MANUFACTURER) } + val byDevice = allDeviceInfos.filter { it.device.equalsIgnoreCase(DEVICE) } + val byModel = allDeviceInfos.filter { it.model.equalsIgnoreCase(MODEL) } + val exactMatch = byOem.find { it.device.equalsIgnoreCase(DEVICE) && it.model.equalsIgnoreCase(MODEL) } deviceInfo = when { exactMatch != null -> { // Exact match, use provided confidence @@ -36,15 +38,15 @@ val currentDeviceInfo: DeviceInfo } byModel.isNotEmpty() || byDevice.isNotEmpty() -> { // We have data from "sister devices", that's way better than taking the OEM average - averageCurrentDeviceInfo(Build.MANUFACTURER, Build.DEVICE, Build.MODEL, (byDevice + byModel).distinct(), CalibrationConfidence.MEDIUM) + averageCurrentDeviceInfo(MANUFACTURER, DEVICE, MODEL, (byDevice + byModel).distinct(), CalibrationConfidence.MEDIUM) } byOem.isNotEmpty() -> { // Fallback to OEM average - averageCurrentDeviceInfo(Build.MANUFACTURER, Build.DEVICE, Build.MODEL, byOem, CalibrationConfidence.LOW) + averageCurrentDeviceInfo(MANUFACTURER, DEVICE, MODEL, byOem, CalibrationConfidence.LOW) } else -> { // Fallback to all device average - averageCurrentDeviceInfo(Build.MANUFACTURER, Build.DEVICE, Build.MODEL, allDeviceInfos, CalibrationConfidence.LOWEST) + averageCurrentDeviceInfo(MANUFACTURER, DEVICE, MODEL, allDeviceInfos, CalibrationConfidence.LOWEST) } } Log.i(TAG, "Selected $deviceInfo") @@ -54,7 +56,7 @@ val currentDeviceInfo: DeviceInfo } val averageDeviceInfo: DeviceInfo - get() = averageCurrentDeviceInfo(Build.MANUFACTURER, Build.DEVICE, Build.MODEL, allDeviceInfos, CalibrationConfidence.LOWEST) + get() = averageCurrentDeviceInfo(MANUFACTURER, DEVICE, MODEL, allDeviceInfos, CalibrationConfidence.LOWEST) @Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN") private fun String.equalsIgnoreCase(other: String): Boolean = (this as java.lang.String).equalsIgnoreCase(other) diff --git a/play-services-nearby/core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/ExposureNotificationService.kt b/play-services-nearby/core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/ExposureNotificationService.kt index d862340867..f40ced3bf0 100644 --- a/play-services-nearby/core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/ExposureNotificationService.kt +++ b/play-services-nearby/core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/ExposureNotificationService.kt @@ -6,7 +6,7 @@ package org.microg.gms.nearby.exposurenotification import android.content.pm.PackageManager -import android.os.Build +import android.os.Build.VERSION.SDK_INT import android.util.Log import com.google.android.gms.common.Feature import com.google.android.gms.common.internal.ConnectionInfo @@ -33,7 +33,7 @@ class ExposureNotificationService : BaseService(TAG, GmsService.NEARBY_EXPOSURE) checkPermission("android.permission.BLUETOOTH") ?: return } - if (Build.VERSION.SDK_INT < 21) { + if (SDK_INT < 21) { callback.onPostInitComplete(FAILED_NOT_SUPPORTED, null, null) return } diff --git a/play-services-nearby/core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/ExposureNotificationServiceImpl.kt b/play-services-nearby/core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/ExposureNotificationServiceImpl.kt index 499ecebe5d..d81f4946e5 100644 --- a/play-services-nearby/core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/ExposureNotificationServiceImpl.kt +++ b/play-services-nearby/core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/ExposureNotificationServiceImpl.kt @@ -791,7 +791,7 @@ class ExposureNotificationServiceImpl(private val context: Context, private val } } - override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean = warnOnTransactionIssues(code, reply, flags) { super.onTransact(code, data, reply, flags) } + override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean = warnOnTransactionIssues(code, reply, flags, TAG) { super.onTransact(code, data, reply, flags) } companion object { private val tempGrantedPermissions: MutableSet> = hashSetOf() diff --git a/play-services-nearby/core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/NotifyService.kt b/play-services-nearby/core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/NotifyService.kt index d05d777066..6207db6ee8 100644 --- a/play-services-nearby/core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/NotifyService.kt +++ b/play-services-nearby/core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/NotifyService.kt @@ -17,7 +17,7 @@ import android.content.IntentFilter import android.content.pm.PackageManager import android.graphics.Color import android.location.LocationManager -import android.os.Build +import android.os.Build.VERSION.SDK_INT import android.util.Log import android.util.TypedValue import androidx.core.app.NotificationCompat @@ -45,7 +45,7 @@ class NotifyService : LifecycleService() { channel.setSound(null, null) channel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC channel.setShowBadge(true) - if (Build.VERSION.SDK_INT >= 29) { + if (SDK_INT >= 29) { channel.setAllowBubbles(false) } channel.vibrationPattern = longArrayOf(0) @@ -58,7 +58,7 @@ class NotifyService : LifecycleService() { val location = !LocationManagerCompat.isLocationEnabled(getSystemService(Context.LOCATION_SERVICE) as LocationManager) val bluetooth = BluetoothAdapter.getDefaultAdapter()?.state.let { it != BluetoothAdapter.STATE_ON && it != BluetoothAdapter.STATE_TURNING_ON } val nearbyPermissions = arrayOf("android.permission.BLUETOOTH_ADVERTISE", "android.permission.BLUETOOTH_SCAN") - val permissionNeedsHandling = Build.VERSION.SDK_INT >= 31 && nearbyPermissions.any { + val permissionNeedsHandling = SDK_INT >= 31 && nearbyPermissions.any { ContextCompat.checkSelfPermission(this, it) != PackageManager.PERMISSION_GRANTED } Log.d( TAG,"notify: location: $location, bluetooth: $bluetooth, permissionNeedsHandling: $permissionNeedsHandling") @@ -74,7 +74,7 @@ class NotifyService : LifecycleService() { } } - if (Build.VERSION.SDK_INT >= 26) { + if (SDK_INT >= 26) { NotificationCompat.Builder(this, createNotificationChannel()) } else { NotificationCompat.Builder(this) @@ -82,13 +82,13 @@ class NotifyService : LifecycleService() { val typedValue = TypedValue() try { var resolved = theme.resolveAttribute(androidx.appcompat.R.attr.colorError, typedValue, true) - if (!resolved && Build.VERSION.SDK_INT >= 26) resolved = theme.resolveAttribute(android.R.attr.colorError, typedValue, true) + if (!resolved && SDK_INT >= 26) resolved = theme.resolveAttribute(android.R.attr.colorError, typedValue, true) color = if (resolved) { ContextCompat.getColor(this@NotifyService, typedValue.resourceId) } else { Color.RED } - if (Build.VERSION.SDK_INT >= 26) setColorized(true) + if (SDK_INT >= 26) setColorized(true) } catch (e: Exception) { // Ignore } @@ -112,7 +112,7 @@ class NotifyService : LifecycleService() { super.onCreate() registerReceiver(trigger, IntentFilter().apply { addAction(BluetoothAdapter.ACTION_STATE_CHANGED) - if (Build.VERSION.SDK_INT >= 19) addAction(LocationManager.MODE_CHANGED_ACTION) + if (SDK_INT >= 19) addAction(LocationManager.MODE_CHANGED_ACTION) addAction(LocationManager.PROVIDERS_CHANGED_ACTION) addAction(NOTIFICATION_UPDATE_ACTION) }) diff --git a/play-services-nearby/core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/ui/DotChartView.kt b/play-services-nearby/core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/ui/DotChartView.kt index 47aa534457..ba9f13ada7 100644 --- a/play-services-nearby/core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/ui/DotChartView.kt +++ b/play-services-nearby/core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/ui/DotChartView.kt @@ -9,7 +9,7 @@ import android.annotation.SuppressLint import android.annotation.TargetApi import android.content.Context import android.graphics.* -import android.os.Build +import android.os.Build.VERSION.SDK_INT import android.text.format.DateFormat import android.text.format.DateUtils import android.util.AttributeSet @@ -194,7 +194,7 @@ class DotChartView : View { canvas.drawText(strRecords, (subLeft + (perWidth + innerPadding) * 4 + 16 * d + fontTempRect.width() + perWidth).toFloat(), (subTop + perHeight / 2.0 + fontTempRect.height() / 2.0).toFloat(), fontPaint) } - if (focusHour != -1 && Build.VERSION.SDK_INT >= 23) { + if (focusHour != -1 && SDK_INT >= 23) { val floatingColor = context.resolveColor(androidx.appcompat.R.attr.colorBackgroundFloating) ?: 0 val line1 = "${displayData[focusDay]?.first}, $focusHour:00" val line2 = displayData[focusDay]?.second?.get(focusHour)?.let { context.getString(R.string.pref_exposure_rpis_histogram_legend_records, it.toString()) } diff --git a/play-services-nearby/core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/ui/ExposureNotificationsConfirmActivity.kt b/play-services-nearby/core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/ui/ExposureNotificationsConfirmActivity.kt index db6a737516..e202dbf536 100644 --- a/play-services-nearby/core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/ui/ExposureNotificationsConfirmActivity.kt +++ b/play-services-nearby/core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/ui/ExposureNotificationsConfirmActivity.kt @@ -14,7 +14,7 @@ import android.content.pm.PackageInfo.REQUESTED_PERMISSION_NEVER_FOR_LOCATION import android.content.pm.PackageManager import android.content.pm.PackageManager.PERMISSION_GRANTED import android.location.LocationManager -import android.os.Build +import android.os.Build.VERSION.SDK_INT import android.os.Bundle import android.os.ResultReceiver import android.provider.Settings @@ -116,7 +116,7 @@ class ExposureNotificationsConfirmActivity : AppCompatActivity() { private var permissionRequestCode = 33 private fun getRequiredPermissions(): Array { return when { - Build.VERSION.SDK_INT >= 31 -> { + SDK_INT >= 31 -> { // We only need bluetooth permission on 31+ if it's "strongly asserted" that we won't use bluetooth for // location. Otherwise, we also need LOCATION permissions. See // https://developer.android.com/guide/topics/connectivity/bluetooth/permissions#assert-never-for-location @@ -136,7 +136,7 @@ class ExposureNotificationsConfirmActivity : AppCompatActivity() { ACCESS_FINE_LOCATION ) } - Build.VERSION.SDK_INT == 29 -> { + SDK_INT == 29 -> { // We only can directly request background location permission on 29. // We need it on 30 (and possibly later) as well, but it has to be requested in a two // step process, see https://fosstodon.org/@utf8equalsX/104359649537615235 @@ -158,11 +158,11 @@ class ExposureNotificationsConfirmActivity : AppCompatActivity() { private fun checkPermissions() { val permissions = getRequiredPermissions() - permissionNeedsHandling = Build.VERSION.SDK_INT >= 23 && permissions.any { + permissionNeedsHandling = SDK_INT >= 23 && permissions.any { checkSelfPermission(this, it) != PERMISSION_GRANTED } - backgroundLocationNeedsHandling = Build.VERSION.SDK_INT >= 30 + backgroundLocationNeedsHandling = SDK_INT >= 30 && ACCESS_FINE_LOCATION in permissions && checkSelfPermission(this, ACCESS_FINE_LOCATION) == PERMISSION_GRANTED && checkSelfPermission(this, ACCESS_BACKGROUND_LOCATION) != PERMISSION_GRANTED @@ -175,13 +175,13 @@ class ExposureNotificationsConfirmActivity : AppCompatActivity() { } private fun requestPermissions() { - if (Build.VERSION.SDK_INT >= 23) { + if (SDK_INT >= 23) { requestPermissions(getRequiredPermissions(), ++permissionRequestCode) } } private fun requestBackgroundLocation() { - if (Build.VERSION.SDK_INT >= 29) { + if (SDK_INT >= 29) { requestPermissions(arrayOf(ACCESS_BACKGROUND_LOCATION), ++permissionRequestCode) } } diff --git a/play-services-nearby/core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/ui/ExposureNotificationsFragment.kt b/play-services-nearby/core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/ui/ExposureNotificationsFragment.kt index 1e2e44220a..379a25aa3e 100644 --- a/play-services-nearby/core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/ui/ExposureNotificationsFragment.kt +++ b/play-services-nearby/core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/ui/ExposureNotificationsFragment.kt @@ -5,51 +5,202 @@ package org.microg.gms.nearby.exposurenotification.ui +import android.annotation.SuppressLint +import android.bluetooth.BluetoothAdapter +import android.content.Context.LOCATION_SERVICE +import android.content.Intent +import android.content.pm.PackageManager +import android.location.LocationManager +import android.os.Build.VERSION.SDK_INT import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.fragment.app.Fragment +import android.os.Handler +import android.provider.Settings +import androidx.core.content.ContextCompat +import androidx.core.location.LocationManagerCompat +import androidx.core.os.bundleOf import androidx.lifecycle.lifecycleScope +import androidx.navigation.fragment.findNavController +import androidx.preference.Preference +import androidx.preference.PreferenceCategory +import androidx.preference.PreferenceFragmentCompat import org.microg.gms.nearby.core.R -import org.microg.gms.nearby.core.databinding.ExposureNotificationsFragmentBinding -import org.microg.gms.nearby.exposurenotification.ServiceInfo -import org.microg.gms.nearby.exposurenotification.getExposureNotificationsServiceInfo -import org.microg.gms.nearby.exposurenotification.setExposureNotificationsServiceConfiguration -import org.microg.gms.ui.PreferenceSwitchBarCallback - -class ExposureNotificationsFragment : Fragment(R.layout.exposure_notifications_fragment) { - private lateinit var binding: ExposureNotificationsFragmentBinding - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - binding = ExposureNotificationsFragmentBinding.inflate(inflater, container, false) - binding.switchBarCallback = object : PreferenceSwitchBarCallback { - override fun onChecked(newStatus: Boolean) { - setEnabled(newStatus) +import org.microg.gms.nearby.exposurenotification.* +import org.microg.gms.ui.AppIconPreference +import org.microg.gms.ui.SwitchBarPreference +import org.microg.gms.ui.getApplicationInfoIfExists +import org.microg.gms.ui.navigate + + +class ExposureNotificationsFragment : PreferenceFragmentCompat() { + private lateinit var switchBarPreference: SwitchBarPreference + private lateinit var exposureEnableInfo: Preference + private lateinit var exposureBluetoothOff: Preference + private lateinit var exposureLocationOff: Preference + private lateinit var exposureNearbyNotGranted: Preference + private lateinit var exposureBluetoothUnsupported: Preference + private lateinit var exposureBluetoothNoAdvertisement: Preference + private lateinit var exposureApps: PreferenceCategory + private lateinit var exposureAppsNone: Preference + private lateinit var collectedRpis: Preference + private lateinit var advertisingId: Preference + private var turningBluetoothOn: Boolean = false + private val handler = Handler() + private val updateStatusRunnable = Runnable { updateStatus() } + private val updateContentRunnable = Runnable { updateContent() } + private var permissionRequestCode = 33 + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + addPreferencesFromResource(R.xml.preferences_exposure_notifications) + } + + @SuppressLint("RestrictedApi") + override fun onBindPreferences() { + switchBarPreference = preferenceScreen.findPreference("pref_exposure_enabled") ?: switchBarPreference + exposureEnableInfo = preferenceScreen.findPreference("pref_exposure_enable_info") ?: exposureEnableInfo + exposureBluetoothOff = preferenceScreen.findPreference("pref_exposure_error_bluetooth_off") ?: exposureBluetoothOff + exposureLocationOff = preferenceScreen.findPreference("pref_exposure_error_location_off") ?: exposureLocationOff + exposureNearbyNotGranted = preferenceScreen.findPreference("pref_exposure_error_nearby_not_granted") ?: exposureNearbyNotGranted + exposureBluetoothUnsupported = preferenceScreen.findPreference("pref_exposure_error_bluetooth_unsupported") ?: exposureBluetoothUnsupported + exposureBluetoothNoAdvertisement = preferenceScreen.findPreference("pref_exposure_error_bluetooth_no_advertise") ?: exposureBluetoothNoAdvertisement + exposureApps = preferenceScreen.findPreference("prefcat_exposure_apps") ?: exposureApps + exposureAppsNone = preferenceScreen.findPreference("pref_exposure_apps_none") ?: exposureAppsNone + collectedRpis = preferenceScreen.findPreference("pref_exposure_collected_rpis") ?: collectedRpis + advertisingId = preferenceScreen.findPreference("pref_exposure_advertising_id") ?: advertisingId + + switchBarPreference.setOnPreferenceChangeListener { _, newValue -> + val newStatus = newValue as Boolean + ExposurePreferences(requireContext()).enabled = newStatus + true + } + + exposureLocationOff.onPreferenceClickListener = Preference.OnPreferenceClickListener { + val intent = Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS) + startActivity(intent) + true + } + + exposureBluetoothOff.onPreferenceClickListener = Preference.OnPreferenceClickListener { + lifecycleScope.launchWhenStarted { + turningBluetoothOn = true + it.isVisible = false + val adapter = BluetoothAdapter.getDefaultAdapter() + if (adapter != null && !adapter.enableAsync(requireContext())) { + turningBluetoothOn = false + val intent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE) + startActivityForResult(intent, 144) + } else { + turningBluetoothOn = false + updateStatus() + } } + true } - return binding.root - } - fun setEnabled(newStatus: Boolean) { - val appContext = requireContext().applicationContext - lifecycleScope.launchWhenResumed { - val info = getExposureNotificationsServiceInfo(appContext) - val newConfiguration = info.configuration.copy(enabled = newStatus) - setExposureNotificationsServiceConfiguration(appContext, newConfiguration) - displayServiceInfo(info.copy(configuration = newConfiguration)) + exposureNearbyNotGranted.onPreferenceClickListener = Preference.OnPreferenceClickListener { + val nearbyPermissions = arrayOf("android.permission.BLUETOOTH_ADVERTISE", "android.permission.BLUETOOTH_SCAN") + requestPermissions(nearbyPermissions, ++permissionRequestCode) + true + } + + collectedRpis.onPreferenceClickListener = Preference.OnPreferenceClickListener { + findNavController().navigate(requireContext(), R.id.openExposureRpis) + true } } - fun displayServiceInfo(serviceInfo: ServiceInfo) { - binding.scannerEnabled = serviceInfo.configuration.enabled + override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + if (requestCode == this.permissionRequestCode) { + updateStatus() + // Tell the NotifyService that it should update the notification + val intent = Intent(NOTIFICATION_UPDATE_ACTION) + requireContext().sendBroadcast(intent) + } } override fun onResume() { super.onResume() + + updateStatus() + updateContent() + } + + override fun onPause() { + super.onPause() + + handler.removeCallbacks(updateStatusRunnable) + handler.removeCallbacks(updateContentRunnable) + } + + private fun updateStatus() { val appContext = requireContext().applicationContext lifecycleScope.launchWhenResumed { - displayServiceInfo(getExposureNotificationsServiceInfo(appContext)) + handler.postDelayed(updateStatusRunnable, UPDATE_STATUS_INTERVAL) + val enabled = getExposureNotificationsServiceInfo(appContext).configuration.enabled + exposureEnableInfo.isVisible = !enabled + + val bluetoothSupported = ScannerService.isSupported(appContext) + val advertisingSupported = if (bluetoothSupported == true) AdvertiserService.isSupported(appContext) else bluetoothSupported + + val nearbyPermissions = arrayOf("android.permission.BLUETOOTH_ADVERTISE", "android.permission.BLUETOOTH_SCAN") + // Expresses implication (API 31+ → all new permissions granted) ≡ (¬API 31+ | all new permissions granted) + val nearbyPermissionsGranted = SDK_INT < 31 || nearbyPermissions.all { + ContextCompat.checkSelfPermission(appContext, it) == PackageManager.PERMISSION_GRANTED + } + exposureNearbyNotGranted.isVisible = enabled && !nearbyPermissionsGranted + exposureLocationOff.isVisible = enabled && bluetoothSupported != false && !LocationManagerCompat.isLocationEnabled(appContext.getSystemService(LOCATION_SERVICE) as LocationManager) + exposureBluetoothOff.isVisible = enabled && bluetoothSupported == null && !turningBluetoothOn + exposureBluetoothUnsupported.isVisible = enabled && bluetoothSupported == false + exposureBluetoothNoAdvertisement.isVisible = enabled && bluetoothSupported == true && advertisingSupported != true + + advertisingId.isVisible = enabled && advertisingSupported == true } } + + private fun updateContent() { + val context = requireContext() + lifecycleScope.launchWhenResumed { + handler.postDelayed(updateContentRunnable, UPDATE_CONTENT_INTERVAL) + val (apps, lastHourKeys, currentId) = ExposureDatabase.with(context) { database -> + val apps = database.appList.map { packageName -> + context.packageManager.getApplicationInfoIfExists(packageName) + }.filterNotNull().mapIndexed { idx, applicationInfo -> + val pref = AppIconPreference(context) + pref.order = idx + pref.applicationInfo = applicationInfo + pref.onPreferenceClickListener = Preference.OnPreferenceClickListener { + findNavController().navigate(requireContext(), R.id.openExposureAppDetails, bundleOf( + "package" to applicationInfo.packageName + )) + true + } + pref.key = "pref_exposure_app_" + applicationInfo.packageName + pref + } + val lastHourKeys = database.hourRpiCount + val currentId = database.currentRpiId + Triple(apps, lastHourKeys, currentId) + } + collectedRpis.summary = getString(R.string.pref_exposure_collected_rpis_summary, lastHourKeys) + if (currentId != null) { + advertisingId.isVisible = true + advertisingId.summary = currentId.toString() + } else { + advertisingId.isVisible = false + } + exposureApps.removeAll() + if (apps.isEmpty()) { + exposureApps.addPreference(exposureAppsNone) + } else { + for (app in apps) { + exposureApps.addPreference(app) + } + } + } + } + + companion object { + private const val UPDATE_STATUS_INTERVAL = 1000L + private const val UPDATE_CONTENT_INTERVAL = 60000L + } } diff --git a/play-services-nearby/core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/ui/ExposureNotificationsPreferencesFragment.kt b/play-services-nearby/core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/ui/ExposureNotificationsPreferencesFragment.kt deleted file mode 100644 index 37d9b16d91..0000000000 --- a/play-services-nearby/core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/ui/ExposureNotificationsPreferencesFragment.kt +++ /dev/null @@ -1,198 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2022 microG Project Team - * SPDX-License-Identifier: Apache-2.0 - */ - -package org.microg.gms.nearby.exposurenotification.ui - -import android.annotation.SuppressLint -import android.bluetooth.BluetoothAdapter -import android.content.Context.LOCATION_SERVICE -import android.content.Intent -import android.content.pm.PackageManager -import android.location.LocationManager -import android.os.Build -import android.os.Bundle -import android.os.Handler -import android.provider.Settings -import androidx.core.content.ContextCompat -import androidx.core.location.LocationManagerCompat -import androidx.core.os.bundleOf -import androidx.lifecycle.lifecycleScope -import androidx.navigation.fragment.findNavController -import androidx.preference.Preference -import androidx.preference.PreferenceCategory -import androidx.preference.PreferenceFragmentCompat -import org.microg.gms.nearby.core.R -import org.microg.gms.nearby.exposurenotification.* -import org.microg.gms.ui.AppIconPreference -import org.microg.gms.ui.getApplicationInfoIfExists -import org.microg.gms.ui.navigate - - -class ExposureNotificationsPreferencesFragment : PreferenceFragmentCompat() { - private lateinit var exposureEnableInfo: Preference - private lateinit var exposureBluetoothOff: Preference - private lateinit var exposureLocationOff: Preference - private lateinit var exposureNearbyNotGranted: Preference - private lateinit var exposureBluetoothUnsupported: Preference - private lateinit var exposureBluetoothNoAdvertisement: Preference - private lateinit var exposureApps: PreferenceCategory - private lateinit var exposureAppsNone: Preference - private lateinit var collectedRpis: Preference - private lateinit var advertisingId: Preference - private var turningBluetoothOn: Boolean = false - private val handler = Handler() - private val updateStatusRunnable = Runnable { updateStatus() } - private val updateContentRunnable = Runnable { updateContent() } - private var permissionRequestCode = 33 - - override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { - addPreferencesFromResource(R.xml.preferences_exposure_notifications) - } - - @SuppressLint("RestrictedApi") - override fun onBindPreferences() { - exposureEnableInfo = preferenceScreen.findPreference("pref_exposure_enable_info") ?: exposureEnableInfo - exposureBluetoothOff = preferenceScreen.findPreference("pref_exposure_error_bluetooth_off") ?: exposureBluetoothOff - exposureLocationOff = preferenceScreen.findPreference("pref_exposure_error_location_off") ?: exposureLocationOff - exposureNearbyNotGranted = preferenceScreen.findPreference("pref_exposure_error_nearby_not_granted") ?: exposureNearbyNotGranted - exposureBluetoothUnsupported = preferenceScreen.findPreference("pref_exposure_error_bluetooth_unsupported") ?: exposureBluetoothUnsupported - exposureBluetoothNoAdvertisement = preferenceScreen.findPreference("pref_exposure_error_bluetooth_no_advertise") ?: exposureBluetoothNoAdvertisement - exposureApps = preferenceScreen.findPreference("prefcat_exposure_apps") ?: exposureApps - exposureAppsNone = preferenceScreen.findPreference("pref_exposure_apps_none") ?: exposureAppsNone - collectedRpis = preferenceScreen.findPreference("pref_exposure_collected_rpis") ?: collectedRpis - advertisingId = preferenceScreen.findPreference("pref_exposure_advertising_id") ?: advertisingId - - exposureLocationOff.onPreferenceClickListener = Preference.OnPreferenceClickListener { - val intent = Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS) - startActivity(intent) - true - } - - exposureBluetoothOff.onPreferenceClickListener = Preference.OnPreferenceClickListener { - lifecycleScope.launchWhenStarted { - turningBluetoothOn = true - it.isVisible = false - val adapter = BluetoothAdapter.getDefaultAdapter() - if (adapter != null && !adapter.enableAsync(requireContext())) { - turningBluetoothOn = false - val intent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE) - startActivityForResult(intent, 144) - } else { - turningBluetoothOn = false - updateStatus() - } - } - true - } - - exposureNearbyNotGranted.onPreferenceClickListener = Preference.OnPreferenceClickListener { - val nearbyPermissions = arrayOf("android.permission.BLUETOOTH_ADVERTISE", "android.permission.BLUETOOTH_SCAN") - requestPermissions(nearbyPermissions, ++permissionRequestCode) - true - } - - collectedRpis.onPreferenceClickListener = Preference.OnPreferenceClickListener { - findNavController().navigate(requireContext(), R.id.openExposureRpis) - true - } - } - - override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { - super.onRequestPermissionsResult(requestCode, permissions, grantResults) - if (requestCode == this.permissionRequestCode) { - updateStatus() - // Tell the NotifyService that it should update the notification - val intent = Intent(NOTIFICATION_UPDATE_ACTION) - requireContext().sendBroadcast(intent) - } - } - - override fun onResume() { - super.onResume() - - updateStatus() - updateContent() - } - - override fun onPause() { - super.onPause() - - handler.removeCallbacks(updateStatusRunnable) - handler.removeCallbacks(updateContentRunnable) - } - - private fun updateStatus() { - val appContext = requireContext().applicationContext - lifecycleScope.launchWhenResumed { - handler.postDelayed(updateStatusRunnable, UPDATE_STATUS_INTERVAL) - val enabled = getExposureNotificationsServiceInfo(appContext).configuration.enabled - exposureEnableInfo.isVisible = !enabled - - val bluetoothSupported = ScannerService.isSupported(appContext) - val advertisingSupported = if (bluetoothSupported == true) AdvertiserService.isSupported(appContext) else bluetoothSupported - - val nearbyPermissions = arrayOf("android.permission.BLUETOOTH_ADVERTISE", "android.permission.BLUETOOTH_SCAN") - // Expresses implication (API 31+ → all new permissions granted) ≡ (¬API 31+ | all new permissions granted) - val nearbyPermissionsGranted = Build.VERSION.SDK_INT < 31 || nearbyPermissions.all { - ContextCompat.checkSelfPermission(appContext, it) == PackageManager.PERMISSION_GRANTED - } - exposureNearbyNotGranted.isVisible = enabled && !nearbyPermissionsGranted - exposureLocationOff.isVisible = enabled && bluetoothSupported != false && !LocationManagerCompat.isLocationEnabled(appContext.getSystemService(LOCATION_SERVICE) as LocationManager) - exposureBluetoothOff.isVisible = enabled && bluetoothSupported == null && !turningBluetoothOn - exposureBluetoothUnsupported.isVisible = enabled && bluetoothSupported == false - exposureBluetoothNoAdvertisement.isVisible = enabled && bluetoothSupported == true && advertisingSupported != true - - advertisingId.isVisible = enabled && advertisingSupported == true - } - } - - private fun updateContent() { - val context = requireContext() - lifecycleScope.launchWhenResumed { - handler.postDelayed(updateContentRunnable, UPDATE_CONTENT_INTERVAL) - val (apps, lastHourKeys, currentId) = ExposureDatabase.with(context) { database -> - val apps = database.appList.map { packageName -> - context.packageManager.getApplicationInfoIfExists(packageName) - }.filterNotNull().mapIndexed { idx, applicationInfo -> - val pref = AppIconPreference(context) - pref.order = idx - pref.title = applicationInfo.loadLabel(context.packageManager) - pref.icon = applicationInfo.loadIcon(context.packageManager) - pref.onPreferenceClickListener = Preference.OnPreferenceClickListener { - findNavController().navigate(requireContext(), R.id.openExposureAppDetails, bundleOf( - "package" to applicationInfo.packageName - )) - true - } - pref.key = "pref_exposure_app_" + applicationInfo.packageName - pref - } - val lastHourKeys = database.hourRpiCount - val currentId = database.currentRpiId - Triple(apps, lastHourKeys, currentId) - } - collectedRpis.summary = getString(R.string.pref_exposure_collected_rpis_summary, lastHourKeys) - if (currentId != null) { - advertisingId.isVisible = true - advertisingId.summary = currentId.toString() - } else { - advertisingId.isVisible = false - } - exposureApps.removeAll() - if (apps.isEmpty()) { - exposureApps.addPreference(exposureAppsNone) - } else { - for (app in apps) { - exposureApps.addPreference(app) - } - } - } - } - - companion object { - private const val UPDATE_STATUS_INTERVAL = 1000L - private const val UPDATE_CONTENT_INTERVAL = 60000L - } -} diff --git a/play-services-nearby/core/src/main/res/layout/exposure_notifications_fragment.xml b/play-services-nearby/core/src/main/res/layout/exposure_notifications_fragment.xml deleted file mode 100644 index 8814ba933d..0000000000 --- a/play-services-nearby/core/src/main/res/layout/exposure_notifications_fragment.xml +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/play-services-nearby/core/src/main/res/xml/preferences_exposure_notifications.xml b/play-services-nearby/core/src/main/res/xml/preferences_exposure_notifications.xml index 8981daa7d9..02cf24049d 100644 --- a/play-services-nearby/core/src/main/res/xml/preferences_exposure_notifications.xml +++ b/play-services-nearby/core/src/main/res/xml/preferences_exposure_notifications.xml @@ -8,6 +8,11 @@ xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools"> + + { public ExposureNotificationApiClient(Context context, ConnectionCallbacks callbacks, OnConnectionFailedListener connectionFailedListener) { - super(context, callbacks, connectionFailedListener, GmsService.NEARBY_EXPOSURE.ACTION, true); + super(context, callbacks, connectionFailedListener, GmsService.NEARBY_EXPOSURE.ACTION); serviceId = GmsService.NEARBY_EXPOSURE.SERVICE_ID; + requireMicrog = true; } @Override diff --git a/play-services-pay/build.gradle b/play-services-pay/build.gradle new file mode 100644 index 0000000000..f08b9672ff --- /dev/null +++ b/play-services-pay/build.gradle @@ -0,0 +1,35 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +apply plugin: 'com.android.library' +apply plugin: 'maven-publish' +apply plugin: 'signing' + +android { + compileSdkVersion androidCompileSdk + buildToolsVersion "$androidBuildVersionTools" + + defaultConfig { + versionName version + minSdkVersion androidMinSdk + targetSdkVersion androidTargetSdk + } + + compileOptions { + sourceCompatibility = 1.8 + targetCompatibility = 1.8 + } +} + +// Nothing to publish yet +//apply from: '../gradle/publish-android.gradle' + +description = 'microG API for play-services-pay' + +dependencies { + api project(':play-services-base') + + implementation "androidx.annotation:annotation:$annotationVersion" +} diff --git a/play-services-pay/core/build.gradle b/play-services-pay/core/build.gradle new file mode 100644 index 0000000000..92f4793d20 --- /dev/null +++ b/play-services-pay/core/build.gradle @@ -0,0 +1,50 @@ +/* + * SPDX-FileCopyrightText: 2023 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-pay') + + implementation project(':play-services-base-core') + + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutineVersion" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutineVersion" +} + +android { + compileSdkVersion androidCompileSdk + buildToolsVersion "$androidBuildVersionTools" + + defaultConfig { + versionName version + minSdkVersion androidMinSdk + targetSdkVersion androidTargetSdk + } + + sourceSets { + main { + java.srcDirs = ['src/main/kotlin'] + } + } + + lintOptions { + disable 'MissingTranslation' + } + + compileOptions { + sourceCompatibility = 1.8 + targetCompatibility = 1.8 + } +} + +// Nothing to publish yet +//apply from: '../gradle/publish-android.gradle' + +description = 'microG service implementation for play-services-pay' diff --git a/play-services-pay/core/src/main/AndroidManifest.xml b/play-services-pay/core/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..f43d6b1828 --- /dev/null +++ b/play-services-pay/core/src/main/AndroidManifest.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/play-services-pay/core/src/main/kotlin/org/microg/gms/pay/PayActivity.kt b/play-services-pay/core/src/main/kotlin/org/microg/gms/pay/PayActivity.kt new file mode 100644 index 0000000000..cf8880241e --- /dev/null +++ b/play-services-pay/core/src/main/kotlin/org/microg/gms/pay/PayActivity.kt @@ -0,0 +1,21 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.pay + +import android.app.Activity +import android.os.Bundle +import android.view.Gravity +import android.widget.TextView + +class PayActivity : Activity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(TextView(this).apply { + text = "Not yet supported:\n${intent?.action}" + gravity = Gravity.CENTER + }) + } +} \ No newline at end of file diff --git a/play-services-pay/core/src/main/kotlin/org/microg/gms/pay/PayService.kt b/play-services-pay/core/src/main/kotlin/org/microg/gms/pay/PayService.kt new file mode 100644 index 0000000000..69b9003d55 --- /dev/null +++ b/play-services-pay/core/src/main/kotlin/org/microg/gms/pay/PayService.kt @@ -0,0 +1,99 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.pay + +import android.os.Parcel +import com.google.android.gms.common.Feature +import com.google.android.gms.common.api.CommonStatusCodes +import com.google.android.gms.common.internal.ConnectionInfo +import com.google.android.gms.common.internal.GetServiceRequest +import com.google.android.gms.common.internal.IGmsCallbacks +import com.google.android.gms.pay.internal.IPayService +import org.microg.gms.BaseService +import org.microg.gms.common.GmsService +import org.microg.gms.utils.warnOnTransactionIssues + +private const val TAG = "GmsPay" + +class PayService : BaseService(TAG, GmsService.PAY) { + override fun handleServiceRequest(callback: IGmsCallbacks, request: GetServiceRequest, service: GmsService) { + callback.onPostInitCompleteWithConnectionInfo(CommonStatusCodes.SUCCESS, PayImpl(), ConnectionInfo().apply { + features = arrayOf( + Feature("pay", 10), + Feature("pay_attestation_signal", 1), + Feature("pay_pay_capabilities", 1), + Feature("pay_feature_check", 1), + Feature("pay_get_card_centric_bundle", 1), + Feature("pay_get_passes", 1), + Feature("pay_get_pay_api_availability_status", 3), + Feature("pay_get_se_prepaid_card", 1), + Feature("pay_debit_se_prepaid_card", 1), + Feature("pay_get_specific_bulletin", 1), + Feature("pay_get_transit_cards", 1), + Feature("pay_get_wallet_status", 1), + Feature("pay_global_actions", 1), + Feature("pay_gp3_support", 1), + Feature("pay_homescreen_sorting", 3), + Feature("pay_homescreen_bulletins", 2), + Feature("pay_onboarding", 2), + Feature("pay_mark_tos_accepted_for_partner", 1), + Feature("pay_move_card_on_other_device", 1), + Feature("pay_passes_field_update_notifications", 1), + Feature("pay_passes_notifications", 2), + Feature("pay_payment_method", 1), + Feature("pay_payment_method_action_tokens", 2), + Feature("pay_payment_method_server_action", 1), + Feature("pay_provision_se_prepaid_card", 1), + Feature("pay_request_module", 1), + Feature("pay_reverse_purchase", 1), + Feature("pay_save_passes", 5), + Feature("pay_save_passes_jwt", 3), + Feature("pay_save_purchased_card", 1), + Feature("pay_sync_bundle", 2), + Feature("pay_settings", 1), + Feature("pay_topup_se_prepaid_card", 1), + Feature("pay_list_commuter_pass_renewal_options_for_se_prepaid_card", 1), + Feature("pay_transactions", 6), + Feature("pay_update_bundle_with_client_settings", 1), + Feature("pay_clock_skew_millis", 1), + Feature("pay_se_postpaid_transactions", 1), + Feature("pay_se_prepaid_transactions", 1), + Feature("pay_get_clock_skew_millis", 1), + Feature("pay_renew_commuter_pass_for_se_prepaid_card", 1), + Feature("pay_remove_se_postpaid_token", 1), + Feature("pay_change_se_postpaid_default_status", 1), + Feature("pay_wear_payment_methods", 2), + Feature("pay_wear_closed_loop_cards", 1), + Feature("pay_perform_wear_operation", 1), + Feature("pay_delete_se_prepaid_card", 1), + Feature("pay_transit_issuer_tos", 1), + Feature("pay_get_se_mfi_prepaid_cards", 1), + Feature("pay_get_last_user_present_timestamp", 1), + Feature("pay_mdoc", 7), + Feature("pay_get_se_feature_readiness_status", 1), + Feature("pay_recover_se_card", 1), + Feature("pay_set_wallet_item_surfacing", 2), + Feature("pay_set_se_transit_default", 1), + Feature("pay_get_wallet_bulletins", 2), + Feature("pay_mse_operation", 1), + Feature("pay_clear_bulletin_interaction_for_dev", 1), + Feature("pay_get_pending_intent_for_wallet_on_wear", 2), + Feature("pay_get_predefined_rotating_barcode_values", 1), + Feature("pay_get_mdl_refresh_timestamps", 1), + Feature("pay_store_mdl_refresh_timestamp", 1), + Feature("pay_perform_id_card_operation", 1), + Feature("pay_block_closed_loop_cards", 1), + Feature("pay_delete_data_for_tests", 1), + Feature("pay_perform_closed_loop_operation", 1) + ) + }) + } +} + +class PayImpl : IPayService.Stub() { + 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-pay/src/main/AndroidManifest.xml b/play-services-pay/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..bcfcb9382b --- /dev/null +++ b/play-services-pay/src/main/AndroidManifest.xml @@ -0,0 +1,6 @@ + + + diff --git a/play-services-pay/src/main/aidl/com/google/android/gms/pay/internal/IPayService.aidl b/play-services-pay/src/main/aidl/com/google/android/gms/pay/internal/IPayService.aidl new file mode 100644 index 0000000000..0efda1cb2d --- /dev/null +++ b/play-services-pay/src/main/aidl/com/google/android/gms/pay/internal/IPayService.aidl @@ -0,0 +1,5 @@ +package com.google.android.gms.pay.internal; + +interface IPayService { + +} \ No newline at end of file diff --git a/play-services-pay/src/main/java/com/google/android/gms/pay/Pay.java b/play-services-pay/src/main/java/com/google/android/gms/pay/Pay.java new file mode 100644 index 0000000000..8cac044623 --- /dev/null +++ b/play-services-pay/src/main/java/com/google/android/gms/pay/Pay.java @@ -0,0 +1,32 @@ +/* + * 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.pay; + +import android.app.Activity; +import android.content.Context; +import org.microg.gms.pay.PayClientImpl; + +/** + * Entry point for Pay API. + */ +public class Pay { + /** + * Creates a new instance of {@link PayClient} for use in an {@link Activity}. This client should not be used outside of the given {@code Activity}. + */ + public static PayClient getClient(Activity activity) { + return new PayClientImpl(activity); + } + + /** + * Creates a new instance of {@link PayClient} for use in a non-{@code Activity} {@link Context}. + */ + public static PayClient getClient(Context context) { + return new PayClientImpl(context); + } +} diff --git a/play-services-pay/src/main/java/com/google/android/gms/pay/PayApiAvailabilityStatus.java b/play-services-pay/src/main/java/com/google/android/gms/pay/PayApiAvailabilityStatus.java new file mode 100644 index 0000000000..ea9a0c51ad --- /dev/null +++ b/play-services-pay/src/main/java/com/google/android/gms/pay/PayApiAvailabilityStatus.java @@ -0,0 +1,33 @@ +/* + * 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.pay; + +import androidx.annotation.IntDef; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Pay API availability status on the device. + */ +@Target({ElementType.TYPE_USE}) +@Retention(RetentionPolicy.SOURCE) +@IntDef({PayApiAvailabilityStatus.AVAILABLE, PayApiAvailabilityStatus.NOT_ELIGIBLE}) +public @interface PayApiAvailabilityStatus { + /** + * Indicates that the Pay API requested is available and ready to be used. + */ + int AVAILABLE = 0; + /** + * Indicates that the user is currently not eligible for using the Pay API requested. The user may become eligible in the future. + */ + int NOT_ELIGIBLE = 2; +} diff --git a/play-services-pay/src/main/java/com/google/android/gms/pay/PayClient.java b/play-services-pay/src/main/java/com/google/android/gms/pay/PayClient.java new file mode 100644 index 0000000000..7c5a9d36e2 --- /dev/null +++ b/play-services-pay/src/main/java/com/google/android/gms/pay/PayClient.java @@ -0,0 +1,147 @@ +/* + * 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.pay; + +import android.app.Activity; +import android.app.PendingIntent; +import androidx.annotation.IntDef; +import com.google.android.gms.common.api.Api; +import com.google.android.gms.common.api.HasApiKey; +import com.google.android.gms.tasks.Task; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Interface for Pay API. + */ +public interface PayClient extends HasApiKey { + /** + * Debug message passed back in {@code onActivityResult()} when calling {@link #savePasses(String, Activity, int)} or + * {@link #savePassesJwt(String, Activity, int)}. + */ + String EXTRA_API_ERROR_MESSAGE = "extra_api_error_message"; + + /** + * Gets the {@link PayApiAvailabilityStatus} of the current user and device. + * + * @param requestType A {@link PayClient.RequestType} for how the API will be used. + * @return One of the possible {@link PayApiAvailabilityStatus}. + */ + Task<@PayApiAvailabilityStatus Integer> getPayApiAvailabilityStatus(@RequestType int requestType); + + /** + * Create a {@link PendingIntent} for the Wear Wallet activity. May return an error if pay is not supported in this region or if the watch is not reachable. + * + * @param wearNodeId The node id of the watch. + * @param intentSource The {@link PayClient.WearWalletIntentSource} that launches the requested page. + */ + Task getPendingIntentForWalletOnWear(String wearNodeId, @WearWalletIntentSource int intentSource); + + /** + * Provides the product name in this market. + */ + PayClient.ProductName getProductName(); + + /** + * Saves one or multiple passes in a JSON format. + *

+ * Must be called from an {@code Activity}. + * + * @param json A JSON string request to save one or multiple passes. The JSON format is consistent with the JWT save link format. Refer to + * Google Pay API for Passes for an overview on how save links are generated. Only focus on how the JSON is formatted. + * There is no need to sign the JSON string. + * @param activity The {@code Activity} that will receive the callback result. + * @param requestCode An integer request code that will be passed back in {@code onActivityResult()}, allowing you to identify whom this result came from. + */ + void savePasses(String json, Activity activity, int requestCode); + + /** + * Saves one or multiple passes in a JWT format. + *

+ * Must be called from an {@code Activity}. + * + * @param jwt A JWT string token to save one or multiple passes. The token format is the same used in the JWT save link format. Refer to + * Google Pay API for Passes for an overview on how save links are generated. + * @param activity The {@code Activity} that will receive the callback result. + * @param requestCode An integer request code that will be passed back in {@code onActivityResult()}, allowing you to identify whom this result came from. + */ + void savePassesJwt(String jwt, Activity activity, int requestCode); + + /** + * Indicates what the product is called in this market + */ + enum ProductName { + GOOGLE_PAY, + GOOGLE_WALLET + } + + /** + * All possible request types that will be used by callers of {@link PayClient#getPayApiAvailabilityStatus(int)}. + */ + @Target({ElementType.TYPE_USE}) + @Retention(RetentionPolicy.SOURCE) + @IntDef({RequestType.CARD_PROVISIONING_DEEP_LINK, RequestType.SAVE_PASSES, RequestType.SAVE_PASSES_JWT}) + @interface RequestType { + /** + * Checks support of card provisioning deep links. + */ + int CARD_PROVISIONING_DEEP_LINK = 1; + /** + * Checks availability of the {@link PayClient#savePasses(String, Activity, int)} API. + */ + int SAVE_PASSES = 2; + /** + * Checks availability of the {@link PayClient#savePassesJwt(String, Activity, int)} API. + */ + int SAVE_PASSES_JWT = 3; + } + + /** + * Possible result codes passed back in {@code onActivityResult()} when calling {@link PayClient#savePasses(String, Activity, int)} or + * {@link PayClient#savePassesJwt(String, Activity, int)}. These are in addition to {@link Activity#RESULT_OK} and {@link Activity#RESULT_CANCELED}. + */ + @Target({ElementType.TYPE_USE}) + @Retention(RetentionPolicy.SOURCE) + @IntDef({Activity.RESULT_OK, Activity.RESULT_CANCELED, SavePassesResult.API_UNAVAILABLE, SavePassesResult.SAVE_ERROR, SavePassesResult.INTERNAL_ERROR}) + @interface SavePassesResult { + /** + * The {@link PayClient#savePasses(String, Activity, int)} or {@link PayClient#savePassesJwt(String, Activity, int) API is unavailable. + * Use {@link PayClient#getPayApiAvailabilityStatus(int)} before calling the API. + */ + int API_UNAVAILABLE = 1; + /** + * An error occurred while saving the passes. Check {@code EXTRA_API_ERROR_MESSAGE} to debug the issue. + */ + int SAVE_ERROR = 2; + /** + * Indicates that an internal error occurred while calling the API. Retry the API call. If the error persists assume that the API is not available. + */ + int INTERNAL_ERROR = 3; + } + + /** + * Intent source for Wear Card Management Activity. Behavior may depend on the source. + */ + @Target({ElementType.TYPE_USE}) + @Retention(RetentionPolicy.SOURCE) + @IntDef({WearWalletIntentSource.OOBE, WearWalletIntentSource.SETTINGS}) + @interface WearWalletIntentSource { + /** + * Start Wear Wallet for out of box experience + */ + int OOBE = 20; + /** + * Start Wear Wallet from settings + */ + int SETTINGS = 21; + } +} diff --git a/play-services-pay/src/main/java/org/microg/gms/pay/PayApiClient.java b/play-services-pay/src/main/java/org/microg/gms/pay/PayApiClient.java new file mode 100644 index 0000000000..866afb0547 --- /dev/null +++ b/play-services-pay/src/main/java/org/microg/gms/pay/PayApiClient.java @@ -0,0 +1,26 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.pay; + +import android.content.Context; +import android.os.IBinder; +import org.microg.gms.common.GmsClient; +import com.google.android.gms.pay.internal.IPayService; +import org.microg.gms.common.GmsService; +import org.microg.gms.common.api.ConnectionCallbacks; +import org.microg.gms.common.api.OnConnectionFailedListener; + +public class PayApiClient extends GmsClient { + public PayApiClient(Context context, ConnectionCallbacks callbacks, OnConnectionFailedListener connectionFailedListener) { + super(context, callbacks, connectionFailedListener, GmsService.PAY.ACTION); + serviceId = GmsService.PAY.SERVICE_ID; + } + + @Override + protected IPayService interfaceFromBinder(IBinder binder) { + return IPayService.Stub.asInterface(binder); + } +} diff --git a/play-services-pay/src/main/java/org/microg/gms/pay/PayClientImpl.java b/play-services-pay/src/main/java/org/microg/gms/pay/PayClientImpl.java new file mode 100644 index 0000000000..8817b2c9fc --- /dev/null +++ b/play-services-pay/src/main/java/org/microg/gms/pay/PayClientImpl.java @@ -0,0 +1,49 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.pay; + +import android.app.Activity; +import android.app.PendingIntent; +import android.content.Context; +import com.google.android.gms.common.api.Api; +import com.google.android.gms.common.api.GoogleApi; +import com.google.android.gms.pay.PayApiAvailabilityStatus; +import com.google.android.gms.pay.PayClient; +import com.google.android.gms.tasks.Task; +import com.google.android.gms.tasks.Tasks; + +public class PayClientImpl extends GoogleApi implements PayClient { + private static final Api API = new Api<>((options, context, looper, clientSettings, callbacks, connectionFailedListener) -> new PayApiClient(context, callbacks, connectionFailedListener)); + + public PayClientImpl(Context context) { + super(context, API); + } + + @Override + public Task<@PayApiAvailabilityStatus Integer> getPayApiAvailabilityStatus(@RequestType int requestType) { + return Tasks.forResult(PayApiAvailabilityStatus.NOT_ELIGIBLE); + } + + @Override + public Task getPendingIntentForWalletOnWear(String wearNodeId, @WearWalletIntentSource int intentSource) { + return null; + } + + @Override + public ProductName getProductName() { + return ProductName.GOOGLE_WALLET; + } + + @Override + public void savePasses(String json, Activity activity, int requestCode) { + + } + + @Override + public void savePassesJwt(String jwt, Activity activity, int requestCode) { + + } +} diff --git a/play-services-recaptcha/core/build.gradle b/play-services-recaptcha/core/build.gradle index c2d801b466..f1529a3103 100644 --- a/play-services-recaptcha/core/build.gradle +++ b/play-services-recaptcha/core/build.gradle @@ -12,6 +12,7 @@ apply plugin: 'signing' dependencies { implementation project(':play-services-base-core') implementation project(':play-services-droidguard-core') + implementation project(':play-services-safetynet-core') implementation project(':play-services-recaptcha') implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion" @@ -21,6 +22,7 @@ dependencies { implementation "androidx.core:core-ktx:$coreVersion" implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion" implementation "androidx.lifecycle:lifecycle-service:$lifecycleVersion" + implementation "androidx.webkit:webkit:$webkitVersion" implementation "com.android.volley:volley:$volleyVersion" implementation "com.squareup.wire:wire-runtime:$wireVersion" diff --git a/play-services-recaptcha/core/src/main/AndroidManifest.xml b/play-services-recaptcha/core/src/main/AndroidManifest.xml index 586b349d75..d3330f629e 100644 --- a/play-services-recaptcha/core/src/main/AndroidManifest.xml +++ b/play-services-recaptcha/core/src/main/AndroidManifest.xml @@ -5,12 +5,14 @@ --> + package="org.microg.gms.recaptcha.core"> - + + - + diff --git a/play-services-recaptcha/core/src/main/kotlin/org/microg/gms/recaptcha/RecaptchaGuardImpl.kt b/play-services-recaptcha/core/src/main/kotlin/org/microg/gms/recaptcha/RecaptchaGuardImpl.kt new file mode 100644 index 0000000000..6494ff94a9 --- /dev/null +++ b/play-services-recaptcha/core/src/main/kotlin/org/microg/gms/recaptcha/RecaptchaGuardImpl.kt @@ -0,0 +1,131 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.recaptcha + +import android.content.Context +import android.os.Build.VERSION.SDK_INT +import android.os.LocaleList +import android.util.Log +import com.android.volley.* +import com.android.volley.toolbox.Volley +import com.google.android.gms.recaptcha.RecaptchaHandle +import com.google.android.gms.recaptcha.RecaptchaResultData +import com.google.android.gms.recaptcha.internal.ExecuteParams +import com.google.android.gms.recaptcha.internal.InitParams +import com.squareup.wire.Message +import com.squareup.wire.ProtoAdapter +import kotlinx.coroutines.CompletableDeferred +import org.microg.gms.droidguard.core.DroidGuardResultCreator +import org.microg.gms.droidguard.core.VersionUtil +import java.util.* +import kotlin.collections.HashMap + +private const val TAG = "RecaptchaGuard" + +class RecaptchaGuardImpl(private val context: Context, private val packageName: String) : RecaptchaImpl { + private val queue = Volley.newRequestQueue(context) + private var lastToken: String? = null + + override suspend fun init(params: InitParams): RecaptchaHandle { + val response = ProtobufPostRequest( + "https://www.recaptcha.net/recaptcha/api3/ac", RecaptchaInitRequest( + data_ = RecaptchaInitRequest.Data( + siteKey = params.siteKey, + packageName = packageName, + version = "${VersionUtil(context).versionCode};${params.version}" + ) + ), RecaptchaInitResponse.ADAPTER + ).sendAndAwait(queue) + lastToken = response.token + return RecaptchaHandle(params.siteKey, packageName, response.acceptableAdditionalArgs.toList()) + } + + override suspend fun execute(params: ExecuteParams): RecaptchaResultData { + if (params.handle.clientPackageName != null && params.handle.clientPackageName != packageName) throw IllegalArgumentException("invalid handle") + val timestamp = System.currentTimeMillis() + val additionalArgs = mutableMapOf() + val guardMap = mutableMapOf() + for (key in params.action.additionalArgs.keySet()) { + val value = params.action.additionalArgs.getString(key) + ?: throw Exception("Only string values are allowed as an additional arg in RecaptchaAction") + if (key !in params.handle.acceptableAdditionalArgs) + throw Exception("AdditionalArgs key[ \"$key\" ] is not accepted by reCATPCHA server") + additionalArgs.put(key, value) + } + Log.d(TAG, "Additional arguments: $additionalArgs") + if (lastToken == null) { + init(InitParams().apply { siteKey = params.handle.siteKey; version = params.version }) + } + val token = lastToken!! + guardMap["token"] = token + guardMap["action"] = params.action.toString() + guardMap["timestamp_millis"] to timestamp.toString() + guardMap.putAll(additionalArgs) + if (params.action.verificationHistoryToken != null) + guardMap["verification_history_token"] = params.action.verificationHistoryToken + val dg = DroidGuardResultCreator.getResults(context, "recaptcha-android", guardMap) + val response = ProtobufPostRequest( + "https://www.recaptcha.net/recaptcha/api3/ae", RecaptchaExecuteRequest( + token = token, + action = params.action.toString(), + timestamp = timestamp, + dg = dg, + additionalArgs = additionalArgs, + verificationHistoryToken = params.action.verificationHistoryToken + ), RecaptchaExecuteResponse.ADAPTER + ).sendAndAwait(queue) + return RecaptchaResultData(response.token) + } + + override suspend fun close(handle: RecaptchaHandle): Boolean { + if (handle.clientPackageName != null && handle.clientPackageName != packageName) throw IllegalArgumentException("invalid handle") + val closed = lastToken != null + lastToken = null + return closed + } +} + +class ProtobufPostRequest, O>(url: String, private val i: I, private val oAdapter: ProtoAdapter) : + Request(Request.Method.POST, url, null) { + private val deferred = CompletableDeferred() + + override fun getHeaders(): Map { + val headers = HashMap(super.getHeaders()) + headers["Accept-Language"] = if (SDK_INT >= 24) LocaleList.getDefault().toLanguageTags() else Locale.getDefault().language + return headers + } + + override fun getBody(): ByteArray = i.encode() + + override fun getBodyContentType(): String = "application/x-protobuf" + + override fun parseNetworkResponse(response: NetworkResponse): Response { + try { + return Response.success(oAdapter.decode(response.data), null) + } catch (e: VolleyError) { + return Response.error(e) + } catch (e: Exception) { + return Response.error(VolleyError()) + } + } + + override fun deliverResponse(response: O) { + Log.d(TAG, "Got response: $response") + deferred.complete(response) + } + + override fun deliverError(error: VolleyError) { + deferred.completeExceptionally(error) + } + + suspend fun await(): O = deferred.await() + + suspend fun sendAndAwait(queue: RequestQueue): O { + Log.d(TAG, "Sending request: $i") + queue.add(this) + return await() + } +} \ No newline at end of file diff --git a/play-services-recaptcha/core/src/main/kotlin/org/microg/gms/recaptcha/RecaptchaImpl.kt b/play-services-recaptcha/core/src/main/kotlin/org/microg/gms/recaptcha/RecaptchaImpl.kt new file mode 100644 index 0000000000..e4252edf5f --- /dev/null +++ b/play-services-recaptcha/core/src/main/kotlin/org/microg/gms/recaptcha/RecaptchaImpl.kt @@ -0,0 +1,32 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.recaptcha + +import com.google.android.gms.recaptcha.RecaptchaHandle +import com.google.android.gms.recaptcha.RecaptchaResultData +import com.google.android.gms.recaptcha.internal.ExecuteParams +import com.google.android.gms.recaptcha.internal.InitParams + +interface RecaptchaImpl { + suspend fun init(params: InitParams): RecaptchaHandle + suspend fun execute(params: ExecuteParams): RecaptchaResultData + suspend fun close(handle: RecaptchaHandle): Boolean + + object Unsupported : RecaptchaImpl { + override suspend fun init(params: InitParams): RecaptchaHandle { + throw UnsupportedOperationException() + } + + override suspend fun execute(params: ExecuteParams): RecaptchaResultData { + throw UnsupportedOperationException() + } + + override suspend fun close(handle: RecaptchaHandle): Boolean { + throw UnsupportedOperationException() + } + } +} + diff --git a/play-services-recaptcha/core/src/main/kotlin/org/microg/gms/recaptcha/RecaptchaService.kt b/play-services-recaptcha/core/src/main/kotlin/org/microg/gms/recaptcha/RecaptchaService.kt index a1541dc562..c4fbf12163 100644 --- a/play-services-recaptcha/core/src/main/kotlin/org/microg/gms/recaptcha/RecaptchaService.kt +++ b/play-services-recaptcha/core/src/main/kotlin/org/microg/gms/recaptcha/RecaptchaService.kt @@ -5,51 +5,44 @@ package org.microg.gms.recaptcha import android.content.Context -import android.os.Build -import android.os.LocaleList +import android.os.Build.VERSION.SDK_INT import android.os.Parcel -import android.util.Base64 import android.util.Log import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope -import com.android.volley.NetworkResponse -import com.android.volley.Request -import com.android.volley.RequestQueue -import com.android.volley.Response -import com.android.volley.VolleyError -import com.android.volley.toolbox.Volley import com.google.android.gms.common.Feature import com.google.android.gms.common.api.CommonStatusCodes import com.google.android.gms.common.api.Status import com.google.android.gms.common.internal.ConnectionInfo -import org.microg.gms.BaseService -import com.google.android.gms.common.internal.IGmsCallbacks import com.google.android.gms.common.internal.GetServiceRequest +import com.google.android.gms.common.internal.IGmsCallbacks import com.google.android.gms.recaptcha.RecaptchaAction import com.google.android.gms.recaptcha.RecaptchaHandle -import com.google.android.gms.recaptcha.RecaptchaResultData import com.google.android.gms.recaptcha.internal.* -import com.squareup.wire.Message -import com.squareup.wire.ProtoAdapter -import kotlinx.coroutines.CompletableDeferred -import org.microg.gms.common.Constants +import kotlinx.coroutines.launch +import org.microg.gms.BaseService import org.microg.gms.common.GmsService import org.microg.gms.common.PackageUtils -import org.microg.gms.droidguard.core.DroidGuardResultCreator -import org.microg.gms.droidguard.core.VersionUtil +import org.microg.gms.droidguard.core.DroidGuardPreferences +import org.microg.gms.safetynet.SafetyNetPreferences import org.microg.gms.utils.warnOnTransactionIssues -import java.nio.charset.Charset -import java.util.Locale private const val TAG = "RecaptchaService" class RecaptchaService : BaseService(TAG, GmsService.RECAPTCHA) { + private fun getRecaptchaImpl(packageName: String) = when { + SafetyNetPreferences.isEnabled(this) && SDK_INT >= 19 -> RecaptchaWebImpl(this, packageName, lifecycle) + DroidGuardPreferences.isAvailable(this) -> RecaptchaGuardImpl(this, packageName) + else -> RecaptchaImpl.Unsupported + } + override fun handleServiceRequest(callback: IGmsCallbacks, request: GetServiceRequest, service: GmsService) { - PackageUtils.getAndCheckCallingPackage(this, request.packageName) + val packageName = PackageUtils.getAndCheckCallingPackage(this, request.packageName)!! + val impl = getRecaptchaImpl(packageName) callback.onPostInitCompleteWithConnectionInfo( CommonStatusCodes.SUCCESS, - RecaptchaServiceImpl(this, request.packageName, lifecycle), + RecaptchaServiceImpl(this, packageName, lifecycle, impl), ConnectionInfo().apply { features = arrayOf( Feature("verify_with_recaptcha_v2_internal", 1), @@ -65,10 +58,9 @@ class RecaptchaService : BaseService(TAG, GmsService.RECAPTCHA) { class RecaptchaServiceImpl( private val context: Context, private val packageName: String, - private val lifecycle: Lifecycle + private val lifecycle: Lifecycle, + private val impl: RecaptchaImpl ) : IRecaptchaService.Stub(), LifecycleOwner { - private val queue = Volley.newRequestQueue(context) - private var lastToken: String? = null override fun getLifecycle(): Lifecycle { return lifecycle @@ -94,38 +86,21 @@ class RecaptchaServiceImpl( } override fun close(callback: ICloseCallback, handle: RecaptchaHandle) { - Log.d(TAG, "close($handle)") - lifecycleScope.launchWhenStarted { + lifecycleScope.launch { + Log.d(TAG, "close($handle)") try { - val closed = lastToken != null - lastToken = null - callback.onClosed(Status.SUCCESS, closed) + callback.onClosed(Status.SUCCESS, impl.close(handle)) } catch (e: Exception) { Log.w(TAG, e) } } } - suspend fun runInit(siteKey: String, version: String): RecaptchaInitResponse { - val response = ProtobufPostRequest( - "https://www.recaptcha.net/recaptcha/api3/ac", RecaptchaInitRequest( - data_ = RecaptchaInitRequest.Data( - siteKey = siteKey, - packageName = packageName, - version = "${VersionUtil(context).versionCode};${version}" - ) - ), RecaptchaInitResponse.ADAPTER - ).sendAndAwait(queue) - lastToken = response.token - return response - } - override fun init2(callback: IInitCallback, params: InitParams) { - Log.d(TAG, "init($params)") - lifecycleScope.launchWhenStarted { + lifecycleScope.launch { + Log.d(TAG, "init($params)") try { - val response = runInit(params.siteKey, params.version) - val handle = RecaptchaHandle(params.siteKey, packageName, response.acceptableAdditionalArgs.toList()) + val handle = impl.init(params) if (params.version == LEGACY_VERSION) { callback.onHandle(Status.SUCCESS, handle) } else { @@ -144,43 +119,13 @@ class RecaptchaServiceImpl( } } } - } override fun execute2(callback: IExecuteCallback, params: ExecuteParams) { Log.d(TAG, "execute($params)") - lifecycleScope.launchWhenStarted { + lifecycleScope.launch { try { - val timestamp = System.currentTimeMillis() - val additionalArgs = mutableMapOf() - val guardMap = mutableMapOf() - for (key in params.action.additionalArgs.keySet()) { - val value = params.action.additionalArgs.getString(key) - ?: throw Exception("Only string values are allowed as an additional arg in RecaptchaAction") - if (key !in params.handle.acceptableAdditionalArgs) - throw Exception("AdditionalArgs key[ \"$key\" ] is not accepted by reCATPCHA server") - additionalArgs.put(key, value) - } - Log.d(TAG, "Additional arguments: $additionalArgs") - val token = lastToken ?: runInit(params.handle.siteKey, params.version).token!! - guardMap["token"] = token - guardMap["action"] = params.action.toString() - guardMap["timestamp_millis"] to timestamp.toString() - guardMap.putAll(additionalArgs) - if (params.action.verificationHistoryToken != null) - guardMap["verification_history_token"] = params.action.verificationHistoryToken - val dg = DroidGuardResultCreator.getResults(context, "recaptcha-android", guardMap) - val response = ProtobufPostRequest( - "https://www.recaptcha.net/recaptcha/api3/ae", RecaptchaExecuteRequest( - token = token, - action = params.action.toString(), - timestamp = timestamp, - dg = dg, - additionalArgs = additionalArgs, - verificationHistoryToken = params.action.verificationHistoryToken - ), RecaptchaExecuteResponse.ADAPTER - ).sendAndAwait(queue) - val data = RecaptchaResultData(response.token) + val data = impl.execute(params) if (params.version == LEGACY_VERSION) { callback.onData(Status.SUCCESS, data) } else { @@ -203,7 +148,7 @@ class RecaptchaServiceImpl( } override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean = - warnOnTransactionIssues(code, reply, flags) { + warnOnTransactionIssues(code, reply, flags, TAG) { super.onTransact(code, data, reply, flags) } @@ -211,45 +156,3 @@ class RecaptchaServiceImpl( const val LEGACY_VERSION = "16.0.0" } } - -class ProtobufPostRequest, O>(url: String, private val i: I, private val oAdapter: ProtoAdapter) : - Request(Request.Method.POST, url, null) { - private val deferred = CompletableDeferred() - - override fun getHeaders(): Map { - val headers = HashMap(super.getHeaders()) - headers["Accept-Language"] = if (Build.VERSION.SDK_INT >= 24) LocaleList.getDefault().toLanguageTags() else Locale.getDefault().language - return headers - } - - override fun getBody(): ByteArray = i.encode() - - override fun getBodyContentType(): String = "application/x-protobuf" - - override fun parseNetworkResponse(response: NetworkResponse): Response { - try { - return Response.success(oAdapter.decode(response.data), null) - } catch (e: VolleyError) { - return Response.error(e) - } catch (e: Exception) { - return Response.error(VolleyError()) - } - } - - override fun deliverResponse(response: O) { - Log.d(TAG, "Got response: $response") - deferred.complete(response) - } - - override fun deliverError(error: VolleyError) { - deferred.completeExceptionally(error) - } - - suspend fun await(): O = deferred.await() - - suspend fun sendAndAwait(queue: RequestQueue): O { - Log.d(TAG, "Sending request: $i") - queue.add(this) - return await() - } -} diff --git a/play-services-recaptcha/core/src/main/kotlin/org/microg/gms/recaptcha/RecaptchaWebImpl.kt b/play-services-recaptcha/core/src/main/kotlin/org/microg/gms/recaptcha/RecaptchaWebImpl.kt new file mode 100644 index 0000000000..b5ca0bac7c --- /dev/null +++ b/play-services-recaptcha/core/src/main/kotlin/org/microg/gms/recaptcha/RecaptchaWebImpl.kt @@ -0,0 +1,738 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.recaptcha + +import android.app.Application +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.media.AudioManager +import android.os.BatteryManager +import android.os.Handler +import android.provider.Settings +import android.text.format.DateFormat +import android.util.Base64 +import android.util.Log +import android.webkit.JavascriptInterface +import android.webkit.WebResourceResponse +import android.webkit.WebView +import androidx.annotation.Keep +import androidx.annotation.RequiresApi +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.lifecycleScope +import androidx.webkit.WebViewClientCompat +import com.google.android.gms.recaptcha.RecaptchaHandle +import com.google.android.gms.recaptcha.RecaptchaResultData +import com.google.android.gms.recaptcha.internal.ExecuteParams +import com.google.android.gms.recaptcha.internal.InitParams +import com.google.android.gms.tasks.Task +import com.google.android.gms.tasks.Tasks +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import okio.ByteString +import org.microg.gms.profile.Build +import org.microg.gms.profile.ProfileManager +import org.microg.gms.tasks.TaskImpl +import org.microg.gms.utils.toBase64 +import java.io.ByteArrayInputStream +import java.lang.reflect.Array +import java.lang.reflect.Constructor +import java.lang.reflect.Field +import java.lang.reflect.Method +import java.lang.reflect.Modifier +import java.lang.reflect.Proxy +import java.net.URLEncoder +import java.nio.ByteBuffer +import java.nio.charset.StandardCharsets +import java.util.ArrayDeque +import java.util.Locale +import java.util.Queue +import java.util.UUID +import java.util.concurrent.atomic.AtomicBoolean +import kotlin.coroutines.Continuation +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlin.coroutines.suspendCoroutine + +private const val TAG = "RecaptchaWeb" + +@RequiresApi(19) +class RecaptchaWebImpl(private val context: Context, private val packageName: String, private val lifecycle: Lifecycle) : RecaptchaImpl, LifecycleOwner { + private var webView: WebView? = null + private var lastRequestToken: String? = null + private var initFinished = AtomicBoolean(true) + private var initContinuation: Continuation? = null + private var executeFinished = AtomicBoolean(true) + private var executeContinuation: Continuation? = null + + override fun getLifecycle(): Lifecycle = lifecycle + + override suspend fun init(params: InitParams): RecaptchaHandle { + lastRequestToken = UUID.randomUUID().toString() + ProfileManager.ensureInitialized(context) + FakeHandler.setDecryptKeyPrefix(IntArray(0)) + FakeApplication.context = context + FakeApplication.packageNameOverride = packageName + suspendCoroutine { continuation -> + initFinished.set(false) + initContinuation = continuation + webView = WebView(context).apply { + settings.javaScriptEnabled = true + addJavascriptInterface(RNJavaScriptInterface(this@RecaptchaWebImpl, CodeInterpreter(this@RecaptchaWebImpl)), "RN") + webViewClient = object : WebViewClientCompat() { + fun String.isRecaptchaUrl() = startsWith("https://www.recaptcha.net/") || startsWith("https://www.gstatic.com/recaptcha/") + + override fun shouldInterceptRequest(view: WebView, url: String): WebResourceResponse? { + if (url.isRecaptchaUrl()) { + return null + } + return WebResourceResponse("text/plain", "UTF-8", ByteArrayInputStream(byteArrayOf())) + } + + override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean { + return !url.isRecaptchaUrl() + } + + override fun onPageFinished(view: WebView?, url: String?) { + } + } + postUrl( + MWV_URL, ("" + + "k=${URLEncoder.encode(params.siteKey, "UTF-8")}&" + + "pk=${URLEncoder.encode(packageName, "UTF-8")}&" + + "mst=ANDROID_ONPLAY&" + + "msv=18.1.1&" + + "msi=${URLEncoder.encode(lastRequestToken, "UTF-8")}&" + + "mov=${Build.VERSION.SDK_INT}" + ).toByteArray() + ) + } + lifecycleScope.launch { + delay(10000) + if (!initFinished.getAndSet(true)) { + try { + continuation.resumeWithException(RuntimeException("Timeout reached")) + } catch (_: Exception) {} + } + } + } + initContinuation = null + return RecaptchaHandle(params.siteKey, packageName, emptyList()) + } + + override suspend fun execute(params: ExecuteParams): RecaptchaResultData { + if (webView == null) { + init(InitParams().apply { siteKey = params.handle.siteKey; version = params.version }) + } + val additionalArgs = mutableMapOf() + for (key in params.action.additionalArgs.keySet()) { + additionalArgs[key] = params.action.additionalArgs.getString(key)!! + } + val request = RecaptchaExecuteRequest(token = lastRequestToken, action = params.action.toString(), additionalArgs = additionalArgs).encode().toBase64(Base64.URL_SAFE, Base64.NO_WRAP) + val token = suspendCoroutine { continuation -> + executeFinished.set(false) + executeContinuation = continuation + eval("recaptcha.m.Main.execute(\"${request}\")") + lifecycleScope.launch { + delay(10000) + if (!executeFinished.getAndSet(true)) { + try { + continuation.resumeWithException(RuntimeException("Timeout reached")) + } catch (_: Exception) {} + } + } + } + return RecaptchaResultData(token) + } + + override suspend fun close(handle: RecaptchaHandle): Boolean { + if (handle.clientPackageName != null && handle.clientPackageName != packageName) throw IllegalArgumentException("invalid handle") + val closed = webView != null + webView?.stopLoading() + webView?.loadUrl("about:blank") + webView = null + return closed + } + + private fun eval(script: String) { + Log.d(TAG, "eval: $script") + webView?.let { + Handler(context.mainLooper).post { + it.evaluateJavascript(script, null) + } + } + } + + protected fun finalize() { + FakeApplication.packageNameOverride = "" + } + + companion object { + private const val MWV_URL = "https://www.recaptcha.net/recaptcha/api3/mwv" + private const val DEBUG = false + object FakeApplication : Application() { + var context: Context + get() = baseContext + set(value) { try { attachBaseContext(value.applicationContext) } catch (_: Exception) { } } + var packageNameOverride: String = "" + override fun getPackageName(): String { + return packageNameOverride + } + } + var codeDecryptKeyPrefix = emptyList() + private set + + class FakeHandler : Exception() { + private var cloudProjectNumber: Long? = 0 + private var nonce: String? = null + + @Keep + fun requestIntegrityToken(request: FakeHandler): Task { + return Tasks.forException(FakeHandler()) + } + + @Keep + fun setCloudProjectNumber(cloudProjectNumber: Long): FakeHandler { + this.cloudProjectNumber = cloudProjectNumber + return this + } + + @Keep + fun setNonce(nonce: String): FakeHandler { + this.nonce = nonce + return this + } + + @Keep + fun build(): FakeHandler { + return this + } + + @Keep + fun cloudProjectNumber(): Long? { + return cloudProjectNumber + } + + @Keep + fun nonce(): String? { + return nonce + } + + @Keep + fun getErrorCode(): Int = -1 + + companion object { + @Keep + @JvmStatic + fun setDecryptKeyPrefix(newKeyPrefix: IntArray) { + codeDecryptKeyPrefix = newKeyPrefix.asList() + } + + @Keep + @JvmStatic + fun getFakeApplication(): Application = FakeApplication + + @Keep + @JvmStatic + fun createFakeIntegrityManager(context: Context): FakeHandler { + return FakeHandler() + } + @Keep + @JvmStatic + fun createFakeIntegrityTokenRequestBuilder(): FakeHandler { + return FakeHandler() + } + } + } + + private class CodeInterpreter(private val impl: RecaptchaWebImpl) { + val dict = mutableMapOf() + var errorHandler = "" + var xorSecret = IntRange(0, 127).random().toByte() + + private val intToClassMap = mapOf( + 1 to java.lang.Integer.TYPE, + 2 to java.lang.Short.TYPE, + 3 to java.lang.Byte.TYPE, + 4 to java.lang.Long.TYPE, + 5 to java.lang.Character.TYPE, + 6 to java.lang.Float.TYPE, + 7 to java.lang.Double.TYPE, + 8 to java.lang.Boolean.TYPE, + 9 to FakeHandler::class.java + ) + + private fun getClass(name: String): Class<*>? = when (name) { + "[I" -> IntArray::class.java + "[B" -> ByteArray::class.java + "android.os.Build" -> Build::class.java + "android.os.Build\$VERSION" -> Build.VERSION::class.java + "android.app.ActivityThread" -> FakeHandler::class.java + "com.google.android.play.core.integrity.IntegrityManager" -> FakeHandler::class.java + "com.google.android.play.core.integrity.IntegrityManagerFactory" -> FakeHandler::class.java + "com.google.android.play.core.integrity.IntegrityTokenRequest" -> FakeHandler::class.java + "com.google.android.play.core.integrity.IntegrityTokenResponse" -> FakeHandler::class.java + "android.content.Intent", "android.content.IntentFilter", "android.content.BroadcastReceiver", + "android.content.Context", "android.content.pm.PackageManager", "android.content.ContentResolver", + "java.lang.String", "java.lang.CharSequence", "java.lang.Long", + "java.nio.charset.Charset", "java.nio.charset.StandardCharsets", + "android.text.format.DateFormat", "java.util.Date", "java.util.Locale", "java.nio.ByteBuffer", + "android.os.BatteryManager", "android.media.AudioManager", + "com.google.android.gms.tasks.OnCompleteListener", + "android.provider.Settings\$System" -> Class.forName(name) + + else -> { + Log.w(TAG, "Not providing class $name", Exception()) + if (DEBUG) Class.forName(name) else null + } + } + + private fun getMethod(cls: Class<*>, name: String, params: kotlin.Array?>): Method? = when { + cls == FakeHandler::class.java && name == "acx" -> FakeHandler::class.java.getMethod("setDecryptKeyPrefix", *params) + cls == FakeHandler::class.java && name == "currentApplication" -> FakeHandler::class.java.getMethod("getFakeApplication", *params) + cls == FakeHandler::class.java && name == "create" -> FakeHandler::class.java.getMethod("createFakeIntegrityManager", *params) + cls == FakeHandler::class.java && name == "builder" -> FakeHandler::class.java.getMethod("createFakeIntegrityTokenRequestBuilder", *params) + cls == FakeHandler::class.java -> cls.getMethod(name, *params) + cls == FakeApplication.javaClass && name == "getContentResolver" -> cls.getMethod(name, *params) + cls == FakeApplication.javaClass && name == "getSystemService" -> cls.getMethod(name, *params) + cls == FakeApplication.javaClass && name == "registerReceiver" -> cls.getMethod(name, *params) + cls == PackageManager::class.java && name == "checkPermission" -> cls.getMethod(name, *params) + cls == Context::class.java && name == "checkSelfPermission" -> cls.getMethod(name, *params) + cls == Context::class.java && name == "getPackageManager" -> cls.getMethod(name, *params) + cls == Context::class.java && name == "getPackageName" -> cls.getMethod(name, *params) + cls == AudioManager::class.java && name == "getStreamVolume" -> cls.getMethod(name, *params) + cls == Settings.System::class.java && name == "getInt" -> cls.getMethod(name, *params) + cls == DateFormat::class.java -> cls.getMethod(name, *params) + cls == Locale::class.java -> cls.getMethod(name, *params) + cls == Intent::class.java -> cls.getMethod(name, *params) + cls == String::class.java -> cls.getMethod(name, *params) + cls == ByteBuffer::class.java -> cls.getMethod(name, *params) + cls == TaskImpl::class.java -> cls.getMethod(name, *params) + name == "toString" -> cls.getMethod(name, *params) + name == "parseLong" -> cls.getMethod(name, *params) + else -> { + Log.w(TAG, "Not providing method $name in ${cls.display()}", Exception()) + if (DEBUG) cls.getMethod(name, *params) else null + } + } + + private fun getField(cls: Class<*>, name: String): Field? = when { + cls == Build::class.java -> cls.getField(name) + cls == Build.VERSION::class.java -> cls.getField(name) + cls == Settings.System::class.java && cls.getField(name).modifiers.and(Modifier.STATIC) > 0 -> cls.getField(name) + cls == BatteryManager::class.java && cls.getField(name).modifiers.and(Modifier.STATIC) > 0 -> cls.getField(name) + cls == AudioManager::class.java && cls.getField(name).modifiers.and(Modifier.STATIC) > 0 -> cls.getField(name) + cls == StandardCharsets::class.java && cls.getField(name).modifiers.and(Modifier.STATIC) > 0 -> cls.getField(name) + else -> { + Log.w(TAG, "Not providing field $name in ${cls.display()}", Exception()) + if (DEBUG) cls.getField(name) else null + } + } + + private operator fun Any?.rem(other: Any?): Any? = when { + this is IntArray && other is Int -> map { it % other }.toIntArray() + else -> throw UnsupportedOperationException("rem ${this?.javaClass} % ${other?.javaClass}") + } + + private infix fun Any?.xor(other: Any?): Any? = when { + this is String && other is Int -> map { it.code xor other }.toIntArray() + this is String && other is Byte -> encodeToByteArray().map { (it.toInt() xor other.toInt()).toByte() }.toByteArray() + this is Long && other is Long -> this xor other + else -> throw UnsupportedOperationException("xor ${this?.javaClass} ^ ${other?.javaClass}") + } + + private fun Any?.join(): Any? = when (this) { + is ByteArray -> decodeToString() + is CharArray -> concatToString() + is IntArray -> joinToString(",", "[", "]") + is LongArray -> joinToString(",", "[", "]") + is ShortArray -> joinToString(",", "[", "]") + is FloatArray -> joinToString(",", "[", "]") + is DoubleArray -> joinToString(",", "[", "]") + is kotlin.Array<*> -> joinToString(",", "[", "]") + is Iterable<*> -> joinToString(",", "[", "]") + else -> this + } + + private fun String.deXor(): String = map { Char(it.code xor xorSecret.toInt()) }.toCharArray().concatToString() + + private fun Any?.deXor(): Any? = when { + this is RecaptchaWebCode.Arg && this.asObject() is String -> this.asObject()!!.deXor() + this is String -> this.deXor() + else -> this + } + + private fun Any.asClass(): Class<*>? = when (this) { + is RecaptchaWebCode.Arg -> asObject()!!.asClass() + is Int -> intToClassMap[this]!! + is String -> getClass(this) + is Class<*> -> this + else -> throw UnsupportedOperationException("$this.asClass()") + } + + private fun Any?.getClass(): Class<*> = when (this) { + is RecaptchaWebCode.Arg -> asObject().getClass() + is Class<*> -> this + null -> Unit.javaClass + else -> this.javaClass + } + + private fun Any?.display(): String = when (this) { + is RecaptchaWebCode.Arg -> asObject().display() + if (index != null) " (d[$index])" else "" + is Int, is Boolean -> "${this}" + is Byte -> "${this}b" + is Short -> "${this}s" + is Long -> "${this}l" + is Double -> "${this}d" + is Float -> "${this}f" + is String -> if (any { !it.isLetterOrDigit() && it !in listOf('.', '=', '-', '_') }) "" else "\"${this}\"" + is Class<*> -> name + is Constructor<*> -> "{new ${declaringClass.name}(${parameterTypes.joinToString { it.name }})}" + is Method -> "{${declaringClass.name}.$name(${parameterTypes.joinToString { it.name }})}" + is Field -> "{${declaringClass.name}.$name}" + is IntArray -> joinToString(prefix = "[", postfix = "]") + is ByteArray -> joinToString(prefix = "[", postfix = "]b") + is ShortArray -> joinToString(prefix = "[", postfix = "]s") + is LongArray -> joinToString(prefix = "[", postfix = "]l") + is FloatArray -> joinToString(prefix = "[", postfix = "]f") + is DoubleArray -> joinToString(prefix = "[", postfix = "]d") + is BooleanArray -> joinToString(prefix = "[", postfix = "]") + null -> "null" + else -> "@{${this.javaClass.name}}" + } + + private fun RecaptchaWebCode.Arg.asObject(): Any? = when { + index != null -> dict[index] + bol != null -> bol + bt != null -> bt[0] + chr != null -> chr[0] + sht != null -> sht.toShort() + i != null -> i + l != null -> l + flt != null -> flt + dbl != null -> dbl + str != null -> str + else -> null + } + + private fun Any.asListValue(): RecaptchaWebList.Value = when(this) { + is Int -> RecaptchaWebList.Value(i = this) + is Short -> RecaptchaWebList.Value(sht = this.toInt()) + is Byte -> RecaptchaWebList.Value(bt = ByteString.of(this)) + is Long -> RecaptchaWebList.Value(l = this) + is Double -> RecaptchaWebList.Value(dbl = this) + is Float -> RecaptchaWebList.Value(flt = this) + is Boolean -> RecaptchaWebList.Value(bol = this) + is Char -> RecaptchaWebList.Value(chr = this.toString()) + is String -> RecaptchaWebList.Value(str = this) + else -> RecaptchaWebList.Value(str = toString()) + } + + fun execute(code: RecaptchaWebCode) { + for (op in code.ops) { + when (op.code) { + 1 -> { + // d[i] = a0 + if (DEBUG) Log.d(TAG, "d[${op.arg1}] = ${op.args[0].display()}") + dict[op.arg1!!] = op.args[0].asObject() + } + + 2 -> { + // d[i] = a0 .. a1 + if (DEBUG) Log.d(TAG, "d[${op.arg1}] = \"${op.args[0].display()}${op.args[1].display()}\"") + dict[op.arg1!!] = "${op.args[0].asObject()}${op.args[1].asObject()}" + } + + 3 -> { + // d[i] = Class(a0) + val cls = op.args[0].asObject().deXor()?.asClass() + if (DEBUG) Log.d(TAG, "d[${op.arg1}] = $cls") + dict[op.arg1!!] = cls + } + + 4 -> { + // d[i] = Class(a0).getConstructor(a1 ...) + val constructor = op.args[0].asClass()!!.getConstructor(*op.args.subList(1, op.args.size).map { it.asClass() }.toTypedArray()) + if (DEBUG) Log.d(TAG, "d[${op.arg1}] = ${constructor.display()}") + dict[op.arg1!!] = constructor + } + + 5 -> { + // d[i] = Class(a0).getMethod(a1, a2 ...) + val methodName = (op.args[1].asObject().deXor() as String) + val cls = op.args[0].getClass() + val method = getMethod(cls, methodName, op.args.subList(2, op.args.size).map { it.asClass() }.toTypedArray()) + if (DEBUG) Log.d(TAG, "d[${op.arg1}] = ${method.display()}") + dict[op.arg1!!] = method + } + + 6 -> { + // d[i] = Class(a0).getField(a1) + val fieldName = (op.args[1].asObject().deXor() as String) + val cls = op.args[0].getClass() + val field = getField(cls, fieldName) + if (DEBUG) Log.d(TAG, "d[${op.arg1}] = ${field.display()}") + dict[op.arg1!!] = field + } + + 7 -> { + // d[i] = Constructor(a0).newInstance(a1 ...) + if (DEBUG) Log.d( + TAG, + "d[${op.arg1}] = new ${(op.args[0].asObject() as Constructor<*>).name}(${ + op.args.subList(1, op.args.size).joinToString { it.display() } + })" + ) + dict[op.arg1!!] = + (op.args[0].asObject() as Constructor<*>).newInstance(*op.args.subList(1, op.args.size).map { it.asObject() }.toTypedArray()) + } + + 8 -> { + // d[i] = Method(a0).invoke(a1, a2 ...) + if (DEBUG) Log.d( + TAG, + "d[${op.arg1}] = (${op.args[1].display()}).${(op.args[0].asObject() as Method).name}(${ + op.args.subList(2, op.args.size).joinToString { it.display() } + })" + ) + dict[op.arg1!!] = (op.args[0].asObject() as Method).invoke( + op.args[1].asObject(), + *op.args.subList(2, op.args.size).map { it.asObject() }.toTypedArray() + ) + } + + 9 -> { + // d[i] = Method(a0).invoke(null, a1 ...) + if (DEBUG) Log.d( + TAG, + "d[${op.arg1}] = ${(op.args[0].asObject() as Method).declaringClass.name}.${(op.args[0].asObject() as Method).name}(${ + op.args.subList( + 1, + op.args.size + ).joinToString { it.display() } + })" + ) + dict[op.arg1!!] = + (op.args[0].asObject() as Method).invoke(null, *op.args.subList(1, op.args.size).map { it.asObject() }.toTypedArray()) + } + + 10 -> { + // d[i] = Field(a0).get(a1) + if (DEBUG) Log.d(TAG, "d[${op.arg1}] = (${op.args[1].display()}).${(op.args[0].asObject() as Field).name}") + dict[op.arg1!!] = (op.args[0].asObject() as Field).get(op.args[1].asObject()) + } + + 11 -> { + // d[i] = Field(a0).get(null) + if (DEBUG) Log.d(TAG, "d[${op.arg1}] = ${(op.args[0].asObject() as Field).declaringClass.name}.${(op.args[0].asObject() as Field).name}") + dict[op.arg1!!] = (op.args[0].asObject() as Field).get(null) + } + + 12 -> { + // Field(a0).set(a1, a2) + if (DEBUG) Log.d(TAG, "(${op.args[1].display()}).${(op.args[0].asObject() as Field).name} = ${op.args[2].display()}") + (op.args[0].asObject() as Field).set(op.args[1].asObject(), op.args[2].asObject()) + } + + 13 -> { + // Field(a0).set(null, a1) + if (DEBUG) Log.d( + TAG, + "(${(op.args[0].asObject() as Field).declaringClass.name}).${(op.args[0].asObject() as Field).name} = ${op.args[1].display()}" + ) + (op.args[0].asObject() as Field).set(null, op.args[1].asObject()) + } + + 15 -> { + // eval(a0(a1)) + impl.eval("${op.args[0].str}(\"${op.args[1].asObject()}\")") + } + + 17 -> { + // d[i] = new a0[a1] + if (DEBUG) Log.d(TAG, "d[${op.arg1}] = new ${op.args[0].asClass()!!.name}[${op.args[1].display()}]") + dict[op.arg1!!] = Array.newInstance(op.args[0].asClass(), op.args[1].asObject() as Int) + } + + 18 -> { + // d[i] = new a1() { * a2(args) { eval(a0(args)); return a3; } } + val callbackName = op.args[0].asObject() as String + val methodName = (op.args[2].asObject() as String).deXor() + val cls = op.args[1].asObject().deXor()?.asClass() + val returnValue = op.args[3].asObject() + val argsTarget = (if (op.args.size == 5) op.args[4].asObject() as? Int else null) ?: -1 + if (DEBUG) Log.d(TAG, "d[${op.arg1}] = new ${cls?.name}() { * ${methodName}(*) { js:$callbackName(*); return ${returnValue.display()}; } }") + dict[op.arg1!!] = + Proxy.newProxyInstance(cls!!.classLoader, arrayOf(cls)) { obj: Any, method: Method, args: kotlin.Array? -> + if (method.name == methodName) { + if (argsTarget != -1) dict[argsTarget] = args + val encoded = RecaptchaWebList(args.orEmpty().map { it.asListValue() }).encode().toBase64(Base64.URL_SAFE, Base64.NO_WRAP) + impl.eval("${callbackName}(\"$encoded\")") + returnValue + } else { + null + } + } + } + + 19 -> { + // d[i] = new Queue(a1) + // d[a0] = new a2() { * a3(args) { d[i].add(args); return a4; } } + val methodName = (op.args[3].asObject() as String).deXor() + val maxSize = op.args[1].asObject() as Int + val queue = ArrayDeque>(maxSize) + val limitedQueue = object : Queue> by queue { + override fun add(element: List?): Boolean { + if (maxSize == 0) return true + if (size == maxSize) remove() + queue.add(element) + return true + } + } + val returnValue = if (op.args.size == 5) op.args[4].asObject() else null + val cls = op.args[2].asObject().deXor()?.asClass() + dict[op.arg1!!] = limitedQueue + dict[op.args[0].asObject() as Int] = Proxy.newProxyInstance(cls!!.classLoader, arrayOf(cls)) { obj: Any, method: Method, args: kotlin.Array? -> + if (method.name == methodName) { + limitedQueue.add(args?.asList().orEmpty()) + returnValue + } else { + null + } + } + } + + 20 -> { + // unset(d, a0 ...) + if (DEBUG) Log.d(TAG, "d[${op.args.joinToString { it.index.toString() }}] = @@@") + for (arg in op.args) { + dict.remove(arg.index) + } + } + + 26 -> { + // e = a0 + errorHandler = op.args[0].str!! + } + + 27 -> { + // clear(d) + dict.clear() + } + + 30 -> { + // d[i] = encode(a0 ...) + val res = RecaptchaWebList(op.args.map { it.asObject()!!.asListValue() }).encode().toBase64(Base64.URL_SAFE, Base64.NO_WRAP) + if (DEBUG) Log.d(TAG, "d[${op.arg1}] = ${res.display()}") + dict[op.arg1!!] = res + } + + 31 -> { + // a0[a1] = a2 + if (DEBUG) Log.d(TAG, "d[${op.args[0].index}][${op.args[1].display()}] = ${op.args[2].display()}") + Array.set(op.args[0].asObject()!!, op.args[1].asObject() as Int, op.args[2].asObject()) + } + + 32 -> { + // d[i] = a0[a1] + if (DEBUG) Log.d(TAG, "d[${op.arg1}] = ${op.args[0].display()}[${op.args[1].display()}]") + val arr = op.args[0].asObject() + val idx = op.args[1].asObject() as Int + val res = when (arr) { + is String -> arr[idx] + is List<*> -> arr[idx] + else -> Array.get(arr, idx) + } + dict[op.arg1!!] = res + } + + 34 -> { + // d[i] = a0 % a1 + if (DEBUG) Log.d(TAG, "d[${op.arg1}] = ${op.args[0].display()} % ${op.args[1].display()}") + dict[op.arg1!!] = op.args[0].asObject() % op.args[1].asObject() + } + + 35 -> { + // d[i] = a0 ^ a1 + if (DEBUG) Log.d(TAG, "d[${op.arg1}] = ${op.args[0].display()} ^ ${op.args[1].display()}") + dict[op.arg1!!] = op.args[0].asObject() xor op.args[1].asObject() + } + + 37 -> { + // d[i] = String(a1[*a0]) + val str = op.args[1].asObject() as String + val res = (op.args[0].asObject() as IntArray).map { str[it] }.toCharArray().concatToString() + if (DEBUG) Log.d(TAG, "d[${op.arg1}] = ${res.display()}") + dict[op.arg1!!] = res + } + + 38 -> { + // x = a0 + xorSecret = op.args[0].asObject() as Byte + } + + 39 -> { + // d[i] = join(a0) + val res = op.args[0].asObject().join() + if (DEBUG) Log.d(TAG, "d[${op.arg1}] = ${res.display()}") + dict[op.arg1!!] = res + } + + else -> { + Log.w(TAG, "Op ${op.encode().toBase64(Base64.URL_SAFE, Base64.NO_WRAP)} not implemented (code=${op.code})") + } + } + } + } + } + + private class RNJavaScriptInterface(private val impl: RecaptchaWebImpl, private val interpreter: CodeInterpreter) { + + @JavascriptInterface + fun zzoed(input: String) { + val result = RecaptchaWebResult.ADAPTER.decode(Base64.decode(input, Base64.URL_SAFE)) + if (DEBUG) Log.d(TAG, "zzoed: $result") + if (!impl.executeFinished.getAndSet(true) && impl.lastRequestToken == result.requestToken) { + if (result.code == 1 && result.token != null) { + impl.executeContinuation?.resume(result.token) + } else { + impl.executeContinuation?.resumeWithException(RuntimeException("Status ${result.code}")) + } + } + } + + @JavascriptInterface + fun zzoid(input: String) { + val status = RecaptchaWebStatusCode.ADAPTER.decode(Base64.decode(input, Base64.URL_SAFE)) + if (DEBUG) Log.d(TAG, "zzoid: $status") + if (!impl.initFinished.getAndSet(true)) { + if (status.code == 1) { + impl.initContinuation?.resume(Unit) + } else { + impl.initContinuation?.resumeWithException(RuntimeException("Status ${status.code}")) + } + } + } + + @JavascriptInterface + fun zzrp(input: String) { + val callback = RecaptchaWebEncryptedCallback.ADAPTER.decode(Base64.decode(input, Base64.URL_SAFE)) + var key = (codeDecryptKeyPrefix + callback.key).reduce { a, b -> a xor b } + fun next(): Int { + key = ((key * 4391) + 277) % 32779 + return key % 255 + } + + val decrypted = callback.data_?.map { Char(it.code xor next()) }?.toCharArray()?.concatToString() + if (DEBUG) Log.d(TAG, "zzrp: $decrypted") + val code = RecaptchaWebCode.ADAPTER.decode(Base64.decode(decrypted, Base64.URL_SAFE + Base64.NO_PADDING)) + interpreter.execute(code) + } + } + + } +} \ No newline at end of file diff --git a/play-services-recaptcha/core/src/main/proto/recaptcha.proto b/play-services-recaptcha/core/src/main/proto/recaptcha.proto index 9735e33c06..46459a5f62 100644 --- a/play-services-recaptcha/core/src/main/proto/recaptcha.proto +++ b/play-services-recaptcha/core/src/main/proto/recaptcha.proto @@ -1,3 +1,5 @@ +syntax = "proto3"; + option java_package = "org.microg.gms.recaptcha"; message RecaptchaInitRequest { @@ -27,3 +29,62 @@ message RecaptchaExecuteRequest { message RecaptchaExecuteResponse { optional string token = 1; } + +message RecaptchaWebEncryptedCallback { + optional string data = 1; + repeated int32 key = 2; +} + +message RecaptchaWebInvokeMultiParameter { + repeated string args = 1; +} + +message RecaptchaWebStatusCode { + optional int32 code = 1; +} + +message RecaptchaWebResult { + optional string requestToken = 1; + optional string token = 2; + optional int32 code = 3; +} + +message RecaptchaWebList { + message Value { + oneof typed { + bool bol = 1; + bytes bt = 2; + string chr = 3; + sint32 sht = 4; + sint32 i = 5; + sint64 l = 7; + float flt = 9; + double dbl = 10; + string str = 11; + } + } + repeated Value values = 1; +} + +message RecaptchaWebCode { + message Arg { + oneof typed { + int32 index = 1; + bool bol = 2; + bytes bt = 3; + string chr = 4; + sint32 sht = 5; + sint32 i = 6; + sint64 l = 8; + float flt = 10; + double dbl = 11; + string str = 12; + } + } + message Op { + optional int32 code = 1; + optional int32 arg1 = 2; + repeated Arg args = 3; + } + repeated Op ops = 1; +} \ No newline at end of file diff --git a/play-services-recaptcha/src/main/java/org/microg/gms/recaptcha/RecaptchaClientImpl.java b/play-services-recaptcha/src/main/java/org/microg/gms/recaptcha/RecaptchaClientImpl.java index 07435b1788..e027d3f20a 100644 --- a/play-services-recaptcha/src/main/java/org/microg/gms/recaptcha/RecaptchaClientImpl.java +++ b/play-services-recaptcha/src/main/java/org/microg/gms/recaptcha/RecaptchaClientImpl.java @@ -8,6 +8,7 @@ import android.content.Context; import android.os.RemoteException; +import android.util.Log; import com.google.android.gms.common.api.Api; import com.google.android.gms.common.api.GoogleApi; import com.google.android.gms.common.api.Status; @@ -32,6 +33,8 @@ import org.microg.gms.tasks.TaskImpl; public class RecaptchaClientImpl extends GoogleApi implements RecaptchaClient { + private int openHandles = 0; + public RecaptchaClientImpl(Context context) { super(context, new Api<>((options, c, looper, clientSettings, callbacks, connectionFailedListener) -> new RecaptchaGmsClient(c, callbacks, connectionFailedListener))); } @@ -52,6 +55,12 @@ public void onClosed(Status status, boolean closed) throws RemoteException { } else { completionSource.trySetException(new RuntimeException(status.getStatusMessage())); } + if (openHandles == 0) { + Log.w("RecaptchaClient", "Can't mark handle closed if none is open"); + return; + } + openHandles--; + if (openHandles == 0) client.disconnect(); } }, handle); }); @@ -63,7 +72,7 @@ public Task execute(RecaptchaHandle handle, RecaptchaAction ExecuteParams params = new ExecuteParams(); params.handle = handle; params.action = action; - params.version = "17.0.1"; + params.version = "18.1.1"; client.execute(new IExecuteCallback.Stub() { @Override public void onData(Status status, RecaptchaResultData data) throws RemoteException { @@ -88,10 +97,11 @@ public void onResults(Status status, ExecuteResults results) throws RemoteExcept @Override public Task init(String siteKey) { + openHandles++; return scheduleTask((PendingGoogleApiCall) (client, completionSource) -> { InitParams params = new InitParams(); params.siteKey = siteKey; - params.version = "17.0.1"; + params.version = "18.1.1"; client.init(new IInitCallback.Stub() { @Override public void onHandle(Status status, RecaptchaHandle handle) throws RemoteException { diff --git a/play-services-safetynet/core/src/main/kotlin/org/microg/gms/safetynet/SafetyNetClientService.kt b/play-services-safetynet/core/src/main/kotlin/org/microg/gms/safetynet/SafetyNetClientService.kt index b78e26e4c2..160afa276a 100644 --- a/play-services-safetynet/core/src/main/kotlin/org/microg/gms/safetynet/SafetyNetClientService.kt +++ b/play-services-safetynet/core/src/main/kotlin/org/microg/gms/safetynet/SafetyNetClientService.kt @@ -20,6 +20,7 @@ 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.safetynet.AttestationData +import com.google.android.gms.safetynet.HarmfulAppsInfo import com.google.android.gms.safetynet.RecaptchaResultData import com.google.android.gms.safetynet.SafeBrowsingData import com.google.android.gms.safetynet.SafetyNetStatusCodes @@ -35,6 +36,7 @@ import org.microg.gms.droidguard.core.DroidGuardResultCreator import org.microg.gms.settings.SettingsContract import org.microg.gms.settings.SettingsContract.CheckIn.getContentUri import org.microg.gms.settings.SettingsContract.getSettings +import org.microg.gms.utils.warnOnTransactionIssues import java.io.IOException import java.net.URLEncoder import java.util.* @@ -66,19 +68,19 @@ class SafetyNetClientServiceImpl( override fun attestWithApiKey(callbacks: ISafetyNetCallbacks, nonce: ByteArray?, apiKey: String) { if (nonce == null) { - callbacks.onAttestationData(Status(SafetyNetStatusCodes.DEVELOPER_ERROR, "Nonce missing"), null) + callbacks.onAttestationResult(Status(SafetyNetStatusCodes.DEVELOPER_ERROR, "Nonce missing"), null) return } if (!SafetyNetPreferences.isEnabled(context)) { Log.d(TAG, "ignoring SafetyNet request, SafetyNet is disabled") - callbacks.onAttestationData(Status(SafetyNetStatusCodes.ERROR, "Disabled"), null) + callbacks.onAttestationResult(Status(SafetyNetStatusCodes.ERROR, "Disabled"), null) return } - if (!DroidGuardPreferences.isEnabled(context)) { + if (!DroidGuardPreferences.isAvailable(context)) { Log.d(TAG, "ignoring SafetyNet request, DroidGuard is disabled") - callbacks.onAttestationData(Status(SafetyNetStatusCodes.ERROR, "Disabled"), null) + callbacks.onAttestationResult(Status(SafetyNetStatusCodes.ERROR, "Unsupported"), null) return } @@ -115,7 +117,7 @@ class SafetyNetClientServiceImpl( } db.insertRecentRequestEnd(requestID, Status.SUCCESS, jsonData) - callbacks.onAttestationData(Status.SUCCESS, AttestationData(jwsResult)) + callbacks.onAttestationResult(Status.SUCCESS, AttestationData(jwsResult)) } catch (e: Exception) { Log.w(TAG, "Exception during attest: ${e.javaClass.name}", e) val code = when (e) { @@ -127,7 +129,7 @@ class SafetyNetClientServiceImpl( // This shouldn't happen, but do not update the database if it didn't insert the start of the request if (requestID != -1L) db.insertRecentRequestEnd(requestID, status, null) try { - callbacks.onAttestationData(Status(code, e.localizedMessage), null) + callbacks.onAttestationResult(Status(code, e.localizedMessage), null) } catch (e: Exception) { Log.w(TAG, "Exception while sending error", e) } @@ -142,7 +144,7 @@ class SafetyNetClientServiceImpl( // TODO Log.d(TAG, "dummy Method: getSharedUuid") - callbacks.onString("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa") + callbacks.onSharedUuid("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa") } override fun lookupUri(callbacks: ISafetyNetCallbacks, apiKey: String, threatTypes: IntArray, i: Int, s2: String) { @@ -150,19 +152,21 @@ class SafetyNetClientServiceImpl( callbacks.onSafeBrowsingData(Status.SUCCESS, SafeBrowsingData()) } - override fun init(callbacks: ISafetyNetCallbacks) { - Log.d(TAG, "dummy Method: init") - callbacks.onBoolean(Status.SUCCESS, true) + override fun enableVerifyApps(callbacks: ISafetyNetCallbacks) { + Log.d(TAG, "dummy Method: enableVerifyApps") + callbacks.onVerifyAppsUserResult(Status.SUCCESS, true) } - override fun getHarmfulAppsList(callbacks: ISafetyNetCallbacks) { - Log.d(TAG, "dummy Method: unknown4") - callbacks.onHarmfulAppsData(Status.SUCCESS, ArrayList()) + override fun listHarmfulApps(callbacks: ISafetyNetCallbacks) { + Log.d(TAG, "dummy Method: listHarmfulApps") + callbacks.onHarmfulAppsInfo(Status.SUCCESS, HarmfulAppsInfo().apply { + lastScanTime = ((System.currentTimeMillis() - VERIFY_APPS_LAST_SCAN_DELAY) / VERIFY_APPS_LAST_SCAN_TIME_ROUNDING) * VERIFY_APPS_LAST_SCAN_TIME_ROUNDING + VERIFY_APPS_LAST_SCAN_OFFSET + }) } override fun verifyWithRecaptcha(callbacks: ISafetyNetCallbacks, siteKey: String?) { if (siteKey == null) { - callbacks.onAttestationData(Status(SafetyNetStatusCodes.RECAPTCHA_INVALID_SITEKEY, "SiteKey missing"), null) + callbacks.onRecaptchaResult(Status(SafetyNetStatusCodes.RECAPTCHA_INVALID_SITEKEY, "SiteKey missing"), null) return } @@ -172,12 +176,6 @@ class SafetyNetClientServiceImpl( return } - if (!DroidGuardPreferences.isEnabled(context)) { - Log.d(TAG, "ignoring SafetyNet request, DroidGuard is disabled") - callbacks.onRecaptchaResult(Status(SafetyNetStatusCodes.ERROR, "Disabled"), null) - return - } - val db = SafetyNetDatabase(context) val requestID = db.insertRecentRequestStart( SafetyNetRequestType.RECAPTCHA, @@ -254,9 +252,26 @@ class SafetyNetClientServiceImpl( context.startActivity(intent) } - override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean { - if (super.onTransact(code, data, reply, flags)) return true - Log.d(TAG, "onTransact [unknown]: $code, $data, $flags") - return false + override fun initSafeBrowsing(callbacks: ISafetyNetCallbacks) { + Log.d(TAG, "dummy: initSafeBrowsing") + callbacks.onInitSafeBrowsingResult(Status.SUCCESS) + } + + override fun shutdownSafeBrowsing() { + Log.d(TAG, "dummy: shutdownSafeBrowsing") + } + + override fun isVerifyAppsEnabled(callbacks: ISafetyNetCallbacks) { + Log.d(TAG, "dummy: isVerifyAppsEnabled") + callbacks.onVerifyAppsUserResult(Status.SUCCESS, true) + } + + override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean = warnOnTransactionIssues(code, reply, flags, TAG) { super.onTransact(code, data, reply, flags) } + + companion object { + // We simulate one scan every day, which will happen at 03:12:02.121 and will be available 32 seconds later + const val VERIFY_APPS_LAST_SCAN_DELAY = 32 * 1000L + const val VERIFY_APPS_LAST_SCAN_OFFSET = ((3 * 60 + 12) * 60 + 2) * 1000L + 121 + const val VERIFY_APPS_LAST_SCAN_TIME_ROUNDING = 24 * 60 * 60 * 1000L } } diff --git a/play-services-safetynet/core/src/main/kotlin/org/microg/gms/safetynet/SafetyNetDatabase.kt b/play-services-safetynet/core/src/main/kotlin/org/microg/gms/safetynet/SafetyNetDatabase.kt index eb60aaffad..1a3d7ce0cb 100644 --- a/play-services-safetynet/core/src/main/kotlin/org/microg/gms/safetynet/SafetyNetDatabase.kt +++ b/play-services-safetynet/core/src/main/kotlin/org/microg/gms/safetynet/SafetyNetDatabase.kt @@ -9,14 +9,14 @@ import android.content.Context import android.database.Cursor import android.database.sqlite.SQLiteDatabase import android.database.sqlite.SQLiteOpenHelper -import android.os.Build +import android.os.Build.VERSION.SDK_INT import android.util.Log import com.google.android.gms.common.api.Status class SafetyNetDatabase(context: Context) : SQLiteOpenHelper(context, DB_NAME, null, DB_VERSION) { init { - if (Build.VERSION.SDK_INT >= 16) { + if (SDK_INT >= 16) { setWriteAheadLoggingEnabled(true) } } diff --git a/play-services-safetynet/core/src/main/kotlin/org/microg/gms/safetynet/SafetyNetRequestType.kt b/play-services-safetynet/core/src/main/kotlin/org/microg/gms/safetynet/SafetyNetRequestType.kt index 921a66dd17..dd5a605bb6 100644 --- a/play-services-safetynet/core/src/main/kotlin/org/microg/gms/safetynet/SafetyNetRequestType.kt +++ b/play-services-safetynet/core/src/main/kotlin/org/microg/gms/safetynet/SafetyNetRequestType.kt @@ -3,5 +3,6 @@ package org.microg.gms.safetynet enum class SafetyNetRequestType { ATTESTATION, RECAPTCHA, + RECAPTCHA_ENTERPRISE, ; } \ No newline at end of file diff --git a/play-services-safetynet/src/main/aidl/com/google/android/gms/safetynet/internal/ISafetyNetCallbacks.aidl b/play-services-safetynet/src/main/aidl/com/google/android/gms/safetynet/internal/ISafetyNetCallbacks.aidl index 9c19965245..db5b0f300c 100644 --- a/play-services-safetynet/src/main/aidl/com/google/android/gms/safetynet/internal/ISafetyNetCallbacks.aidl +++ b/play-services-safetynet/src/main/aidl/com/google/android/gms/safetynet/internal/ISafetyNetCallbacks.aidl @@ -9,12 +9,13 @@ import com.google.android.gms.safetynet.RemoveHarmfulAppData; import com.google.android.gms.safetynet.SafeBrowsingData; interface ISafetyNetCallbacks { - oneway void onAttestationData(in Status status, in AttestationData attestationData) = 0; - oneway void onString(String s) = 1; + oneway void onAttestationResult(in Status status, in AttestationData attestationData) = 0; + oneway void onSharedUuid(String s) = 1; oneway void onSafeBrowsingData(in Status status, in SafeBrowsingData safeBrowsingData) = 2; - oneway void onBoolean(in Status status, boolean b) = 3; + oneway void onVerifyAppsUserResult(in Status status, boolean enabled) = 3; oneway void onHarmfulAppsData(in Status status, in List harmfulAppsData) = 4; oneway void onRecaptchaResult(in Status status, in RecaptchaResultData recaptchaResultData) = 5; oneway void onHarmfulAppsInfo(in Status status, in HarmfulAppsInfo harmfulAppsInfo) = 7; + oneway void onInitSafeBrowsingResult(in Status status) = 10; oneway void onRemoveHarmfulAppData(in Status status, in RemoveHarmfulAppData removeHarmfulAppData) = 14; } diff --git a/play-services-safetynet/src/main/aidl/com/google/android/gms/safetynet/internal/ISafetyNetService.aidl b/play-services-safetynet/src/main/aidl/com/google/android/gms/safetynet/internal/ISafetyNetService.aidl index 24db01b679..7c4c6631ea 100644 --- a/play-services-safetynet/src/main/aidl/com/google/android/gms/safetynet/internal/ISafetyNetService.aidl +++ b/play-services-safetynet/src/main/aidl/com/google/android/gms/safetynet/internal/ISafetyNetService.aidl @@ -6,17 +6,17 @@ interface ISafetyNetService { void attest(ISafetyNetCallbacks callbacks, in byte[] nonce) = 0; void attestWithApiKey(ISafetyNetCallbacks callbacks, in byte[] nonce, String apiKey) = 6; void getSharedUuid(ISafetyNetCallbacks callbacks) = 1; - void lookupUri(ISafetyNetCallbacks callbacks, String apiKey, in int[] threatTypes, int i, String s2) = 2; - void init(ISafetyNetCallbacks callbacks) = 3; - void getHarmfulAppsList(ISafetyNetCallbacks callbacks) = 4; + void lookupUri(ISafetyNetCallbacks callbacks, String apiKey, in int[] threatTypes, int version, String uri) = 2; + void enableVerifyApps(ISafetyNetCallbacks callbacks) = 3; + void listHarmfulApps(ISafetyNetCallbacks callbacks) = 4; void verifyWithRecaptcha(ISafetyNetCallbacks callbacks, String siteKey) = 5; // void fun9(ISafetyNetCallbacks callbacks) = 8; // void fun10(ISafetyNetCallbacks callbacks, String s1, int i1, in byte[] b1) = 9; // void fun11(int i1, in Bundle b1) = 10; -// void fun12(ISafetyNetCallbacks callbacks) = 11; -// void fun13() = 12; -// void fun14(ISafetyNetCallbacks callbacks) = 13; + void initSafeBrowsing(ISafetyNetCallbacks callbacks) = 11; + void shutdownSafeBrowsing() = 12; + void isVerifyAppsEnabled(ISafetyNetCallbacks callbacks) = 13; // // void fun18(ISafetyNetCallbacks callbacks, int i1, String s1) = 17; // void fun19(ISafetyNetCallbacks callbacks, int i1) = 18; diff --git a/play-services-safetynet/src/main/java/com/google/android/gms/safetynet/HarmfulAppsInfo.java b/play-services-safetynet/src/main/java/com/google/android/gms/safetynet/HarmfulAppsInfo.java index 3b34df71a2..2c260b31ce 100644 --- a/play-services-safetynet/src/main/java/com/google/android/gms/safetynet/HarmfulAppsInfo.java +++ b/play-services-safetynet/src/main/java/com/google/android/gms/safetynet/HarmfulAppsInfo.java @@ -9,13 +9,13 @@ public class HarmfulAppsInfo extends AutoSafeParcelable { @Field(2) - public long field2; + public long lastScanTime; @Field(3) - public HarmfulAppsData[] data; + public HarmfulAppsData[] harmfulApps = new HarmfulAppsData[0]; @Field(4) - public int field4; + public int hoursSinceLastScanWithHarmfulApp = -1; @Field(5) - public boolean field5; + public boolean harmfulAppInOtherProfile = false; public static final Creator CREATOR = new AutoCreator(HarmfulAppsInfo.class); } diff --git a/play-services-safetynet/src/main/java/com/google/android/gms/safetynet/SafeBrowsingData.java b/play-services-safetynet/src/main/java/com/google/android/gms/safetynet/SafeBrowsingData.java index b14c5670d4..b73fab584c 100644 --- a/play-services-safetynet/src/main/java/com/google/android/gms/safetynet/SafeBrowsingData.java +++ b/play-services-safetynet/src/main/java/com/google/android/gms/safetynet/SafeBrowsingData.java @@ -18,17 +18,16 @@ public class SafeBrowsingData extends AutoSafeParcelable { @Field(1) public int versionCode = 1; @Field(2) - public String status; + public String metadata; @Field(3) public DataHolder data; @Field(4) public ParcelFileDescriptor fileDescriptor; - public File file; public byte[] fileContents; @Field(5) - public long field5; + public long lastUpdateTime; @Field(6) - public byte[] field6; + public byte[] state; public static final Creator CREATOR = new AutoCreator(SafeBrowsingData.class); } diff --git a/play-services-safetynet/src/main/java/com/google/android/gms/safetynet/SafetyNetClient.java b/play-services-safetynet/src/main/java/com/google/android/gms/safetynet/SafetyNetClient.java index 845e2d2d85..5e42787e2c 100644 --- a/play-services-safetynet/src/main/java/com/google/android/gms/safetynet/SafetyNetClient.java +++ b/play-services-safetynet/src/main/java/com/google/android/gms/safetynet/SafetyNetClient.java @@ -56,7 +56,7 @@ public Task attest(byte[] nonce, String apiKey try { client.attest(new ISafetyNetCallbacksDefaultStub() { @Override - public void onAttestationData(Status status, AttestationData attestationData) throws RemoteException { + public void onAttestationResult(Status status, AttestationData attestationData) throws RemoteException { SafetyNetApi.AttestationResponse response = new SafetyNetApi.AttestationResponse(); response.setResult(new SafetyNetApi.AttestationResult() { @Override diff --git a/play-services-safetynet/src/main/java/org/microg/gms/safetynet/ISafetyNetCallbacksDefaultStub.java b/play-services-safetynet/src/main/java/org/microg/gms/safetynet/ISafetyNetCallbacksDefaultStub.java index 0bad59ec85..25de726592 100644 --- a/play-services-safetynet/src/main/java/org/microg/gms/safetynet/ISafetyNetCallbacksDefaultStub.java +++ b/play-services-safetynet/src/main/java/org/microg/gms/safetynet/ISafetyNetCallbacksDefaultStub.java @@ -20,11 +20,11 @@ public class ISafetyNetCallbacksDefaultStub extends ISafetyNetCallbacks.Stub { @Override - public void onAttestationData(Status status, AttestationData attestationData) throws RemoteException { + public void onAttestationResult(Status status, AttestationData attestationData) throws RemoteException { } @Override - public void onString(String s) throws RemoteException { + public void onSharedUuid(String s) throws RemoteException { } @Override @@ -32,7 +32,8 @@ public void onSafeBrowsingData(Status status, SafeBrowsingData safeBrowsingData) } @Override - public void onBoolean(Status status, boolean b) throws RemoteException { + public void onVerifyAppsUserResult(Status status, boolean enabled) throws RemoteException { + } @Override @@ -47,6 +48,10 @@ public void onRecaptchaResult(Status status, RecaptchaResultData recaptchaResult public void onHarmfulAppsInfo(Status status, HarmfulAppsInfo harmfulAppsInfo) throws RemoteException { } + @Override + public void onInitSafeBrowsingResult(Status status) throws RemoteException { + } + @Override public void onRemoveHarmfulAppData(Status status, RemoveHarmfulAppData removeHarmfulAppData) throws RemoteException { } diff --git a/play-services-tapandpay/core/src/main/AndroidManifest.xml b/play-services-tapandpay/core/src/main/AndroidManifest.xml index 64ee486c4a..7edb46201e 100644 --- a/play-services-tapandpay/core/src/main/AndroidManifest.xml +++ b/play-services-tapandpay/core/src/main/AndroidManifest.xml @@ -4,12 +4,13 @@ ~ SPDX-License-Identifier: Apache-2.0 --> + package="org.microg.gms.tapandpay.core"> - + - + diff --git a/play-services-tapandpay/core/src/main/kotlin/org/microg/gms/tapandpay/TapAndPayService.kt b/play-services-tapandpay/core/src/main/kotlin/org/microg/gms/tapandpay/TapAndPayService.kt index 0c894b484e..965f9e3db7 100644 --- a/play-services-tapandpay/core/src/main/kotlin/org/microg/gms/tapandpay/TapAndPayService.kt +++ b/play-services-tapandpay/core/src/main/kotlin/org/microg/gms/tapandpay/TapAndPayService.kt @@ -7,6 +7,7 @@ package org.microg.gms.tapandpay import android.os.Parcel import android.os.RemoteException import android.util.Log +import android.util.SparseArray import com.google.android.gms.common.Feature import com.google.android.gms.common.api.CommonStatusCodes import com.google.android.gms.common.api.Status @@ -14,10 +15,18 @@ import com.google.android.gms.common.internal.ConnectionInfo import com.google.android.gms.common.internal.GetServiceRequest import com.google.android.gms.common.internal.IGmsCallbacks import com.google.android.gms.tapandpay.TapAndPayStatusCodes.TAP_AND_PAY_NO_ACTIVE_WALLET +import com.google.android.gms.tapandpay.firstparty.GetActiveAccountResponse +import com.google.android.gms.tapandpay.firstparty.GetAllCardsResponse +import com.google.android.gms.tapandpay.firstparty.RefreshSeCardsResponse import com.google.android.gms.tapandpay.internal.ITapAndPayService import com.google.android.gms.tapandpay.internal.ITapAndPayServiceCallbacks +import com.google.android.gms.tapandpay.internal.firstparty.GetActiveAccountRequest +import com.google.android.gms.tapandpay.internal.firstparty.GetAllCardsRequest +import com.google.android.gms.tapandpay.internal.firstparty.RefreshSeCardsRequest +import com.google.android.gms.tapandpay.internal.firstparty.SetActiveAccountRequest import org.microg.gms.BaseService import org.microg.gms.common.GmsService +import org.microg.gms.utils.warnOnTransactionIssues private const val TAG = "GmsTapAndPay" @@ -25,31 +34,73 @@ class TapAndPayService : BaseService(TAG, GmsService.TAP_AND_PAY) { override fun handleServiceRequest(callback: IGmsCallbacks, request: GetServiceRequest, service: GmsService) { callback.onPostInitCompleteWithConnectionInfo(CommonStatusCodes.SUCCESS, TapAndPayImpl(), ConnectionInfo().apply { features = arrayOf( - Feature("tapandpay_token_listing", 3) + Feature("tapandpay", 1), + Feature("tapandpay_account_linking", 1), + Feature("tapandpay_block_payment_cards", 1), + Feature("tapandpay_check_contactless_eligibility", 1), + Feature("tapandpay_dismiss_quick_access_wallet", 1), + Feature("tapandpay_get_all_cards_for_account", 1), + Feature("tapandpay_get_contactless_setup_configuration", 1), + Feature("tapandpay_get_last_attestation_result", 1), + Feature("tapandpay_get_token_pan", 1), + Feature("tapandpay_global_actions", 1), + Feature("tapandpay_issuer_api", 2), + Feature("tapandpay_perform_tokenization_operation", 1), + Feature("tapandpay_push_tokenize", 1), + Feature("tapandpay_push_tokenize_session", 6), + Feature("tapandpay_quick_access_wallet", 1), + Feature("tapandpay_secureelement", 1), + Feature("tapandpay_show_wear_card_management_view", 1), + Feature("tapandpay_send_wear_request_to_phone", 1), + Feature("tapandpay_sync_device_info", 1), + Feature("tapandpay_tokenize_account", 1), + Feature("tapandpay_tokenize_cache", 1), + Feature("tapandpay_tokenize_pan", 1), + Feature("tapandpay_transmission_event", 1), + Feature("tapandpay_token_listing", 3), + Feature("tapandpay_wallet_feedback_psd", 1) ) }) } } class TapAndPayImpl : ITapAndPayService.Stub() { + + override fun getAllCards(request: GetAllCardsRequest?, callbacks: ITapAndPayServiceCallbacks) { + Log.d(TAG, "getAllCards()") + callbacks.onAllCardsRetrieved(Status.SUCCESS, GetAllCardsResponse(emptyArray(), null, null, null, SparseArray(), ByteArray(0))) + } + + override fun setActiveAccount(request: SetActiveAccountRequest?, callbacks: ITapAndPayServiceCallbacks) { + Log.d(TAG, "setActiveAccount(${request?.accountName})") + callbacks.onActiveAccountSet(Status.SUCCESS) + } + + override fun getActiveAccount(request: GetActiveAccountRequest?, callbacks: ITapAndPayServiceCallbacks) { + Log.d(TAG, "getActiveAccount()") + callbacks.onActiveAccountDetermined(Status.SUCCESS, GetActiveAccountResponse(null)) + } + override fun registerDataChangedListener(callbacks: ITapAndPayServiceCallbacks) { Log.d(TAG, "registerDataChangedListener()") - callbacks.onStatus9(Status.SUCCESS) + callbacks.onStatus(Status.SUCCESS) } override fun getTokenStatus(tokenProvider: Int, issuerTokenId: String, callbacks: ITapAndPayServiceCallbacks) { Log.d(TAG, "getTokenStatus($tokenProvider, $issuerTokenId)") - callbacks.onTokenStatus(Status(TAP_AND_PAY_NO_ACTIVE_WALLET), null) + callbacks.onTokenStatusRetrieved(Status(TAP_AND_PAY_NO_ACTIVE_WALLET), null) } override fun getStableHardwareId(callbacks: ITapAndPayServiceCallbacks) { Log.d(TAG, "getStableHardwareId()") - callbacks.onGetStableHardwareIdResponse(Status.SUCCESS, "") + callbacks.onStableHardwareIdRetrieved(Status.SUCCESS, "") } - override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean { - if (super.onTransact(code, data, reply, flags)) return true - Log.d(TAG, "onTransact [unknown]: $code, $data, $flags") - return false + override fun refreshSeCards(request: RefreshSeCardsRequest?, callbacks: ITapAndPayServiceCallbacks) { + Log.d(TAG, "refreshSeCards()") + callbacks.onRefreshSeCardsResponse(Status.SUCCESS, RefreshSeCardsResponse()) } + + override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean = + warnOnTransactionIssues(code, reply, flags, TAG) { super.onTransact(code, data, reply, flags) } } diff --git a/play-services-tapandpay/src/main/AndroidManifest.xml b/play-services-tapandpay/src/main/AndroidManifest.xml index 0b3e189bbf..a864617beb 100644 --- a/play-services-tapandpay/src/main/AndroidManifest.xml +++ b/play-services-tapandpay/src/main/AndroidManifest.xml @@ -3,4 +3,4 @@ ~ SPDX-FileCopyrightText: 2021, microG Project Team ~ SPDX-License-Identifier: Apache-2.0 --> - + diff --git a/play-services-tapandpay/src/main/aidl/com/google/android/gms/tapandpay/firstparty/GetActiveAccountResponse.aidl b/play-services-tapandpay/src/main/aidl/com/google/android/gms/tapandpay/firstparty/GetActiveAccountResponse.aidl new file mode 100644 index 0000000000..972f895bcb --- /dev/null +++ b/play-services-tapandpay/src/main/aidl/com/google/android/gms/tapandpay/firstparty/GetActiveAccountResponse.aidl @@ -0,0 +1,3 @@ +package com.google.android.gms.tapandpay.firstparty; + +parcelable GetActiveAccountResponse; \ No newline at end of file diff --git a/play-services-tapandpay/src/main/aidl/com/google/android/gms/tapandpay/firstparty/GetAllCardsResponse.aidl b/play-services-tapandpay/src/main/aidl/com/google/android/gms/tapandpay/firstparty/GetAllCardsResponse.aidl new file mode 100644 index 0000000000..1914ea6fa5 --- /dev/null +++ b/play-services-tapandpay/src/main/aidl/com/google/android/gms/tapandpay/firstparty/GetAllCardsResponse.aidl @@ -0,0 +1,3 @@ +package com.google.android.gms.tapandpay.firstparty; + +parcelable GetAllCardsResponse; \ No newline at end of file diff --git a/play-services-tapandpay/src/main/aidl/com/google/android/gms/tapandpay/firstparty/RefreshSeCardsResponse.aidl b/play-services-tapandpay/src/main/aidl/com/google/android/gms/tapandpay/firstparty/RefreshSeCardsResponse.aidl new file mode 100644 index 0000000000..82c1e4b827 --- /dev/null +++ b/play-services-tapandpay/src/main/aidl/com/google/android/gms/tapandpay/firstparty/RefreshSeCardsResponse.aidl @@ -0,0 +1,3 @@ +package com.google.android.gms.tapandpay.firstparty; + +parcelable RefreshSeCardsResponse; \ No newline at end of file diff --git a/play-services-tapandpay/src/main/aidl/com/google/android/gms/tapandpay/internal/ITapAndPayService.aidl b/play-services-tapandpay/src/main/aidl/com/google/android/gms/tapandpay/internal/ITapAndPayService.aidl index 9ee5001000..2468d490e8 100644 --- a/play-services-tapandpay/src/main/aidl/com/google/android/gms/tapandpay/internal/ITapAndPayService.aidl +++ b/play-services-tapandpay/src/main/aidl/com/google/android/gms/tapandpay/internal/ITapAndPayService.aidl @@ -1,15 +1,19 @@ package com.google.android.gms.tapandpay.internal; import com.google.android.gms.tapandpay.internal.ITapAndPayServiceCallbacks; +import com.google.android.gms.tapandpay.internal.firstparty.GetActiveAccountRequest; +import com.google.android.gms.tapandpay.internal.firstparty.GetAllCardsRequest; +import com.google.android.gms.tapandpay.internal.firstparty.RefreshSeCardsRequest; +import com.google.android.gms.tapandpay.internal.firstparty.SetActiveAccountRequest; interface ITapAndPayService { // void setSelectedToken(in SetSelectedTokenRequest request, ITapAndPayServiceCallbacks callbacks) = 0; -// void getAllCards(in GetAllCardsRequest request, ITapAndPayServiceCallbacks callbacks) = 1; + void getAllCards(in GetAllCardsRequest request, ITapAndPayServiceCallbacks callbacks) = 1; // void deleteToken(in DeleteTokenRequest request, ITapAndPayServiceCallbacks callbacks) = 2; // void firstPartyTokenizePan(in FirstPartyTokenizePanRequest request, ITapAndPayServiceCallbacks callbacks) = 3; -// void setActiveAccount(in SetActiveAccountRequest request, ITapAndPayServiceCallbacks callbacks) = 4; + void setActiveAccount(in SetActiveAccountRequest request, ITapAndPayServiceCallbacks callbacks) = 4; // void showSecurityPrompt(in ShowSecurityPromptRequest request, ITapAndPayServiceCallbacks callbacks) = 7; -// void getActiveAccount(in GetActiveAccountRequest request, ITapAndPayServiceCallbacks callbacks) = 8; + void getActiveAccount(in GetActiveAccountRequest request, ITapAndPayServiceCallbacks callbacks) = 8; void registerDataChangedListener(ITapAndPayServiceCallbacks callbacks) = 9; // void isDeviceUnlockedForPayment(in IsDeviceUnlockedForPaymentRequest request, ITapAndPayServiceCallbacks callbacks) = 10; // void promptDeviceUnlockForPayment(in PromptDeviceUnlockForPaymentRequest request, ITapAndPayServiceCallbacks callbacks) = 11; @@ -32,8 +36,15 @@ interface ITapAndPayService { // void getEnvironment(ITapAndPayServiceCallbacks callbacks) = 30; // void enablePayOnWear(in EnablePayOnWearRequest request, ITapAndPayServiceCallbacks callbacks) = 31; // void isPayPalAvailable(ITapAndPayServiceCallbacks callbacks) = 32; -// void unknown34(ITapAndPayServiceCallbacks callbacks) = 33; // void getSecurityParams(ITapAndPayServiceCallbacks callbacks) = 34; // void getNotificationSettings(in GetNotificationSettingsRequest request, ITapAndPayServiceCallbacks callbacks) = 36; // void setNotificationSettings(in SetNotificationSettingsRequest request, ITapAndPayServiceCallbacks callbacks) = 37; +// void addOtherPaymentOption(in AddOtherPaymentOptionRequest request, ITapAndPayServiceCallbacks callbacks) = 38; +// void getAvailableOtherPaymentMethods(in GetAvailableOtherPaymentMethodsRequest request, ITapAndPayServiceCallbacks callbacks) = 39; +// Status enableNfc() = 42; +// void getSeChipTransactions(in GetSeChipTransactionsRequest request, ITapAndPayServiceCallbacks callbacks) = 48; +// void disableSelectedToken(in DisableSelectedTokenRequest request, ITapAndPayServiceCallbacks callbacks) = 52; + void refreshSeCards(in RefreshSeCardsRequest request, ITapAndPayServiceCallbacks callbacks) = 56; +// void tokenizeAccount(in TokenizeAccountRequest request, ITapAndPayServiceCallbacks callbacks) = 57; +// void syncDeviceInfo(in SyncDeviceInfoRequest request, ITapAndPayServiceCallbacks callbacks) = 64; } diff --git a/play-services-tapandpay/src/main/aidl/com/google/android/gms/tapandpay/internal/ITapAndPayServiceCallbacks.aidl b/play-services-tapandpay/src/main/aidl/com/google/android/gms/tapandpay/internal/ITapAndPayServiceCallbacks.aidl index 980d3657b6..b23c21c08c 100644 --- a/play-services-tapandpay/src/main/aidl/com/google/android/gms/tapandpay/internal/ITapAndPayServiceCallbacks.aidl +++ b/play-services-tapandpay/src/main/aidl/com/google/android/gms/tapandpay/internal/ITapAndPayServiceCallbacks.aidl @@ -1,47 +1,58 @@ package com.google.android.gms.tapandpay.internal; import com.google.android.gms.common.api.Status; +import com.google.android.gms.tapandpay.firstparty.GetActiveAccountResponse; +import com.google.android.gms.tapandpay.firstparty.GetAllCardsResponse; +import com.google.android.gms.tapandpay.firstparty.RefreshSeCardsResponse; import com.google.android.gms.tapandpay.issuer.TokenStatus; interface ITapAndPayServiceCallbacks { - void onSetSelectedTokenResponse(in Status status) = 1; - void onStatus3(in Status status, in Bundle data) = 2; -// void onGetAllCardsResponse(in Status status, in GetAllCardsResponse response) = 3; - void onDeleteTokenResponse(in Status status) = 4; - void onSetActiveAccountResponse(in Status status) = 5; -// void onGetActiveAccountResponse(in Status status, in GetActiveAccountResponse response) = 7; - void onStatus9(in Status status) = 8; - void onReturn10() = 9; - void onIsDeviceUnlockedForPaymentResponse(in Status status, boolean isDeviceUnlockedForPayment) = 10; - void onStatus12(in Status status) = 11; - void onGetReceivesTransactionNotificationsResponse(in Status status, boolean receivesTransactionNotifications) = 12; - void onSetReceivesTransactionNotificationsResponse(in Status status) = 13; -// void onGetActiveCardsForAccountResponse(in Status status, in GetActiveCardsForAccountResponse response) = 14; -// void onRetrieveInAppPaymentCredentialResponse(in Status status, in RetrieveInAppPaymentCredentialResponse response) = 16; - void onGetAnalyticsContextResponse(in Status status, String analyticsContext) = 17; - void onTokenStatus(in Status status, in TokenStatus tokenStatus) = 19; - void onIsDeviceUnlockedForInAppPaymentResponse(in Status status, boolean isDeviceUnlockedForInAppPayment) = 20; - void onReportInAppTransactionCompletedResponse(in Status status) = 21; - void onGetStableHardwareIdResponse(in Status status, String stableHardwareId) = 22; - void onGetEnvironmentResponse(in Status status, String env) = 23; - void onEnablePayOnWearResponse(in Status status) = 24; + void onTokenSelected(in Status status) = 1; + void onHandleStatusPendingIntent(in Status status, in Bundle data) = 2; + void onAllCardsRetrieved(in Status status, in GetAllCardsResponse response) = 3; + void onTokenDeleted(in Status status) = 4; + void onActiveAccountSet(in Status status) = 5; + void onActiveAccountDetermined(in Status status, in GetActiveAccountResponse response) = 7; + void onStatus(in Status status) = 8; + void onDataChanged() = 9; + void onDeviceUnlockStatusDetermined(in Status status, boolean isDeviceUnlockedForPayment) = 10; + void onTapSent(in Status status) = 11; + void onReceivesTransactionNotificationsRetrieved(in Status status, boolean receivesTransactionNotifications) = 12; + void onReceivesTransactionNotificationsSet(in Status status) = 13; +// void onActiveCardsForAccountRetrieved(in Status status, in GetActiveCardsForAccountResponse response) = 14; +// void onInAppPaymentCredentialRetrieved(in Status status, in RetrieveInAppPaymentCredentialResponse response) = 16; + void onAnalyticsContextRetrieved(in Status status, String analyticsContext) = 17; + void onActiveWalletIdRetrieved(in Status status, String walletId) = 18; + void onTokenStatusRetrieved(in Status status, in TokenStatus tokenStatus) = 19; + void onDeviceUnlockStatusDeterminedForInAppPayment(in Status status, boolean isDeviceUnlockedForInAppPayment) = 20; + void onIncreaseInAppTransaction(in Status status) = 21; + void onStableHardwareIdRetrieved(in Status status, String stableHardwareId) = 22; + void onEnvironmentRetrieved(in Status status, String env) = 23; + void onEnablePayOnWear(in Status status) = 24; void onIsPayPalAvailableResponse(in Status status, boolean IsPayPalAvailable) = 25; -// void onGetSecurityParamsResponse(in Status status, in GetSecurityParamsResponse response) = 26; -// void onGetNotificationSettingsResponse(in Status status, in GetNotificationSettingsResponse response) = 27; - void onSetNotificationSettingsResponse(in Status status) = 28; -// void onGetAvailableOtherPaymentMethodsResponse(in Status status, in GetAvailableOtherPaymentMethodsResponse response) = 29; -// void onGetActiveTokensForAccountResponse(in Status status, in GetActiveTokensForAccountResponse response) = 30; -// void onGetSeChipTransactionsResponse(in Status status, in GetSeChipTransactionsResponse response) = 34; +// void onSecurityPraramsDetermined(in Status status, in GetSecurityParamsResponse response) = 26; +// void onNotificationSettingsRetrieved(in Status status, in GetNotificationSettingsResponse response) = 27; + void onNotificationSettingsSet(in Status status) = 28; +// void onAvailableOtherPaymentMethodsRetrieved(in Status status, in GetAvailableOtherPaymentMethodsResponse response) = 29; +// void onActiveTokensForAccountRetrieved(in Status status, in GetActiveTokensForAccountResponse response) = 30; +// void onSeChipTransactionsRetrieved(in Status status, in GetSeChipTransactionsResponse response) = 34; // void onReserveResourceResponse(in Status status, in ReserveResourceResponse response) = 35; void onReleaseResourceResponse(in Status status) = 36; - void onDisableSelectedTokenResponse(in Status status) = 37; -// void onGetFelicaTosAcceptanceResponse(in Status status, in GetFelicaTosAcceptanceResponse response) = 38; - void onSetFelicaTosAcceptanceResponse(in Status status) = 39; -// void onRefreshSeCardsResponse(in Status status, in RefreshSeCardsResponse response) = 40; -// void onGetGlobalActionCardsResponse(in Status status, in GetGlobalActionCardsResponse response) = 41; - void onGetLinkingTokenResponse(in Status status, String linkingToken) = 42; - void onBlockPaymentCardsResponse(in Status status) = 43; - void onUnblockPaymentCardsResponse(in Status status) = 44; -// void onGetLastAttestationResultResponse(in Status status, in GetLastAttestationResultResponse response) = 45; + void onSelectedTokenDisabled(in Status status) = 37; +// void onFelicaTosAcceptanceRetrieved(in Status status, in GetFelicaTosAcceptanceResponse response) = 38; + void onFelicaTosAcceptanceSet(in Status status) = 39; + void onRefreshSeCardsResponse(in Status status, in RefreshSeCardsResponse response) = 40; +// void onGlobalActionCardsRetrieved(in Status status, in GetGlobalActionCardsResponse response) = 41; + void onLinkingTokenRetrieved(in Status status, String linkingToken) = 42; + void onPaymentCardsBlocked(in Status status) = 43; + void onPaymentCardsUnblocked(in Status status) = 44; +// void onLastAttestationResultRetrieved(in Status status, in GetLastAttestationResultResponse response) = 45; // void onQuickAccessWalletConfig(in Status status, in QuickAccessWalletConfig config) = 46; +// void onContactlessSetupStatusRetrieved(in Status status, in GetContactlessSetupStatusResponse response) = 47; + void onIsTokenizedRetrieved(in Status status, boolean isTokenized) = 48; +// void onListTokensRetrieved(in Status status, in TokenInfo[] tokens) = 49; +// void onContactlessEligibilityRetrieved(in Status status, in CheckContactlessEligibilityResponse response) = 50; + void onProto(in Status status, in byte[] proto) = 51; +// void onPushProvisionSessionContextRetrieved(in Status status, in PushProvisionSessionContext context) = 52; + void onTokenPanRetrieved(in Status status, String tokenPan) = 53; } diff --git a/play-services-tapandpay/src/main/aidl/com/google/android/gms/tapandpay/internal/firstparty/GetActiveAccountRequest.aidl b/play-services-tapandpay/src/main/aidl/com/google/android/gms/tapandpay/internal/firstparty/GetActiveAccountRequest.aidl new file mode 100644 index 0000000000..d56396eba1 --- /dev/null +++ b/play-services-tapandpay/src/main/aidl/com/google/android/gms/tapandpay/internal/firstparty/GetActiveAccountRequest.aidl @@ -0,0 +1,3 @@ +package com.google.android.gms.tapandpay.internal.firstparty; + +parcelable GetActiveAccountRequest; \ No newline at end of file diff --git a/play-services-tapandpay/src/main/aidl/com/google/android/gms/tapandpay/internal/firstparty/GetAllCardsRequest.aidl b/play-services-tapandpay/src/main/aidl/com/google/android/gms/tapandpay/internal/firstparty/GetAllCardsRequest.aidl new file mode 100644 index 0000000000..5eb3eef9d7 --- /dev/null +++ b/play-services-tapandpay/src/main/aidl/com/google/android/gms/tapandpay/internal/firstparty/GetAllCardsRequest.aidl @@ -0,0 +1,3 @@ +package com.google.android.gms.tapandpay.internal.firstparty; + +parcelable GetAllCardsRequest; \ No newline at end of file diff --git a/play-services-tapandpay/src/main/aidl/com/google/android/gms/tapandpay/internal/firstparty/RefreshSeCardsRequest.aidl b/play-services-tapandpay/src/main/aidl/com/google/android/gms/tapandpay/internal/firstparty/RefreshSeCardsRequest.aidl new file mode 100644 index 0000000000..e973419392 --- /dev/null +++ b/play-services-tapandpay/src/main/aidl/com/google/android/gms/tapandpay/internal/firstparty/RefreshSeCardsRequest.aidl @@ -0,0 +1,3 @@ +package com.google.android.gms.tapandpay.internal.firstparty; + +parcelable RefreshSeCardsRequest; \ No newline at end of file diff --git a/play-services-tapandpay/src/main/aidl/com/google/android/gms/tapandpay/internal/firstparty/SetActiveAccountRequest.aidl b/play-services-tapandpay/src/main/aidl/com/google/android/gms/tapandpay/internal/firstparty/SetActiveAccountRequest.aidl new file mode 100644 index 0000000000..37727b6cc0 --- /dev/null +++ b/play-services-tapandpay/src/main/aidl/com/google/android/gms/tapandpay/internal/firstparty/SetActiveAccountRequest.aidl @@ -0,0 +1,3 @@ +package com.google.android.gms.tapandpay.internal.firstparty; + +parcelable SetActiveAccountRequest; \ No newline at end of file diff --git a/play-services-tapandpay/src/main/java/com/google/android/gms/tapandpay/firstparty/AccountInfo.java b/play-services-tapandpay/src/main/java/com/google/android/gms/tapandpay/firstparty/AccountInfo.java new file mode 100644 index 0000000000..1008dc3ab1 --- /dev/null +++ b/play-services-tapandpay/src/main/java/com/google/android/gms/tapandpay/firstparty/AccountInfo.java @@ -0,0 +1,53 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.tapandpay.firstparty; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import org.microg.gms.utils.ToStringHelper; +import org.microg.safeparcel.AutoSafeParcelable; + +import java.util.Arrays; +import java.util.Objects; + +public class AccountInfo extends AutoSafeParcelable { + @Field(2) + public final String accountId; + @Field(3) + public final String accountName; + + private AccountInfo() { + accountId = null; + accountName = null; + } + + public AccountInfo(String accountId, String accountName) { + this.accountId = accountId; + this.accountName = accountName; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (!(obj instanceof AccountInfo)) return false; + return Objects.equals(accountId, ((AccountInfo) obj).accountId) && Objects.equals(accountName, ((AccountInfo) obj).accountName); + } + + @Override + public int hashCode() { + return Arrays.hashCode(new Object[]{accountId, accountName}); + } + + @NonNull + @Override + public String toString() { + return new ToStringHelper("AccountInfo") + .field("accountId", accountId) + .field("accountName", accountName) + .end(); + } + + public static final Creator CREATOR = new AutoCreator<>(AccountInfo.class); +} diff --git a/play-services-tapandpay/src/main/java/com/google/android/gms/tapandpay/firstparty/CardInfo.java b/play-services-tapandpay/src/main/java/com/google/android/gms/tapandpay/firstparty/CardInfo.java new file mode 100644 index 0000000000..4f1e8c2e29 --- /dev/null +++ b/play-services-tapandpay/src/main/java/com/google/android/gms/tapandpay/firstparty/CardInfo.java @@ -0,0 +1,87 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.tapandpay.firstparty; + +import org.microg.safeparcel.AutoSafeParcelable; + +public class CardInfo extends AutoSafeParcelable { + @Field(2) + public String billingCardId; + @Field(3) + public byte[] serverToken; + @Field(4) + public String cardholderName; + @Field(5) + public String displayName; + @Field(6) + public int cardNetwork; + @Field(7) + public TokenStatus tokenStatus; + @Field(8) + public String panLastDigits; + @Field(9) + public String cardImageUrl; + @Field(10) + public int cardColor; + @Field(11) + public int overlayTextColor; +// @Field(12) +// public IssuerInfo issuerInfo; + @Field(13) + public String tokenLastDigits; +// @Field(15) +// public TransactionInfo transactionInfo; + @Field(16) + public String ssuerTokenId; + @Field(17) + public byte[] inAppCardToken; + @Field(18) + public int cachedEligibility; + @Field(20) + public int paymentProtocol; + @Field(21) + public int tokenType; +// @Field(22) +// public InStoreCvmConfig inStoreCvmConfig; +// @Field(23) +// public InAppCvmConfig inAppCvmConfig; + @Field(24) + public String tokenDisplayName; +// @Field(25) +// public OnlineAccountCardLinkInfo[] onlineAccountCardLinkInfos; + @Field(26) + public boolean allowAidSelection; +// @Field(27) +// public List badges; + @Field(28) + public boolean upgradeAvailable; + @Field(29) + public boolean requiresSignature; + @Field(30) + public long googleTokenId; + @Field(31) + public long lastTapTimestamp; + @Field(32) + public boolean isTransit; + @Field(33) + public long googleWalletId; + @Field(34) + public String devicePaymentMethodId; + @Field(35) + public String cloudPaymentMethodId; +// @Field(36) +// public CardRewardsInfo cardRewardsInfo; + @Field(37) + public int tapStrategy; + @Field(38) + public boolean hideFromGlobalActions; + @Field(39) + public String rawPanLastDigits; + @Field(40) + public int cardDisplayType; + + public static final Creator CREATOR = new AutoCreator<>(CardInfo.class); +} diff --git a/play-services-tapandpay/src/main/java/com/google/android/gms/tapandpay/firstparty/GetActiveAccountResponse.java b/play-services-tapandpay/src/main/java/com/google/android/gms/tapandpay/firstparty/GetActiveAccountResponse.java new file mode 100644 index 0000000000..7a1c268705 --- /dev/null +++ b/play-services-tapandpay/src/main/java/com/google/android/gms/tapandpay/firstparty/GetActiveAccountResponse.java @@ -0,0 +1,25 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.tapandpay.firstparty; + +import androidx.annotation.Nullable; +import org.microg.safeparcel.AutoSafeParcelable; + +public class GetActiveAccountResponse extends AutoSafeParcelable { + @Field(2) + @Nullable + public final AccountInfo accountInfo; + + private GetActiveAccountResponse() { + accountInfo = null; + } + + public GetActiveAccountResponse(@Nullable AccountInfo accountInfo) { + this.accountInfo = accountInfo; + } + + public static final Creator CREATOR = new AutoCreator<>(GetActiveAccountResponse.class); +} diff --git a/play-services-tapandpay/src/main/java/com/google/android/gms/tapandpay/firstparty/GetAllCardsResponse.java b/play-services-tapandpay/src/main/java/com/google/android/gms/tapandpay/firstparty/GetAllCardsResponse.java new file mode 100644 index 0000000000..fd11747039 --- /dev/null +++ b/play-services-tapandpay/src/main/java/com/google/android/gms/tapandpay/firstparty/GetAllCardsResponse.java @@ -0,0 +1,46 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.tapandpay.firstparty; + +import android.util.SparseArray; +import androidx.annotation.Nullable; +import org.microg.safeparcel.AutoSafeParcelable; + +public class GetAllCardsResponse extends AutoSafeParcelable { + @Field(2) + public final CardInfo[] cardInfos; + @Field(3) + public final AccountInfo accountInfo; + @Field(4) + public final String defaultClientTokenId; + @Field(5) + public final String overrideClientTokenId; + // FIXME: Add support for SparseArray in SafeParcelable library +// @Field(6) + public final SparseArray seDefaultCards; + @Field(7) + public final byte[] wearSortOrder; + + private GetAllCardsResponse() { + cardInfos = new CardInfo[0]; + accountInfo = null; + defaultClientTokenId = null; + overrideClientTokenId = null; + seDefaultCards = new SparseArray<>(); + wearSortOrder = new byte[0]; + } + + public GetAllCardsResponse(CardInfo[] cardInfos, AccountInfo accountInfo, String defaultClientTokenId, String overrideClientTokenId, SparseArray seDefaultCards, byte[] wearSortOrder) { + this.cardInfos = cardInfos; + this.accountInfo = accountInfo; + this.defaultClientTokenId = defaultClientTokenId; + this.overrideClientTokenId = overrideClientTokenId; + this.seDefaultCards = seDefaultCards; + this.wearSortOrder = wearSortOrder; + } + + public static final Creator CREATOR = new AutoCreator<>(GetAllCardsResponse.class); +} diff --git a/play-services-tapandpay/src/main/java/com/google/android/gms/tapandpay/firstparty/RefreshSeCardsResponse.java b/play-services-tapandpay/src/main/java/com/google/android/gms/tapandpay/firstparty/RefreshSeCardsResponse.java new file mode 100644 index 0000000000..d2da44ea0d --- /dev/null +++ b/play-services-tapandpay/src/main/java/com/google/android/gms/tapandpay/firstparty/RefreshSeCardsResponse.java @@ -0,0 +1,12 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.tapandpay.firstparty; + +import org.microg.safeparcel.AutoSafeParcelable; + +public class RefreshSeCardsResponse extends AutoSafeParcelable { + public static final Creator CREATOR = new AutoCreator<>(RefreshSeCardsResponse.class); +} diff --git a/play-services-tapandpay/src/main/java/com/google/android/gms/tapandpay/internal/firstparty/GetActiveAccountRequest.java b/play-services-tapandpay/src/main/java/com/google/android/gms/tapandpay/internal/firstparty/GetActiveAccountRequest.java new file mode 100644 index 0000000000..0848819582 --- /dev/null +++ b/play-services-tapandpay/src/main/java/com/google/android/gms/tapandpay/internal/firstparty/GetActiveAccountRequest.java @@ -0,0 +1,12 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.tapandpay.internal.firstparty; + +import org.microg.safeparcel.AutoSafeParcelable; + +public class GetActiveAccountRequest extends AutoSafeParcelable { + public static final Creator CREATOR = new AutoCreator<>(GetActiveAccountRequest.class); +} diff --git a/play-services-tapandpay/src/main/java/com/google/android/gms/tapandpay/internal/firstparty/GetAllCardsRequest.java b/play-services-tapandpay/src/main/java/com/google/android/gms/tapandpay/internal/firstparty/GetAllCardsRequest.java new file mode 100644 index 0000000000..2a3da0418e --- /dev/null +++ b/play-services-tapandpay/src/main/java/com/google/android/gms/tapandpay/internal/firstparty/GetAllCardsRequest.java @@ -0,0 +1,20 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.tapandpay.internal.firstparty; + +import android.accounts.Account; +import org.microg.safeparcel.AutoSafeParcelable; + +public class GetAllCardsRequest extends AutoSafeParcelable { + @Field(2) + public boolean refreshSeCards; + @Field(3) + public Account account; + @Field(4) + public int sortOrderCollectionId; + + public static final Creator CREATOR = new AutoCreator<>(GetAllCardsRequest.class); +} diff --git a/play-services-tapandpay/src/main/java/com/google/android/gms/tapandpay/internal/firstparty/RefreshSeCardsRequest.java b/play-services-tapandpay/src/main/java/com/google/android/gms/tapandpay/internal/firstparty/RefreshSeCardsRequest.java new file mode 100644 index 0000000000..69f955e752 --- /dev/null +++ b/play-services-tapandpay/src/main/java/com/google/android/gms/tapandpay/internal/firstparty/RefreshSeCardsRequest.java @@ -0,0 +1,12 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.tapandpay.internal.firstparty; + +import org.microg.safeparcel.AutoSafeParcelable; + +public class RefreshSeCardsRequest extends AutoSafeParcelable { + public static final Creator CREATOR = new AutoCreator<>(RefreshSeCardsRequest.class); +} diff --git a/play-services-tapandpay/src/main/java/com/google/android/gms/tapandpay/internal/firstparty/SetActiveAccountRequest.java b/play-services-tapandpay/src/main/java/com/google/android/gms/tapandpay/internal/firstparty/SetActiveAccountRequest.java new file mode 100644 index 0000000000..20a2e66b94 --- /dev/null +++ b/play-services-tapandpay/src/main/java/com/google/android/gms/tapandpay/internal/firstparty/SetActiveAccountRequest.java @@ -0,0 +1,17 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.tapandpay.internal.firstparty; + +import org.microg.safeparcel.AutoSafeParcelable; + +public class SetActiveAccountRequest extends AutoSafeParcelable { + @Field(2) + public String accountName; + @Field(3) + public boolean allowSetupErrorMessage; + + public static final Creator CREATOR = new AutoCreator<>(SetActiveAccountRequest.class); +} diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableImpl.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableImpl.java index c16840301e..1f0ed12669 100644 --- a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableImpl.java +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableImpl.java @@ -69,6 +69,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.CountDownLatch; import okio.ByteString; @@ -89,6 +90,7 @@ public class WearableImpl { private ConnectionConfiguration[] configurations; private boolean configurationsUpdated = false; private ClockworkNodePreferences clockworkNodePreferences; + private CountDownLatch networkHandlerLock = new CountDownLatch(1); public Handler networkHandler; public WearableImpl(Context context, NodeDatabaseHelper nodeDatabase, ConfigurationDatabaseHelper configDatabase) { @@ -100,6 +102,7 @@ public WearableImpl(Context context, NodeDatabaseHelper nodeDatabase, Configurat new Thread(() -> { Looper.prepare(); networkHandler = new Handler(Looper.myLooper()); + networkHandlerLock.countDown(); Looper.loop(); }).start(); } @@ -619,7 +622,12 @@ public int sendMessage(String packageName, String targetNodeId, String path, byt } public void stop() { - this.networkHandler.getLooper().quit(); + try { + this.networkHandlerLock.await(); + this.networkHandler.getLooper().quit(); + } catch (InterruptedException e) { + Log.w(TAG, e); + } } private class ListenerInfo { diff --git a/settings.gradle b/settings.gradle index 4ca7a91f6f..7f72ad5025 100644 --- a/settings.gradle +++ b/settings.gradle @@ -10,8 +10,13 @@ include ':play-services-tasks' include ':play-services-api' +include ':play-services-ads' +include ':play-services-ads-base' +include ':play-services-ads-identifier' +include ':play-services-ads-lite' include ':play-services-appinvite' include ':play-services-auth' +include ':play-services-auth-api-phone' include ':play-services-auth-base' include ':play-services-base' include ':play-services-cast' @@ -28,6 +33,7 @@ include ':play-services-maps' include ':play-services-measurement-base' include ':play-services-nearby' include ':play-services-oss-licenses' +include ':play-services-pay' include ':play-services-places' include ':play-services-places-placereport' include ':play-services-recaptcha' @@ -49,6 +55,10 @@ include ':play-services-core-proto' sublude ':play-services-basement:ktx' sublude ':play-services-tasks:ktx' +sublude ':play-services-ads:core' +sublude ':play-services-ads-identifier:core' +sublude ':play-services-ads-lite:core' +sublude ':play-services-appinvite:core' sublude ':play-services-base:core' sublude ':play-services-cast:core' sublude ':play-services-cast-framework:core' @@ -59,12 +69,15 @@ sublude ':play-services-droidguard:core' sublude ':play-services-fido:core' sublude ':play-services-gmscompliance:core' sublude ':play-services-location:core' -sublude ':play-services-location:system-api' +sublude ':play-services-location:core:base' +sublude ':play-services-location:core:provider' +sublude ':play-services-location:core:system-api' include ':play-services-maps-core-mapbox' include ':play-services-maps-core-vtm' include ':play-services-maps-core-vtm:vtm-microg-theme' sublude ':play-services-nearby:core' sublude ':play-services-oss-licenses:core' +sublude ':play-services-pay:core' sublude ':play-services-safetynet:core' sublude ':play-services-recaptcha:core' sublude ':play-services-tapandpay:core'