chromium/chrome/android/java/src/org/chromium/chrome/browser/sync/ui/SyncTrustedVaultProxyActivity.java

// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.chrome.browser.sync.ui;

import android.app.PendingIntent;
import android.content.Intent;
import android.content.IntentSender;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import org.chromium.base.ContextUtils;
import org.chromium.base.Log;
import org.chromium.base.supplier.OneshotSupplier;
import org.chromium.base.supplier.OneshotSupplierImpl;
import org.chromium.chrome.browser.init.AsyncInitializationActivity;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.profiles.ProfileProvider;
import org.chromium.chrome.browser.sync.TrustedVaultClient;
import org.chromium.components.sync.TrustedVaultUserActionTriggerForUMA;

/**
 * {@link SyncTrustedVaultProxyActivity} has no own UI and just acts as a proxy to launch an
 * activity related to trusted vault user actions (passed via PendingIntent). The reason for using
 * this proxy activity is to detect when the proxied activity (key retrieval or degraded
 * recoverability fix UI) finishes and notify TrustedVaultClient about changes.
 */
public class SyncTrustedVaultProxyActivity extends AsyncInitializationActivity {
    private static final String TAG = "SyncUI";

    // Note that the implementation relies on request codes being >0 (default value for
    // |mRequestCode|).
    private static final int REQUEST_CODE_TRUSTED_VAULT_KEY_RETRIEVAL = 1;
    private static final int REQUEST_CODE_TRUSTED_VAULT_RECOVERABILITY_DEGRADED = 2;

    // Key names used when propagating extra param in the proxy intent.
    private static final String EXTRA_KEY_PROXIED_INTENT = "proxied_intent";
    private static final String EXTRA_KEY_REQUEST_CODE = "request_code";
    private static final String EXTRA_KEY_USER_ACTION_TRIGGER = "user_action_trigger";

    private @TrustedVaultUserActionTriggerForUMA int mUserActionTrigger;
    private int mRequestCode;

    /**
     * Creates an intent that launches an SyncTrustedVaultProxyActivity for the purpose of
     * key retrieval.
     *
     * @param keyRetrievalIntent Actual key retrieval intent, which will be launched by
     * SyncTrustedVaultProxyActivity.
     * @param userActionTrigger Enum representing which UI surface triggered the intent.
     *
     * @return the intent for launching SyncTrustedVaultProxyActivity
     */
    public static Intent createKeyRetrievalProxyIntent(
            PendingIntent keyRetrievalIntent,
            @TrustedVaultUserActionTriggerForUMA int userActionTrigger) {
        return createProxyIntent(
                keyRetrievalIntent, userActionTrigger, REQUEST_CODE_TRUSTED_VAULT_KEY_RETRIEVAL);
    }

    /**
     * Creates an intent that launches an SyncTrustedVaultProxyActivity for the purpose of
     * fixing the recoverability degraded case.
     *
     * @param recoverabilityDegradedIntent Actual recoverability degraded fix intent, which will be
     *         launched by SyncTrustedVaultProxyActivity.
     * @param userActionTrigger Enum representing which UI surface triggered the intent.
     *
     * @return the intent for launching SyncTrustedVaultProxyActivity
     */
    public static Intent createRecoverabilityDegradedProxyIntent(
            PendingIntent recoverabilityDegradedIntent,
            @TrustedVaultUserActionTriggerForUMA int userActionTrigger) {
        return createProxyIntent(
                recoverabilityDegradedIntent,
                userActionTrigger,
                REQUEST_CODE_TRUSTED_VAULT_RECOVERABILITY_DEGRADED);
    }

    private static Intent createProxyIntent(
            PendingIntent proxiedIntent,
            @TrustedVaultUserActionTriggerForUMA int userActionTrigger,
            int requestCode) {
        Intent proxyIntent =
                new Intent(
                        ContextUtils.getApplicationContext(), SyncTrustedVaultProxyActivity.class);
        proxyIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        proxyIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
        proxyIntent.putExtra(EXTRA_KEY_PROXIED_INTENT, proxiedIntent);
        proxyIntent.putExtra(EXTRA_KEY_REQUEST_CODE, requestCode);
        proxyIntent.putExtra(EXTRA_KEY_USER_ACTION_TRIGGER, userActionTrigger);
        return proxyIntent;
    }

    @Override
    public boolean shouldStartGpuProcess() {
        return false;
    }

    @Override
    protected void triggerLayoutInflation() {
        // This Activity has no own UI and uses external pending intent to provide it. Since this
        // Activity requires native initialization it implements AsyncInitializationActivity and
        // thus the pending intent is sent inside triggerLayoutInflation() instead of onCreate().
        PendingIntent proxiedIntent = getIntent().getParcelableExtra(EXTRA_KEY_PROXIED_INTENT);
        mRequestCode = getIntent().getIntExtra(EXTRA_KEY_REQUEST_CODE, -1);
        mUserActionTrigger = getIntent().getIntExtra(EXTRA_KEY_USER_ACTION_TRIGGER, -1);

        assert proxiedIntent != null;
        assert mRequestCode != -1;
        assert mUserActionTrigger != -1;

        try {
            startIntentSenderForResult(
                    proxiedIntent.getIntentSender(),
                    mRequestCode,
                    /* fillInIntent= */ null,
                    /* flagsMask= */ 0,
                    /* flagsValues= */ 0,
                    /* extraFlags= */ 0,
                    /* options= */ null);
        } catch (IntentSender.SendIntentException exception) {
            Log.w(TAG, "Error sending trusted vault intent: ", exception);
        }
        onInitialLayoutInflationComplete();
    }

    @Override
    protected OneshotSupplier<ProfileProvider> createProfileProvider() {
        OneshotSupplierImpl<ProfileProvider> supplier = new OneshotSupplierImpl<>();
        ProfileProvider profileProvider =
                new ProfileProvider() {
                    @NonNull
                    @Override
                    public Profile getOriginalProfile() {
                        throw new IllegalStateException(
                                "Unexpected access of the original profile.");
                    }

                    @Nullable
                    @Override
                    public Profile getOffTheRecordProfile(boolean createIfNeeded) {
                        throw new IllegalStateException(
                                "Unexpected access of the incognito profile.");
                    }

                    @Override
                    public boolean hasOffTheRecordProfile() {
                        return false;
                    }
                };
        supplier.set(profileProvider);
        return supplier;
    }

    @Override
    public void finishNativeInitialization() {
        super.finishNativeInitialization();

        if (getSavedInstanceState() != null) {
            // The activity might be restored and this shouldn't cause recording the histogram
            // second time.
            return;
        }

        // Note that the metric-recording methods are invoked here, and not earlier, because:
        // a) The native part must be loaded (which is not guaranteed in triggerLayoutInflation).
        // b) It cannot be done too early, e.g. upon intent creation, because that doesn't always
        // mean the intent will actually be launched. This is particularly relevant for Android
        // notifications (SyncErrorNotifier), because the user may ignore or dismiss a notification.
        switch (mRequestCode) {
            case REQUEST_CODE_TRUSTED_VAULT_KEY_RETRIEVAL:
                TrustedVaultClient.get().recordKeyRetrievalTrigger(mUserActionTrigger);
                break;

            case REQUEST_CODE_TRUSTED_VAULT_RECOVERABILITY_DEGRADED:
                TrustedVaultClient.get().recordRecoverabilityDegradedFixTrigger(mUserActionTrigger);
                break;

            default:
                assert false;
        }
    }

    @Override
    public boolean onActivityResultWithNative(int requestCode, int resultCode, Intent intent) {
        boolean result = super.onActivityResultWithNative(requestCode, resultCode, intent);

        switch (requestCode) {
            case REQUEST_CODE_TRUSTED_VAULT_KEY_RETRIEVAL:
                // Upon key retrieval completion, the keys in TrustedVaultClient could have changed.
                // This is done even if the user cancelled the flow (i.e. resultCode != RESULT_OK)
                // because it's harmless to issue a redundant notifyKeysChanged().
                TrustedVaultClient.get().notifyKeysChanged();
                break;

            case REQUEST_CODE_TRUSTED_VAULT_RECOVERABILITY_DEGRADED:
                // Same as above, it is harmless to issue redundant notifyRecoverabilityChanged().
                TrustedVaultClient.get().notifyRecoverabilityChanged();
                break;

            default:
                assert false;
        }

        finish();
        return result;
    }
}