/*
 * Copyright (C) 2021 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.view.autofill;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.Activity;
import android.app.ActivityOptions;
import android.app.Application;
import android.content.ComponentName;
import android.content.Intent;
import android.content.IntentSender;
import android.graphics.Rect;
import android.os.Bundle;
import android.os.IBinder;
import android.text.TextUtils;
import android.util.Dumpable;
import android.util.Log;
import android.util.Slog;
import android.view.KeyEvent;
import android.view.View;
import android.view.ViewRootImpl;
import android.view.WindowManagerGlobal;

import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;

/**
 * A controller to manage the autofill requests for the {@link Activity}.
 *
 * @hide
 */
public final class AutofillClientController implements AutofillManager.AutofillClient, Dumpable {

    private static final String TAG = "AutofillClientController";

    private static final String LOG_TAG = "autofill_client";
    public static final boolean DEBUG = Log.isLoggable(LOG_TAG, Log.DEBUG);

    public static final String LAST_AUTOFILL_ID = "android:lastAutofillId";
    public static final String AUTOFILL_RESET_NEEDED = "@android:autofillResetNeeded";
    public static final String AUTO_FILL_AUTH_WHO_PREFIX = "@android:autoFillAuth:";

    public static final String DUMPABLE_NAME = "AutofillManager";

    /** The last autofill id that was returned from {@link #getNextAutofillId()} */
    public int mLastAutofillId = View.LAST_APP_AUTOFILL_ID;

    @NonNull
    private final Activity mActivity;
    /** The autofill manager. Always access via {@link #getAutofillManager()}. */
    @Nullable
    private AutofillManager mAutofillManager;
    /** The autofill dropdown fill ui. */
    @Nullable
    private AutofillPopupWindow mAutofillPopupWindow;
    private boolean mAutoFillResetNeeded;
    private boolean mAutoFillIgnoreFirstResumePause;

    /**
     * AutofillClientController constructor.
     */
    public AutofillClientController(Activity activity) {
        mActivity = activity;
    }

    private AutofillManager getAutofillManager() {
        if (mAutofillManager == null) {
            mAutofillManager = mActivity.getSystemService(AutofillManager.class);
        }
        return mAutofillManager;
    }

    // ------------------ Called for Activity events ------------------

    /**
     * Called when the Activity is attached.
     */
    public void onActivityAttached(Application application) {
        mActivity.setAutofillOptions(application.getAutofillOptions());
    }

    /**
     * Called when the {@link Activity#onCreate(Bundle)} is called.
     */
    public void onActivityCreated(@NonNull Bundle savedInstanceState) {
        mAutoFillResetNeeded = savedInstanceState.getBoolean(AUTOFILL_RESET_NEEDED, false);
        mLastAutofillId = savedInstanceState.getInt(LAST_AUTOFILL_ID, View.LAST_APP_AUTOFILL_ID);
        if (mAutoFillResetNeeded) {
            getAutofillManager().onCreate(savedInstanceState);
        }
    }

    /**
     * Called when the {@link Activity#onStart()} is called.
     */
    public void onActivityStarted() {
        if (mAutoFillResetNeeded) {
            getAutofillManager().onVisibleForAutofill();
        }
    }

    /**
     * Called when the {@link Activity#onResume()} is called.
     */
    public void onActivityResumed() {
        enableAutofillCompatibilityIfNeeded();
        if (mAutoFillResetNeeded) {
            if (!mAutoFillIgnoreFirstResumePause) {
                View focus = mActivity.getCurrentFocus();
                if (focus != null && focus.canNotifyAutofillEnterExitEvent()) {
                    // TODO(b/148815880): Bring up keyboard if resumed from inline authentication.
                    // TODO: in Activity killed/recreated case, i.e. SessionLifecycleTest#
                    // testDatasetVisibleWhileAutofilledAppIsLifecycled: the View's initial
                    // window visibility after recreation is INVISIBLE in onResume() and next frame
                    // ViewRootImpl.performTraversals() changes window visibility to VISIBLE.
                    // So we cannot call View.notifyEnterOrExited() which will do nothing
                    // when View.isVisibleToUser() is false.
                    getAutofillManager().notifyViewEntered(focus);
                }
            }
        }
    }

    /**
     * Called when the Activity is performing resume.
     */
    public void onActivityPerformResume(boolean followedByPause) {
        if (mAutoFillResetNeeded) {
            // When Activity is destroyed in paused state, and relaunch activity, there will be
            // extra onResume and onPause event,  ignore the first onResume and onPause.
            // see ActivityThread.handleRelaunchActivity()
            mAutoFillIgnoreFirstResumePause = followedByPause;
            if (mAutoFillIgnoreFirstResumePause && DEBUG) {
                Slog.v(TAG, "autofill will ignore first pause when relaunching " + this);
            }
        }
    }

    /**
     * Called when the {@link Activity#onPause()} is called.
     */
    public void onActivityPaused() {
        if (mAutoFillResetNeeded) {
            if (!mAutoFillIgnoreFirstResumePause) {
                if (DEBUG) Log.v(TAG, "autofill notifyViewExited " + this);
                View focus = mActivity.getCurrentFocus();
                if (focus != null && focus.canNotifyAutofillEnterExitEvent()) {
                    getAutofillManager().notifyViewExited(focus);
                }
            } else {
                // reset after first pause()
                if (DEBUG) Log.v(TAG, "autofill got first pause " + this);
                mAutoFillIgnoreFirstResumePause = false;
            }
        }
    }

    /**
     * Called when the {@link Activity#onStop()} is called.
     */
    public void onActivityStopped(Intent intent, boolean changingConfigurations) {
        if (mAutoFillResetNeeded) {
            // If stopped without changing the configurations, the response should expire.
            getAutofillManager().onInvisibleForAutofill(!changingConfigurations);
        } else if (intent != null
                && intent.hasExtra(AutofillManager.EXTRA_RESTORE_SESSION_TOKEN)
                && intent.hasExtra(AutofillManager.EXTRA_RESTORE_CROSS_ACTIVITY)) {
            restoreAutofillSaveUi(intent);
        }
    }

    /**
     * Called when the {@link Activity#onDestroy()} is called.
     */
    public void onActivityDestroyed() {
        if (mActivity.isFinishing() && mAutoFillResetNeeded) {
            getAutofillManager().onActivityFinishing();
        }
    }

    /**
     * Called when the {@link Activity#onSaveInstanceState(Bundle)} is called.
     */
    public void onSaveInstanceState(Bundle outState) {
        outState.putInt(LAST_AUTOFILL_ID, mLastAutofillId);
        if (mAutoFillResetNeeded) {
            outState.putBoolean(AUTOFILL_RESET_NEEDED, true);
            getAutofillManager().onSaveInstanceState(outState);
        }
    }

    /**
     * Called when the {@link Activity#finish()} is called.
     */
    public void onActivityFinish(Intent intent) {
        // Activity was launched when user tapped a link in the Autofill Save UI - Save UI must
        // be restored now.
        if (intent != null && intent.hasExtra(AutofillManager.EXTRA_RESTORE_SESSION_TOKEN)) {
            restoreAutofillSaveUi(intent);
        }
    }

    /**
     * Called when the {@link Activity#onBackPressed()} is called.
     */
    public void onActivityBackPressed(Intent intent) {
        // Activity was launched when user tapped a link in the Autofill Save UI - Save UI must
        // be restored now.
        if (intent != null && intent.hasExtra(AutofillManager.EXTRA_RESTORE_SESSION_TOKEN)) {
            restoreAutofillSaveUi(intent);
        }
    }

    /**
     * Called when the Activity is dispatching the result.
     */
    public void onDispatchActivityResult(int requestCode, int resultCode, Intent data) {
        Intent resultData = (resultCode == Activity.RESULT_OK) ? data : null;
        getAutofillManager().onAuthenticationResult(requestCode, resultData,
                mActivity.getCurrentFocus());
    }

    /**
     * Called when the {@link Activity#startActivity(Intent, Bundle)} is called.
     */
    public void onStartActivity(Intent startIntent, Intent cachedIntent) {
        if (cachedIntent != null
                && cachedIntent.hasExtra(AutofillManager.EXTRA_RESTORE_SESSION_TOKEN)
                && cachedIntent.hasExtra(AutofillManager.EXTRA_RESTORE_CROSS_ACTIVITY)) {
            if (TextUtils.equals(mActivity.getPackageName(),
                    startIntent.resolveActivity(mActivity.getPackageManager()).getPackageName())) {
                // Apply Autofill restore mechanism on the started activity by startActivity()
                final IBinder token =
                        cachedIntent.getIBinderExtra(AutofillManager.EXTRA_RESTORE_SESSION_TOKEN);
                // Remove restore ability from current activity
                cachedIntent.removeExtra(AutofillManager.EXTRA_RESTORE_SESSION_TOKEN);
                cachedIntent.removeExtra(AutofillManager.EXTRA_RESTORE_CROSS_ACTIVITY);
                // Put restore token
                startIntent.putExtra(AutofillManager.EXTRA_RESTORE_SESSION_TOKEN, token);
                startIntent.putExtra(AutofillManager.EXTRA_RESTORE_CROSS_ACTIVITY, true);
            }
        }
    }

    /**
     * Restore the autofill save ui.
     */
    public void restoreAutofillSaveUi(Intent intent) {
        final IBinder token =
                intent.getIBinderExtra(AutofillManager.EXTRA_RESTORE_SESSION_TOKEN);
        // Make only restore Autofill once
        intent.removeExtra(AutofillManager.EXTRA_RESTORE_SESSION_TOKEN);
        intent.removeExtra(AutofillManager.EXTRA_RESTORE_CROSS_ACTIVITY);
        getAutofillManager().onPendingSaveUi(AutofillManager.PENDING_UI_OPERATION_RESTORE,
                token);
    }

    /**
     * Enable autofill compatibility mode for the Activity if the compatibility mode is enabled
     * for the package.
     */
    public void enableAutofillCompatibilityIfNeeded() {
        if (mActivity.isAutofillCompatibilityEnabled()) {
            final AutofillManager afm = mActivity.getSystemService(AutofillManager.class);
            if (afm != null) {
                afm.enableCompatibilityMode();
            }
        }
    }

    @Override
    public String getDumpableName() {
        return DUMPABLE_NAME;
    }

    @Override
    public void dump(PrintWriter writer, String[] args) {
        final String prefix = "";
        final AutofillManager afm = getAutofillManager();
        if (afm != null) {
            afm.dump(prefix, writer);
            writer.print(prefix); writer.print("Autofill Compat Mode: ");
            writer.println(mActivity.isAutofillCompatibilityEnabled());
        } else {
            writer.print(prefix); writer.println("No AutofillManager");
        }
    }

    /**
     * Returns the next autofill ID that is unique in the activity
     *
     * <p>All IDs will be bigger than {@link View#LAST_APP_AUTOFILL_ID}. All IDs returned
     * will be unique.
     */
    public int getNextAutofillId() {
        if (mLastAutofillId == Integer.MAX_VALUE - 1) {
            mLastAutofillId = View.LAST_APP_AUTOFILL_ID;
        }

        mLastAutofillId++;

        return mLastAutofillId;
    }

    // ------------------ AutofillClient implementation ------------------

    @Override
    public AutofillId autofillClientGetNextAutofillId() {
        return new AutofillId(getNextAutofillId());
    }

    @Override
    public boolean autofillClientIsCompatibilityModeEnabled() {
        return mActivity.isAutofillCompatibilityEnabled();
    }

    @Override
    public boolean autofillClientIsVisibleForAutofill() {
        return mActivity.isVisibleForAutofill();
    }

    @Override
    public ComponentName autofillClientGetComponentName() {
        return mActivity.getComponentName();
    }

    @Override
    public IBinder autofillClientGetActivityToken() {
        return mActivity.getActivityToken();
    }

    @Override
    public boolean[] autofillClientGetViewVisibility(AutofillId[] autofillIds) {
        final int autofillIdCount = autofillIds.length;
        final boolean[] visible = new boolean[autofillIdCount];
        for (int i = 0; i < autofillIdCount; i++) {
            final AutofillId autofillId = autofillIds[i];
            if (autofillId == null) {
                visible[i] = false;
                continue;
            }
            final View view = autofillClientFindViewByAutofillIdTraversal(autofillId);
            if (view != null) {
                if (!autofillId.isVirtualInt()) {
                    visible[i] = view.isVisibleToUser();
                } else {
                    visible[i] = view.isVisibleToUserForAutofill(autofillId.getVirtualChildIntId());
                }
            }
        }
        if (android.view.autofill.Helper.sVerbose) {
            Log.v(TAG, "autofillClientGetViewVisibility(): " + Arrays.toString(visible));
        }
        return visible;
    }

    @Override
    public View autofillClientFindViewByAccessibilityIdTraversal(int viewId, int windowId) {
        final ArrayList<ViewRootImpl> roots = WindowManagerGlobal.getInstance()
                .getRootViews(mActivity.getActivityToken());
        for (int rootNum = 0; rootNum < roots.size(); rootNum++) {
            final View rootView = roots.get(rootNum).getView();
            if (rootView != null && rootView.getAccessibilityWindowId() == windowId) {
                final View view = rootView.findViewByAccessibilityIdTraversal(viewId);
                if (view != null) {
                    return view;
                }
            }
        }
        return null;
    }

    @Override
    public View autofillClientFindViewByAutofillIdTraversal(AutofillId autofillId) {
        if (autofillId == null) return null;
        final ArrayList<ViewRootImpl> roots =
                WindowManagerGlobal.getInstance().getRootViews(mActivity.getActivityToken());
        for (int rootNum = 0; rootNum < roots.size(); rootNum++) {
            final View rootView = roots.get(rootNum).getView();

            if (rootView != null) {
                final View view = rootView.findViewByAutofillIdTraversal(autofillId.getViewId());
                if (view != null) {
                    return view;
                }
            }
        }
        return null;
    }

    @Override
    public View[] autofillClientFindViewsByAutofillIdTraversal(AutofillId[] autofillIds) {
        final View[] views = new View[autofillIds.length];
        final ArrayList<ViewRootImpl> roots =
                WindowManagerGlobal.getInstance().getRootViews(mActivity.getActivityToken());

        for (int rootNum = 0; rootNum < roots.size(); rootNum++) {
            final View rootView = roots.get(rootNum).getView();

            if (rootView != null) {
                final int viewCount = autofillIds.length;
                for (int viewNum = 0; viewNum < viewCount; viewNum++) {
                    if (autofillIds[viewNum] != null && views[viewNum] == null) {
                        views[viewNum] = rootView.findViewByAutofillIdTraversal(
                                autofillIds[viewNum].getViewId());
                    }
                }
            }
        }
        return views;
    }

    @Override
    public boolean autofillClientIsFillUiShowing() {
        return mAutofillPopupWindow != null && mAutofillPopupWindow.isShowing();
    }

    @Override
    public boolean autofillClientRequestHideFillUi() {
        if (mAutofillPopupWindow == null) {
            return false;
        }
        mAutofillPopupWindow.dismiss();
        mAutofillPopupWindow = null;
        return true;
    }

    @Override
    public boolean autofillClientRequestShowFillUi(@NonNull View anchor, int width,
            int height, @Nullable Rect anchorBounds, IAutofillWindowPresenter presenter) {
        final boolean wasShowing;

        if (mAutofillPopupWindow == null) {
            wasShowing = false;
            mAutofillPopupWindow = new AutofillPopupWindow(presenter);
        } else {
            wasShowing = mAutofillPopupWindow.isShowing();
        }
        mAutofillPopupWindow.update(anchor, 0, 0, width, height, anchorBounds);

        return !wasShowing && mAutofillPopupWindow.isShowing();
    }

    @Override
    public void autofillClientDispatchUnhandledKey(View anchor, KeyEvent keyEvent) {
        ViewRootImpl rootImpl = anchor.getViewRootImpl();
        if (rootImpl != null) {
            // don't care if anchorView is current focus, for example a custom view may only receive
            // touchEvent, not focusable but can still trigger autofill window. The Key handling
            // might be inside parent of the custom view.
            rootImpl.dispatchKeyFromAutofill(keyEvent);
        }
    }

    @Override
    public boolean isDisablingEnterExitEventForAutofill() {
        return mAutoFillIgnoreFirstResumePause || !mActivity.isResumed();
    }

    @Override
    public void autofillClientResetableStateAvailable() {
        mAutoFillResetNeeded = true;
    }

    @Override
    public void autofillClientRunOnUiThread(Runnable action) {
        mActivity.runOnUiThread(action);
    }

    @Override
    public void autofillClientAuthenticate(int authenticationId, IntentSender intent,
            Intent fillInIntent, boolean authenticateInline) {
        try {
            ActivityOptions activityOptions = ActivityOptions.makeBasic()
                    .setPendingIntentBackgroundActivityStartMode(
                        ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED);
            mActivity.startIntentSenderForResult(intent, AUTO_FILL_AUTH_WHO_PREFIX,
                    authenticationId, fillInIntent, 0, 0, activityOptions.toBundle());
        } catch (IntentSender.SendIntentException e) {
            Log.e(TAG, "authenticate() failed for intent:" + intent, e);
        }
    }
}
