Loading core/api/current.txt +2 −1 Original line number Diff line number Diff line Loading @@ -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 core/java/android/app/SystemServiceRegistry.java +54 −3 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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(); Loading Loading @@ -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 Loading Loading @@ -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); } Loading @@ -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 Loading core/java/android/content/Context.java +4 −0 Original line number Diff line number Diff line Loading @@ -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 */ Loading core/java/android/service/chooser/ChooserSession.java +12 −11 Original line number Diff line number Diff line Loading @@ -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(); Loading Loading @@ -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(); } /** Loading Loading @@ -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( Loading Loading @@ -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); } }); } Loading core/tests/coretests/src/android/service/chooser/ChooserSessionTest.kt +19 −19 Original line number Diff line number Diff line Loading @@ -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) Loading Loading @@ -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) Loading @@ -142,7 +142,7 @@ class ChooserSessionTest { assertThat(state).isEqualTo(ChooserSession.STATE_CLOSED) } override fun onBoundsChanged(size: Rect) { override fun onBoundsChanged(bounds: Rect) { invocationCounter.incrementAndGet() } } Loading Loading @@ -178,7 +178,7 @@ class ChooserSessionTest { assertThat(state).isEqualTo(ChooserSession.STATE_CLOSED) } override fun onBoundsChanged(size: Rect) { override fun onBoundsChanged(bounds: Rect) { invocationCounter.incrementAndGet() } } Loading Loading @@ -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) Loading Loading
core/api/current.txt +2 −1 Original line number Diff line number Diff line Loading @@ -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
core/java/android/app/SystemServiceRegistry.java +54 −3 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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(); Loading Loading @@ -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 Loading Loading @@ -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); } Loading @@ -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 Loading
core/java/android/content/Context.java +4 −0 Original line number Diff line number Diff line Loading @@ -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 */ Loading
core/java/android/service/chooser/ChooserSession.java +12 −11 Original line number Diff line number Diff line Loading @@ -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(); Loading Loading @@ -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(); } /** Loading Loading @@ -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( Loading Loading @@ -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); } }); } Loading
core/tests/coretests/src/android/service/chooser/ChooserSessionTest.kt +19 −19 Original line number Diff line number Diff line Loading @@ -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) Loading Loading @@ -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) Loading @@ -142,7 +142,7 @@ class ChooserSessionTest { assertThat(state).isEqualTo(ChooserSession.STATE_CLOSED) } override fun onBoundsChanged(size: Rect) { override fun onBoundsChanged(bounds: Rect) { invocationCounter.incrementAndGet() } } Loading Loading @@ -178,7 +178,7 @@ class ChooserSessionTest { assertThat(state).isEqualTo(ChooserSession.STATE_CLOSED) } override fun onBoundsChanged(size: Rect) { override fun onBoundsChanged(bounds: Rect) { invocationCounter.incrementAndGet() } } Loading Loading @@ -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) Loading