chromium/components/content_capture/android/java/src/org/chromium/components/content_capture/NotificationTask.java

// Copyright 2019 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.components.content_capture;

import android.graphics.Rect;
import android.os.Build;
import android.text.TextUtils;
import android.view.ViewStructure;
import android.view.autofill.AutofillId;
import android.view.contentcapture.ContentCaptureSession;

import androidx.annotation.RequiresApi;

import org.chromium.base.Log;
import org.chromium.base.task.AsyncTask;
import org.chromium.components.content_capture.PlatformSession.PlatformSessionData;

/** The background task to talk to the ContentCapture Service. */
@RequiresApi(Build.VERSION_CODES.Q)
abstract class NotificationTask extends AsyncTask<Boolean> {
    private static final String TAG = "ContentCapture";
    private static Boolean sDump;

    protected final FrameSession mSession;
    protected final PlatformSession mPlatformSession;

    private boolean mHasPlatformExceptionForTesting;

    /**
     * A specific framework ContentCapture exception in Android Q and R shall be caught to prevent
     * the crash, the current NotificationTask can't be recovered from exception and has to
     * exit, the next task shall continue to run even it could cause the inconsistent state in
     * Android framework and aiai service who shall bear with it.
     *
     * Refer to crbug.com/1131430 for details.
     */
    private static boolean isMainContentCaptureSesionSentEventException(NullPointerException e) {
        for (StackTraceElement s : e.getStackTrace()) {
            if (s.getClassName().startsWith("android.view.contentcapture.MainContentCaptureSession")
                    && s.getMethodName().startsWith("sendEvent")) {
                return true;
            }
        }
        return false;
    }

    public NotificationTask(FrameSession session, PlatformSession platformSession) {
        mSession = session;
        mPlatformSession = platformSession;
        if (sDump == null) sDump = ContentCaptureFeatures.isDumpForTestingEnabled();
    }

    // Build up FrameIdToPlatformSessionData map of mSession, and return the current
    // session the task should run against.
    public PlatformSessionData buildCurrentSession() {
        if (mSession == null || mSession.isEmpty()) {
            return mPlatformSession.getRootPlatformSessionData();
        }
        // Build the session from root.
        PlatformSessionData platformSessionData = mPlatformSession.getRootPlatformSessionData();
        for (int i = mSession.size() - 1; i >= 0; i--) {
            platformSessionData = createOrGetSession(platformSessionData, mSession.get(i));
            if (platformSessionData == null) break;
        }
        return platformSessionData;
    }

    protected AutofillId notifyViewAppeared(
            PlatformSessionData parentPlatformSessionData, ContentCaptureDataBase data) {
        ViewStructure viewStructure =
                PlatformAPIWrapper.getInstance()
                        .newVirtualViewStructure(
                                parentPlatformSessionData.contentCaptureSession,
                                parentPlatformSessionData.autofillId,
                                data.getId());

        viewStructure.setText(data.getText());
        Rect rect = data.getBounds();
        // Always set scroll as (0, 0).
        viewStructure.setDimens(rect.left, rect.top, 0, 0, rect.width(), rect.height());
        PlatformAPIWrapper.getInstance()
                .notifyViewAppeared(parentPlatformSessionData.contentCaptureSession, viewStructure);
        return viewStructure.getAutofillId();
    }

    public PlatformSessionData createOrGetSession(
            PlatformSessionData parentPlatformSessionData, ContentCaptureFrame frame) {
        PlatformSessionData platformSessionData =
                mPlatformSession.getFrameIdToPlatformSessionData().get(frame.getId());
        if (platformSessionData == null && !TextUtils.isEmpty(frame.getUrl())) {
            ContentCaptureSession session =
                    PlatformAPIWrapper.getInstance()
                            .createContentCaptureSession(
                                    parentPlatformSessionData.contentCaptureSession,
                                    frame.getUrl(),
                                    frame.getFavicon());
            AutofillId autofillId =
                    PlatformAPIWrapper.getInstance()
                            .newAutofillId(
                                    parentPlatformSessionData.contentCaptureSession,
                                    mPlatformSession.getRootPlatformSessionData().autofillId,
                                    frame.getId());
            autofillId = notifyViewAppeared(parentPlatformSessionData, frame);
            platformSessionData = new PlatformSessionData(session, autofillId);
            mPlatformSession
                    .getFrameIdToPlatformSessionData()
                    .put(frame.getId(), platformSessionData);
        }
        return platformSessionData;
    }

    public boolean hasPlatformExceptionForTesting() {
        return mHasPlatformExceptionForTesting;
    }

    protected void log(String message) {
        if (sDump.booleanValue()) Log.i(TAG, message);
    }

    @Override
    protected void onPostExecute(Boolean result) {}

    @Override
    public final Boolean doInBackground() {
        try {
            runTask();
        } catch (NullPointerException e) {
            if (isMainContentCaptureSesionSentEventException(e)) {
                mHasPlatformExceptionForTesting = true;
                Log.e(TAG, "PlatformException", e);
            } else {
                throw e;
            }
        }
        return true;
    }

    protected abstract void runTask();
}