Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Unverified Commit dcf5f1fe authored by Marvin W.'s avatar Marvin W. 🐿️
Browse files

Add basic Google account settings

This uses WebView instead of the native solution, meaning some options are not available.

Hopefully can still be useful to recover access to accounts that are signed in microG when Google decides that it can't verify your identity through other means.
parent f10dd758
Loading
Loading
Loading
Loading
+29 −3
Original line number Diff line number Diff line
@@ -362,7 +362,8 @@
            android:name="org.microg.tools.AccountPickerActivity"
            android:excludeFromRecents="true"
            android:exported="true"
            android:process=":ui">
            android:process=":ui"
            android:theme="@style/Theme.AppCompat.DayNight.Dialog.Alert.NoActionBar">
            <intent-filter>
                <action android:name="com.google.android.gms.common.account.CHOOSE_ACCOUNT" />

@@ -545,16 +546,41 @@
            android:taskAffinity="org.microg.gms.settings" />

        <activity
            android:name="org.microg.gms.ui.AccountSettingsActivity"
            android:name="org.microg.gms.ui.LegacyAccountSettingsActivity"
            android:process=":ui"
            android:taskAffinity="org.microg.gms.settings">
            android:taskAffinity="org.microg.gms.settings"></activity>

        <activity
            android:name="org.microg.gms.accountsettings.ui.MainActivity"
            android:process=":ui"
            android:exported="false"
            android:taskAffinity="org.microg.gms.settings"
            android:theme="@style/Theme.AppCompat.Light.NoActionBar">
            <intent-filter>
                <action android:name="com.google.android.gms.accountsettings.MY_ACCOUNT" />
                <action android:name="com.google.android.gms.accountsettings.ACCOUNT_PREFERENCES_SETTINGS" />
                <action android:name="com.google.android.gms.accountsettings.PRIVACY_SETTINGS" />
                <action android:name="com.google.android.gms.accountsettings.SECURITY_SETTINGS" />

                <category android:name="android.intent.category.DEFAULT" />
            </intent-filter>
        </activity>

        <activity
            android:name="org.microg.gms.accountsettings.ui.LoaderActivity"
            android:process=":ui"
            android:exported="true"
            android:excludeFromRecents="true"
            android:theme="@style/Theme.AppCompat.DayNight.Dialog.Alert.NoActionBar">
            <intent-filter>
                <action android:name="com.google.android.gms.accountsettings.action.VIEW_SETTINGS" />
                <category android:name="android.intent.category.DEFAULT" />
            </intent-filter>
            <intent-filter>
                <action android:name="com.google.android.gms.accountsettings.action.BROWSE_SETTINGS" />

                <category android:name="android.intent.category.DEFAULT" />
                <category android:name="android.intent.category.BROWSABLE" />
            </intent-filter>
        </activity>

+1 −1
Original line number Diff line number Diff line
@@ -36,7 +36,7 @@ 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 {
public class LegacyAccountSettingsActivity extends AbstractSettingsActivity {

    @Override
    protected Fragment getFragment() {
+5 −16
Original line number Diff line number Diff line
@@ -16,18 +16,15 @@

package org.microg.tools;

import android.app.Activity;
import android.content.ComponentName;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import androidx.appcompat.app.AppCompatActivity;

/**
 * This is just an activity that forwards to the systems native account picker
 */
public class AccountPickerActivity extends Activity {
    private static final int REQUEST_CODE = AccountPickerActivity.class.hashCode();

public class AccountPickerActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
@@ -37,16 +34,8 @@ public class AccountPickerActivity extends Activity {
                ComponentName.unflattenFromString("android/.accounts.ChooseTypeAndAccountActivity");
        intent.setClassName(componentName.getPackageName(), componentName.getClassName());
        intent.putExtras(extras);
        startActivityForResult(intent, REQUEST_CODE);
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (requestCode == REQUEST_CODE) {
            setResult(resultCode, data);
        intent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT);
        startActivity(intent);
        finish();
        } else {
            super.onActivityResult(requestCode, resultCode, data);
        }
    }
}
+162 −0
Original line number Diff line number Diff line
/*
 * SPDX-FileCopyrightText: 2023 microG Project Team
 * SPDX-License-Identifier: Apache-2.0
 */

package org.microg.gms.accountsettings.ui

import android.accounts.AccountManager
import android.app.Activity
import android.content.Intent
import android.content.Intent.*
import android.net.Uri
import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import org.microg.gms.auth.AuthConstants.DEFAULT_ACCOUNT_TYPE
import org.microg.gms.common.PackageUtils
import org.microg.tools.AccountPickerActivity

private const val TAG = "AccountSettingsLoader"

private val ALLOWED_FALLBACK_PREFIXES = setOf("https://myaccount.google.com/", "https://takeout.google.com/")
private val BROWSABLE_SCREEN_IDS = setOf(1, 200, 400, 502, 527, 10003, 10050, 12700, 12701)
private val ACCOUNT_CHOOSER_URI = Uri.parse("https://accounts.google.com/AccountChooser")

private const val QUERY_PARAM_CONTINUE = "continue"
private const val QUERY_PARAM_LANG = "hl"
private const val QUERY_PARAM_EMAIL = "Email"

private const val EXTRA_ALLOWABLE_ACCOUNT_TYPES = "allowableAccountTypes"

private const val REQUEST_ACCOUNT_PICKER = 1

class LoaderActivity : AppCompatActivity() {
    private var canAskForAccount = false

    private fun launchFallback() {
        val fallbackUrl = intent?.getStringExtra(EXTRA_FALLBACK_URL)

        if (fallbackUrl == null) {
            Log.d(TAG, "No fallback")
            finishResult(RESULT_CANCELED)
        } else if (fallbackUrl in ALLOWED_FALLBACK_PREFIXES) {
            // TODO: Error screen?
            Log.d(TAG, "Illegal fallback url")
            finishResult(RESULT_CANCELED)
        } else {
            val fallbackAuth = intent?.getBooleanExtra(EXTRA_FALLBACK_AUTH, false) ?: false
            val uri = if (fallbackAuth) {
                val builder = ACCOUNT_CHOOSER_URI.buildUpon().appendQueryParameter(QUERY_PARAM_CONTINUE, fallbackUrl)
                val accountName = intent?.getStringExtra(EXTRA_ACCOUNT_NAME)
                if (!accountName.isNullOrBlank()) {
                    builder.appendQueryParameter(QUERY_PARAM_EMAIL, accountName)
                }
                val lang = Uri.parse(fallbackUrl).getQueryParameter(QUERY_PARAM_LANG)
                if (lang != null) {
                    builder.appendQueryParameter(QUERY_PARAM_LANG, lang)
                }
                builder.build()
            } else {
                Uri.parse(fallbackUrl)
            }
            Log.d(TAG, "Opening fallback $fallbackUrl")
            val intent = Intent(ACTION_VIEW, uri).apply { addCategory(CATEGORY_BROWSABLE) }
            startActivity(intent)
            finishResult(RESULT_OK)
        }
    }

    private fun launchMain() {
        val requestedAccountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME)
        val ignoreAccount = intent?.getBooleanExtra(EXTRA_IGNORE_ACCOUNT, false) ?: false
        val accounts = AccountManager.get(this).getAccountsByType(DEFAULT_ACCOUNT_TYPE)
        val account = if (requestedAccountName != null) {
            val account = accounts.find { it.name == requestedAccountName }
            if (account == null) {
                // TODO: Error screen?
                Log.d(TAG, "Account not found: $requestedAccountName")
                return finishResult(RESULT_CANCELED)
            }
            account
        } else if (accounts.isEmpty()) {
            if (intent?.getStringExtra(EXTRA_FALLBACK_URL) != null) {
                return launchFallback()
            } else {
                // TODO: Error screen?
                Log.d(TAG, "No account configured")
                return finishResult(RESULT_CANCELED)
            }
        } else if (accounts.size > 1) {
            if (canAskForAccount) {
                val intent = Intent(this, AccountPickerActivity::class.java)
                intent.putExtra(EXTRA_ALLOWABLE_ACCOUNT_TYPES, arrayOf(DEFAULT_ACCOUNT_TYPE))
                startActivityForResult(intent, REQUEST_ACCOUNT_PICKER)
                canAskForAccount = false
                return
            } else {
                return finishResult(RESULT_CANCELED)
            }
        } else {
            accounts.first()
        }

        val intent = Intent(this, MainActivity::class.java).apply {
            action = intent.action
            if (intent.hasExtra(EXTRA_THEME_CHOICE)) putExtra(EXTRA_THEME_CHOICE, intent.getIntExtra(EXTRA_THEME_CHOICE, 0))
            putExtra(EXTRA_ACCOUNT_NAME, account.name)
            if (ignoreAccount) putExtra(EXTRA_IGNORE_ACCOUNT, true)
            putExtra(EXTRA_SCREEN_ID, intent.getIntExtra(EXTRA_SCREEN_ID, 1))
            for (it in intent.extras?.keySet().orEmpty()) {
                if (it.startsWith(EXTRA_SCREEN_OPTIONS_PREFIX)) putExtra(it, intent.getStringExtra(it))
            }
            putExtra(EXTRA_CALLING_PACKAGE_NAME, callingActivity?.packageName)
            if (intent.action != ACTION_BROWSE_SETTINGS) {
                addFlags(FLAG_ACTIVITY_FORWARD_RESULT)
            }
        }

        startActivity(intent)

        if (!isFinishing && !isChangingConfigurations) {
            finishResult(RESULT_OK)
        }
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        if (requestCode == REQUEST_ACCOUNT_PICKER) {
            if (resultCode == RESULT_OK && data?.hasExtra(AccountManager.KEY_ACCOUNT_NAME) == true) {
                intent.putExtra(EXTRA_ACCOUNT_NAME, data.getStringExtra(AccountManager.KEY_ACCOUNT_NAME))
                launchMain()
            } else {
                finishResult(resultCode)
            }
        } else {
            super.onActivityResult(requestCode, resultCode, data)
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        canAskForAccount = true
        val extras = intent?.extras?.also { it.keySet() }
        Log.d(TAG, "Invoked with ${intent.action} and extras $extras")

        super.onCreate(savedInstanceState)

        val isMainAllowed = if (intent == null || intent.action != ACTION_BROWSE_SETTINGS)
            PackageUtils.isGooglePackage(this, callingActivity?.packageName)
        else
            extras?.getInt(EXTRA_SCREEN_ID, -1) in BROWSABLE_SCREEN_IDS

        if (!isMainAllowed) {
            launchFallback()
        } else if (!isFinishing && !isChangingConfigurations){
            launchMain()
        }
    }

    private fun finishResult(resultCode: Int) {
        setResult(resultCode)
        finish()
    }
}
 No newline at end of file
+184 −0
Original line number Diff line number Diff line
/*
 * SPDX-FileCopyrightText: 2023 microG Project Team
 * SPDX-License-Identifier: Apache-2.0
 */

package org.microg.gms.accountsettings.ui

import android.accounts.Account
import android.accounts.AccountManager
import android.os.Bundle
import android.util.Log
import android.view.View
import android.webkit.WebView
import android.widget.FrameLayout
import android.widget.ProgressBar
import android.widget.RelativeLayout
import android.widget.RelativeLayout.LayoutParams.MATCH_PARENT
import android.widget.RelativeLayout.LayoutParams.WRAP_CONTENT
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.updateLayoutParams
import org.microg.gms.auth.AuthConstants
import org.microg.gms.common.Constants

private const val TAG = "AccountSettings"

// TODO: There likely is some API to figure those out...
private val SCREEN_ID_TO_URL = hashMapOf(
    1 to "https://myaccount.google.com",
    200 to "https://myaccount.google.com/privacycheckup",
    203 to "https://myaccount.google.com/email",
    204 to "https://myaccount.google.com/phone",
    205 to "https://myaccount.google.com/birthday",
    206 to "https://myaccount.google.com/gender",
    210 to "https://myaccount.google.com/locationsharing",
    214 to "https://myaccount.google.com/dashboard",
    215 to "https://takeout.google.com",
    216 to "https://myaccount.google.com/inactive",
    219 to "https://myactivity.google.com/myactivity",
    220 to "https://www.google.com/maps/timeline",
    224 to "https://myactivity.google.com/activitycontrols?settings=search",
    227 to "https://myactivity.google.com/activitycontrols?settings=location",
    231 to "https://myactivity.google.com/activitycontrols?settings=youtube",
    235 to "https://myactivity.google.com/activitycontrols/youtube",
    238 to "https://www.google.com/setting/search/privateresults/",
    241 to "https://myaccount.google.com/communication-preferences",
    300 to "https://myaccount.google.com/language",
    301 to "https://drive.google.com/settings/storage",
    302 to "https://myaccount.google.com/deleteservices",
    303 to "https://myaccount.google.com/deleteaccount",
    307 to "https://payments.google.com/payments/home",
    308 to "https://myaccount.google.com/subscriptions",
    309 to "https://myaccount.google.com/purchases",
    310 to "https://myaccount.google.com/reservations",
    312 to "https://myaccount.google.com/accessibility",
    313 to "https://myaccount.google.com/inputtools",
    400 to "https://myaccount.google.com/security-checkup/",
    401 to "https://myaccount.google.com/signinoptions/password",
    403 to "https://myaccount.google.com/signinoptions/two-step-verification",
    406 to "https://myaccount.google.com/signinoptions/rescuephone",
    407 to "https://myaccount.google.com/recovery/email",
    409 to "https://myaccount.google.com/notifications",
    410 to "https://myaccount.google.com/device-activity",
    417 to "https://myaccount.google.com/find-your-phone",
    425 to "https://myaccount.google.com/account-enhanced-safe-browsing",
    426 to "https://myaccount.google.com/two-step-verification/authenticator",
    427 to "https://myaccount.google.com/two-step-verification/backup-codes",
    429 to "https://myaccount.google.com/two-step-verification/security-keys",
    430 to "https://myaccount.google.com/two-step-verification/prompt",
    431 to "https://myaccount.google.com/connections",
    432 to "https://myaccount.google.com/two-step-verification/phone-numbers",
    433 to "https://myaccount.google.com/signinoptions/passkeys",
    437 to "https://myaccount.google.com/signinoptions/passwordoptional",
    500 to "https://policies.google.com/privacy",
    503 to "https://policies.google.com/terms",
    519 to "https://myaccount.google.com/yourdata/maps",
    520 to "https://myaccount.google.com/yourdata/search",
    530 to "https://fit.google.com/privacy/settings",
    547 to "https://myactivity.google.com/product/search",
    562 to "https://myaccount.google.com/yourdata/youtube",
    10003 to "https://myaccount.google.com/personal-info",
    10004 to "https://myaccount.google.com/data-and-privacy",
    10005 to "https://myaccount.google.com/people-and-sharing",
    10006 to "https://myaccount.google.com/security",
    10007 to "https://myaccount.google.com/payments-and-subscriptions",
    10015 to "https://support.google.com/accounts",
    10050 to "https://myaccount.google.com/profile",
    10090 to "https://myaccount.google.com/profile/name",
    10704 to "https://www.google.com/account/about",
    10706 to "https://myaccount.google.com/profile/profiles-summary",
    10728 to "https://myaccount.google.com/data-and-privacy/how-data-improves-experience",
    10729 to "https://myaccount.google.com/data-and-privacy/data-visibility",
    10759 to "https://myaccount.google.com/address/home",
    10760 to "https://myaccount.google.com/address/work",
)

private val ALLOWED_WEB_PREFIXES = setOf(
    "https://accounts.google.com/",
    "https://myaccount.google.com/",
    "https://one.google.com/",
    "https://myactivity.google.com/",
    "https://timeline.google.com/",
    "https://takeout.google.com/",
    "https://www.google.com/maps/timeline",
    "https://www.google.com/setting/",
    "https://drive.google.com/settings/",
    "https://drive.google.com/accounts/",
    "https://drive.google.com/u/1/settings/",
    "https://payments.google.com/",
    "https://policies.google.com/",
    "https://fit.google.com/privacy/settings",
)

private val ACTION_TO_SCREEN_ID = hashMapOf(
    ACTION_MY_ACCOUNT to 1,
    ACTION_ACCOUNT_PREFERENCES_SETTINGS to 1,
    ACTION_SECURITY_SETTINGS to 10006,
    ACTION_PRIVACY_SETTINGS to 10004,
)

class MainActivity : AppCompatActivity() {
    private lateinit var webView: WebView

    private fun getSelectedAccountName(): String? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        val extras = intent?.extras?.also { it.keySet() }
        Log.d(TAG, "Invoked with ${intent.action} and extras $extras")
        super.onCreate(savedInstanceState)

        val screenId = intent?.getIntExtra(EXTRA_SCREEN_ID, -1).takeIf { it != -1 } ?: ACTION_TO_SCREEN_ID[intent.action] ?: 1
        val screenOptions = intent.extras?.keySet().orEmpty()
            .filter { it.startsWith(EXTRA_SCREEN_OPTIONS_PREFIX) }
            .map { it.substring(EXTRA_SCREEN_OPTIONS_PREFIX.length) to intent.getStringExtra(it) }
            .toMap()

        val callingPackage = intent?.getStringExtra(EXTRA_CALLING_PACKAGE_NAME) ?: callingActivity?.packageName ?: Constants.GMS_PACKAGE_NAME

        val ignoreAccount = intent?.getBooleanExtra(EXTRA_IGNORE_ACCOUNT, false) ?: false
        val accountName = if (ignoreAccount) null else {
            val accounts = AccountManager.get(this).getAccountsByType(AuthConstants.DEFAULT_ACCOUNT_TYPE)
            val accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME) ?: intent.getParcelableExtra<Account>("account")?.name ?: getSelectedAccountName()
            accounts.find { it.name.equals(accountName) }?.name
        }

        if (accountName == null) {
            Log.w(TAG, "No account, going without!")
        }

        if (screenId in SCREEN_ID_TO_URL) {
            val layout = RelativeLayout(this)
            layout.addView(ProgressBar(this).apply {
                layoutParams = RelativeLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT).apply {
                    addRule(RelativeLayout.CENTER_HORIZONTAL)
                    addRule(RelativeLayout.CENTER_VERTICAL)
                }
                isIndeterminate = true
            })
            webView = WebView(this).apply {
                layoutParams = RelativeLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)
                visibility = View.INVISIBLE
            }
            layout.addView(webView)
            setContentView(layout)
            WebViewHelper(this, webView, ALLOWED_WEB_PREFIXES).openWebView(SCREEN_ID_TO_URL[screenId], accountName)
            setResult(RESULT_OK)
        } else {
            Log.w(TAG, "Unknown screen id, can't open corresponding web page")
            setResult(RESULT_CANCELED)
            finish()
        }
    }

    override fun onDestroy() {
        super.onDestroy()
    }

    override fun onBackPressed() {
        if (this::webView.isInitialized && webView.canGoBack()) {
            webView.goBack()
        } else {
            super.onBackPressed()
        }
    }
}
 No newline at end of file
Loading