diff --git a/build.gradle b/build.gradle index ae1cd09be2..c05a2b956f 100644 --- a/build.gradle +++ b/build.gradle @@ -24,6 +24,7 @@ buildscript { ext.preferenceVersion = '1.2.0' ext.recyclerviewVersion = '1.3.2' ext.webkitVersion = '1.10.0' + ext.workVersion = '2.7.0' ext.slf4jVersion = '1.7.36' ext.volleyVersion = '1.2.1' diff --git a/play-services-core/build.gradle b/play-services-core/build.gradle index 5648333872..8fc2896bbc 100644 --- a/play-services-core/build.gradle +++ b/play-services-core/build.gradle @@ -102,6 +102,8 @@ dependencies { implementation "androidx.lifecycle:lifecycle-service:$lifecycleVersion" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion" + + implementation "androidx.work:work-runtime-ktx:$workVersion" } android { diff --git a/play-services-core/src/main/AndroidManifest.xml b/play-services-core/src/main/AndroidManifest.xml index 6f593efdf8..3ad889f6c8 100644 --- a/play-services-core/src/main/AndroidManifest.xml +++ b/play-services-core/src/main/AndroidManifest.xml @@ -412,8 +412,7 @@ + android:name="org.microg.gms.common.PersistentTrustedReceiver"> diff --git a/play-services-core/src/main/java/org/microg/gms/gcm/UnregisterReceiver.java b/play-services-core/src/main/java/org/microg/gms/gcm/UnregisterReceiver.java deleted file mode 100644 index bba2e414c4..0000000000 --- a/play-services-core/src/main/java/org/microg/gms/gcm/UnregisterReceiver.java +++ /dev/null @@ -1,50 +0,0 @@ -package org.microg.gms.gcm; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.util.Log; - -import java.util.List; - -import static android.content.Intent.ACTION_PACKAGE_REMOVED; -import static android.content.Intent.ACTION_PACKAGE_DATA_CLEARED; -import static android.content.Intent.ACTION_PACKAGE_FULLY_REMOVED; -import static android.content.Intent.EXTRA_DATA_REMOVED; -import static android.content.Intent.EXTRA_REPLACING; - -public class UnregisterReceiver extends BroadcastReceiver { - private static final String TAG = "GmsGcmUnregisterRcvr"; - - @Override - public void onReceive(final Context context, Intent intent) { - Log.d(TAG, "Package changed: " + intent); - if ((ACTION_PACKAGE_REMOVED.contains(intent.getAction()) && intent.getBooleanExtra(EXTRA_DATA_REMOVED, false) && - !intent.getBooleanExtra(EXTRA_REPLACING, false)) || - ACTION_PACKAGE_FULLY_REMOVED.contains(intent.getAction()) || - ACTION_PACKAGE_DATA_CLEARED.contains(intent.getAction())) { - final GcmDatabase database = new GcmDatabase(context); - final String packageName = intent.getData().getSchemeSpecificPart(); - Log.d(TAG, "Package removed or data cleared: " + packageName); - final GcmDatabase.App app = database.getApp(packageName); - if (app != null) { - new Thread(new Runnable() { - @Override - public void run() { - List registrations = database.getRegistrationsByApp(packageName); - boolean deletedAll = true; - for (GcmDatabase.Registration registration : registrations) { - deletedAll &= PushRegisterManager.unregister(context, registration.packageName, registration.signature, null, null).deleted != null; - } - if (deletedAll) { - database.removeApp(packageName); - } - database.close(); - } - }).start(); - } else { - database.close(); - } - } - } -} diff --git a/play-services-core/src/main/kotlin/org/microg/gms/auth/credentials/identity/AuthorizationService.kt b/play-services-core/src/main/kotlin/org/microg/gms/auth/credentials/identity/AuthorizationService.kt index 61e6618b8a..39121d7018 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/auth/credentials/identity/AuthorizationService.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/auth/credentials/identity/AuthorizationService.kt @@ -41,6 +41,7 @@ import org.microg.gms.auth.signin.scopeUris import org.microg.gms.common.Constants import org.microg.gms.common.GmsService import org.microg.gms.common.PackageUtils +import java.util.concurrent.atomic.AtomicInteger private const val TAG = "AuthorizationService" @@ -60,17 +61,16 @@ class AuthorizationService : BaseService(TAG, GmsService.AUTHORIZATION) { class AuthorizationServiceImpl(val context: Context, val packageName: String, override val lifecycle: Lifecycle) : IAuthorizationService.Stub(), LifecycleOwner { + companion object{ + private val nextRequestCode = AtomicInteger(0) + } + override fun authorize(callback: IAuthorizationCallback?, request: AuthorizationRequest?) { Log.d(TAG, "Method: authorize called, request:$request") lifecycleScope.launchWhenStarted { val account = request?.account ?: SignInConfigurationService.getDefaultAccount(context, packageName) - if (account == null) { - Log.d(TAG, "Method: authorize called, but account is null") - callback?.onAuthorized(Status.CANCELED, null) - return@launchWhenStarted - } val googleSignInOptions = GoogleSignInOptions.Builder().apply { - setAccountName(account.name) + account?.name?.let { setAccountName(it) } request?.requestedScopes?.forEach { requestScopes(it) } if (request?.idTokenRequested == true && request.serverClientId != null) requestIdToken(request.serverClientId) if (request?.serverAuthCodeRequested == true && request.serverClientId != null) requestServerAuthCode(request.serverClientId, request.forceCodeForRefreshToken) @@ -79,7 +79,7 @@ class AuthorizationServiceImpl(val context: Context, val packageName: String, ov `package` = Constants.GMS_PACKAGE_NAME putExtra("config", SignInConfiguration(packageName, googleSignInOptions)) } - val signInAccount = performSignIn(context, packageName, googleSignInOptions, account, false) + val signInAccount = account?.let { performSignIn(context, packageName, googleSignInOptions, account, false) } callback?.onAuthorized(Status.SUCCESS, AuthorizationResult( signInAccount?.serverAuthCode, @@ -87,7 +87,7 @@ class AuthorizationServiceImpl(val context: Context, val packageName: String, ov signInAccount?.idToken, signInAccount?.grantedScopes?.toList().orEmpty().map { it.scopeUri }, signInAccount, - PendingIntent.getActivity(context, account.hashCode(), intent, FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE) + PendingIntent.getActivity(context, nextRequestCode.incrementAndGet(), intent, FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE) ).also { Log.d(TAG, "authorize: result:$it") }) } } diff --git a/play-services-core/src/main/kotlin/org/microg/gms/auth/credentials/identity/IdentitySignInService.kt b/play-services-core/src/main/kotlin/org/microg/gms/auth/credentials/identity/IdentitySignInService.kt index 41d46066d7..e112eb7de3 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/auth/credentials/identity/IdentitySignInService.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/auth/credentials/identity/IdentitySignInService.kt @@ -5,7 +5,6 @@ package org.microg.gms.auth.credentials.identity -import android.accounts.AccountManager import android.app.PendingIntent import android.content.Context import android.content.Intent @@ -14,6 +13,9 @@ import android.util.Base64 import android.util.Log import androidx.core.app.PendingIntentCompat import androidx.core.os.bundleOf +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.lifecycleScope import com.google.android.gms.auth.api.identity.BeginSignInRequest import com.google.android.gms.auth.api.identity.BeginSignInResult import com.google.android.gms.auth.api.identity.GetPhoneNumberHintIntentRequest @@ -34,16 +36,17 @@ import com.google.android.gms.fido.common.Transport import com.google.android.gms.fido.fido2.api.common.PublicKeyCredentialDescriptor import com.google.android.gms.fido.fido2.api.common.PublicKeyCredentialRequestOptions import com.google.android.gms.fido.fido2.api.common.UserVerificationRequirement +import kotlinx.coroutines.launch import org.json.JSONArray import org.json.JSONObject import org.microg.gms.BaseService -import org.microg.gms.auth.AuthConstants import org.microg.gms.auth.signin.ACTION_ASSISTED_SIGN_IN import org.microg.gms.auth.signin.BEGIN_SIGN_IN_REQUEST import org.microg.gms.auth.signin.GET_SIGN_IN_INTENT_REQUEST import org.microg.gms.auth.credentials.FEATURES import org.microg.gms.auth.signin.CLIENT_PACKAGE_NAME import org.microg.gms.auth.signin.GOOGLE_SIGN_IN_OPTIONS +import org.microg.gms.auth.signin.SignInConfigurationService import org.microg.gms.auth.signin.performSignOut import org.microg.gms.common.GmsService import org.microg.gms.fido.core.Database @@ -61,13 +64,13 @@ class IdentitySignInService : BaseService(TAG, GmsService.IDENTITY_SIGN_IN) { val connectionInfo = ConnectionInfo() connectionInfo.features = FEATURES callback.onPostInitCompleteWithConnectionInfo( - ConnectionResult.SUCCESS, IdentitySignInServiceImpl(this, request.packageName).asBinder(), connectionInfo + ConnectionResult.SUCCESS, IdentitySignInServiceImpl(this, request.packageName, lifecycle).asBinder(), connectionInfo ) } } -class IdentitySignInServiceImpl(private val context: Context, private val clientPackageName: String) : - ISignInService.Stub() { +class IdentitySignInServiceImpl(private val context: Context, private val clientPackageName: String, override val lifecycle: Lifecycle) : + ISignInService.Stub(), LifecycleOwner { private val requestMap = mutableMapOf() @@ -130,10 +133,13 @@ class IdentitySignInServiceImpl(private val context: Context, private val client override fun signOut(callback: IStatusCallback, requestTag: String) { Log.d(TAG, "method signOut called, requestTag=$requestTag") - if (requestMap.containsKey(requestTag)) { - val accounts = AccountManager.get(context).getAccountsByType(AuthConstants.DEFAULT_ACCOUNT_TYPE) - if (accounts.isNotEmpty()) { - accounts.forEach { performSignOut(context, clientPackageName, requestMap[requestTag], it) } + lifecycleScope.launch { + val signInAccount = SignInConfigurationService.getDefaultAccount(context, clientPackageName) + val authOptions = SignInConfigurationService.getAuthOptions(context, clientPackageName).plus(requestMap[requestTag]) + if (signInAccount != null && authOptions.isNotEmpty()) { + authOptions.forEach { + performSignOut(context, clientPackageName, it, signInAccount) + } } } callback.onResult(Status.SUCCESS) 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 index c6e624c3b7..9c28971444 100644 --- 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 @@ -16,7 +16,6 @@ package org.microg.gms.auth.signin import android.accounts.Account -import android.accounts.AccountManager import android.content.Context import android.os.Bundle import android.os.Parcel @@ -39,7 +38,6 @@ 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.auth.AuthConstants import org.microg.gms.auth.AuthPrefs import org.microg.gms.common.GmsService import org.microg.gms.common.PackageUtils @@ -122,14 +120,15 @@ class AuthSignInServiceImpl( try { val account = account ?: options?.account ?: SignInConfigurationService.getDefaultAccount(context, packageName) if (account != null) { - val defaultOptions = SignInConfigurationService.getDefaultOptions(context, packageName) - Log.d(TAG, "$packageName:signOut defaultOptions:($defaultOptions)") - performSignOut(context, packageName, defaultOptions ?: options, account) + SignInConfigurationService.getAuthOptions(context, packageName).forEach { + Log.d(TAG, "$packageName:signOut authOption:($it)") + performSignOut(context, packageName, it, account) + } } if (options?.scopes?.any { it.scopeUri.contains(Scopes.GAMES) } == true) { GamesConfigurationService.setDefaultAccount(context, packageName, null) } - SignInConfigurationService.setDefaultSignInInfo(context, packageName, null, null) + SignInConfigurationService.setAuthInfo(context, packageName, null, null) runCatching { callbacks.onSignOut(Status.SUCCESS) } } catch (e: Exception) { Log.w(TAG, e) @@ -162,7 +161,7 @@ class AuthSignInServiceImpl( authManager.invalidateAuthToken(token) authManager.isPermitted = false } - SignInConfigurationService.setDefaultSignInInfo(context, packageName, account, options?.toJson()) + SignInConfigurationService.setAuthInfo(context, packageName, account, options?.toJson()) runCatching { callbacks.onRevokeAccess(Status.SUCCESS) } } catch (e: Exception) { Log.w(TAG, e) diff --git a/play-services-core/src/main/kotlin/org/microg/gms/auth/signin/SignInConfigurationService.kt b/play-services-core/src/main/kotlin/org/microg/gms/auth/signin/SignInConfigurationService.kt index 5cc24f7994..7bafef129a 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/auth/signin/SignInConfigurationService.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/auth/signin/SignInConfigurationService.kt @@ -54,7 +54,7 @@ class SignInConfigurationService : Service() { val packageName = msg.data?.getString(MSG_DATA_PACKAGE_NAME) val account = msg.data?.getParcelable(MSG_DATA_ACCOUNT) val googleSignInOptions = msg.data?.getString(MSG_DATA_SIGN_IN_OPTIONS) - packageName?.let { setDefaultSignInInfo(it, account, googleSignInOptions) } + packageName?.let { setAuthInfo(it, account, googleSignInOptions) } bundleOf( MSG_DATA_PACKAGE_NAME to packageName, MSG_DATA_ACCOUNT to account, @@ -64,10 +64,10 @@ class SignInConfigurationService : Service() { MSG_GET_DEFAULT_OPTIONS -> { val packageName = msg.data?.getString(MSG_DATA_PACKAGE_NAME) - val googleSignInOptions = packageName?.let { getDefaultOptions(it) } + val googleSignInOptions = packageName?.let { getAuthOptions(it) } bundleOf( MSG_DATA_PACKAGE_NAME to packageName, - MSG_DATA_SIGN_IN_OPTIONS to googleSignInOptions + MSG_DATA_SIGN_IN_OPTIONS to googleSignInOptions?.toTypedArray() ) } @@ -95,23 +95,27 @@ class SignInConfigurationService : Service() { return null } - private fun getDefaultOptions(packageName: String): String? { - val data = preferences.getString(DEFAULT_SIGN_IN_OPTIONS_PREFIX + getPackageNameSuffix(packageName), null) - if (data.isNullOrBlank()) return null + private fun getAuthOptions(packageName: String): Set? { + val data = preferences.getStringSet(DEFAULT_SIGN_IN_OPTIONS_PREFIX + getPackageNameSuffix(packageName), null) + if (data.isNullOrEmpty()) return null return data } - private fun setDefaultSignInInfo(packageName: String, account: Account?, optionsJson: String?) { + private fun setAuthInfo(packageName: String, account: Account?, optionsJson: String?) { val editor: SharedPreferences.Editor = preferences.edit() + val accountPrefix = DEFAULT_ACCOUNT_PREFIX + getPackageNameSuffix(packageName) + val optionsPrefix = DEFAULT_SIGN_IN_OPTIONS_PREFIX + getPackageNameSuffix(packageName) if (account == null || account.name == AuthConstants.DEFAULT_ACCOUNT) { - editor.remove(DEFAULT_ACCOUNT_PREFIX + getPackageNameSuffix(packageName)) + editor.remove(accountPrefix) + editor.remove(optionsPrefix) } else { - editor.putString(DEFAULT_ACCOUNT_PREFIX + getPackageNameSuffix(packageName), account.name) + editor.putString(accountPrefix, account.name) } - if (optionsJson == null) { - editor.remove(DEFAULT_SIGN_IN_OPTIONS_PREFIX + getPackageNameSuffix(packageName)) - } else { - editor.putString(DEFAULT_SIGN_IN_OPTIONS_PREFIX + getPackageNameSuffix(packageName), optionsJson) + if (optionsJson != null) { + val savedOptions = preferences.getStringSet(optionsPrefix, emptySet()) ?: emptySet() + val newSet = HashSet(savedOptions) + newSet.add(optionsJson) + editor.putStringSet(optionsPrefix, newSet) } editor.apply() } @@ -156,16 +160,16 @@ class SignInConfigurationService : Service() { }).data?.getParcelable(MSG_DATA_ACCOUNT) } - suspend fun getDefaultOptions(context: Context, packageName: String): GoogleSignInOptions? { + suspend fun getAuthOptions(context: Context, packageName: String): Set { return singleRequest(context, Message.obtain().apply { what = MSG_GET_DEFAULT_OPTIONS data = bundleOf( MSG_DATA_PACKAGE_NAME to packageName ) - }).data?.getString(MSG_DATA_SIGN_IN_OPTIONS)?.let { GoogleSignInOptions.fromJson(it) } + }).data?.getStringArray(MSG_DATA_SIGN_IN_OPTIONS)?.map { GoogleSignInOptions.fromJson(it) }?.toSet() ?: emptySet() } - suspend fun setDefaultSignInInfo(context: Context, packageName: String, account: Account?, optionsJson: String?) { + suspend fun setAuthInfo(context: Context, packageName: String, account: Account?, optionsJson: String?) { singleRequest(context, Message.obtain().apply { what = MSG_SET_DEFAULT_SIGN_IN_INFO data = bundleOf( diff --git a/play-services-core/src/main/kotlin/org/microg/gms/auth/signin/extensions.kt b/play-services-core/src/main/kotlin/org/microg/gms/auth/signin/extensions.kt index a3a2ae23a0..de90a9a230 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/auth/signin/extensions.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/auth/signin/extensions.kt @@ -169,7 +169,7 @@ suspend fun performSignIn(context: Context, packageName: String, options: Google if (options?.includeGame == true) { GamesConfigurationService.setDefaultAccount(context, packageName, account) } - SignInConfigurationService.setDefaultSignInInfo(context, packageName, account, options?.toJson()) + SignInConfigurationService.setAuthInfo(context, packageName, account, options?.toJson()) return GoogleSignInAccount( id, tokenId, diff --git a/play-services-core/src/main/kotlin/org/microg/gms/common/PackageIntentOpWorker.kt b/play-services-core/src/main/kotlin/org/microg/gms/common/PackageIntentOpWorker.kt new file mode 100644 index 0000000000..221acca607 --- /dev/null +++ b/play-services-core/src/main/kotlin/org/microg/gms/common/PackageIntentOpWorker.kt @@ -0,0 +1,68 @@ +/** + * SPDX-FileCopyrightText: 2025 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.common + +import android.content.Context +import android.util.Log +import androidx.work.CoroutineWorker +import androidx.work.WorkerParameters +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.microg.gms.auth.signin.SignInConfigurationService +import org.microg.gms.auth.signin.performSignOut +import org.microg.gms.gcm.GcmDatabase +import org.microg.gms.gcm.PushRegisterManager + +class PackageIntentOpWorker( + val appContext: Context, + params: WorkerParameters +) : CoroutineWorker(appContext, params) { + + companion object { + private const val TAG = "PackageIntentOpWorker" + const val PACKAGE_NAME = "packageName" + } + + override suspend fun doWork(): Result { + val packageName = inputData.getString(PACKAGE_NAME) ?: return Result.failure() + Log.d(TAG, "doWork: $packageName clearing.") + + clearGcmData(packageName) + clearAuthInfo(packageName) + + Log.d(TAG, "doWork: $packageName cleared.") + return Result.success() + } + + private suspend fun clearGcmData(packageName: String) = withContext(Dispatchers.IO) { + val database = GcmDatabase(appContext) + val app = database.getApp(packageName) + if (app != null) { + val registrations = database.getRegistrationsByApp(packageName) + var deletedAll = true + for (registration in registrations) { + deletedAll = deletedAll and (PushRegisterManager.unregister(appContext, registration.packageName, registration.signature, null, null).deleted != null) + } + if (deletedAll) { + database.removeApp(packageName) + } + database.close() + } else { + database.close() + } + } + + private suspend fun clearAuthInfo(packageName: String) = withContext(Dispatchers.IO) { + val authOptions = SignInConfigurationService.getAuthOptions(appContext, packageName) + val authAccount = SignInConfigurationService.getDefaultAccount(appContext, packageName) + if (authOptions.isNotEmpty() && authAccount != null) { + authOptions.forEach { + Log.d(TAG, "$packageName:clear authAccount: ${authAccount.name} authOption:($it)") + performSignOut(appContext, packageName, it, authAccount) + } + } + } +} \ No newline at end of file diff --git a/play-services-core/src/main/kotlin/org/microg/gms/common/PersistentTrustedReceiver.kt b/play-services-core/src/main/kotlin/org/microg/gms/common/PersistentTrustedReceiver.kt new file mode 100644 index 0000000000..62631912ba --- /dev/null +++ b/play-services-core/src/main/kotlin/org/microg/gms/common/PersistentTrustedReceiver.kt @@ -0,0 +1,44 @@ +/** + * SPDX-FileCopyrightText: 2025 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.common + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.util.Log +import androidx.work.Data +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.WorkManager + +class PersistentTrustedReceiver : BroadcastReceiver() { + + companion object { + private const val TAG = "TrustedReceiver" + } + + override fun onReceive(context: Context, intent: Intent?) { + Log.d(TAG, "Package changed: $intent") + val action = intent?.action ?: return + val pkg = intent.data?.schemeSpecificPart ?: return + + if ((Intent.ACTION_PACKAGE_REMOVED.contains(action) + && intent.getBooleanExtra(Intent.EXTRA_DATA_REMOVED, false) + && !intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) + || Intent.ACTION_PACKAGE_FULLY_REMOVED.contains(action) + || Intent.ACTION_PACKAGE_DATA_CLEARED.contains(action) + ) { + Log.d(TAG, "Package removed or data cleared: $pkg") + val data = Data.Builder() + .putString(PackageIntentOpWorker.PACKAGE_NAME, pkg) + .build() + val request = OneTimeWorkRequestBuilder() + .setInputData(data) + .build() + WorkManager.getInstance(context).enqueue(request) + } + } + +} \ No newline at end of file