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

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

Chooser Interactive Session API, add setTargetsEnabled method.

Renane ChooserSession getSize to getBounds for a consistent naming.
Do not regiser ChooserManager on wearables, TVs, and Autos.

Bug: 404593897
Test: cts tests
Test: atest FrameworksCoreTests:android.service.chooser.ChooserManagerTest
Test: atest FrameworksCoreTests:android.service.chooser.ChooserSessionTest
Flag: android.service.chooser.interactive_chooser
Change-Id: I76f16935825b0118898832e496b5b2f25c6727d2
parent 8106795f
Loading
Loading
Loading
Loading
+2 −1
Original line number Diff line number Diff line
@@ -42016,10 +42016,11 @@ package android.service.chooser {
  @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 @Nullable public android.graphics.Rect getBounds();
    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 setTargetsEnabled(boolean);
    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
+54 −3
Original line number Diff line number Diff line
@@ -283,6 +283,7 @@ import android.view.translation.UiTranslationManager;
import android.webkit.WebViewBootstrapFrameworkInitializer;

import com.android.internal.R;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.app.IAppOpsService;
import com.android.internal.app.IBatteryStats;
import com.android.internal.app.ISoundTriggerService;
@@ -1857,8 +1858,15 @@ public final class SystemServiceRegistry {
                });

        if (interactiveChooser()) {
            registerService(Context.CHOOSER_SERVICE, ChooserManager.class,
            registerService(
                    Context.CHOOSER_SERVICE,
                    ChooserManager.class,
                    new StaticServiceFetcher<>() {
                        @Override
                        public boolean isServiceEnabled(ContextImpl ctx) {
                            return isChooserManagerSupported(ctx);
                        }

                        @Override
                        public ChooserManager createService() {
                            return new ChooserManager();
@@ -2008,12 +2016,29 @@ public final class SystemServiceRegistry {
                    }
                    break;
            }
            // TODO (b/404593897): make it a case of the switch statement above when the flag is
            //  removed.
            if (interactiveChooser()) {
                if (Context.CHOOSER_SERVICE.equals(name) && !isChooserManagerSupported(ctx)) {
                    return null;
                }
            }
            Slog.wtf(TAG, "Manager wrapper not available: " + name);
            return null;
        }
        return ret;
    }

    private static boolean isChooserManagerSupported(ContextImpl ctx) {
        PackageManager pm = ctx.getPackageManager();
        if (pm == null) {
            return true;
        }
        return !pm.hasSystemFeature(PackageManager.FEATURE_WATCH)
                && !pm.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
                && !pm.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE);
    }

    /**
     * Gets a system service which has opted-in to being fetched without a context.
     * @hide
@@ -2452,14 +2477,36 @@ public final class SystemServiceRegistry {
     * and should be cached and retained process-wide.
     */
    static abstract class StaticServiceFetcher<T> implements ServiceFetcher<T> {
        /**
         * Indicates whether a service reference has been cached.
         * The cached reference can be {@code null} if the service is not available i.e.
         * {@link #isServiceEnabled(ContextImpl)} returns {@code false}.
         */
        @GuardedBy("StaticServiceFetcher.this")
        private boolean mIsCached = false;
        @GuardedBy("StaticServiceFetcher.this")
        private T mCachedInstance;

        @Override
        public final T getService(ContextImpl ctx) {
            synchronized (StaticServiceFetcher.this) {
                if (mCachedInstance == null) {
                if (mIsCached) {
                    return mCachedInstance;
                }
            }
            // In case isServiceEnabled would require an IPC, run it outside a synchronized block.
            boolean isEnabled = isServiceEnabled(ctx);
            synchronized (StaticServiceFetcher.this) {
                if (!mIsCached) {
                    try {
                        if (isEnabled) {
                            mCachedInstance = createService();
                            // for a bug-to-bug compatibility, do not cache null-references
                            mIsCached = mCachedInstance != null;
                        } else {
                            mCachedInstance = null;
                            mIsCached = true;
                        }
                    } catch (ServiceNotFoundException e) {
                        onServiceNotFound(e);
                    }
@@ -2468,6 +2515,10 @@ public final class SystemServiceRegistry {
            }
        }

        protected boolean isServiceEnabled(ContextImpl ctx) {
            return true;
        }

        public abstract T createService() throws ServiceNotFoundException;

        // Services that do not need a Context can potentially be fetched without one, but the
+4 −0
Original line number Diff line number Diff line
@@ -7005,6 +7005,10 @@ public abstract class Context {
     * Use with {@link #getSystemService(String)} to retrieve a
     * {@link android.service.chooser.ChooserManager}.
     *
     * <p class="note"><b>Note:</b> This service is not available on Wear OS, Android TV, or Android
     * Auto devices. On these form factors, calls to {@code #getSystemService} for this service will
     * return {@code null}.
     *
     * @see #getSystemService(String)
     * @see android.service.chooser.ChooserManager
     */
+12 −11
Original line number Diff line number Diff line
@@ -96,7 +96,7 @@ public final class ChooserSession {
         * Gets invoked when the Chooser bounds are changed. The rect parameter represents Chooser
         * window bounds in pixels.
         */
        void onBoundsChanged(@NonNull Rect size);
        void onBoundsChanged(@NonNull Rect bounds);
    }

    private final ChooserSessionImpl mChooserSession = new ChooserSessionImpl();
@@ -177,25 +177,26 @@ public final class ChooserSession {
    }

    /**
     * Sets whether the targets in the chooser UI are enabled.
     * Sets whether the targets in the chooser UI are enabled. By default targets are 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.
     * <p>A no-op when the session is not in the {@link #STATE_STARTED}.</p>
     *
     * @hide
     */
    public void setTargetsEnabled(boolean isEnabled) {
        mChooserSession.setTargetsEnabled(isEnabled);
    }

    /**
     * Get last reported Chooser size or null.
     * Gets the last bounds reported by the Chooser.
     *
     * @return the most recently reported Chooser bounds, or {@code null} if bounds have not yet
     * been received via {@link ChooserSession.StateListener#onBoundsChanged(Rect)}.
     */
    @Nullable
    public Rect getSize() {
        return mChooserSession.mSize.get();
    public Rect getBounds() {
        return mChooserSession.mBounds.get();
    }

    /**
@@ -237,7 +238,7 @@ public final class ChooserSession {
        @ChooserSession.State
        private int mState = STATE_INITIALIZED;

        private final AtomicReference<Rect> mSize = new AtomicReference<>();
        private final AtomicReference<Rect> mBounds = new AtomicReference<>();

        @Override
        public void registerChooserController(
@@ -282,11 +283,11 @@ public final class ChooserSession {
        }

        @Override
        public void onBoundsChanged(Rect size) {
            mSize.set(size);
        public void onBoundsChanged(@NonNull Rect bounds) {
            mBounds.set(bounds);
            notifyListeners((listener) -> {
                if (isActive()) {
                    listener.onBoundsChanged(size);
                    listener.onBoundsChanged(bounds);
                }
            });
        }
+19 −19
Original line number Diff line number Diff line
@@ -56,7 +56,7 @@ class ChooserSessionTest {
                    assertThat(state).isEqualTo(ChooserSession.STATE_STARTED)
                }

                override fun onBoundsChanged(size: Rect) {}
                override fun onBoundsChanged(bounds: Rect) {}
            }
        session.addStateListener(ImmediateExecutor(), stateListener)

@@ -98,28 +98,28 @@ class ChooserSessionTest {

    @EnableFlags(Flags.FLAG_INTERACTIVE_CHOOSER)
    @Test
    fun test_chooserSizeChanged_sizeReported() {
    fun test_chooserBoundsChanged_boundsReported() {
        val (session, controllerCallback) = prepareChooserSession()
        val sizes = listOf(Rect(1, 2, 3, 4), Rect(5, 6, 7, 8))
        val sizeUpdates = mutableListOf<Rect>()
        val bounds = listOf(Rect(1, 2, 3, 4), Rect(5, 6, 7, 8))
        val boundsUpdates = mutableListOf<Rect>()
        val stateListener =
            object : ChooserSession.StateListener {
                override fun onStateChanged(state: Int) {}

                override fun onBoundsChanged(size: Rect) {
                    assertThat(session.size).isEqualTo(size)
                    sizeUpdates.add(size)
                override fun onBoundsChanged(bounds: Rect) {
                    assertThat(session.bounds).isEqualTo(bounds)
                    boundsUpdates.add(bounds)
                }
            }
        session.addStateListener(ImmediateExecutor(), stateListener)

        assertThat(session.size).isNull()
        assertThat(session.bounds).isNull()

        for (size in sizes) {
            controllerCallback.onBoundsChanged(size)
        for (b in bounds) {
            controllerCallback.onBoundsChanged(b)
        }

        assertThat(sizeUpdates).containsExactlyElementsIn(sizes).inOrder()
        assertThat(boundsUpdates).containsExactlyElementsIn(bounds).inOrder()
    }

    @EnableFlags(Flags.FLAG_INTERACTIVE_CHOOSER)
@@ -142,7 +142,7 @@ class ChooserSessionTest {
                    assertThat(state).isEqualTo(ChooserSession.STATE_CLOSED)
                }

                override fun onBoundsChanged(size: Rect) {
                override fun onBoundsChanged(bounds: Rect) {
                    invocationCounter.incrementAndGet()
                }
            }
@@ -178,7 +178,7 @@ class ChooserSessionTest {
                    assertThat(state).isEqualTo(ChooserSession.STATE_CLOSED)
                }

                override fun onBoundsChanged(size: Rect) {
                override fun onBoundsChanged(bounds: Rect) {
                    invocationCounter.incrementAndGet()
                }
            }
@@ -224,12 +224,12 @@ class ChooserSessionTest {
        session.removeStateListener(firstListener)
        controllerCallback.onBoundsChanged(secondSize)

        var sizeCapture = argumentCaptor<Rect>()
        verify(firstListener) { 1 * { onBoundsChanged(sizeCapture.capture()) } }
        assertThat(sizeCapture.firstValue).isEqualTo(firstSize)
        sizeCapture = argumentCaptor<Rect>()
        verify(secondListener) { 1 * { onBoundsChanged(sizeCapture.capture()) } }
        assertThat(sizeCapture.firstValue).isEqualTo(secondSize)
        var boundsCapture = argumentCaptor<Rect>()
        verify(firstListener) { 1 * { onBoundsChanged(boundsCapture.capture()) } }
        assertThat(boundsCapture.firstValue).isEqualTo(firstSize)
        boundsCapture = argumentCaptor<Rect>()
        verify(secondListener) { 1 * { onBoundsChanged(boundsCapture.capture()) } }
        assertThat(boundsCapture.firstValue).isEqualTo(secondSize)
    }

    @EnableFlags(Flags.FLAG_INTERACTIVE_CHOOSER)