diff --git a/play-services-base/core/src/main/java/org/microg/gms/auth/AuthResponse.java b/play-services-base/core/src/main/java/org/microg/gms/auth/AuthResponse.java
index 6e945b75e6..5a6fc498c2 100644
--- a/play-services-base/core/src/main/java/org/microg/gms/auth/AuthResponse.java
+++ b/play-services-base/core/src/main/java/org/microg/gms/auth/AuthResponse.java
@@ -73,6 +73,8 @@ public class AuthResponse {
public String auths;
@ResponseField("capabilities")
public String capabilities;
+ @ResponseField("ExpiresInDurationSec")
+ public int expiresInDurationSec;
public static AuthResponse parse(String result) {
AuthResponse response = new AuthResponse();
@@ -129,6 +131,7 @@ public String toString() {
if (itMetadata != null) sb.append(", itMetadata='").append(itMetadata).append('\'');
if (resolutionDataBase64 != null) sb.append(", resolutionDataBase64='").append(resolutionDataBase64).append('\'');
if (capabilities != null) sb.append(", capabilitites='").append(capabilities).append('\'');
+ if (expiresInDurationSec != 0) sb.append(", expiresInDurationSec='").append(expiresInDurationSec).append('\'');
sb.append('}');
return sb.toString();
}
diff --git a/play-services-base/core/src/main/kotlin/org/microg/gms/auth/AuthPrefs.kt b/play-services-base/core/src/main/kotlin/org/microg/gms/auth/AuthPrefs.kt
index f284b24392..75b1e20c96 100644
--- a/play-services-base/core/src/main/kotlin/org/microg/gms/auth/AuthPrefs.kt
+++ b/play-services-base/core/src/main/kotlin/org/microg/gms/auth/AuthPrefs.kt
@@ -34,4 +34,10 @@ object AuthPrefs {
}
}
+ @JvmStatic
+ fun shouldReceiveTwoStepVerification(context: Context): Boolean {
+ return SettingsContract.getSettings(context, Auth.getContentUri(context), arrayOf(Auth.TWO_STEP_VERIFICATION)) { c ->
+ c.getInt(0) != 0
+ }
+ }
}
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 9ab57b83b0..b1114c7451 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
@@ -163,12 +163,14 @@ object SettingsContract {
const val VISIBLE = "auth_manager_visible"
const val INCLUDE_ANDROID_ID = "auth_include_android_id"
const val STRIP_DEVICE_NAME = "auth_strip_device_name"
+ const val TWO_STEP_VERIFICATION = "auth_two_step_verification"
val PROJECTION = arrayOf(
TRUST_GOOGLE,
VISIBLE,
INCLUDE_ANDROID_ID,
STRIP_DEVICE_NAME,
+ TWO_STEP_VERIFICATION,
)
}
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 77f98ff2aa..4af8144d87 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
@@ -209,6 +209,7 @@ class SettingsProvider : ContentProvider() {
Auth.VISIBLE -> getSettingsBoolean(key, false)
Auth.INCLUDE_ANDROID_ID -> getSettingsBoolean(key, true)
Auth.STRIP_DEVICE_NAME -> getSettingsBoolean(key, false)
+ Auth.TWO_STEP_VERIFICATION -> getSettingsBoolean(key, false)
else -> throw IllegalArgumentException("Unknown key: $key")
}
}
@@ -222,6 +223,7 @@ class SettingsProvider : ContentProvider() {
Auth.VISIBLE -> editor.putBoolean(key, value as Boolean)
Auth.INCLUDE_ANDROID_ID -> editor.putBoolean(key, value as Boolean)
Auth.STRIP_DEVICE_NAME -> editor.putBoolean(key, value as Boolean)
+ Auth.TWO_STEP_VERIFICATION -> editor.putBoolean(key, value as Boolean)
else -> throw IllegalArgumentException("Unknown key: $key")
}
}
diff --git a/play-services-basement/src/main/java/org/microg/gms/gcm/GcmConstants.java b/play-services-basement/src/main/java/org/microg/gms/gcm/GcmConstants.java
index a023f9a2c4..b369c565ae 100644
--- a/play-services-basement/src/main/java/org/microg/gms/gcm/GcmConstants.java
+++ b/play-services-basement/src/main/java/org/microg/gms/gcm/GcmConstants.java
@@ -70,6 +70,11 @@ public final class GcmConstants {
public static final String EXTRA_TOPIC = "gcm.topic";
public static final String EXTRA_TTL = "google.ttl";
public static final String EXTRA_UNREGISTERED = "unregistered";
+ public static final String EXTRA_ACCOUNT_NAME = "a";
+ public static final String EXTRA_REG_ID = "id";
+ public static final String EXTRA_AUTHS_TOKEN = "t";
+ public static final String EXTRA_GCM_BODY = "gcmb";
+ public static final String EXTRA_GMS_GNOTS_PAYLOAD = "gms.gnots.payload";
public static final String MESSAGE_TYPE_GCM = "gcm";
public static final String MESSAGE_TYPE_DELETED_MESSAGE = "deleted_message";
diff --git a/play-services-core-proto/src/main/proto/auth.proto b/play-services-core-proto/src/main/proto/auth.proto
index d6c759f02a..5ba2f70804 100644
--- a/play-services-core-proto/src/main/proto/auth.proto
+++ b/play-services-core-proto/src/main/proto/auth.proto
@@ -56,3 +56,39 @@ message Cookie {
optional string discard = 10;
optional string comment = 12;
}
+
+message ItAuthData {
+ optional bytes auth = 1;
+ repeated bytes tokens = 2;
+ optional bytes signature = 3;
+}
+
+message ItMetadataData {
+ message ScopeEntry {
+ repeated string name = 1;
+ optional int32 id = 2;
+ }
+ repeated ScopeEntry entries = 1;
+ optional TokenField field = 3;
+ optional int32 liveTime = 4;
+}
+
+message TokenField {
+ enum FieldType {
+ UNKNOWN = 0;
+ SCOPE = 1;
+ EXPIRATION = 2;
+ }
+ repeated FieldType types = 1 [packed = true];
+}
+
+message OAuthAuthorization {
+ repeated int32 scopeIds = 1 [packed = true];
+ optional int32 effectiveDurationSeconds = 2;
+}
+
+message OAuthTokenData {
+ optional int32 fieldType = 1;
+ optional bytes authorization = 2;
+ optional int32 durationMillis = 3;
+}
\ No newline at end of file
diff --git a/play-services-core-proto/src/main/proto/gnots.proto b/play-services-core-proto/src/main/proto/gnots.proto
new file mode 100644
index 0000000000..a458702ce4
--- /dev/null
+++ b/play-services-core-proto/src/main/proto/gnots.proto
@@ -0,0 +1,183 @@
+package social.boq.notifications.gmscoreapi;
+
+option java_outer_classname = "GunsGmscoreApiService";
+option java_package = "org.microg.gms.gcm";
+
+service GunsGmscoreApiService {
+ rpc GmsGnotsFetchByIdentifier(FetchByIdentifierRequest) returns (FetchByIdentifierResponse);
+ rpc GmsGnotsSetReadStates(GmsGnotsSetReadStatesRequest) returns (GmsGnotsSetReadStatesResponse);
+}
+
+message FetchByIdentifierRequest {
+ optional GmsConfig config = 1;
+ optional NotificationIdentifierList identifiers = 2;
+}
+
+message GmsConfig {
+ message GmsVersionInfo {
+ optional int32 version = 10;
+ }
+ optional GmsVersionInfo versionInfo = 3;
+}
+
+message NotificationIdentifierList {
+ repeated NotificationIdentifier notifications = 1;
+ optional DeviceInfo deviceInfo = 2;
+}
+
+message FetchByIdentifierResponse {
+ optional NotificationList notifications = 2;
+}
+
+message NotificationList {
+ repeated NotificationData notifications = 1;
+ optional uint64 serverTime = 3;
+}
+
+message NotificationData {
+ optional UserInfo userInfo = 1;
+ optional NotificationIdentifier identifier = 2;
+ optional bool isActive = 3;
+ optional NotificationContent content = 4;
+ optional NotificationAction action = 5;
+ optional DeviceInfo deviceInfo = 6;
+ optional uint64 createTime = 7;
+ optional IntentActions intentActions = 8;
+ optional uint64 expiryTime = 9;
+ optional BinaryPayload binaryPayload = 10;
+}
+
+message IntentActions {
+ optional IntentPayload primaryPayload = 1;
+ optional IntentPayload secondaryPayload = 2;
+}
+
+message UserInfo {
+ optional string userId = 1;
+}
+
+message NotificationIdentifier {
+ optional string type = 1;
+ optional string uniqueId = 2;
+ optional uint64 timestamp = 3;
+ optional string source = 4;
+ optional string registrationId = 5;
+ optional int64 receivedTime = 6;
+ optional bytes payload = 7;
+}
+
+message NotificationContent {
+ optional int32 priority = 1;
+ optional IconInfo icon = 2;
+ optional string title = 3;
+ optional string accountName = 4;
+ optional string email = 5;
+ optional string description = 6;
+ optional string additionalText = 7;
+ optional string eventType = 8;
+ optional string errorMessage = 9;
+ optional bool isDismissible = 10;
+ optional bool requiresAuth = 11;
+ optional bool isUserVisible = 12;
+ optional bool isAutoCancel = 13;
+ optional ActionButtons buttons = 14;
+ optional bool isPersistent = 15;
+ optional string tickerText = 16;
+ repeated NotificationButton actionButtons = 17;
+ optional NotificationChannelInfo channelInfo = 18;
+ optional string groupKey = 19;
+ optional string sortKey = 20;
+}
+
+message NotificationChannelInfo {
+ optional string id = 1;
+ optional string description = 2;
+ optional string groupId = 3;
+ optional string groupName = 4;
+ optional int32 importance = 5;
+ optional string name = 6;
+}
+
+message IconInfo {
+ optional string iconUrl = 1;
+}
+
+message ActionButtons {
+ optional string primaryText = 1;
+ optional string secondaryText = 2;
+}
+
+message NotificationButton {
+ optional string text = 1;
+ optional NotificationAction action = 2;
+ optional string icon = 3;
+ optional bool isEnabled = 4;
+ optional int32 buttonType = 6;
+}
+
+message NotificationAction {
+ optional ActionMetadata metadata = 1;
+ optional ActionIntent intent = 2;
+}
+
+message ActionMetadata {
+ optional string actionUrl = 1;
+ optional bool value = 2;
+}
+
+message ActionIntent {
+ optional IntentPayload intentPayload = 4;
+}
+
+message IntentPayload {
+ optional string className = 1;
+ optional string action = 2;
+ optional int32 launchType = 3;
+ repeated IntentExtra extras = 4;
+ optional int32 flags = 5;
+}
+
+message IntentExtra {
+ optional string key = 1;
+ optional string value = 2;
+}
+
+message GmsGnotsSetReadStatesRequest {
+ optional GmsConfig config = 1;
+ optional ReadStateList readStates = 2;
+}
+
+message ReadStateList {
+ repeated ReadStateItem items = 1;
+}
+
+message ReadStateItem {
+ optional NotificationIdentifier notification = 1;
+ optional string state = 3;
+ optional int32 status = 4;
+}
+
+message GmsGnotsSetReadStatesResponse {
+}
+
+message DeviceInfo {
+ optional DensityQualifier densityQualifier = 1;
+ enum DensityQualifier {
+ MDPI = 0;
+ TVDPI = 1;
+ XHDPI = 2;
+ XXHDPI = 3;
+ HDPI = 4;
+ XXXHDPI = 5;
+ }
+ optional string localeTag = 2;
+ optional int32 sdkVersion = 3;
+ optional float density = 4;
+ optional string timeZoneId = 5;
+ repeated NotificationChannelInfo notificationChannels = 6;
+}
+
+message BinaryPayload {
+ required string type = 1;
+ required bytes data = 2;
+}
\ No newline at end of file
diff --git a/play-services-core/src/huawei/AndroidManifest.xml b/play-services-core/src/huawei/AndroidManifest.xml
index 10eb1d71a0..b78e4ec904 100644
--- a/play-services-core/src/huawei/AndroidManifest.xml
+++ b/play-services-core/src/huawei/AndroidManifest.xml
@@ -22,6 +22,9 @@
+
diff --git a/play-services-core/src/huaweilh/AndroidManifest.xml b/play-services-core/src/huaweilh/AndroidManifest.xml
index 244b93bfa6..7af5388198 100644
--- a/play-services-core/src/huaweilh/AndroidManifest.xml
+++ b/play-services-core/src/huaweilh/AndroidManifest.xml
@@ -22,6 +22,9 @@
+
diff --git a/play-services-core/src/main/AndroidManifest.xml b/play-services-core/src/main/AndroidManifest.xml
index 4d8ffaddee..1b60542387 100644
--- a/play-services-core/src/main/AndroidManifest.xml
+++ b/play-services-core/src/main/AndroidManifest.xml
@@ -323,6 +323,24 @@
android:name="org.microg.gms.gcm.McsService"
android:process=":persistent" />
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/play-services-core/src/main/java/org/microg/gms/auth/login/LoginActivity.java b/play-services-core/src/main/java/org/microg/gms/auth/login/LoginActivity.java
index c0822cd618..d9459f3d8d 100644
--- a/play-services-core/src/main/java/org/microg/gms/auth/login/LoginActivity.java
+++ b/play-services-core/src/main/java/org/microg/gms/auth/login/LoginActivity.java
@@ -54,6 +54,7 @@
import org.microg.gms.auth.AuthResponse;
import org.microg.gms.checkin.CheckinManager;
import org.microg.gms.checkin.LastCheckinInfo;
+import org.microg.gms.common.Constants;
import org.microg.gms.common.HttpFormClient;
import org.microg.gms.common.Utils;
import org.microg.gms.people.PeopleManager;
@@ -79,6 +80,8 @@
import static org.microg.gms.common.Constants.GMS_PACKAGE_NAME;
import static org.microg.gms.common.Constants.GMS_VERSION_CODE;
import static org.microg.gms.common.Constants.VENDING_PACKAGE_NAME;
+import static org.microg.gms.gcm.GcmInGmsServiceKt.ACTION_GCM_REGISTER_ACCOUNT;
+import static org.microg.gms.gcm.GcmInGmsServiceKt.KEY_GCM_REGISTER_ACCOUNT_NAME;
public class LoginActivity extends AssistantActivity {
public static final String TMPL_NEW_ACCOUNT = "new_account";
@@ -392,6 +395,7 @@ public void onResponse(AuthResponse response) {
}
checkin(true);
returnSuccessResponse(account);
+ notifyGcmGroupUpdate(account.name);
if (SDK_INT >= LOLLIPOP) { finishAndRemoveTask(); } else finish();
}
@@ -407,6 +411,13 @@ public void onException(Exception exception) {
});
}
+ private void notifyGcmGroupUpdate(String accountName) {
+ Intent intent = new Intent(ACTION_GCM_REGISTER_ACCOUNT);
+ intent.setPackage(Constants.GMS_PACKAGE_NAME);
+ intent.putExtra(KEY_GCM_REGISTER_ACCOUNT_NAME, accountName);
+ sendBroadcast(intent);
+ }
+
private boolean checkin(boolean force) {
try {
CheckinManager.checkin(LoginActivity.this, force);
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 ff2f0ce660..37d19a0004 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
@@ -47,6 +47,7 @@
import com.squareup.wire.Message;
import org.microg.gms.checkin.LastCheckinInfo;
+import org.microg.gms.common.Constants;
import org.microg.gms.common.ForegroundServiceContext;
import org.microg.gms.common.ForegroundServiceInfo;
import org.microg.gms.common.PackageUtils;
@@ -80,6 +81,7 @@
import static android.os.Build.VERSION.SDK_INT;
import static org.microg.gms.common.PackageUtils.warnIfNotPersistentProcess;
import static org.microg.gms.gcm.GcmConstants.*;
+import static org.microg.gms.gcm.GcmInGmsServiceKt.ACTION_GCM_REGISTERED;
import static org.microg.gms.gcm.McsConstants.*;
@ForegroundServiceInfo(value = "Cloud messaging", resName = "service_name_mcs", resPackage = "com.google.android.gms")
@@ -367,9 +369,7 @@ private void handleSendMessage(Intent intent) {
ttl = maxTtl;
}
} catch (NumberFormatException e) {
- // TODO: error TtlUnsupported
- Log.w(TAG, e);
- return;
+ ttl = maxTtl;
}
String to = intent.getStringExtra(EXTRA_SEND_TO);
@@ -422,9 +422,13 @@ private void handleSendMessage(Intent intent) {
.to(to)
.category(packageName)
.raw_data(rawData)
+ .ttl(ttl)
.app_data(appData).build();
send(MCS_DATA_MESSAGE_STANZA_TAG, msg);
+ if (messenger != null) {
+ messenger.send(android.os.Message.obtain());
+ }
database.noteAppMessage(packageName, DataMessageStanza.ADAPTER.encodedSize(msg));
} catch (Exception e) {
Log.w(TAG, e);
@@ -491,12 +495,19 @@ private void handleLoginResponse(LoginResponse loginResponse) {
if (loginResponse.error == null) {
GcmPrefs.clearLastPersistedId(this);
logd(this, "Logged in");
+ notifyGcmRegistered();
wakeLock.release();
} else {
throw new RuntimeException("Could not login: " + loginResponse.error);
}
}
+ private void notifyGcmRegistered() {
+ Intent intent = new Intent(ACTION_GCM_REGISTERED);
+ intent.setPackage(Constants.GMS_PACKAGE_NAME);
+ sendBroadcast(intent);
+ }
+
private void handleCloudMessage(DataMessageStanza message) {
if (message.persistent_id != null) {
GcmPrefs.get(this).extendLastPersistedId(this, message.persistent_id);
diff --git a/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/MainActivity.kt b/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/MainActivity.kt
index a08ac95637..0bd2cce698 100644
--- a/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/MainActivity.kt
+++ b/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/MainActivity.kt
@@ -147,6 +147,7 @@ class MainActivity : AppCompatActivity() {
val screenId = ACTION_TO_SCREEN_ID[intent.action] ?: intent?.getIntExtra(EXTRA_SCREEN_ID, -1)?.takeIf { it > 0 } ?: 1
val product = intent?.getStringExtra(EXTRA_SCREEN_MY_ACTIVITY_PRODUCT)
val kidOnboardingParams = intent?.getStringExtra(EXTRA_SCREEN_KID_ONBOARDING_PARAMS)
+ val screenUrl = intent?.getStringExtra(EXTRA_URL)
val screenOptions = intent.extras?.keySet().orEmpty()
.filter { it.startsWith(EXTRA_SCREEN_OPTIONS_PREFIX) }
@@ -167,7 +168,7 @@ class MainActivity : AppCompatActivity() {
}
if (screenId in SCREEN_ID_TO_URL) {
- val screenUrl = SCREEN_ID_TO_URL[screenId]?.run {
+ val screenUrl = screenUrl ?: SCREEN_ID_TO_URL[screenId]?.run {
if (screenId == 547 && !product.isNullOrEmpty()) {
replace("search", product)
} else if (screenId == 580 && !kidOnboardingParams.isNullOrEmpty()){
diff --git a/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/WebViewHelper.kt b/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/WebViewHelper.kt
index 25ddc5e075..64d2f7d955 100644
--- a/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/WebViewHelper.kt
+++ b/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/WebViewHelper.kt
@@ -24,11 +24,12 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.json.JSONObject
import org.microg.gms.auth.AuthManager
-import org.microg.gms.common.Constants
import org.microg.gms.common.Constants.GMS_PACKAGE_NAME
import org.microg.gms.common.PackageUtils
+import org.microg.gms.gcm.ACTION_GCM_NOTIFY_COMPLETE
+import org.microg.gms.gcm.EXTRA_NOTIFICATION_ACCOUNT
import java.net.URLEncoder
-import java.util.*
+import java.util.Locale
private const val TAG = "AccountSettingsWebView"
@@ -47,6 +48,15 @@ class WebViewHelper(private val activity: AppCompatActivity, private val webView
override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean {
Log.d(TAG, "Navigating to $url")
+ val overrideUri = Uri.parse(url)
+ if (overrideUri.getQueryParameter(QUERY_GNOTS_ACTION) == ACTION_CLOSE || overrideUri.getQueryParameter(QUERY_WC_ACTION) == ACTION_CLOSE) {
+ Intent(ACTION_GCM_NOTIFY_COMPLETE).apply {
+ setPackage(GMS_PACKAGE_NAME)
+ putExtra(EXTRA_NOTIFICATION_ACCOUNT, accountName)
+ }.let { activity.sendBroadcast(it) }
+ activity.finish()
+ return true
+ }
if (url.startsWith("intent:")) {
try {
val intent = Intent.parseUri(url, URI_INTENT_SCHEME)
diff --git a/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/extensions.kt b/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/extensions.kt
index 7947944786..7f89e8f107 100644
--- a/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/extensions.kt
+++ b/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/extensions.kt
@@ -22,6 +22,11 @@ const val EXTRA_FALLBACK_AUTH = "extra.fallbackAuth"
const val EXTRA_THEME_CHOICE = "extra.themeChoice"
const val EXTRA_SCREEN_MY_ACTIVITY_PRODUCT = "extra.screen.myactivityProduct"
const val EXTRA_SCREEN_KID_ONBOARDING_PARAMS = "extra.screen.kidOnboardingParams"
+const val EXTRA_URL = "extra.url"
+
+const val QUERY_WC_ACTION = "wv_action"
+const val QUERY_GNOTS_ACTION = "gnotswvaction"
+const val ACTION_CLOSE = "close"
const val KEY_UPDATED_PHOTO_URL = "updatedPhotoUrl"
diff --git a/play-services-core/src/main/kotlin/org/microg/gms/gcm/GcmInGmsService.kt b/play-services-core/src/main/kotlin/org/microg/gms/gcm/GcmInGmsService.kt
new file mode 100644
index 0000000000..9da9ebb7c0
--- /dev/null
+++ b/play-services-core/src/main/kotlin/org/microg/gms/gcm/GcmInGmsService.kt
@@ -0,0 +1,428 @@
+/**
+ * SPDX-FileCopyrightText: 2025 microG Project Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package org.microg.gms.gcm
+
+import android.Manifest
+import android.accounts.Account
+import android.accounts.AccountManager
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import android.content.SharedPreferences
+import android.content.pm.PackageManager
+import android.os.Binder
+import android.os.Bundle
+import android.os.Handler
+import android.os.Looper
+import android.os.Message
+import android.os.Messenger
+import android.os.Process
+import android.util.Base64
+import android.util.Log
+import androidx.core.app.ActivityCompat
+import androidx.core.app.NotificationCompat
+import androidx.core.app.NotificationManagerCompat
+import androidx.legacy.content.WakefulBroadcastReceiver
+import androidx.lifecycle.LifecycleService
+import androidx.lifecycle.lifecycleScope
+import com.google.android.gms.BuildConfig
+import com.google.android.gms.R
+import com.squareup.wire.GrpcClient
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import okhttp3.Interceptor
+import okhttp3.OkHttpClient
+import okio.ByteString
+import org.microg.gms.accountsettings.ui.MainActivity
+import org.microg.gms.auth.AuthConstants
+import org.microg.gms.auth.AuthManager
+import org.microg.gms.auth.AuthPrefs
+import org.microg.gms.auth.AuthResponse
+import org.microg.gms.auth.ItAuthData
+import org.microg.gms.auth.ItMetadataData
+import org.microg.gms.auth.OAuthAuthorization
+import org.microg.gms.auth.OAuthTokenData
+import org.microg.gms.auth.TokenField
+import org.microg.gms.checkin.LastCheckinInfo
+import org.microg.gms.common.Constants
+import java.util.Locale
+import java.util.TimeZone
+import java.util.concurrent.atomic.AtomicInteger
+import javax.crypto.Mac
+import javax.crypto.spec.SecretKeySpec
+import kotlin.math.min
+
+const val ACTION_GCM_RECONNECT = "org.microg.gms.gcm.RECONNECT"
+const val ACTION_GCM_REGISTERED = "org.microg.gms.gcm.REGISTERED"
+const val ACTION_GCM_REGISTER_ACCOUNT = "org.microg.gms.gcm.REGISTER_ACCOUNT"
+const val ACTION_GCM_NOTIFY_COMPLETE = "org.microg.gms.gcm.NOTIFY_COMPLETE"
+const val KEY_GCM_REGISTER_ACCOUNT_NAME = "register_account_name"
+const val EXTRA_NOTIFICATION_ACCOUNT = "notification_account"
+
+private const val TAG = "GcmInGmsService"
+
+private const val KEY_GCM_REG_ID = "regId"
+private const val KEY_GCM_REG_SENDER = "sender"
+private const val KEY_GCM_REG_TIME = "reg_time"
+private const val KEY_GCM_REG_ACCOUNT_LIST = "accountList"
+
+private const val GMS_GCM_REGISTER_SCOPE = "GCM"
+private const val GMS_GCM_REGISTER_SENDER = "745476177629"
+private const val GMS_GCM_REGISTER_SUBTYPE = "745476177629"
+private const val GMS_GCM_REGISTER_SUBSCRIPTION = "745476177629"
+private const val GCM_GROUP_SENDER = "google.com"
+private const val GCM_GMS_REG_REFRESH_S = 604800L
+
+private const val DEFAULT_FLAGS = Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING
+private const val AUTHS_TOKEN_PREFIX = "ya29.m."
+private const val GMS_GCM_OAUTH_SERVICE = "oauth2:https://www.googleapis.com/auth/gcm"
+
+private const val CHANNEL_ID = "gcm_notification"
+private const val CHANNEL_NAME = "gnots"
+private const val GMS_GCM_NOTIFICATIONS = "notifications"
+private const val GMS_NOTS_OAUTH_SERVICE = "oauth2:https://www.googleapis.com/auth/notifications"
+private const val NOTIFICATION_STATUS_READY = 2
+private const val NOTIFICATION_STATUS_COMPLETE = 5
+
+class GcmInGmsService : LifecycleService() {
+ private val notificationIdGenerator = AtomicInteger(0)
+ private var sp: SharedPreferences? = null
+ private var accountManager: AccountManager? = null
+ private val activeNotifications = HashMap()
+
+ override fun onCreate() {
+ super.onCreate()
+ sp = getSharedPreferences("com.google.android.gcm", MODE_PRIVATE) ?: throw RuntimeException("sp get error")
+ accountManager = getSystemService(ACCOUNT_SERVICE) as AccountManager? ?: throw RuntimeException("accountManager is null")
+ if (android.os.Build.VERSION.SDK_INT >= 26) {
+ val channel = NotificationChannel(CHANNEL_ID, CHANNEL_NAME, NotificationManager.IMPORTANCE_HIGH)
+ val notificationManager: NotificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
+ notificationManager.createNotificationChannel(channel)
+ }
+ }
+
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ if (intent != null) {
+ WakefulBroadcastReceiver.completeWakefulIntent(intent)
+ Log.d(TAG, "onStartCommand: $intent")
+ lifecycleScope.launchWhenStarted {
+ if (checkGcmStatus()) {
+ handleIntent(intent)
+ } else {
+ val intent = Intent(ACTION_GCM_RECONNECT).apply {
+ setPackage(Constants.GMS_PACKAGE_NAME)
+ }
+ sendBroadcast(intent)
+ }
+ }
+ }
+ return super.onStartCommand(intent, flags, startId)
+ }
+
+ private fun checkGcmStatus(): Boolean {
+ if (McsService.isConnected(this)) {
+ Log.d(TAG, "checkGcmStatus: gcm isConnected")
+ return true
+ }
+ Log.d(TAG, "checkGcmStatus: gcm need reconnect")
+ return false
+ }
+
+ private suspend fun handleIntent(intent: Intent) {
+ val action = intent.action
+ if (checkGmsGcmStatus()) {
+ Log.d(TAG, "handleIntent: checkGmsGcmStatus -> reset")
+ registerGcmInGms(this, intent)
+ return
+ }
+ Log.d(TAG, "handleIntent: action: $action")
+ if (action == ACTION_GCM_REGISTERED) {
+ updateLocalAccountGroups()
+ } else if (action == ACTION_GCM_REGISTER_ACCOUNT) {
+ val accountName = intent.getStringExtra(KEY_GCM_REGISTER_ACCOUNT_NAME) ?: return
+ Log.d(TAG, "GCM groups update account name: $accountName")
+ val account = accountManager?.getAccountsByType(AuthConstants.DEFAULT_ACCOUNT_TYPE)?.find { it.name == accountName } ?: return
+ updateGroupsWithAccount(account)
+ } else if (action == GcmConstants.ACTION_C2DM_RECEIVE) {
+ Log.d(TAG, "start handle gcm message")
+ intent.extras?.let { notifyVerificationInfo(it) }
+ } else if (action == ACTION_GCM_NOTIFY_COMPLETE) {
+ val accountName = intent.getStringExtra(EXTRA_NOTIFICATION_ACCOUNT)
+ val notificationData = activeNotifications[accountName]
+ if (notificationData != null) {
+ Log.d(TAG, "Notification with $accountName updateNotificationReadState to Completed.")
+ updateNotificationReadState(accountName!!, notificationData, NOTIFICATION_STATUS_COMPLETE)
+ activeNotifications.remove(accountName)
+ NotificationManagerCompat.from(this).cancel(accountName.hashCode())
+ }
+ }
+ }
+
+ private suspend fun notifyVerificationInfo(data: Bundle) {
+ Log.d(TAG, "notifyVerificationInfo: from: ${data.getString(GcmConstants.EXTRA_FROM)} data: $data")
+ val gcmBodyType = data.getString(GcmConstants.EXTRA_GCM_BODY) ?: return
+ if (GMS_GCM_NOTIFICATIONS != gcmBodyType) return
+ val payloadData = data.getString(GcmConstants.EXTRA_GMS_GNOTS_PAYLOAD) ?: return
+ val notificationData = NotificationData.ADAPTER.decode(Base64.decode(payloadData, DEFAULT_FLAGS))
+ if (notificationData.isActive == true) return
+ val account = notificationData.userInfo?.userId?.let { id ->
+ accountManager?.getAccountsByType(AuthConstants.DEFAULT_ACCOUNT_TYPE)?.find {
+ accountManager?.getUserData(it, AuthConstants.GOOGLE_USER_ID) == id
+ }
+ } ?: return
+ Log.d(TAG, "notifyVerificationInfo: account: ${account.name}")
+ val identifierResponse = withContext(Dispatchers.IO) {
+ getGunsApiServiceClient(account, accountManager!!).GmsGnotsFetchByIdentifier().executeBlocking(FetchByIdentifierRequest.Builder().apply {
+ config(GmsConfig.Builder().apply {
+ versionInfo(GmsConfig.GmsVersionInfo(Constants.GMS_VERSION_CODE))
+ }.build())
+ identifiers(NotificationIdentifierList.Builder().apply {
+ deviceInfo(DeviceInfo.Builder().apply {
+ localeTag(Locale.getDefault().language)
+ sdkVersion(android.os.Build.VERSION.SDK_INT)
+ density(resources.displayMetrics.density)
+ timeZoneId(TimeZone.getDefault().id)
+ }.build())
+ notifications(notificationData.identifier?.let { listOf(it) } ?: emptyList())
+ }.build())
+ }.build())
+ }
+ Log.d(TAG, "notifyVerificationInfo: identifierResponse: $identifierResponse")
+ val notifications = identifierResponse.notifications?.notifications ?: return
+ notifications.forEachIndexed { index, it ->
+ Log.d(TAG, "notifyVerificationInfo: notifications: index:$index it: $it")
+ sendNotification(account.name.hashCode(), it)
+ updateNotificationReadState(account.name, it, NOTIFICATION_STATUS_READY)
+ activeNotifications.put(account.name, it)
+ }
+ }
+
+ private fun sendNotification(notificationId: Int, notificationData: NotificationData) {
+ if (notificationData.isActive == true) return
+ val content = notificationData.content ?: return
+ val intentExtras = notificationData.intentActions?.primaryPayload?.extras ?: return
+ val intent = Intent(this, MainActivity::class.java).apply {
+ `package` = Constants.GMS_PACKAGE_NAME
+ flags = Intent.FLAG_ACTIVITY_NEW_TASK
+ intentExtras.forEach { putExtra(it.key, it.value_) }
+ }
+ val requestCode = notificationIdGenerator.incrementAndGet()
+ val pendingIntent = PendingIntent.getActivity(this, requestCode, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
+ val builder = NotificationCompat.Builder(this, CHANNEL_ID)
+ .setContentTitle(content.accountName)
+ .setContentText(content.description)
+ .setStyle(NotificationCompat.BigTextStyle().bigText(content.description))
+ .setPriority(NotificationCompat.PRIORITY_DEFAULT)
+ .setContentIntent(pendingIntent)
+ .setAutoCancel(true)
+ .setSmallIcon(R.drawable.ic_google_logo)
+ if (ActivityCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) ==
+ PackageManager.PERMISSION_GRANTED
+ ) {
+ NotificationManagerCompat.from(this).notify(notificationId, builder.build())
+ }
+ startActivity(intent)
+ }
+
+ private suspend fun updateGroupsWithAccount(account: Account) {
+ Log.d(TAG, "updateGroupsWithAccount: account: ${account.name}")
+ var regId = sp?.getString(KEY_GCM_REG_ID, null) ?: return
+ var authManager = AuthManager(this, account.name, Constants.GMS_PACKAGE_NAME, GMS_GCM_OAUTH_SERVICE).apply {
+ setItCaveatTypes("2")
+ }
+ val authsToken = runCatching { withContext(Dispatchers.IO) { authManager.requestAuth(true) }.parseAuthsToken() }.getOrNull() ?: return
+ val extras = Bundle().apply {
+ putString(GcmConstants.EXTRA_ACCOUNT_NAME, account.name)
+ putString(GcmConstants.EXTRA_REG_ID, regId)
+ putString(GcmConstants.EXTRA_AUTHS_TOKEN, authsToken)
+ putString(GcmConstants.EXTRA_SEND_TO, GCM_GROUP_SENDER)
+ putString(GcmConstants.EXTRA_SEND_FROM, GCM_GROUP_SENDER)
+ putString(GcmConstants.EXTRA_MESSAGE_ID, "${System.currentTimeMillis() / 1000}-0")
+ }
+ Log.d(TAG, "updateGroupsWithAccount extras: $extras")
+ val intent = Intent(GcmConstants.ACTION_GCM_SEND).apply {
+ setPackage(Constants.GMS_PACKAGE_NAME)
+ putExtras(extras)
+ putExtra(GcmConstants.EXTRA_APP, Intent().apply { setPackage(Constants.GMS_PACKAGE_NAME) }.let { PendingIntent.getBroadcast(this@GcmInGmsService, 0, it, 0) })
+ }.also {
+ it.putExtra(GcmConstants.EXTRA_MESSENGER, Messenger(object : Handler(Looper.getMainLooper()) {
+ override fun handleMessage(msg: Message) {
+ if (Binder.getCallingUid() == Process.myUid()) {
+ Log.d(TAG, "updateGroupsWithAccount handleMessage save: ${account.name}")
+ val history = sp?.getString(KEY_GCM_REG_ACCOUNT_LIST, null)
+ sp?.edit()?.putString(KEY_GCM_REG_ACCOUNT_LIST, if (history != null) "${account.name}/$history" else account.name)?.apply()
+ }
+ }
+ }))
+ }
+ sendOrderedBroadcast(intent, null)
+ }
+
+ private suspend fun updateLocalAccountGroups() {
+ Log.d(TAG, "GMS $GMS_GCM_REGISTER_SENDER already registered, start updateLocalAccount")
+ val localGoogleAccounts = accountManager?.getAccountsByType(AuthConstants.DEFAULT_ACCOUNT_TYPE) ?: return
+ val accountList = sp?.getString(KEY_GCM_REG_ACCOUNT_LIST, null)
+ Log.d(TAG, "updateLocalAccountGroups: accountList: $accountList")
+ val needRegisterAccounts = if (accountList == null) localGoogleAccounts.toList() else localGoogleAccounts.filter { !accountList.contains(it.name) }
+ Log.d(TAG, "updateLocalAccountGroups: needRegisterAccounts: ${needRegisterAccounts.map { it.name }.joinToString(" ")}")
+ if (needRegisterAccounts.isEmpty()) return
+ for (account in needRegisterAccounts) {
+ updateGroupsWithAccount(account)
+ }
+ }
+
+ private suspend fun registerGcmInGms(context: Context, intent: Intent) {
+ Log.i(TAG, "Registering GMS $GMS_GCM_REGISTER_SENDER")
+ val regId = withContext(Dispatchers.IO) {
+ completeRegisterRequest(
+ context, GcmDatabase(context), RegisterRequest().build(context)
+ .checkin(LastCheckinInfo.read(context))
+ .app(Constants.GMS_PACKAGE_NAME, Constants.GMS_PACKAGE_SIGNATURE_SHA1, BuildConfig.VERSION_CODE)
+ .sender(GMS_GCM_REGISTER_SENDER)
+ .extraParam("subscription", GMS_GCM_REGISTER_SUBSCRIPTION)
+ .extraParam("X-subscription", GMS_GCM_REGISTER_SUBSCRIPTION)
+ .extraParam("subtype", GMS_GCM_REGISTER_SUBTYPE)
+ .extraParam("X-subtype", GMS_GCM_REGISTER_SUBTYPE)
+ .extraParam("scope", GMS_GCM_REGISTER_SCOPE)
+ )
+ .getString(GcmConstants.EXTRA_REGISTRATION_ID)
+ }
+ Log.d(TAG, "GCM IN GMS regId: $regId")
+ val sharedPreferencesEditor = sp?.edit()
+ sharedPreferencesEditor?.putString(KEY_GCM_REG_ID, regId)
+ sharedPreferencesEditor?.putString(KEY_GCM_REG_SENDER, GMS_GCM_REGISTER_SENDER)
+ sharedPreferencesEditor?.putLong(KEY_GCM_REG_TIME, System.currentTimeMillis())
+ sharedPreferencesEditor?.remove(KEY_GCM_REG_ACCOUNT_LIST)
+ if (sharedPreferencesEditor?.commit() == false) {
+ Log.d(TAG, "Failed to write GMS registration")
+ } else {
+ Log.d(TAG, "registerGcmInGms: sendBroadcast: ${intent.action}")
+ Intent(intent.action).apply {
+ setPackage(Constants.GMS_PACKAGE_NAME)
+ putExtras(intent)
+ }.let { sendBroadcast(it) }
+ }
+ }
+
+ private fun updateNotificationReadState(accountName: String, notificationData: NotificationData, readState: Int) {
+ if (accountName.isEmpty() || notificationData.identifier?.uniqueId?.isEmpty() == true) {
+ return
+ }
+ try {
+ val identifier = notificationData.identifier
+ val actionButtons = notificationData.content?.actionButtons
+ if (actionButtons.isNullOrEmpty()) {
+ return
+ }
+ val readStateList = actionButtons.map { actionButton ->
+ ReadStateItem.Builder().apply {
+ notification = identifier
+ state = actionButton.icon
+ status = readState
+ }.build()
+ }
+ sendNotificationReadState(accountName, ReadStateList.Builder().apply { items = readStateList }.build())
+ Log.i(TAG, "Notification read state updated successfully for account: $accountName")
+ } catch (e: Exception) {
+ Log.w(TAG, "Failed to update the notification(s) read state.", e)
+ }
+ }
+
+ private fun sendNotificationReadState(accountName: String, readStateList: ReadStateList) {
+ val account = accountManager?.getAccountsByType(AuthConstants.DEFAULT_ACCOUNT_TYPE)?.find { it.name == accountName } ?: return
+ getGunsApiServiceClient(account, accountManager!!).GmsGnotsSetReadStates().executeBlocking(
+ GmsGnotsSetReadStatesRequest.Builder().apply {
+ config = GmsConfig.Builder().apply {
+ versionInfo(GmsConfig.GmsVersionInfo(Constants.GMS_VERSION_CODE))
+ }.build()
+ readStates = readStateList
+ }.build()
+ )
+ }
+
+ private fun checkGmsGcmStatus(): Boolean {
+ val regSender = sp?.getString(KEY_GCM_REG_SENDER, null)
+ val regId = sp?.getString(KEY_GCM_REG_ID, null)
+ val regTime = sp?.getLong(KEY_GCM_REG_TIME, 0) ?: 0L
+ return regSender == null || regId == null || regTime + GCM_GMS_REG_REFRESH_S * 1000 < System.currentTimeMillis()
+ }
+
+ private fun AuthResponse.parseAuthsToken(): String? {
+ Log.d(TAG, "parseAuthsToken start: auths: $auths itMetadata: $itMetadata")
+ if (auths.isNullOrEmpty() || itMetadata.isNullOrEmpty()) return null
+ if (!auths.startsWith(AUTHS_TOKEN_PREFIX)) return null
+ try {
+ val tokenBase64 = auths.substring(AUTHS_TOKEN_PREFIX.length)
+ val authData = ItAuthData.ADAPTER.decode(Base64.decode(tokenBase64, DEFAULT_FLAGS))
+ val metadata = ItMetadataData.ADAPTER.decode(Base64.decode(itMetadata, DEFAULT_FLAGS))
+ val authorization = OAuthAuthorization.Builder().apply {
+ effectiveDurationSeconds(min(metadata.liveTime ?: Int.MAX_VALUE, expiresInDurationSec))
+ if (metadata.field_?.types?.contains(TokenField.FieldType.SCOPE) == true) {
+ val scopeIds = metadata.entries.flatMap { entry ->
+ entry.name.map { scope -> entry to scope }
+ }.filter { (_, scope) ->
+ scope in grantedScopes
+ }.mapNotNull { (entry, _) ->
+ entry.id
+ }.toSet()
+ scopeIds(scopeIds.toList())
+ }
+ }.build()
+ val oAuthTokenData = OAuthTokenData.Builder().apply {
+ fieldType(TokenField.FieldType.SCOPE.value)
+ authorization(authorization.encodeByteString())
+ durationMillis(0)
+ }.build()
+ val tokenDataBytes = oAuthTokenData.encode()
+ val secretKey: ByteArray? = authData.signature?.toByteArray()
+ val mac = Mac.getInstance("HmacSHA256").apply { init(SecretKeySpec(secretKey, "HmacSHA256")) }
+ val bytes: ByteArray = mac.doFinal(tokenDataBytes)
+ val newAuthData = authData.newBuilder().apply {
+ tokens(arrayListOf(oAuthTokenData.encodeByteString()))
+ signature(ByteString.of(*bytes))
+ }.build()
+ return AUTHS_TOKEN_PREFIX + Base64.encodeToString(newAuthData.encode(), DEFAULT_FLAGS)
+ } catch (e: Exception) {
+ Log.w(TAG, "parseAuthsToken: ", e);
+ return null;
+ }
+ }
+
+ private fun getGunsApiServiceClient(account: Account, accountManager: AccountManager): GunsGmscoreApiServiceClient {
+ val token = accountManager.blockingGetAuthToken(account, GMS_NOTS_OAUTH_SERVICE, true)
+ val client = OkHttpClient().newBuilder().addInterceptor(HeaderInterceptor(token)).build()
+ val grpcClient = GrpcClient.Builder().client(client).baseUrl("https://notifications-pa.googleapis.com").minMessageToCompress(Long.MAX_VALUE).build()
+ return grpcClient.create(GunsGmscoreApiServiceClient::class)
+ }
+
+ private class HeaderInterceptor(
+ private val oauthToken: String,
+ ) : Interceptor {
+ override fun intercept(chain: Interceptor.Chain): okhttp3.Response {
+ val original = chain.request().newBuilder().header("Authorization", "Bearer $oauthToken")
+ return chain.proceed(original.build())
+ }
+ }
+}
+
+class GcmRegistrationReceiver : WakefulBroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ val shouldReceiveTwoStepVerification = AuthPrefs.shouldReceiveTwoStepVerification(context)
+ if (!shouldReceiveTwoStepVerification) {
+ Log.d(TAG, "GcmRegistrationReceiver onReceive: Switch not allowed ")
+ return
+ }
+ Log.d(TAG, "GcmRegistrationReceiver onReceive: action: ${intent.action}")
+ val callIntent = Intent(context, GcmInGmsService::class.java)
+ callIntent.action = intent.action
+ if (ACTION_GCM_REGISTER_ACCOUNT == intent.action || ACTION_GCM_NOTIFY_COMPLETE == intent.action || GcmConstants.ACTION_C2DM_RECEIVE == intent.action) {
+ callIntent.putExtras(intent.extras!!)
+ }
+ startWakefulService(context, callIntent)
+ }
+}
\ No newline at end of file
diff --git a/play-services-core/src/main/kotlin/org/microg/gms/ui/AccountsFragment.kt b/play-services-core/src/main/kotlin/org/microg/gms/ui/AccountsFragment.kt
index a5dc94efd5..3ef55fb54e 100644
--- a/play-services-core/src/main/kotlin/org/microg/gms/ui/AccountsFragment.kt
+++ b/play-services-core/src/main/kotlin/org/microg/gms/ui/AccountsFragment.kt
@@ -21,6 +21,8 @@ import com.google.android.gms.R
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.microg.gms.auth.AuthConstants
+import org.microg.gms.common.Constants
+import org.microg.gms.gcm.ACTION_GCM_REGISTERED
import org.microg.gms.people.DatabaseHelper
import org.microg.gms.people.PeopleManager
import org.microg.gms.settings.SettingsContract
@@ -34,6 +36,7 @@ val TWO_STATE_SETTINGS = listOf(
Auth.VISIBLE,
Auth.INCLUDE_ANDROID_ID,
Auth.STRIP_DEVICE_NAME,
+ Auth.TWO_STEP_VERIFICATION,
)
class AccountsFragment : PreferenceFragmentCompat() {
@@ -57,6 +60,12 @@ class AccountsFragment : PreferenceFragmentCompat() {
Bitmap.createScaledBitmap(bitmap, 100, 100, true)
}).also { it.isCircular = true } else null
+ private fun registerGcmInGms() {
+ Intent(ACTION_GCM_REGISTERED).apply {
+ `package` = Constants.GMS_PACKAGE_NAME
+ }.let { requireContext().sendBroadcast(it) }
+ }
+
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.preferences_accounts)
updateSettings()
@@ -65,6 +74,7 @@ class AccountsFragment : PreferenceFragmentCompat() {
if (newValue is Boolean && preference.key in TWO_STATE_SETTINGS) {
SettingsContract.setSettings(requireContext(), Auth.getContentUri(requireContext())) { put(preference.key, newValue) }
updateSettings()
+ if (preference.key == Auth.TWO_STEP_VERIFICATION && newValue) registerGcmInGms()
true
} else false
}
diff --git a/play-services-core/src/main/res/values-zh-rCN/strings.xml b/play-services-core/src/main/res/values-zh-rCN/strings.xml
index 6a2128e0a8..3f467133bd 100644
--- a/play-services-core/src/main/res/values-zh-rCN/strings.xml
+++ b/play-services-core/src/main/res/values-zh-rCN/strings.xml
@@ -82,6 +82,8 @@
已安装的 %1$s 不兼容,或者您未对其启用签名伪装。请查阅文档以了解兼容的应用或 ROM。
允许应用寻找 Google 账号
启用后,您设备上的所有应用将可以看到与您 Google 账号关联的电子邮件地址,而无需您的许可。
+ 允许接收 Google 二次验证信息
+ 启用后,设备可以接收来自 Google 的两步验证通知(需启用云端消息推送并保持连接)。
将您的设备注册到 Google 服务,并创建唯一的设备识别码。microG 将去除注册信息中您 Google 账户名以外的用于识别的信息。
注册设备
状态
diff --git a/play-services-core/src/main/res/values/strings.xml b/play-services-core/src/main/res/values/strings.xml
index c31d814ea5..07a52f4539 100644
--- a/play-services-core/src/main/res/values/strings.xml
+++ b/play-services-core/src/main/res/values/strings.xml
@@ -166,6 +166,8 @@ Please set up a password, PIN, or pattern lock screen."
When disabled, authentication requests won\'t be linked to the device registration, which may allow unauthorized devices to sign in, but may have unforeseen consequences.
Strip device name for authentication
When enabled, authentication requests won\'t include the device\'s name, which may allow unauthorized devices to sign in, but may have unforeseen consequences.
+ Receive two-step verification prompts
+ When enabled, the device can receive two-step verification prompts from Google (Cloud Messaging is required).
Registers your device to Google services and creates a unique device identifier. microG strips identifying bits other than your Google account name from registration data.
Android ID
diff --git a/play-services-core/src/main/res/xml/preferences_accounts.xml b/play-services-core/src/main/res/xml/preferences_accounts.xml
index 1ea220169f..9a89fe2157 100644
--- a/play-services-core/src/main/res/xml/preferences_accounts.xml
+++ b/play-services-core/src/main/res/xml/preferences_accounts.xml
@@ -48,5 +48,11 @@
android:summary="@string/pref_auth_strip_device_name_summary"
android:title="@string/pref_auth_strip_device_name_title"
app:iconSpaceReserved="false" />
+