diff options
-rw-r--r-- | core/api/current.txt | 3 | ||||
-rw-r--r-- | core/java/android/app/ContextImpl.java | 144 | ||||
-rw-r--r-- | core/java/android/content/Context.java | 103 | ||||
-rw-r--r-- | core/java/android/content/ContextWrapper.java | 26 | ||||
-rw-r--r-- | core/java/com/android/internal/policy/DecorContext.java | 9 | ||||
-rw-r--r-- | test-mock/src/android/test/mock/MockContext.java | 6 |
6 files changed, 281 insertions, 10 deletions
diff --git a/core/api/current.txt b/core/api/current.txt index 1ba99f995ec7..38db3ee16d1e 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -9851,6 +9851,7 @@ package android.content { method @Deprecated public abstract int getWallpaperDesiredMinimumHeight(); method @Deprecated public abstract int getWallpaperDesiredMinimumWidth(); method public abstract void grantUriPermission(String, android.net.Uri, int); + method public boolean isDeviceContext(); method public abstract boolean isDeviceProtectedStorage(); method public boolean isRestricted(); method public boolean isUiContext(); @@ -9866,6 +9867,7 @@ package android.content { method public abstract android.database.sqlite.SQLiteDatabase openOrCreateDatabase(String, int, android.database.sqlite.SQLiteDatabase.CursorFactory, @Nullable android.database.DatabaseErrorHandler); method @Deprecated public abstract android.graphics.drawable.Drawable peekWallpaper(); method public void registerComponentCallbacks(android.content.ComponentCallbacks); + method public void registerDeviceIdChangeListener(@NonNull java.util.concurrent.Executor, @NonNull java.util.function.IntConsumer); method @Nullable public abstract android.content.Intent registerReceiver(@Nullable android.content.BroadcastReceiver, android.content.IntentFilter); method @Nullable public abstract android.content.Intent registerReceiver(@Nullable android.content.BroadcastReceiver, android.content.IntentFilter, int); method @Nullable public abstract android.content.Intent registerReceiver(android.content.BroadcastReceiver, android.content.IntentFilter, @Nullable String, @Nullable android.os.Handler); @@ -9905,6 +9907,7 @@ package android.content { method public abstract boolean stopService(android.content.Intent); method public abstract void unbindService(@NonNull android.content.ServiceConnection); method public void unregisterComponentCallbacks(android.content.ComponentCallbacks); + method public void unregisterDeviceIdChangeListener(@NonNull java.util.function.IntConsumer); method public abstract void unregisterReceiver(android.content.BroadcastReceiver); method public void updateServiceGroup(@NonNull android.content.ServiceConnection, int, int); field public static final String ACCESSIBILITY_SERVICE = "accessibility"; diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java index 39f71539b380..48a4daeddbcd 100644 --- a/core/java/android/app/ContextImpl.java +++ b/core/java/android/app/ContextImpl.java @@ -21,12 +21,14 @@ import static android.content.pm.PackageManager.PERMISSION_GRANTED; import static android.os.StrictMode.vmIncorrectContextUseEnabled; import static android.view.WindowManager.LayoutParams.WindowType; +import android.annotation.CallbackExecutor; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UiContext; import android.companion.virtual.VirtualDevice; import android.companion.virtual.VirtualDeviceManager; +import android.companion.virtual.VirtualDeviceParams; import android.compat.annotation.UnsupportedAppUsage; import android.content.AttributionSource; import android.content.AutofillOptions; @@ -121,6 +123,7 @@ import java.util.List; import java.util.Objects; import java.util.Set; import java.util.concurrent.Executor; +import java.util.function.IntConsumer; class ReceiverRestrictedContext extends ContextWrapper { @UnsupportedAppUsage @@ -257,6 +260,13 @@ class ContextImpl extends Context { /** @see Context#isConfigurationContext() */ private boolean mIsConfigurationBasedContext; + /** + * Indicates that this context was created with an explicit device ID association via + * Context#createDeviceContext and under no circumstances will it ever change, even if + * this context is not associated with a display id, or if the associated display id changes. + */ + private boolean mIsExplicitDeviceId = false; + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) private final int mFlags; @@ -372,6 +382,24 @@ class ContextImpl extends Context { @ServiceInitializationState final int[] mServiceInitializationStateArray = new int[mServiceCache.length]; + private final Object mDeviceIdListenerLock = new Object(); + /** + * List of listeners for deviceId changes and their associated Executor. + * List is lazy-initialized on first registration + */ + @GuardedBy("mDeviceIdListenerLock") + @Nullable + private ArrayList<DeviceIdChangeListenerDelegate> mDeviceIdChangeListeners; + + private static class DeviceIdChangeListenerDelegate { + final @NonNull IntConsumer mListener; + final @NonNull Executor mExecutor; + DeviceIdChangeListenerDelegate(IntConsumer listener, Executor executor) { + mListener = listener; + mExecutor = executor; + } + } + @UnsupportedAppUsage static ContextImpl getImpl(Context context) { Context nextContext; @@ -2699,15 +2727,7 @@ class ContextImpl extends Context { @Override public @NonNull Context createDeviceContext(int deviceId) { - boolean validDeviceId = deviceId == VirtualDeviceManager.DEVICE_ID_DEFAULT; - if (deviceId > VirtualDeviceManager.DEVICE_ID_DEFAULT) { - VirtualDeviceManager vdm = getSystemService(VirtualDeviceManager.class); - if (vdm != null) { - List<VirtualDevice> virtualDevices = vdm.getVirtualDevices(); - validDeviceId = virtualDevices.stream().anyMatch(d -> d.getDeviceId() == deviceId); - } - } - if (!validDeviceId) { + if (!isValidDeviceId(deviceId)) { throw new IllegalArgumentException( "Not a valid ID of the default device or any virtual device: " + deviceId); } @@ -2718,9 +2738,35 @@ class ContextImpl extends Context { mSplitName, mToken, mUser, mFlags, mClassLoader, null); context.mDeviceId = deviceId; + context.mIsExplicitDeviceId = true; return context; } + /** + * Checks whether the passed {@code deviceId} is valid or not. + * {@link VirtualDeviceManager#DEVICE_ID_DEFAULT} is valid as it is the ID of the default + * device when no additional virtual devices exist. If {@code deviceId} is the id of + * a virtual device, it should correspond to a virtual device created by + * {@link VirtualDeviceManager#createVirtualDevice(int, VirtualDeviceParams)}. + */ + private boolean isValidDeviceId(int deviceId) { + if (deviceId == VirtualDeviceManager.DEVICE_ID_DEFAULT) { + return true; + } + if (deviceId > VirtualDeviceManager.DEVICE_ID_DEFAULT) { + VirtualDeviceManager vdm = getSystemService(VirtualDeviceManager.class); + if (vdm != null) { + List<VirtualDevice> virtualDevices = vdm.getVirtualDevices(); + for (int i = 0; i < virtualDevices.size(); i++) { + if (virtualDevices.get(i).getDeviceId() == deviceId) { + return true; + } + } + } + } + return false; + } + @NonNull @Override public WindowContext createWindowContext(@WindowType int type, @@ -2965,6 +3011,21 @@ class ContextImpl extends Context { if (mContextType == CONTEXT_TYPE_NON_UI) { mContextType = CONTEXT_TYPE_DISPLAY_CONTEXT; } + // TODO(b/253201821): Update deviceId when display is updated. + } + + @Override + public void updateDeviceId(int updatedDeviceId) { + if (!isValidDeviceId(updatedDeviceId)) { + throw new IllegalArgumentException( + "Not a valid ID of the default device or any virtual device: " + mDeviceId); + } + if (mIsExplicitDeviceId) { + throw new UnsupportedOperationException( + "Cannot update device ID on a Context created with createDeviceContext()"); + } + mDeviceId = updatedDeviceId; + notifyOnDeviceChangedListeners(updatedDeviceId); } @Override @@ -2973,6 +3034,69 @@ class ContextImpl extends Context { } @Override + public boolean isDeviceContext() { + return mIsExplicitDeviceId || isAssociatedWithDisplay(); + } + + @Override + public void registerDeviceIdChangeListener(@NonNull @CallbackExecutor Executor executor, + @NonNull IntConsumer listener) { + Objects.requireNonNull(executor, "executor cannot be null"); + Objects.requireNonNull(listener, "listener cannot be null"); + + synchronized (mDeviceIdListenerLock) { + if (getDeviceIdListener(listener) != null) { + throw new IllegalArgumentException( + "attempt to call registerDeviceIdChangeListener() " + + "on a previously registered listener"); + } + // lazy initialization + if (mDeviceIdChangeListeners == null) { + mDeviceIdChangeListeners = new ArrayList<>(); + } + mDeviceIdChangeListeners.add(new DeviceIdChangeListenerDelegate(listener, executor)); + } + } + + @Override + public void unregisterDeviceIdChangeListener(@NonNull IntConsumer listener) { + Objects.requireNonNull(listener, "listener cannot be null"); + synchronized (mDeviceIdListenerLock) { + DeviceIdChangeListenerDelegate listenerToRemove = getDeviceIdListener(listener); + if (listenerToRemove != null) { + mDeviceIdChangeListeners.remove(listenerToRemove); + } + } + } + + @GuardedBy("mDeviceIdListenerLock") + @Nullable + private DeviceIdChangeListenerDelegate getDeviceIdListener( + @Nullable IntConsumer listener) { + if (mDeviceIdChangeListeners == null) { + return null; + } + for (int i = 0; i < mDeviceIdChangeListeners.size(); i++) { + DeviceIdChangeListenerDelegate delegate = mDeviceIdChangeListeners.get(i); + if (delegate.mListener == listener) { + return delegate; + } + } + return null; + } + + private void notifyOnDeviceChangedListeners(int deviceId) { + synchronized (mDeviceIdListenerLock) { + if (mDeviceIdChangeListeners != null) { + for (DeviceIdChangeListenerDelegate delegate : mDeviceIdChangeListeners) { + delegate.mExecutor.execute(() -> + delegate.mListener.accept(deviceId)); + } + } + } + } + + @Override public DisplayAdjustments getDisplayAdjustments(int displayId) { return mResources.getDisplayAdjustments(); } @@ -3227,6 +3351,8 @@ class ContextImpl extends Context { opPackageName = container.mOpPackageName; setResources(container.mResources); mDisplay = container.mDisplay; + mDeviceId = container.mDeviceId; + mIsExplicitDeviceId = container.mIsExplicitDeviceId; mForceDisplayOverrideInResources = container.mForceDisplayOverrideInResources; mIsConfigurationBasedContext = container.mIsConfigurationBasedContext; mContextType = container.mContextType; diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index 382e2bb6ee43..7e6574197ed2 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -109,6 +109,7 @@ import java.util.Collections; import java.util.List; import java.util.concurrent.Executor; import java.util.function.Consumer; +import java.util.function.IntConsumer; /** * Interface to global information about an application environment. This is @@ -6906,6 +6907,10 @@ public abstract class Context { * {@link android.companion.virtual.VirtualDeviceManager#DEVICE_ID_DEFAULT}. Similarly, * applications running on the default device may access the functionality of virtual devices. * </p> + * <p> + * Note that the newly created instance will be associated with the same display as the parent + * Context, regardless of the device ID passed here. + * </p> * @param deviceId The ID of the device to associate with this context. * @return A context associated with the given device ID. * @@ -7241,20 +7246,116 @@ public abstract class Context { public abstract void updateDisplay(int displayId); /** - * Get the device ID this context is associated with. Applications can use this method to + * Updates the device ID association of this Context. Since a Context created with + * {@link #createDeviceContext} cannot change its device association, this method must + * not be called for instances created with {@link #createDeviceContext}. + * + * @param deviceId The new device ID to assign to this Context. + * @throws UnsupportedOperationException if the method is called on an instance that was + * created with {@link Context#createDeviceContext(int)} + * @throws IllegalArgumentException if the given device ID is not a valid ID of the default + * device or a virtual device. + * + * @see #isDeviceContext() + * @see #createDeviceContext(int) + * @hide + */ + public void updateDeviceId(int deviceId) { + throw new RuntimeException("Not implemented. Must override in a subclass."); + } + + /** + * Gets the device ID this context is associated with. Applications can use this method to * determine whether they are running on a virtual device and identify that device. * * The device ID of the host device is * {@link android.companion.virtual.VirtualDeviceManager#DEVICE_ID_DEFAULT} * + * <p> + * If the underlying device ID is changed by the system, for example, when an + * {@link Activity} is moved to a different virtual device, applications can register to listen + * to changes by calling + * {@link Context#registerDeviceIdChangeListener(Executor, IntConsumer)}. + * </p> + * + * <p> + * This method will only return a reliable value for this instance if + * {@link Context#isDeviceContext()} is {@code true}. The system can assign an arbitrary device + * id value for Contexts not logically associated with a device. + * </p> + * * @return the ID of the device this context is associated with. + * @see #isDeviceContext() * @see #createDeviceContext(int) + * @see #registerDeviceIdChangeListener(Executor, IntConsumer) */ public int getDeviceId() { throw new RuntimeException("Not implemented. Must override in a subclass."); } /** + * Indicates whether the value of {@link Context#getDeviceId()} can be relied upon for + * this instance. It will return {@code true} for Contexts created by + * {@link Context#createDeviceContext(int)}, as well as for UI and Display Contexts. + * <p> + * Contexts created with {@link Context#createDeviceContext(int)} will have an explicit + * device association, which will never change. UI Contexts and Display Contexts are + * already associated with a display, so if the device association is not explicitly + * given, {@link Context#getDeviceId()} will return the ID of the device associated with + * the associated display. The system can assign an arbitrary device id value for Contexts not + * logically associated with a device. + * </p> + * + * @return {@code true} if {@link Context#getDeviceId()} is reliable, {@code false} otherwise. + * + * @see #createDeviceContext(int) + * @see #getDeviceId()} + * @see #createDisplayContext(Display) + * @see #isUiContext() + */ + + public boolean isDeviceContext() { + throw new RuntimeException("Not implemented. Must override in a subclass."); + } + + /** + * Adds a new device ID changed listener to the {@code Context}, which will be called when + * the device association is changed by the system. + * <p> + * The callback can be called when an app is moved to a different device and the {@code Context} + * is not explicily associated with a specific device. + * </p> + * <p> When an application receives a device id update callback, this Context is guaranteed to + * also have an updated display ID(if any) and {@link Configuration}. + * <p/> + * @param executor The Executor on whose thread to execute the callbacks of the {@code listener} + * object. + * @param listener The listener {@code IntConsumer} to call which will receive the updated + * device ID. + * + * @see Context#isDeviceContext() + * @see Context#getDeviceId() + * @see Context#createDeviceContext(int) + */ + public void registerDeviceIdChangeListener(@NonNull @CallbackExecutor Executor executor, + @NonNull IntConsumer listener) { + throw new RuntimeException("Not implemented. Must override in a subclass."); + } + + /** + * Removes a device ID changed listener from the Context. It's a no-op if + * the listener is not already registered. + * + * @param listener The {@code Consumer} to remove. + * + * @see #getDeviceId() + * @see #registerDeviceIdChangeListener(Executor, IntConsumer) + */ + public void unregisterDeviceIdChangeListener(@NonNull IntConsumer listener) { + throw new RuntimeException("Not implemented. Must override in a subclass."); + } + + /** * Indicates whether this Context is restricted. * * @return {@code true} if this Context is restricted, {@code false} otherwise. diff --git a/core/java/android/content/ContextWrapper.java b/core/java/android/content/ContextWrapper.java index a1646a172521..0a32dd78092f 100644 --- a/core/java/android/content/ContextWrapper.java +++ b/core/java/android/content/ContextWrapper.java @@ -16,6 +16,7 @@ package android.content; +import android.annotation.CallbackExecutor; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; @@ -61,6 +62,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.concurrent.Executor; +import java.util.function.IntConsumer; /** * Proxying implementation of Context that simply delegates all of its calls to @@ -1171,12 +1173,36 @@ public class ContextWrapper extends Context { mBase.updateDisplay(displayId); } + /** + * @hide + */ + @Override + public void updateDeviceId(int deviceId) { + mBase.updateDeviceId(deviceId); + } + @Override public int getDeviceId() { return mBase.getDeviceId(); } @Override + public boolean isDeviceContext() { + return mBase.isDeviceContext(); + } + + @Override + public void registerDeviceIdChangeListener(@NonNull @CallbackExecutor Executor executor, + @NonNull IntConsumer listener) { + mBase.registerDeviceIdChangeListener(executor, listener); + } + + @Override + public void unregisterDeviceIdChangeListener(@NonNull IntConsumer listener) { + mBase.unregisterDeviceIdChangeListener(listener); + } + + @Override public Context createDeviceProtectedStorageContext() { return mBase.createDeviceProtectedStorageContext(); } diff --git a/core/java/com/android/internal/policy/DecorContext.java b/core/java/com/android/internal/policy/DecorContext.java index 134a91710c0b..63785f270b59 100644 --- a/core/java/com/android/internal/policy/DecorContext.java +++ b/core/java/com/android/internal/policy/DecorContext.java @@ -139,6 +139,15 @@ public class DecorContext extends ContextThemeWrapper { } @Override + public boolean isDeviceContext() { + Context context = mContext.get(); + if (context != null) { + return context.isDeviceContext(); + } + return false; + } + + @Override public boolean isConfigurationContext() { Context context = mContext.get(); if (context != null) { diff --git a/test-mock/src/android/test/mock/MockContext.java b/test-mock/src/android/test/mock/MockContext.java index 8fc8c7d162f4..b63fbe679e23 100644 --- a/test-mock/src/android/test/mock/MockContext.java +++ b/test-mock/src/android/test/mock/MockContext.java @@ -887,6 +887,12 @@ public class MockContext extends Context { throw new UnsupportedOperationException(); } + /** @hide */ + @Override + public void updateDeviceId(int deviceId) { + throw new UnsupportedOperationException(); + } + @Override public int getDeviceId() { throw new UnsupportedOperationException(); |