Use system API

Bug: 404593897
Test: N/A
Change-Id: I7f9d87bd99e4a1c034f435ed617c98703cd12234
diff --git a/apps/ShareTest/Android.bp b/apps/ShareTest/Android.bp
index d0eb24a..0c8eb05 100644
--- a/apps/ShareTest/Android.bp
+++ b/apps/ShareTest/Android.bp
@@ -21,7 +21,6 @@
     srcs: [
         "src/**/*.java",
         "src/**/*.kt",
-        "aidl/**/I*.aidl",
     ],
     asset_dirs: ["assets"],
     sdk_version: "current",
@@ -39,7 +38,4 @@
         "kotlinx_coroutines",
         "kotlinx-coroutines-android",
     ],
-    aidl: {
-        local_include_dirs: ["aidl"],
-    },
 }
diff --git a/apps/ShareTest/aidl/android/service/chooser/IChooserController.aidl b/apps/ShareTest/aidl/android/service/chooser/IChooserController.aidl
deleted file mode 100644
index b3d8d81..0000000
--- a/apps/ShareTest/aidl/android/service/chooser/IChooserController.aidl
+++ /dev/null
@@ -1,24 +0,0 @@
-/*
- * 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 android.content.Intent;
-
-/** {@hide} */
-interface IChooserController {
-    oneway void updateIntent(in Intent intent);
-}
diff --git a/apps/ShareTest/aidl/android/service/chooser/IChooserControllerCallback.aidl b/apps/ShareTest/aidl/android/service/chooser/IChooserControllerCallback.aidl
deleted file mode 100644
index bd0c37a..0000000
--- a/apps/ShareTest/aidl/android/service/chooser/IChooserControllerCallback.aidl
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- * 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 android.graphics.Rect;
-import android.service.chooser.IChooserController;
-
-/** {@hide} */
-interface IChooserControllerCallback {
-    oneway void registerChooserController(in IChooserController updater);
-    oneway void onSizeChanged(in Rect size);
-    oneway void onClosed();
-}
diff --git a/apps/ShareTest/src/android/service/chooser/ChooserSession.java b/apps/ShareTest/src/android/service/chooser/ChooserSession.java
deleted file mode 100644
index 3da0b76..0000000
--- a/apps/ShareTest/src/android/service/chooser/ChooserSession.java
+++ /dev/null
@@ -1,457 +0,0 @@
-/*
- * 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 android.app.ActivityOptions;
-import android.content.Context;
-import android.content.Intent;
-import android.graphics.Rect;
-import android.os.Bundle;
-import android.os.IBinder;
-import android.os.RemoteException;
-import android.util.Log;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.concurrent.Executor;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.function.Consumer;
-
-import javax.annotation.concurrent.GuardedBy;
-
-/**
- * <p>A class that represents an interactive Chooser session.</p>
- * <p>An instance of the class can be used as a value for <em>an</em> {@link Intent#ACTION_CHOOSER}
- * extra to establish a bi-directional communication channel with Chooser.
- * <p>A {@link UpdateListener} callback can be used to receive updates about the
- * session and communication from Chooser.</p>
- */
-public final class ChooserSession {
-
-    /**
-     * A callback interface for Chooser session state updates.
-     */
-    public interface UpdateListener {
-
-        /**
-         * Gets invoked when a {@link ChooserController} becomes available.
-         * @param chooserController active chooser controller.
-         */
-        void onChooserConnected(ChooserController chooserController);
-
-        /**
-         * Gets invoked when the session is closed by the Chooser.
-         */
-        void onClosed();
-
-        /**
-         * Gets invoked when drawer size is changed. The rect parameter represents Chooser window
-         * position in pixels.
-         */
-        void onSizeChanged(Rect size);
-    }
-
-    /**
-     * An interface for updating the Chooser.
-     */
-    public interface ChooserController {
-
-        /**
-         * Update chooser intent in a Chooser session.
-         * <p>Updatable Chooser parameters:
-         * <ul>
-         * <li> {@link Intent#EXTRA_INTENT}
-         * <li> {@link Intent#EXTRA_EXCLUDE_COMPONENTS}
-         * <li> {@link Intent#EXTRA_CHOOSER_TARGETS}
-         * <li> {@link Intent#EXTRA_ALTERNATE_INTENTS}
-         * <li> {@link Intent#EXTRA_REPLACEMENT_EXTRAS}
-         * <li> {@link Intent#EXTRA_INITIAL_INTENTS}
-         * <li> {@link Intent#EXTRA_CHOOSER_RESULT_INTENT_SENDER}
-         * <li> {@link Intent#EXTRA_CHOOSER_REFINEMENT_INTENT_SENDER}
-         * <li> {@link Intent#EXTRA_CONTENT_ANNOTATIONS}
-         * </ul>
-         * </p>
-         */
-        void updateIntent(Intent intent) throws RemoteException;
-
-        /**
-         * Collapses Chooser to temporary yield more screen space for the app.
-         * Chooser will stay collapsed until its first user interaction.
-         */
-        void collapse() throws RemoteException;
-
-        /**
-         * Sets whether Chooser targets should be enabled.
-         * <p>
-         * This method is primarily intended to allow for managing a transient state,
-         * particularly useful during long-running operations. By disabling targets,
-         * launching application can prevent unintended interactions.
-         */
-        void setTargetsEnabled(boolean isEnabled) throws RemoteException;
-    }
-
-    /**
-     * @hide
-     */
-    public static final String EXTRA_CHOOSER_SESSION =
-            "com.android.extra.EXTRA_CHOOSER_INTERACTIVE_CALLBACK";
-
-    private static final String TAG = "ChooserSession";
-
-    private final ChooserSessionImpl mChooserSession = new ChooserSessionImpl();
-
-    /**
-     * Start a new interactive Chooser session. The method is idempotent and will start Chooser only
-     * once.
-     * @param chooserIntent a {@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}</li>
-     * <li>{@link Intent#EXTRA_CHOOSER_RESULT_INTENT_SENDER}</li>
-     * <li>{@link Intent#EXTRA_CHOSEN_COMPONENT_INTENT_SENDER}</li>
-     * <li>{@link Intent#EXTRA_CONTENT_ANNOTATIONS}</li>
-     * <li>{@link Intent#EXTRA_AUTO_LAUNCH_SINGLE_CHOICE}</li>
-     * </ul>
-     * </p>
-     * <p>See also {@link Intent#createChooser(Intent, CharSequence) }.</p>
-     */
-    public void start(@NonNull Context context, @NonNull Intent chooserIntent) {
-        if (context == null) {
-            throw new IllegalArgumentException("context should not be null");
-        }
-        if (chooserIntent == null) {
-            throw new IllegalArgumentException("chooserIntent should not be null");
-        }
-        if (!Intent.ACTION_CHOOSER.equals(chooserIntent.getAction())) {
-            throw new IllegalArgumentException("A chooser intent is expected");
-        }
-        chooserIntent = new Intent(chooserIntent);
-        Bundle binderExtras = new Bundle();
-        binderExtras.putBinder(EXTRA_CHOOSER_SESSION, mChooserSession);
-        chooserIntent.putExtras(binderExtras);
-        ActivityOptions options = ActivityOptions.makeBasic();
-        options.setAllowPassThroughOnTouchOutside(true);
-        context.startActivity(chooserIntent, options.toBundle());
-    }
-
-    /**
-     * @return true if the session is active: i.e. is not being cancelled by the client
-     * (see {@link #close()}) or closed by the Chooser.
-     */
-    public boolean isActive() {
-        return mChooserSession.isActive();
-    }
-
-    /**
-     * Cancel the session and close the Chooser.
-     */
-    public void close() {
-        mChooserSession.close();
-    }
-
-    /**
-     * <p>Get the active {@link ChooserController} or {@code null} if none is available.</p>
-     * A chooser controller becomes available after the Chooser has registered it and stays
-     * available while the session is active and the Chooser process is alive. It is possible for a
-     * session to remain active without a Chooser process. For example, this could happen when the
-     * client launches another activity on top of the Chooser session and the system reclaims the
-     * new backgrounded chooser process. In such example, upon navigating back to the session, a
-     * restored Chooser should register a new {@link ChooserController}.
-     */
-    @Nullable
-    public ChooserController getChooserController() {
-        return mChooserSession.getChooserController();
-    }
-
-    /**
-     * @param listener make sure that the callback is cleared at the end of a component's lifecycle
-     * (e.g. Activity) or provide a properly maintained WeakReference wrapper to avoid memory leaks.
-     */
-    public void addUpdateListener(
-            @NonNull Executor executor, @NonNull UpdateListener listener) {
-        if (executor == null) {
-            throw new IllegalArgumentException("executor should not be null");
-        }
-        if (listener == null) {
-            throw new IllegalArgumentException("listener should not be null");
-        }
-        mChooserSession.addUpdateListener(executor, listener);
-    }
-
-    /**
-     * Removes a previously added UpdateListener callback.
-     */
-    public void removeUpdateListener(@NonNull UpdateListener listener) {
-        if (listener == null) {
-            throw new IllegalArgumentException("listener should not be null");
-        }
-        mChooserSession.removeUpdateListener(listener);
-    }
-
-    // Just to hide Chooser binder object from the client.
-    private static class ChooserControllerWrapper implements ChooserController {
-        public final IChooserController controller;
-
-        private ChooserControllerWrapper(IChooserController controller) {
-            this.controller = controller;
-        }
-
-        @Override
-        public void updateIntent(Intent intent) throws RemoteException {
-            controller.updateIntent(intent);
-        }
-
-        @Override
-        public void collapse() throws RemoteException {
-            // TODO: implement
-        }
-
-        @Override
-        public void setTargetsEnabled(boolean isEnabled) throws RemoteException {
-            // TODO: implement
-        }
-    }
-
-    private static class ChooserSessionImpl extends IChooserControllerCallback.Stub {
-        private final Object mListenerLock = new Object();
-        @GuardedBy("mListenerLock")
-        private Map<UpdateListener, UpdateListenerWrapper> mListenerMap = new HashMap<>();
-        private final AtomicBoolean mIsActive = new AtomicBoolean(true);
-
-        private final Object mControllerLock = new Object();
-
-        @GuardedBy("mControllerLock")
-        @Nullable
-        private volatile ChooserControllerWrapper mChooserController;
-
-        @GuardedBy("mControllerLock")
-        @Nullable
-        private volatile IBinder.DeathRecipient mChooserControllerLinkToDeath;
-
-        @Override
-        public void registerChooserController(
-                @Nullable final IChooserController chooserController) {
-            if (chooserController == null) {
-                // Interaction session did not start.
-                onClosed();
-                return;
-            }
-            Log.d(
-                    TAG,
-                    "setIntentUpdater; isOpen: " + mIsActive
-                            + ", chooserController: " + chooserController);
-            if (!mIsActive.get()) {
-                // close Chooser
-                safeUpdateChooserIntent(chooserController, null);
-                return;
-            }
-            ChooserControllerWrapper controllerWrapper;
-            synchronized (mControllerLock) {
-                if (areEqual(mChooserController, chooserController)) {
-                    return;
-                }
-                disconnectCurrentController();
-                controllerWrapper = connectController(chooserController);
-            }
-            if (controllerWrapper == null) {
-                // we've got a binder that had died, notify session closed
-                onClosed();
-            } else {
-                notifyListeners((listener -> {
-                    if (mIsActive.get()) {
-                        listener.onChooserConnected(controllerWrapper);
-                    }
-                }));
-            }
-        }
-
-        @Override
-        public void onSizeChanged(Rect size) {
-            if (mIsActive.get()) {
-                notifyListeners((listener) -> {
-                    if (mIsActive.get()) {
-                        listener.onSizeChanged(size);
-                    }
-                });
-            }
-        }
-
-        @Override
-        public void onClosed() {
-            doClose(true);
-        }
-
-        public boolean isActive() {
-            return mIsActive.get();
-        }
-
-        public void close() {
-            doClose(false);
-        }
-
-        @Nullable
-        public ChooserController getChooserController() {
-            synchronized (mControllerLock) {
-                return mChooserController;
-            }
-        }
-
-        public void addUpdateListener(Executor executor, UpdateListener listener) {
-            synchronized (mListenerLock) {
-                if (!mListenerMap.containsKey(listener)) {
-                    mListenerMap = new HashMap<>(mListenerMap);
-                    mListenerMap.put(listener, new UpdateListenerWrapper(listener, executor));
-                }
-            }
-        }
-
-        public void removeUpdateListener(UpdateListener listener) {
-            synchronized (mListenerLock) {
-                if (mListenerMap.containsKey(listener)) {
-                    mListenerMap = new HashMap<>(mListenerMap);
-                    UpdateListenerWrapper lw = mListenerMap.remove(listener);
-                    lw.isSubscribed.set(false);
-                }
-            }
-        }
-
-        private void notifyListeners(Consumer<UpdateListener> block) {
-            Collection<UpdateListenerWrapper> listeners;
-            synchronized (mListenerLock) {
-                listeners = mListenerMap.values();
-            }
-            for (UpdateListenerWrapper lw: listeners) {
-                lw.executor.execute(() -> {
-                    if (lw.isSubscribed.get()) {
-                        block.accept(lw.listener);
-                    }
-                });
-            }
-        }
-
-        private void doClose(boolean isClosedByChooser) {
-            boolean wasActive = mIsActive.compareAndSet(true, false);
-            synchronized (mControllerLock) {
-                if (!isClosedByChooser && mChooserController != null) {
-                    safeUpdateChooserIntent(mChooserController.controller, null);
-                }
-                disconnectCurrentController();
-            }
-            if (wasActive && isClosedByChooser) {
-                notifyListeners((UpdateListener::onClosed));
-            }
-            synchronized (mListenerLock) {
-                mListenerMap = Collections.emptyMap();
-            }
-        }
-
-        @GuardedBy("mControllerLock")
-        private void disconnectCurrentController() {
-            if (mChooserController != null && mChooserControllerLinkToDeath != null) {
-                safeUnlinkToDeath(
-                        mChooserController.controller.asBinder(), mChooserControllerLinkToDeath);
-            }
-            mChooserController = null;
-            mChooserControllerLinkToDeath = null;
-        }
-
-        @GuardedBy("mControllerLock")
-        private ChooserControllerWrapper connectController(IChooserController chooserController) {
-            ChooserControllerWrapper controllerWrapper =
-                    new ChooserControllerWrapper(chooserController);
-            this.mChooserController = controllerWrapper;
-            mChooserControllerLinkToDeath = createDeathRecipient(chooserController);
-            try {
-                chooserController.asBinder().linkToDeath(mChooserControllerLinkToDeath, 0);
-            } catch (RemoteException e) {
-                // binder has already died
-                mChooserController = null;
-                mChooserControllerLinkToDeath = null;
-                controllerWrapper = null;
-            }
-            return controllerWrapper;
-        }
-
-        private IBinder.DeathRecipient createDeathRecipient(IChooserController chooserController) {
-            return () -> {
-                Log.d(TAG, "chooser died");
-                boolean shouldClose = false;
-                synchronized (mControllerLock) {
-                    if (areEqual(mChooserController, chooserController)) {
-                        // is it ever true?
-                        mChooserController = null;
-                        mChooserControllerLinkToDeath = null;
-                        shouldClose = true;
-                    }
-                }
-                if (shouldClose) {
-                    doClose(true);
-                }
-            };
-        }
-
-        private static void safeUpdateChooserIntent(
-                IChooserController chooserController, @Nullable Intent chooserIntent) {
-            try {
-                chooserController.updateIntent(chooserIntent);
-            } catch (RemoteException ignored) {
-            }
-        }
-
-        private static void safeUnlinkToDeath(IBinder binder, IBinder.DeathRecipient linkToDeath) {
-            try {
-                binder.unlinkToDeath(linkToDeath, 0);
-            } catch (Exception ignored) {
-            }
-        }
-
-        private static boolean areEqual(
-                @Nullable ChooserControllerWrapper wrapper, @Nullable IChooserController right) {
-            IChooserController left = wrapper == null ? null : wrapper.controller;
-            if (left == null && right == null) {
-                return true;
-            }
-            if (left == null || right == null) {
-                return false;
-            }
-            return left.asBinder().equals(right.asBinder());
-        }
-    }
-
-    private static class UpdateListenerWrapper {
-        public final ChooserSession.UpdateListener listener;
-        public final Executor executor;
-        public final AtomicBoolean isSubscribed = new AtomicBoolean(true);
-
-        UpdateListenerWrapper(ChooserSession.UpdateListener listener, Executor executor) {
-            this.listener = listener;
-            this.executor = executor;
-        }
-    }
-}
diff --git a/apps/ShareTest/src/com/android/sharetest/InteractiveShareTestActivity.kt b/apps/ShareTest/src/com/android/sharetest/InteractiveShareTestActivity.kt
index 51bcffa..3bba9d9 100644
--- a/apps/ShareTest/src/com/android/sharetest/InteractiveShareTestActivity.kt
+++ b/apps/ShareTest/src/com/android/sharetest/InteractiveShareTestActivity.kt
@@ -28,9 +28,10 @@
 import android.graphics.Rect
 import android.os.Bundle
 import android.provider.MediaStore
+import android.service.chooser.ChooserManager
 import android.service.chooser.ChooserSession
-import android.service.chooser.ChooserSession.ChooserController
 import android.util.Log
+import android.widget.Toast
 import androidx.activity.compose.setContent
 import androidx.activity.viewModels
 import androidx.compose.foundation.clickable
@@ -82,6 +83,7 @@
     private var chooserWindowTopOffset = MutableStateFlow(-1)
     private val isInMultiWindowMode = MutableStateFlow<Boolean>(false)
     private val viewModel: InteractiveShareTestViewModel by viewModels()
+    private lateinit var chooserManager: ChooserManager
     private val chooserSession: MutableStateFlow<ChooserSession?>
         get() = viewModel.chooserSession
 
@@ -99,17 +101,17 @@
         }
 
     private val sessionStateListener =
-        object : ChooserSession.UpdateListener {
-            override fun onChooserConnected(chooserController: ChooserController) {
-                Log.d(TAG, "onChooserConnected")
+        object : ChooserSession.StateListener {
+            override fun onStateChanged(state: Int) {
+                if (state == ChooserSession.STATE_STARTED) {
+                    Log.d(TAG, "onChooserConnected")
+                } else if (state == ChooserSession.STATE_CLOSED) {
+                    Log.d(TAG, "onSessionClosed")
+                    chooserSession.value = null
+                }
             }
 
-            override fun onClosed() {
-                Log.d(TAG, "onSessionClosed")
-                chooserSession.value = null
-            }
-
-            override fun onSizeChanged(size: Rect) {
+            override fun onBoundsChanged(size: Rect) {
                 Log.d(TAG, "onSizeChanged")
                 chooserWindowTopOffset.value = size.top
             }
@@ -119,6 +121,14 @@
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
 
+        val cm = getSystemService(ChooserManager::class.java)
+        if (cm == null) {
+            Toast.makeText(this, "ChooserManager is not available", Toast.LENGTH_LONG).show()
+            finish()
+            return
+        }
+        chooserManager = cm
+
         isInMultiWindowMode.value = isInMultiWindowMode()
 
         lifecycleScope.launch {
@@ -126,7 +136,7 @@
                 chooserSession
                     .scan<ChooserSession?, ChooserSession?>(null) { prevSession, newSession ->
                         prevSession?.close()
-                        newSession?.addUpdateListener(mainExecutor, sessionStateListener)
+                        newSession?.addStateListener(mainExecutor, sessionStateListener)
                         newSession
                     }
                     .collect {}
@@ -279,7 +289,7 @@
         if (useRefinementFlow.value) {
             unregisterReceiver(refinementReceiver)
         }
-        chooserSession.value?.removeUpdateListener(sessionStateListener)
+        chooserSession.value?.removeStateListener(sessionStateListener)
         super.onDestroy()
     }
 
@@ -373,7 +383,7 @@
     }
 
     private fun startOrUpdate(chooserIntent: Intent) {
-        val chooserController = chooserSession.value?.takeIf { it.isActive }?.chooserController
+        val session = chooserSession.value?.takeIf { it.isActive }
         if (useRefinementFlow.value) {
             chooserIntent.putExtra(
                 Intent.EXTRA_CHOOSER_REFINEMENT_INTENT_SENDER,
@@ -381,14 +391,17 @@
             )
         }
         chooserIntent.putExtra(EXTRA_CHOOSER_RESULT_INTENT_SENDER, createResultIntentSender(this))
-        if (chooserController == null) {
-            ChooserSession().also { chooserSession.value = it }.start(this, chooserIntent)
+        if (session == null) {
+            chooserManager.startSession(this, chooserIntent).also { chooserSession.value = it }
         } else {
-            chooserController.updateIntent(chooserIntent)
+            session.updateIntent(chooserIntent)
         }
     }
 }
 
+private val ChooserSession.isActive: Boolean
+    get() = state != ChooserSession.STATE_CLOSED
+
 private fun Intent.setColorScheme(colorScheme: Int) {
     putExtra("com.android.extra.CHOOSER_COLOR_SCHEME", colorScheme)
 }