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

Commit a2ada8bf authored by Felipe Leme's avatar Felipe Leme Committed by Android (Google) Code Review
Browse files

Merge "Split ContentCaptureSession in 2 classes."

parents 708e111e b63e0ddc
Loading
Loading
Loading
Loading
+6 −6
Original line number Diff line number Diff line
@@ -52223,13 +52223,13 @@ package android.view.contentcapture {
    method public void setContentCaptureEnabled(boolean);
  }
  public final class ContentCaptureSession implements java.lang.AutoCloseable {
  public abstract class ContentCaptureSession implements java.lang.AutoCloseable {
    method public void close();
    method public void destroy();
    method public android.view.contentcapture.ContentCaptureSessionId getContentCaptureSessionId();
    method public void notifyViewAppeared(android.view.ViewStructure);
    method public void notifyViewDisappeared(android.view.autofill.AutofillId);
    method public void notifyViewTextChanged(android.view.autofill.AutofillId, java.lang.CharSequence, int);
    method public final void destroy();
    method public final android.view.contentcapture.ContentCaptureSessionId getContentCaptureSessionId();
    method public final void notifyViewAppeared(android.view.ViewStructure);
    method public final void notifyViewDisappeared(android.view.autofill.AutofillId);
    method public final void notifyViewTextChanged(android.view.autofill.AutofillId, java.lang.CharSequence, int);
    field public static final int FLAG_USER_INPUT = 1; // 0x1
  }
+2 −1
Original line number Diff line number Diff line
@@ -34,6 +34,7 @@ import android.os.RemoteException;
import android.util.ArrayMap;
import android.util.Log;
import android.util.Slog;
import android.view.contentcapture.ActivityContentCaptureSession;
import android.view.contentcapture.ContentCaptureContext;
import android.view.contentcapture.ContentCaptureEvent;
import android.view.contentcapture.ContentCaptureManager;
@@ -323,7 +324,7 @@ public abstract class ContentCaptureService extends Service {
            final Bundle extras;
            if (binder != null) {
                extras = new Bundle();
                extras.putBinder(ContentCaptureSession.EXTRA_BINDER, binder);
                extras.putBinder(ActivityContentCaptureSession.EXTRA_BINDER, binder);
            } else {
                extras = null;
            }
+471 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2018 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.contentcapture;

import static android.view.contentcapture.ContentCaptureEvent.TYPE_VIEW_APPEARED;
import static android.view.contentcapture.ContentCaptureEvent.TYPE_VIEW_DISAPPEARED;
import static android.view.contentcapture.ContentCaptureEvent.TYPE_VIEW_TEXT_CHANGED;
import static android.view.contentcapture.ContentCaptureManager.DEBUG;
import static android.view.contentcapture.ContentCaptureManager.VERBOSE;

import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.ComponentName;
import android.content.Context;
import android.content.pm.ParceledListSlice;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.IBinder.DeathRecipient;
import android.os.RemoteException;
import android.os.SystemClock;
import android.util.Log;
import android.util.TimeUtils;
import android.view.autofill.AutofillId;
import android.view.contentcapture.ViewNode.ViewStructureImpl;

import com.android.internal.os.IResultReceiver;

import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * Main session associated with a context.
 *
 * <p>This session is created when the activity starts and finished when it stops; clients can use
 * it to create children activities.
 *
 * <p><b>NOTE: all methods in this class should return right away, or do the real work in a handler
 * thread. Hence, the only field that must be thread-safe is {@code mEnabled}, which is called at
 * the beginning of every method.
 *
 * @hide
 */
public final class ActivityContentCaptureSession extends ContentCaptureSession {

    /**
     * Handler message used to flush the buffer.
     */
    private static final int MSG_FLUSH = 1;

    /**
     * Maximum number of events that are buffered before sent to the app.
     */
    // TODO(b/121044064): use settings
    private static final int MAX_BUFFER_SIZE = 100;

    /**
     * Frequency the buffer is flushed if stale.
     */
    // TODO(b/121044064): use settings
    private static final int FLUSHING_FREQUENCY_MS = 5_000;

    /**
     * Name of the {@link IResultReceiver} extra used to pass the binder interface to the service.
     * @hide
     */
    public static final String EXTRA_BINDER = "binder";

    @NonNull
    private final AtomicBoolean mDisabled;

    @NonNull
    private final Context mContext;

    @NonNull
    private final Handler mHandler;

    /**
     * Interface to the system_server binder object - it's only used to start the session (and
     * notify when the session is finished).
     */
    @Nullable
    private final IContentCaptureManager mSystemServerInterface;

    /**
     * Direct interface to the service binder object - it's used to send the events, including the
     * last ones (when the session is finished)
     */
    @Nullable
    private IContentCaptureDirectManager mDirectServiceInterface;
    @Nullable
    private DeathRecipient mDirectServiceVulture;

    private int mState = STATE_UNKNOWN;

    @Nullable
    private IBinder mApplicationToken;

    @Nullable
    private ComponentName mComponentName;

    /**
     * List of events held to be sent as a batch.
     */
    @Nullable
    private ArrayList<ContentCaptureEvent> mEvents;

    // Used just for debugging purposes (on dump)
    private long mNextFlush;

    // Lazily created on demand.
    private ContentCaptureSessionId mContentCaptureSessionId;

    /**
     * @hide */
    protected ActivityContentCaptureSession(@NonNull Context context, @NonNull Handler handler,
            @Nullable IContentCaptureManager systemServerInterface, @NonNull AtomicBoolean disabled,
            @Nullable ContentCaptureContext clientContext) {
        super(clientContext);
        mContext = context;
        mHandler = handler;
        mSystemServerInterface = systemServerInterface;
        mDisabled = disabled;
    }

    /**
     * Starts this session.
     *
     * @hide
     */
    void start(@NonNull IBinder applicationToken, @NonNull ComponentName activityComponent) {
        if (!isContentCaptureEnabled()) return;

        if (VERBOSE) {
            Log.v(mTag, "start(): token=" + applicationToken + ", comp="
                    + ComponentName.flattenToShortString(activityComponent));
        }

        mHandler.sendMessage(obtainMessage(ActivityContentCaptureSession::handleStartSession, this,
                applicationToken, activityComponent));
    }

    @Override
    void flush() {
        mHandler.sendMessage(obtainMessage(ActivityContentCaptureSession::handleForceFlush, this));
    }

    @Override
    void onDestroy() {
        mHandler.sendMessage(
                obtainMessage(ActivityContentCaptureSession::handleDestroySession, this));
    }

    private void handleStartSession(@NonNull IBinder token, @NonNull ComponentName componentName) {
        if (mState != STATE_UNKNOWN) {
            // TODO(b/111276913): revisit this scenario
            Log.w(mTag, "ignoring handleStartSession(" + token + ") while on state "
                    + getStateAsString(mState));
            return;
        }
        mState = STATE_WAITING_FOR_SERVER;
        mApplicationToken = token;
        mComponentName = componentName;

        if (VERBOSE) {
            Log.v(mTag, "handleStartSession(): token=" + token + ", act="
                    + getActivityDebugName() + ", id=" + mId);
        }
        final int flags = 0; // TODO(b/111276913): get proper flags

        try {
            mSystemServerInterface.startSession(mContext.getUserId(), mApplicationToken,
                    componentName, mId, mClientContext, flags, new IResultReceiver.Stub() {
                        @Override
                        public void send(int resultCode, Bundle resultData) {
                            IBinder binder = null;
                            if (resultData != null) {
                                binder = resultData.getBinder(EXTRA_BINDER);
                                if (binder == null) {
                                    Log.wtf(mTag, "No " + EXTRA_BINDER + " extra result");
                                    handleResetState();
                                    return;
                                }
                            }
                            handleSessionStarted(resultCode, binder);
                        }
                    });
        } catch (RemoteException e) {
            Log.w(mTag, "Error starting session for " + componentName.flattenToShortString() + ": "
                    + e);
        }
    }

    /**
     * Callback from {@code system_server} after call to
     * {@link IContentCaptureManager#startSession(int, IBinder, ComponentName, String,
     * ContentCaptureContext, int, IResultReceiver)}.
     *
     * @param resultCode session state
     * @param binder handle to {@code IContentCaptureDirectManager}
     */
    private void handleSessionStarted(int resultCode, @Nullable IBinder binder) {
        mState = resultCode;
        if (binder != null) {
            mDirectServiceInterface = IContentCaptureDirectManager.Stub.asInterface(binder);
            mDirectServiceVulture = () -> {
                Log.w(mTag, "Destroying session " + mId + " because service died");
                destroy();
            };
            try {
                binder.linkToDeath(mDirectServiceVulture, 0);
            } catch (RemoteException e) {
                Log.w(mTag, "Failed to link to death on " + binder + ": " + e);
            }
        }
        if (resultCode == STATE_DISABLED || resultCode == STATE_DISABLED_DUPLICATED_ID) {
            mDisabled.set(true);
            handleResetSession(/* resetState= */ false);
        } else {
            mDisabled.set(false);
        }
        if (VERBOSE) {
            Log.v(mTag, "handleSessionStarted() result: code=" + resultCode + ", id=" + mId
                    + ", state=" + getStateAsString(mState) + ", disabled=" + mDisabled.get()
                    + ", binder=" + binder);
        }
    }

    private void handleSendEvent(@NonNull ContentCaptureEvent event, boolean forceFlush) {
        if (mEvents == null) {
            if (VERBOSE) {
                Log.v(mTag, "Creating buffer for " + MAX_BUFFER_SIZE + " events");
            }
            mEvents = new ArrayList<>(MAX_BUFFER_SIZE);
        }
        mEvents.add(event);

        final int numberEvents = mEvents.size();

        // TODO(b/120784831): need to optimize it so we buffer changes until a number of X are
        // buffered (either total or per autofillid). For
        // example, if the user typed "a", "b", "c" and the threshold is 3, we should buffer
        // "a" and "b" then send "abc".
        final boolean bufferEvent = numberEvents < MAX_BUFFER_SIZE;

        if (bufferEvent && !forceFlush) {
            handleScheduleFlush(/* checkExisting= */ true);
            return;
        }

        if (mState != STATE_ACTIVE) {
            // Callback from startSession hasn't been called yet - typically happens on system
            // apps that are started before the system service
            // TODO(b/111276913): try to ignore session while system is not ready / boot
            // not complete instead. Similarly, the manager service should return right away
            // when the user does not have a service set
            if (VERBOSE) {
                Log.v(mTag, "Closing session for " + getActivityDebugName()
                        + " after " + numberEvents + " delayed events and state "
                        + getStateAsString(mState));
            }
            handleResetState();
            // TODO(b/111276913): blacklist activity / use special flag to indicate that
            // when it's launched again
            return;
        }

        handleForceFlush();
    }

    private void handleScheduleFlush(boolean checkExisting) {
        if (checkExisting && mHandler.hasMessages(MSG_FLUSH)) {
            // "Renew" the flush message by removing the previous one
            mHandler.removeMessages(MSG_FLUSH);
        }
        mNextFlush = SystemClock.elapsedRealtime() + FLUSHING_FREQUENCY_MS;
        if (VERBOSE) {
            Log.v(mTag, "Scheduled to flush in " + FLUSHING_FREQUENCY_MS + "ms: " + mNextFlush);
        }
        mHandler.sendMessageDelayed(
                obtainMessage(ActivityContentCaptureSession::handleFlushIfNeeded, this)
                .setWhat(MSG_FLUSH), FLUSHING_FREQUENCY_MS);
    }

    private void handleFlushIfNeeded() {
        if (mEvents.isEmpty()) {
            if (VERBOSE) Log.v(mTag, "Nothing to flush");
            return;
        }
        handleForceFlush();
    }

    private void handleForceFlush() {
        if (mEvents == null) return;

        if (mDirectServiceInterface == null) {
            Log.w(mTag, "handleForceFlush(): client not available yet");
            if (!mHandler.hasMessages(MSG_FLUSH)) {
                handleScheduleFlush(/* checkExisting= */ false);
            }
            return;
        }

        final int numberEvents = mEvents.size();
        try {
            if (DEBUG) {
                Log.d(mTag, "Flushing " + numberEvents + " event(s) for " + getActivityDebugName());
            }
            mHandler.removeMessages(MSG_FLUSH);

            final ParceledListSlice<ContentCaptureEvent> events = handleClearEvents();
            mDirectServiceInterface.sendEvents(mId, events);
        } catch (RemoteException e) {
            Log.w(mTag, "Error sending " + numberEvents + " for " + getActivityDebugName()
                    + ": " + e);
        }
    }

    /**
     * Resets the buffer and return a {@link ParceledListSlice} with the previous events.
     */
    @NonNull
    private ParceledListSlice<ContentCaptureEvent> handleClearEvents() {
        // NOTE: we must save a reference to the current mEvents and then set it to to null,
        // otherwise clearing it would clear it in the receiving side if the service is also local.
        final List<ContentCaptureEvent> events = mEvents == null
                ? Collections.emptyList()
                : mEvents;
        mEvents = null;
        return new ParceledListSlice<>(events);
    }

    private void handleDestroySession() {
        if (DEBUG) {
            Log.d(mTag, "Destroying session (ctx=" + mContext + ", id=" + mId + ") with "
                    + (mEvents == null ? 0 : mEvents.size()) + " event(s) for "
                    + getActivityDebugName());
        }

        try {
            mSystemServerInterface.finishSession(mContext.getUserId(), mId);
        } catch (RemoteException e) {
            Log.e(mTag, "Error destroying system-service session " + mId + " for "
                    + getActivityDebugName() + ": " + e);
        }
    }

    private void handleResetState() {
        handleResetSession(/* resetState= */ true);
    }

    // TODO(b/121042846): once we support multiple sessions, we might need to move some of these
    // clearings out.
    private void handleResetSession(boolean resetState) {
        if (resetState) {
            mState = STATE_UNKNOWN;
        }
        mContentCaptureSessionId = null;
        mApplicationToken = null;
        mComponentName = null;
        mEvents = null;
        if (mDirectServiceInterface != null) {
            mDirectServiceInterface.asBinder().unlinkToDeath(mDirectServiceVulture, 0);
        }
        mDirectServiceInterface = null;
        mHandler.removeMessages(MSG_FLUSH);
    }

    @Override
    void internalNotifyViewAppeared(@NonNull ViewStructureImpl node) {
        mHandler.sendMessage(obtainMessage(ActivityContentCaptureSession::handleSendEvent, this,
                new ContentCaptureEvent(TYPE_VIEW_APPEARED)
                        .setViewNode(node.mNode), /* forceFlush= */ false));
    }

    @Override
    void internalNotifyViewDisappeared(@NonNull AutofillId id) {
        mHandler.sendMessage(obtainMessage(ActivityContentCaptureSession::handleSendEvent, this,
                new ContentCaptureEvent(TYPE_VIEW_DISAPPEARED).setAutofillId(id),
                        /* forceFlush= */ false));
    }

    @Override
    void internalNotifyViewTextChanged(@NonNull AutofillId id, @Nullable CharSequence text,
            int flags) {
        mHandler.sendMessage(obtainMessage(ActivityContentCaptureSession::handleSendEvent, this,
                new ContentCaptureEvent(TYPE_VIEW_TEXT_CHANGED, flags).setAutofillId(id)
                        .setText(text), /* forceFlush= */ false));
    }

    @Override
    boolean isContentCaptureEnabled() {
        return mSystemServerInterface != null && !mDisabled.get();
    }

    @Override
    void dump(@NonNull String prefix, @NonNull PrintWriter pw) {
        pw.print(prefix); pw.print("id: "); pw.println(mId);
        pw.print(prefix); pw.print("mContext: "); pw.println(mContext);
        pw.print(prefix); pw.print("user: "); pw.println(mContext.getUserId());
        if (mSystemServerInterface != null) {
            pw.print(prefix); pw.print("mSystemServerInterface: ");
            pw.println(mSystemServerInterface);
        }
        if (mDirectServiceInterface != null) {
            pw.print(prefix); pw.print("mDirectServiceInterface: ");
            pw.println(mDirectServiceInterface);
        }
        if (mClientContext != null) {
            // NOTE: we don't dump clientContent because it could have PII
            pw.print(prefix); pw.println("hasClientContext");

        }
        pw.print(prefix); pw.print("mDisabled: "); pw.println(mDisabled.get());
        pw.print(prefix); pw.print("isEnabled(): "); pw.println(isContentCaptureEnabled());
        if (mContentCaptureSessionId != null) {
            pw.print(prefix); pw.print("public id: "); pw.println(mContentCaptureSessionId);
        }
        pw.print(prefix); pw.print("state: "); pw.print(mState); pw.print(" (");
        pw.print(getStateAsString(mState)); pw.println(")");
        if (mApplicationToken != null) {
            pw.print(prefix); pw.print("app token: "); pw.println(mApplicationToken);
        }
        if (mComponentName != null) {
            pw.print(prefix); pw.print("component name: ");
            pw.println(mComponentName.flattenToShortString());
        }
        if (mEvents != null && !mEvents.isEmpty()) {
            final int numberEvents = mEvents.size();
            pw.print(prefix); pw.print("buffered events: "); pw.print(numberEvents);
            pw.print('/'); pw.println(MAX_BUFFER_SIZE);
            if (VERBOSE && numberEvents > 0) {
                final String prefix3 = prefix + "  ";
                for (int i = 0; i < numberEvents; i++) {
                    final ContentCaptureEvent event = mEvents.get(i);
                    pw.print(prefix3); pw.print(i); pw.print(": "); event.dump(pw);
                    pw.println();
                }
            }
            pw.print(prefix); pw.print("flush frequency: "); pw.println(FLUSHING_FREQUENCY_MS);
            pw.print(prefix); pw.print("next flush: ");
            TimeUtils.formatDuration(mNextFlush - SystemClock.elapsedRealtime(), pw); pw.println();
        }
    }

    /**
     * Gets a string that can be used to identify the activity on logging statements.
     */
    private String getActivityDebugName() {
        return mComponentName == null ? mContext.getPackageName()
                : mComponentName.flattenToShortString();
    }
}
+5 −5
Original line number Diff line number Diff line
@@ -66,7 +66,7 @@ public final class ContentCaptureManager {
    @NonNull
    private final Handler mHandler;

    private ContentCaptureSession mMainSession;
    private ActivityContentCaptureSession mMainSession;

    /** @hide */
    public ContentCaptureManager(@NonNull Context context,
@@ -110,7 +110,7 @@ public final class ContentCaptureManager {
        // 4.Close (and delete) these sessions when onActivityStopped() is called.
        // 5.Figure out whether each session will have its own mDisabled AtomicBoolean.
        if (mMainSession == null) {
            mMainSession = new ContentCaptureSession(mContext, mHandler, mService,
            mMainSession = new ActivityContentCaptureSession(mContext, mHandler, mService,
                    mDisabled, Preconditions.checkNotNull(context));
        } else {
            throw new IllegalStateException("Manager already has a session: " + mMainSession);
@@ -127,12 +127,12 @@ public final class ContentCaptureManager {
     * @hide
     */
    @NonNull
    public ContentCaptureSession getMainContentCaptureSession() {
    public ActivityContentCaptureSession getMainContentCaptureSession() {
        // TODO(b/121033016): figure out how to manage the "default" session when it support
        // multiple sessions (can't just be the first one, as it could be closed).
        if (mMainSession == null) {
            mMainSession = new ContentCaptureSession(mContext, mHandler, mService, mDisabled,
                    /* contentCaptureContext=  */ null);
            mMainSession = new ActivityContentCaptureSession(mContext, mHandler, mService,
                    mDisabled, /* clientContext= */ null);
            if (VERBOSE) {
                Log.v(TAG, "getDefaultContentCaptureSession(): created " + mMainSession);
            }
+43 −427

File changed.

Preview size limit exceeded, changes collapsed.