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

Commit c10a9904 authored by Andrey Yepin's avatar Andrey Yepin
Browse files

Chooser interactive session API.

Chooser interactive session API introduction.
ChooserSession#collapse and ChooserSession#setTaargetsEnabled are remain
hidden for now until they are implemented in Chooser.

Bug: 404593897
Test: atest FrameworksCoreTests:android.service.chooser.ChooserSessionTest
Test: atest FrameworksCoreTests:android.service.chooser.ChooserManager
Flag: android.service.chooser.interactive_chooser
Change-Id: I48bf23b8fc50b30f6957eca5a05119c8cfb69658
parent d783b2db
Loading
Loading
Loading
Loading
+30 −0
Original line number Diff line number Diff line
@@ -11126,6 +11126,7 @@ package android.content {
    field public static final String CAMERA_SERVICE = "camera";
    field public static final String CAPTIONING_SERVICE = "captioning";
    field public static final String CARRIER_CONFIG_SERVICE = "carrier_config";
    field @FlaggedApi("android.service.chooser.interactive_chooser") public static final String CHOOSER_SERVICE = "chooser";
    field public static final String CLIPBOARD_SERVICE = "clipboard";
    field public static final String COMPANION_DEVICE_SERVICE = "companiondevice";
    field public static final String CONNECTIVITY_DIAGNOSTICS_SERVICE = "connectivity_diagnostics";
@@ -41752,6 +41753,11 @@ package android.service.chooser {
    method @NonNull public android.service.chooser.ChooserAction build();
  }
  @FlaggedApi("android.service.chooser.interactive_chooser") public class ChooserManager {
    method @Nullable public android.service.chooser.ChooserSession getSession(@NonNull android.service.chooser.ChooserSessionToken);
    method @NonNull public android.service.chooser.ChooserSession startSession(@NonNull android.content.Context, @NonNull android.content.Intent);
  }
  public final class ChooserResult implements android.os.Parcelable {
    method public int describeContents();
    method @Nullable public android.content.ComponentName getSelectedComponent();
@@ -41765,6 +41771,30 @@ package android.service.chooser {
    field @NonNull public static final android.os.Parcelable.Creator<android.service.chooser.ChooserResult> CREATOR;
  }
  @FlaggedApi("android.service.chooser.interactive_chooser") public final class ChooserSession {
    method public void addStateListener(@NonNull java.util.concurrent.Executor, @NonNull android.service.chooser.ChooserSession.StateListener);
    method public void close();
    method @Nullable public android.graphics.Rect getSize();
    method public int getState();
    method @NonNull public android.service.chooser.ChooserSessionToken getToken();
    method public void removeStateListener(@NonNull android.service.chooser.ChooserSession.StateListener);
    method public void updateIntent(@NonNull android.content.Intent);
    field public static final int STATE_CLOSED = 2; // 0x2
    field public static final int STATE_INITIALIZED = 0; // 0x0
    field public static final int STATE_STARTED = 1; // 0x1
  }
  public static interface ChooserSession.StateListener {
    method public void onBoundsChanged(@NonNull android.graphics.Rect);
    method public void onStateChanged(int);
  }
  @FlaggedApi("android.service.chooser.interactive_chooser") public final class ChooserSessionToken implements android.os.Parcelable {
    method public int describeContents();
    method public void writeToParcel(@NonNull android.os.Parcel, int);
    field @NonNull public static final android.os.Parcelable.Creator<android.service.chooser.ChooserSessionToken> CREATOR;
  }
  @Deprecated public final class ChooserTarget implements android.os.Parcelable {
    ctor @Deprecated public ChooserTarget(CharSequence, android.graphics.drawable.Icon, float, android.content.ComponentName, @Nullable android.os.Bundle);
    method @Deprecated public int describeContents();
+12 −0
Original line number Diff line number Diff line
@@ -19,6 +19,7 @@ package android.app;
import static android.app.appfunctions.flags.Flags.enableAppFunctionManager;
import static android.provider.flags.Flags.newStoragePublicApi;
import static android.server.Flags.removeGameManagerServiceFromWear;
import static android.service.chooser.Flags.interactiveChooser;

import android.accounts.AccountManager;
import android.accounts.IAccountManager;
@@ -245,6 +246,7 @@ import android.security.authenticationpolicy.IAuthenticationPolicyService;
import android.security.intrusiondetection.IIntrusionDetectionService;
import android.security.intrusiondetection.IntrusionDetectionManager;
import android.security.keystore.KeyStoreManager;
import android.service.chooser.ChooserManager;
import android.service.oemlock.IOemLockService;
import android.service.oemlock.OemLockManager;
import android.service.persistentdata.IPersistentDataBlockService;
@@ -1839,6 +1841,16 @@ public final class SystemServiceRegistry {
                    }
                });

        if (interactiveChooser()) {
            registerService(Context.CHOOSER_SERVICE, ChooserManager.class,
                    new StaticServiceFetcher<>() {
                        @Override
                        public ChooserManager createService() {
                            return new ChooserManager();
                        }
                    });
        }

        sInitializing = true;
        try {
            // Note: the following functions need to be @SystemApis, once they become mainline
+10 −0
Original line number Diff line number Diff line
@@ -6975,6 +6975,16 @@ public abstract class Context {
     */
    public static final String DYNAMIC_INSTRUMENTATION_SERVICE = "dynamic_instrumentation";

    /**
     * Use with {@link #getSystemService(String)} to retrieve a
     * {@link android.service.chooser.ChooserManager}.
     *
     * @see #getSystemService(String)
     * @see android.service.chooser.ChooserManager
     */
    @FlaggedApi(android.service.chooser.Flags.FLAG_INTERACTIVE_CHOOSER)
    public static final String CHOOSER_SERVICE = "chooser";

    /**
     * Determine whether the given permission is allowed for a particular
     * process and user ID running in the system.
+147 −0
Original line number Diff line number Diff line
/*
 * Copyright 2025 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.service.chooser;

import static com.android.window.flags.Flags.touchPassThroughOptIn;

import android.annotation.FlaggedApi;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityOptions;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

/**
 * Manages the creation, tracking, and retrieval of chooser sessions.
 *
 *<p>An Interactive Chooser Session allows apps to invoke the system Chooser without entirely
 * covering app UI. Users may interact with both the app and Chooser, while bidirectional
 * communication between the two ensures a consistent state.</p>
 * <h3>Usage Example:</h3>
 * <pre>{@code
 * ChooserManager chooserManager = context.getSystemService(ChooserManager.class);
 *
 * // Construct the sharing intent
 * Intent targetIntent = new Intent(Intent.ACTION_SEND);
 * targetIntent.setType("text/plain");
 * targetIntent.putExtra(Intent.EXTRA_TEXT, "This is a message that will be shared.");
 *
 * Intent chooserIntent = Intent.createChooser(targetIntent, null);
 *
 * // Start a new chooser session
 * ChooserSession session = chooserManager.startSession(context, chooserIntent);
 * ChooserSessionToken token = session.getToken();
 * // Optionally, store the token int an activity saved state to re-associate with the session later
 *
 * // Later, to retrieve a session using a token:
 * ChooserSessionToken retrievedToken = ... // obtain the stored token
 * ChooserSession existingSession = chooserManager.getSession(retrievedToken);
 * if (existingSession != null) {
 * // Interact with the existing session
 * }
 * }</pre>
 *
 * @see ChooserSession
 * @see ChooserSessionToken
 */
@FlaggedApi(Flags.FLAG_INTERACTIVE_CHOOSER)
public class ChooserManager {
    private static final String TAG = "ChooserManager";

    /**
     * @hide
     */
    public ChooserManager() {}

    private final Map<ChooserSessionToken, ChooserSession> mSessions =
            Collections.synchronizedMap(new HashMap<>());

    /**
     * Starts a new interactive Chooser session. The method is idempotent and will start Chooser
     * only once.
     * @param chooserIntent an {@link Intent#ACTION_CHOOSER} intent that will be used as a base
     * for the new Chooser session.
     * <p>An interactive Chooser session also supports the following chooser parameters:
     * <ul>
     * <li>{@link Intent#EXTRA_ALTERNATE_INTENTS}</li>
     * <li>{@link Intent#EXTRA_INITIAL_INTENTS}</li>
     * <li>{@link Intent#EXTRA_EXCLUDE_COMPONENTS}</li>
     * <li>{@link Intent#EXTRA_REPLACEMENT_EXTRAS}</li>
     * <li>{@link Intent#EXTRA_CHOOSER_TARGETS}</li>
     * <li>{@link Intent#EXTRA_CHOOSER_REFINEMENT_INTENT_SENDER}</li>
     * <li>{@link Intent#EXTRA_CHOOSER_RESULT_INTENT_SENDER}</li>
     * <li>{@link Intent#EXTRA_CHOSEN_COMPONENT_INTENT_SENDER}</li>
     * </ul>
     * </p>
     * <p>See also {@link Intent#createChooser(Intent, CharSequence) }.</p>
     */
    @NonNull
    public ChooserSession startSession(@NonNull Context context, @NonNull Intent chooserIntent) {
        Objects.requireNonNull(context, "context should not be null");
        Objects.requireNonNull(chooserIntent, "chooserIntent should not be null");
        if (!Intent.ACTION_CHOOSER.equals(chooserIntent.getAction())) {
            throw new IllegalArgumentException("A chooser intent is expected");
        }
        chooserIntent = new Intent(chooserIntent);
        // FLAG_ACTIVITY_NEW_DOCUMENT can be overridden by the documentLaunchMode in the manifest
        // so it is ignored below.
        chooserIntent.removeFlags(
                Intent.FLAG_ACTIVITY_SINGLE_TOP
                | Intent.FLAG_ACTIVITY_NEW_TASK
                | Intent.FLAG_ACTIVITY_CLEAR_TASK
                | Intent.FLAG_ACTIVITY_CLEAR_TOP
                | Intent.FLAG_ACTIVITY_MULTIPLE_TASK
                | Intent.FLAG_ACTIVITY_REORDER_TO_FRONT
                | Intent.FLAG_ACTIVITY_TASK_ON_HOME
                | Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT);
        Bundle binderExtras = new Bundle();
        ChooserSession chooserSession = new ChooserSession();
        binderExtras.putBinder(ChooserSession.EXTRA_CHOOSER_SESSION, chooserSession.getBinder());
        chooserIntent.putExtras(binderExtras);
        ActivityOptions options = ActivityOptions.makeBasic();
        if (touchPassThroughOptIn()) {
            options.setAllowPassThroughOnTouchOutside(true);
        }
        context.startActivity(chooserIntent, options.toBundle());
        // TODO: should we listen for session closures and remove them from the collection?
        mSessions.put(chooserSession.getToken(), chooserSession);
        return chooserSession;
    }

    /**
     * Returns a {@link ChooserSession} associated with this token or {@code null} if there is no
     * active session.
     * @param token {@link ChooserSessionToken}.
     * @see ChooserSession#getToken()
     */
    @Nullable
    public ChooserSession getSession(@NonNull ChooserSessionToken token) {
        Objects.requireNonNull(token, "Token should not be null");
        ChooserSession session = mSessions.get(token);
        if (session != null && session.getState() == ChooserSession.STATE_CLOSED) {
            mSessions.remove(token);
            return null;
        }
        return session;
    }
}
+485 −0

File added.

Preview size limit exceeded, changes collapsed.

Loading