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

Commit d2503c2d authored by Andrey Yepin's avatar Andrey Yepin Committed by Android (Google) Code Review
Browse files

Merge "Chooser interactive session API." into main

parents 19b4e651 c10a9904
Loading
Loading
Loading
Loading
+30 −0
Original line number Diff line number Diff line
@@ -11269,6 +11269,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";
@@ -41925,6 +41926,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();
@@ -41938,6 +41944,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