Merge "Add a switch for update ownership enforcement"
diff --git a/core/api/current.txt b/core/api/current.txt
index 945531f..b63b0aa 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -24224,6 +24224,7 @@
method public int getDisableReason();
method public int getFlags();
method @NonNull public String getRouteId();
+ method public int getSessionParticipantCount();
method public void writeToParcel(@NonNull android.os.Parcel, int);
field @NonNull public static final android.os.Parcelable.Creator<android.media.RouteListingPreference.Item> CREATOR;
field public static final int DISABLE_REASON_AD = 3; // 0x3
@@ -24239,6 +24240,7 @@
method @NonNull public android.media.RouteListingPreference.Item build();
method @NonNull public android.media.RouteListingPreference.Item.Builder setDisableReason(int);
method @NonNull public android.media.RouteListingPreference.Item.Builder setFlags(int);
+ method @NonNull public android.media.RouteListingPreference.Item.Builder setSessionParticipantCount(@IntRange(from=0) int);
}
public final class RoutingSessionInfo implements android.os.Parcelable {
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index c09c6c5..ef074d8 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -4902,17 +4902,15 @@
public final class VirtualTouchscreenConfig extends android.hardware.input.VirtualInputDeviceConfig implements android.os.Parcelable {
method public int describeContents();
- method public int getHeightInPixels();
- method public int getWidthInPixels();
+ method public int getHeight();
+ method public int getWidth();
method public void writeToParcel(@NonNull android.os.Parcel, int);
field @NonNull public static final android.os.Parcelable.Creator<android.hardware.input.VirtualTouchscreenConfig> CREATOR;
}
public static final class VirtualTouchscreenConfig.Builder extends android.hardware.input.VirtualInputDeviceConfig.Builder<android.hardware.input.VirtualTouchscreenConfig.Builder> {
- ctor public VirtualTouchscreenConfig.Builder();
+ ctor public VirtualTouchscreenConfig.Builder(@IntRange(from=1) int, @IntRange(from=1) int);
method @NonNull public android.hardware.input.VirtualTouchscreenConfig build();
- method @NonNull public android.hardware.input.VirtualTouchscreenConfig.Builder setHeightInPixels(int);
- method @NonNull public android.hardware.input.VirtualTouchscreenConfig.Builder setWidthInPixels(int);
}
}
@@ -6606,6 +6604,7 @@
public class AudioManager {
method @Deprecated public int abandonAudioFocus(android.media.AudioManager.OnAudioFocusChangeListener, android.media.AudioAttributes);
method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void addAssistantServicesUids(@NonNull int[]);
+ method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void addOnNonDefaultDevicesForStrategyChangedListener(@NonNull java.util.concurrent.Executor, @NonNull android.media.AudioManager.OnNonDefaultDevicesForStrategyChangedListener) throws java.lang.SecurityException;
method @Deprecated @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void addOnPreferredDeviceForStrategyChangedListener(@NonNull java.util.concurrent.Executor, @NonNull android.media.AudioManager.OnPreferredDeviceForStrategyChangedListener) throws java.lang.SecurityException;
method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void addOnPreferredDevicesForCapturePresetChangedListener(@NonNull java.util.concurrent.Executor, @NonNull android.media.AudioManager.OnPreferredDevicesForCapturePresetChangedListener) throws java.lang.SecurityException;
method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void addOnPreferredDevicesForStrategyChangedListener(@NonNull java.util.concurrent.Executor, @NonNull android.media.AudioManager.OnPreferredDevicesForStrategyChangedListener) throws java.lang.SecurityException;
@@ -6627,6 +6626,7 @@
method @IntRange(from=0) @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public int getMaxVolumeIndexForAttributes(@NonNull android.media.AudioAttributes);
method @IntRange(from=0) @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public int getMinVolumeIndexForAttributes(@NonNull android.media.AudioAttributes);
method @Nullable @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public android.media.AudioDeviceAttributes getMutingExpectedDevice();
+ method @NonNull @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public java.util.List<android.media.AudioDeviceAttributes> getNonDefaultDevicesForStrategy(@NonNull android.media.audiopolicy.AudioProductStrategy);
method @Nullable @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public android.media.AudioDeviceAttributes getPreferredDeviceForStrategy(@NonNull android.media.audiopolicy.AudioProductStrategy);
method @NonNull @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public java.util.List<android.media.AudioDeviceAttributes> getPreferredDevicesForCapturePreset(int);
method @NonNull @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public java.util.List<android.media.AudioDeviceAttributes> getPreferredDevicesForStrategy(@NonNull android.media.audiopolicy.AudioProductStrategy);
@@ -6642,6 +6642,8 @@
method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void registerMuteAwaitConnectionCallback(@NonNull java.util.concurrent.Executor, @NonNull android.media.AudioManager.MuteAwaitConnectionCallback);
method public void registerVolumeGroupCallback(@NonNull java.util.concurrent.Executor, @NonNull android.media.AudioManager.VolumeGroupCallback);
method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void removeAssistantServicesUids(@NonNull int[]);
+ method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public boolean removeDeviceAsNonDefaultForStrategy(@NonNull android.media.audiopolicy.AudioProductStrategy, @NonNull android.media.AudioDeviceAttributes);
+ method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void removeOnNonDefaultDevicesForStrategyChangedListener(@NonNull android.media.AudioManager.OnNonDefaultDevicesForStrategyChangedListener);
method @Deprecated @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void removeOnPreferredDeviceForStrategyChangedListener(@NonNull android.media.AudioManager.OnPreferredDeviceForStrategyChangedListener);
method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void removeOnPreferredDevicesForCapturePresetChangedListener(@NonNull android.media.AudioManager.OnPreferredDevicesForCapturePresetChangedListener);
method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void removeOnPreferredDevicesForStrategyChangedListener(@NonNull android.media.AudioManager.OnPreferredDevicesForStrategyChangedListener);
@@ -6653,6 +6655,7 @@
method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public boolean setAdditionalOutputDeviceDelay(@NonNull android.media.AudioDeviceInfo, @IntRange(from=0) long);
method public void setAudioServerStateCallback(@NonNull java.util.concurrent.Executor, @NonNull android.media.AudioManager.AudioServerStateCallback);
method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void setBluetoothVariableLatencyEnabled(boolean);
+ method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public boolean setDeviceAsNonDefaultForStrategy(@NonNull android.media.audiopolicy.AudioProductStrategy, @NonNull android.media.AudioDeviceAttributes);
method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void setDeviceVolumeBehavior(@NonNull android.media.AudioDeviceAttributes, int);
method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void setFocusRequestResult(@NonNull android.media.AudioFocusInfo, int, @NonNull android.media.audiopolicy.AudioPolicy);
method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public boolean setPreferredDeviceForCapturePreset(int, @NonNull android.media.AudioDeviceAttributes);
@@ -6697,6 +6700,10 @@
field public static final int EVENT_TIMEOUT = 2; // 0x2
}
+ public static interface AudioManager.OnNonDefaultDevicesForStrategyChangedListener {
+ method public void onNonDefaultDevicesForStrategyChanged(@NonNull android.media.audiopolicy.AudioProductStrategy, @NonNull java.util.List<android.media.AudioDeviceAttributes>);
+ }
+
@Deprecated public static interface AudioManager.OnPreferredDeviceForStrategyChangedListener {
method @Deprecated public void onPreferredDeviceForStrategyChanged(@NonNull android.media.audiopolicy.AudioProductStrategy, @Nullable android.media.AudioDeviceAttributes);
}
@@ -7781,6 +7788,7 @@
field public static final int STATUS_DATA_READY = 1; // 0x1
field public static final int STATUS_HIGH_WATER = 4; // 0x4
field public static final int STATUS_LOW_WATER = 2; // 0x2
+ field public static final int STATUS_NO_DATA = 16; // 0x10
field public static final int STATUS_OVERFLOW = 8; // 0x8
field public static final int SUBTYPE_AUDIO = 3; // 0x3
field public static final int SUBTYPE_DOWNLOAD = 5; // 0x5
diff --git a/core/java/android/companion/virtual/IVirtualDevice.aidl b/core/java/android/companion/virtual/IVirtualDevice.aidl
index 22ea9f20..9ab7cf9 100644
--- a/core/java/android/companion/virtual/IVirtualDevice.aidl
+++ b/core/java/android/companion/virtual/IVirtualDevice.aidl
@@ -60,62 +60,73 @@
/**
* Closes the virtual device and frees all associated resources.
*/
+ @EnforcePermission("CREATE_VIRTUAL_DEVICE")
void close();
/**
* Notifies of an audio session being started.
*/
+ @EnforcePermission("CREATE_VIRTUAL_DEVICE")
void onAudioSessionStarting(
int displayId,
IAudioRoutingCallback routingCallback,
IAudioConfigChangedCallback configChangedCallback);
+ @EnforcePermission("CREATE_VIRTUAL_DEVICE")
void onAudioSessionEnded();
- @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)")
+
+ @EnforcePermission("CREATE_VIRTUAL_DEVICE")
void createVirtualDpad(
in VirtualDpadConfig config,
IBinder token);
- @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)")
+ @EnforcePermission("CREATE_VIRTUAL_DEVICE")
void createVirtualKeyboard(
in VirtualKeyboardConfig config,
IBinder token);
- @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)")
+ @EnforcePermission("CREATE_VIRTUAL_DEVICE")
void createVirtualMouse(
in VirtualMouseConfig config,
IBinder token);
- @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)")
+ @EnforcePermission("CREATE_VIRTUAL_DEVICE")
void createVirtualTouchscreen(
in VirtualTouchscreenConfig config,
IBinder token);
- @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)")
+ @EnforcePermission("CREATE_VIRTUAL_DEVICE")
void createVirtualNavigationTouchpad(
in VirtualNavigationTouchpadConfig config,
IBinder token);
+ @EnforcePermission("CREATE_VIRTUAL_DEVICE")
void unregisterInputDevice(IBinder token);
int getInputDeviceId(IBinder token);
+ @EnforcePermission("CREATE_VIRTUAL_DEVICE")
boolean sendDpadKeyEvent(IBinder token, in VirtualKeyEvent event);
+ @EnforcePermission("CREATE_VIRTUAL_DEVICE")
boolean sendKeyEvent(IBinder token, in VirtualKeyEvent event);
+ @EnforcePermission("CREATE_VIRTUAL_DEVICE")
boolean sendButtonEvent(IBinder token, in VirtualMouseButtonEvent event);
+ @EnforcePermission("CREATE_VIRTUAL_DEVICE")
boolean sendRelativeEvent(IBinder token, in VirtualMouseRelativeEvent event);
+ @EnforcePermission("CREATE_VIRTUAL_DEVICE")
boolean sendScrollEvent(IBinder token, in VirtualMouseScrollEvent event);
+ @EnforcePermission("CREATE_VIRTUAL_DEVICE")
boolean sendTouchEvent(IBinder token, in VirtualTouchEvent event);
/**
* Creates a virtual sensor, capable of injecting sensor events into the system.
*/
- @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)")
+ @EnforcePermission("CREATE_VIRTUAL_DEVICE")
void createVirtualSensor(IBinder tokenm, in VirtualSensorConfig config);
/**
* Removes the sensor corresponding to the given token from the system.
*/
- @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)")
+ @EnforcePermission("CREATE_VIRTUAL_DEVICE")
void unregisterSensor(IBinder token);
/**
* Sends an event to the virtual sensor corresponding to the given token.
*/
- @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)")
+ @EnforcePermission("CREATE_VIRTUAL_DEVICE")
boolean sendSensorEvent(IBinder token, in VirtualSensorEvent event);
/**
@@ -126,6 +137,7 @@
PointF getCursorPosition(IBinder token);
/** Sets whether to show or hide the cursor while this virtual device is active. */
+ @EnforcePermission("CREATE_VIRTUAL_DEVICE")
void setShowPointerIcon(boolean showPointerIcon);
/**
@@ -133,9 +145,9 @@
* when matching the provided IntentFilter and calls the callback with the intercepted
* intent.
*/
- @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)")
+ @EnforcePermission("CREATE_VIRTUAL_DEVICE")
void registerIntentInterceptor(
in IVirtualDeviceIntentInterceptor intentInterceptor, in IntentFilter filter);
- @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)")
+ @EnforcePermission("CREATE_VIRTUAL_DEVICE")
void unregisterIntentInterceptor(in IVirtualDeviceIntentInterceptor intentInterceptor);
}
diff --git a/core/java/android/companion/virtual/VirtualDeviceManager.java b/core/java/android/companion/virtual/VirtualDeviceManager.java
index 8561018..adf59fb 100644
--- a/core/java/android/companion/virtual/VirtualDeviceManager.java
+++ b/core/java/android/companion/virtual/VirtualDeviceManager.java
@@ -775,13 +775,11 @@
final Point size = new Point();
display.getDisplay().getSize(size);
VirtualTouchscreenConfig touchscreenConfig =
- new VirtualTouchscreenConfig.Builder()
+ new VirtualTouchscreenConfig.Builder(size.x, size.y)
.setVendorId(vendorId)
.setProductId(productId)
.setInputDeviceName(inputDeviceName)
.setAssociatedDisplayId(display.getDisplay().getDisplayId())
- .setWidthInPixels(size.x)
- .setHeightInPixels(size.y)
.build();
return createVirtualTouchscreen(touchscreenConfig);
}
diff --git a/core/java/android/hardware/input/VirtualNavigationTouchpadConfig.java b/core/java/android/hardware/input/VirtualNavigationTouchpadConfig.java
index f2805bb..5ad5fd9 100644
--- a/core/java/android/hardware/input/VirtualNavigationTouchpadConfig.java
+++ b/core/java/android/hardware/input/VirtualNavigationTouchpadConfig.java
@@ -88,12 +88,17 @@
* Builder for creating a {@link VirtualNavigationTouchpadConfig}.
*/
public static final class Builder extends VirtualInputDeviceConfig.Builder<Builder> {
-
private final int mHeight;
private final int mWidth;
- public Builder(@IntRange(from = 1) int touchpadHeight,
- @IntRange(from = 1) int touchpadWidth) {
+ /**
+ * Creates a new instance for the given dimensions of the {@link VirtualNavigationTouchpad}.
+ *
+ * @param touchpadWidth The width of the touchpad.
+ * @param touchpadHeight The height of the touchpad.
+ */
+ public Builder(@IntRange(from = 1) int touchpadWidth,
+ @IntRange(from = 1) int touchpadHeight) {
if (touchpadHeight <= 0 || touchpadWidth <= 0) {
throw new IllegalArgumentException(
"Cannot create a virtual navigation touchpad, touchpad dimensions must be "
diff --git a/core/java/android/hardware/input/VirtualTouchscreenConfig.java b/core/java/android/hardware/input/VirtualTouchscreenConfig.java
index e358619..aac341cc 100644
--- a/core/java/android/hardware/input/VirtualTouchscreenConfig.java
+++ b/core/java/android/hardware/input/VirtualTouchscreenConfig.java
@@ -16,6 +16,7 @@
package android.hardware.input;
+import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.SystemApi;
import android.os.Parcel;
@@ -29,31 +30,31 @@
@SystemApi
public final class VirtualTouchscreenConfig extends VirtualInputDeviceConfig implements Parcelable {
- /** The touchscreen width in pixels. */
- private final int mWidthInPixels;
- /** The touchscreen height in pixels. */
- private final int mHeightInPixels;
+ /** The touchscreen width. */
+ private final int mWidth;
+ /** The touchscreen height. */
+ private final int mHeight;
private VirtualTouchscreenConfig(@NonNull Builder builder) {
super(builder);
- mWidthInPixels = builder.mWidthInPixels;
- mHeightInPixels = builder.mHeightInPixels;
+ mWidth = builder.mWidth;
+ mHeight = builder.mHeight;
}
private VirtualTouchscreenConfig(@NonNull Parcel in) {
super(in);
- mWidthInPixels = in.readInt();
- mHeightInPixels = in.readInt();
+ mWidth = in.readInt();
+ mHeight = in.readInt();
}
- /** Returns the touchscreen width in pixels. */
- public int getWidthInPixels() {
- return mWidthInPixels;
+ /** Returns the touchscreen width. */
+ public int getWidth() {
+ return mWidth;
}
- /** Returns the touchscreen height in pixels. */
- public int getHeightInPixels() {
- return mHeightInPixels;
+ /** Returns the touchscreen height. */
+ public int getHeight() {
+ return mHeight;
}
@Override
@@ -64,8 +65,8 @@
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
super.writeToParcel(dest, flags);
- dest.writeInt(mWidthInPixels);
- dest.writeInt(mHeightInPixels);
+ dest.writeInt(mWidth);
+ dest.writeInt(mHeight);
}
@NonNull
@@ -86,25 +87,29 @@
* Builder for creating a {@link VirtualTouchscreenConfig}.
*/
public static final class Builder extends VirtualInputDeviceConfig.Builder<Builder> {
- private int mWidthInPixels;
- private int mHeightInPixels;
+ private int mWidth;
+ private int mHeight;
/**
- * @see VirtualTouchscreenConfig#getWidthInPixels().
+ * Creates a new instance for the given dimensions of the {@link VirtualTouchscreen}.
+ *
+ * <p>The dimensions are not pixels but in the touchscreens raw coordinate space. They do
+ * not necessarily have to correspond to the display size or aspect ratio. In this case the
+ * framework will handle the scaling appropriately.
+ *
+ * @param touchscreenWidth The width of the touchscreen.
+ * @param touchscreenHeight The height of the touchscreen.
*/
- @NonNull
- public Builder setWidthInPixels(int widthInPixels) {
- mWidthInPixels = widthInPixels;
- return this;
- }
-
- /**
- * @see VirtualTouchscreenConfig#getHeightInPixels().
- */
- @NonNull
- public Builder setHeightInPixels(int heightInPixels) {
- mHeightInPixels = heightInPixels;
- return this;
+ public Builder(@IntRange(from = 1) int touchscreenWidth,
+ @IntRange(from = 1) int touchscreenHeight) {
+ if (touchscreenHeight <= 0 || touchscreenWidth <= 0) {
+ throw new IllegalArgumentException(
+ "Cannot create a virtual touchscreen, touchscreen dimensions must be "
+ + "positive. Got: (" + touchscreenHeight + ", "
+ + touchscreenWidth + ")");
+ }
+ mHeight = touchscreenHeight;
+ mWidth = touchscreenWidth;
}
/**
diff --git a/core/java/com/android/internal/app/AppLocaleCollector.java b/core/java/com/android/internal/app/AppLocaleCollector.java
index 65e8c64..a50fbb8 100644
--- a/core/java/com/android/internal/app/AppLocaleCollector.java
+++ b/core/java/com/android/internal/app/AppLocaleCollector.java
@@ -19,49 +19,184 @@
import static com.android.internal.app.AppLocaleStore.AppLocaleResult.LocaleStatus.GET_SUPPORTED_LANGUAGE_FROM_ASSET;
import static com.android.internal.app.AppLocaleStore.AppLocaleResult.LocaleStatus.GET_SUPPORTED_LANGUAGE_FROM_LOCAL_CONFIG;
+import android.app.LocaleManager;
import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
import android.os.Build;
import android.os.LocaleList;
+import android.os.SystemProperties;
+import android.provider.Settings;
import android.util.Log;
+import android.view.inputmethod.InputMethodInfo;
+import android.view.inputmethod.InputMethodManager;
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.HashMap;
import java.util.HashSet;
+import java.util.List;
import java.util.Locale;
import java.util.Set;
+import java.util.stream.Collectors;
/** The Locale data collector for per-app language. */
-class AppLocaleCollector implements LocalePickerWithRegion.LocaleCollectorBase {
+public class AppLocaleCollector implements LocalePickerWithRegion.LocaleCollectorBase {
private static final String TAG = AppLocaleCollector.class.getSimpleName();
private final Context mContext;
private final String mAppPackageName;
- private final LocaleStore.LocaleInfo mAppCurrentLocale;
+ private LocaleStore.LocaleInfo mAppCurrentLocale;
+ private Set<LocaleStore.LocaleInfo> mAllAppActiveLocales;
+ private Set<LocaleStore.LocaleInfo> mImeLocales;
+ private static final String PROP_APP_LANGUAGE_SUGGESTION =
+ "android.app.language.suggestion.enhanced";
+ private static final boolean ENABLED = true;
- AppLocaleCollector(Context context, String appPackageName) {
+ public AppLocaleCollector(Context context, String appPackageName) {
mContext = context;
mAppPackageName = appPackageName;
- mAppCurrentLocale = LocaleStore.getAppCurrentLocaleInfo(
- mContext, mAppPackageName);
+ }
+
+ @VisibleForTesting
+ public LocaleStore.LocaleInfo getAppCurrentLocale() {
+ return LocaleStore.getAppActivatedLocaleInfo(mContext, mAppPackageName, true);
+ }
+
+ /**
+ * Get all applications' activated locales.
+ * @return A set which includes all applications' activated LocaleInfo.
+ */
+ @VisibleForTesting
+ public Set<LocaleStore.LocaleInfo> getAllAppActiveLocales() {
+ PackageManager pm = mContext.getPackageManager();
+ LocaleManager lm = mContext.getSystemService(LocaleManager.class);
+ HashSet<LocaleStore.LocaleInfo> result = new HashSet<>();
+ if (pm != null && lm != null) {
+ HashMap<String, LocaleStore.LocaleInfo> map = new HashMap<>();
+ for (ApplicationInfo appInfo : pm.getInstalledApplications(
+ PackageManager.ApplicationInfoFlags.of(0))) {
+ LocaleStore.LocaleInfo localeInfo = LocaleStore.getAppActivatedLocaleInfo(
+ mContext, appInfo.packageName, false);
+ // For the locale to be added into the suggestion area, its country could not be
+ // empty.
+ if (localeInfo != null && localeInfo.getLocale().getCountry().length() > 0) {
+ map.put(localeInfo.getId(), localeInfo);
+ }
+ }
+ map.forEach((language, localeInfo) -> result.add(localeInfo));
+ }
+ return result;
+ }
+
+ /**
+ * Get all locales that active IME supports.
+ *
+ * @return A set which includes all LocaleInfo that active IME supports.
+ */
+ @VisibleForTesting
+ public Set<LocaleStore.LocaleInfo> getActiveImeLocales() {
+ Set<LocaleStore.LocaleInfo> activeImeLocales = null;
+ InputMethodManager imm = mContext.getSystemService(InputMethodManager.class);
+ if (imm != null) {
+ InputMethodInfo activeIme = getActiveIme(imm);
+ if (activeIme != null) {
+ activeImeLocales = LocaleStore.transformImeLanguageTagToLocaleInfo(
+ imm.getEnabledInputMethodSubtypeList(activeIme, true));
+ }
+ }
+ if (activeImeLocales == null) {
+ return Set.of();
+ } else {
+ return activeImeLocales.stream().filter(
+ // For the locale to be added into the suggestion area, its country could not be
+ // empty.
+ info -> info.getLocale().getCountry().length() > 0).collect(
+ Collectors.toSet());
+ }
+ }
+
+ private InputMethodInfo getActiveIme(InputMethodManager imm) {
+ InputMethodInfo activeIme = null;
+ List<InputMethodInfo> infoList = imm.getEnabledInputMethodList();
+ String imeId = Settings.Secure.getStringForUser(mContext.getContentResolver(),
+ Settings.Secure.DEFAULT_INPUT_METHOD, mContext.getUserId());
+ for (InputMethodInfo method : infoList) {
+ if (method.getId().equals(imeId)) {
+ activeIme = method;
+ }
+ }
+ return activeIme;
+ }
+
+ /**
+ * Get the AppLocaleResult that the application supports.
+ * @return The AppLocaleResult that the application supports.
+ */
+ @VisibleForTesting
+ public AppLocaleStore.AppLocaleResult getAppSupportedLocales() {
+ return AppLocaleStore.getAppSupportedLocales(mContext, mAppPackageName);
+ }
+
+ /**
+ * Get the locales that system supports excluding langTagsToIgnore.
+ *
+ * @param langTagsToIgnore A language set to be ignored.
+ * @param parent The parent locale.
+ * @param translatedOnly specified if is is only for translation.
+ * @return A set which includes the LocaleInfo that system supports, excluding langTagsToIgnore.
+ */
+ @VisibleForTesting
+ public Set<LocaleStore.LocaleInfo> getSystemSupportedLocale(Set<String> langTagsToIgnore,
+ LocaleStore.LocaleInfo parent, boolean translatedOnly) {
+ return LocaleStore.getLevelLocales(mContext, langTagsToIgnore, parent, translatedOnly);
+ }
+
+ /**
+ * Get the locales that system activates.
+ * @return A set which includes all the locales that system activates.
+ */
+ @VisibleForTesting
+ public List<LocaleStore.LocaleInfo> getSystemCurrentLocale() {
+ return LocaleStore.getSystemCurrentLocaleInfo();
}
@Override
public HashSet<String> getIgnoredLocaleList(boolean translatedOnly) {
HashSet<String> langTagsToIgnore = new HashSet<>();
- LocaleList systemLangList = LocaleList.getDefault();
- for(int i = 0; i < systemLangList.size(); i++) {
- langTagsToIgnore.add(systemLangList.get(i).toLanguageTag());
- }
-
if (mAppCurrentLocale != null) {
langTagsToIgnore.add(mAppCurrentLocale.getLocale().toLanguageTag());
}
+
+ if (SystemProperties.getBoolean(PROP_APP_LANGUAGE_SUGGESTION, ENABLED)) {
+ // Add the locale that other App activated
+ mAllAppActiveLocales.forEach(
+ info -> langTagsToIgnore.add(info.getLocale().toLanguageTag()));
+ // Add the locale that active IME enabled
+ mImeLocales.forEach(info -> langTagsToIgnore.add(info.getLocale().toLanguageTag()));
+ }
+
+ // Add System locales
+ LocaleList systemLangList = LocaleList.getDefault();
+ for (int i = 0; i < systemLangList.size(); i++) {
+ langTagsToIgnore.add(systemLangList.get(i).toLanguageTag());
+ }
return langTagsToIgnore;
}
@Override
public Set<LocaleStore.LocaleInfo> getSupportedLocaleList(LocaleStore.LocaleInfo parent,
boolean translatedOnly, boolean isForCountryMode) {
- AppLocaleStore.AppLocaleResult result =
- AppLocaleStore.getAppSupportedLocales(mContext, mAppPackageName);
+ if (mAppCurrentLocale == null) {
+ mAppCurrentLocale = getAppCurrentLocale();
+ }
+ if (mAllAppActiveLocales == null) {
+ mAllAppActiveLocales = getAllAppActiveLocales();
+ }
+ if (mImeLocales == null) {
+ mImeLocales = getActiveImeLocales();
+ }
+ AppLocaleStore.AppLocaleResult result = getAppSupportedLocales();
Set<String> langTagsToIgnore = getIgnoredLocaleList(translatedOnly);
Set<LocaleStore.LocaleInfo> appLocaleList = new HashSet<>();
Set<LocaleStore.LocaleInfo> systemLocaleList;
@@ -71,11 +206,9 @@
// Get system supported locale list
if (isForCountryMode) {
- systemLocaleList = LocaleStore.getLevelLocales(mContext,
- langTagsToIgnore, parent, translatedOnly);
+ systemLocaleList = getSystemSupportedLocale(langTagsToIgnore, parent, translatedOnly);
} else {
- systemLocaleList = LocaleStore.getLevelLocales(mContext, langTagsToIgnore,
- null /* no parent */, translatedOnly);
+ systemLocaleList = getSystemSupportedLocale(langTagsToIgnore, null, translatedOnly);
}
// Add current app locale
@@ -84,19 +217,46 @@
}
// Add current system language into suggestion list
- for(LocaleStore.LocaleInfo localeInfo:
- LocaleStore.getSystemCurrentLocaleInfo()) {
- boolean isNotCurrentLocale = mAppCurrentLocale == null
- || !localeInfo.getLocale().equals(mAppCurrentLocale.getLocale());
- if (!isForCountryMode && isNotCurrentLocale) {
- appLocaleList.add(localeInfo);
+ if (!isForCountryMode) {
+ boolean isCurrentLocale, isInAppOrIme;
+ for (LocaleStore.LocaleInfo localeInfo : getSystemCurrentLocale()) {
+ isCurrentLocale = mAppCurrentLocale != null
+ && localeInfo.getLocale().equals(mAppCurrentLocale.getLocale());
+ isInAppOrIme = existsInAppOrIme(localeInfo.getLocale());
+ if (!isCurrentLocale && !isInAppOrIme) {
+ appLocaleList.add(localeInfo);
+ }
}
}
- // Add the languages that included in system supported locale
+ // Add the languages that are included in system supported locale
+ Set<LocaleStore.LocaleInfo> suggestedSet = null;
if (shouldShowList) {
- appLocaleList.addAll(filterTheLanguagesNotIncludedInSystemLocale(
- systemLocaleList, result.mAppSupportedLocales));
+ appLocaleList.addAll(filterSupportedLocales(systemLocaleList,
+ result.mAppSupportedLocales));
+ suggestedSet = getSuggestedLocales(appLocaleList);
+ }
+
+ if (!isForCountryMode && SystemProperties.getBoolean(PROP_APP_LANGUAGE_SUGGESTION,
+ ENABLED)) {
+ // Add the language that other apps activate into the suggestion list.
+ Set<LocaleStore.LocaleInfo> localeSet = filterSupportedLocales(mAllAppActiveLocales,
+ result.mAppSupportedLocales);
+ if (suggestedSet != null) {
+ // Filter out the locale with the same language and country
+ // like zh-TW vs zh-Hant-TW.
+ localeSet = filterSameLanguageAndCountry(localeSet, suggestedSet);
+ }
+ appLocaleList.addAll(localeSet);
+ suggestedSet.addAll(localeSet);
+
+ // Add the language that the active IME enables into the suggestion list.
+ localeSet = filterSupportedLocales(mImeLocales, result.mAppSupportedLocales);
+ if (suggestedSet != null) {
+ localeSet = filterSameLanguageAndCountry(localeSet, suggestedSet);
+ }
+ appLocaleList.addAll(localeSet);
+ suggestedSet.addAll(localeSet);
}
// Add "system language" option
@@ -117,17 +277,55 @@
return true;
}
- private Set<LocaleStore.LocaleInfo> filterTheLanguagesNotIncludedInSystemLocale(
- Set<LocaleStore.LocaleInfo> systemLocaleList,
+ private Set<LocaleStore.LocaleInfo> getSuggestedLocales(Set<LocaleStore.LocaleInfo> localeSet) {
+ return localeSet.stream().filter(localeInfo -> localeInfo.isSuggested()).collect(
+ Collectors.toSet());
+ }
+
+ private Set<LocaleStore.LocaleInfo> filterSameLanguageAndCountry(
+ Set<LocaleStore.LocaleInfo> newLocaleList,
+ Set<LocaleStore.LocaleInfo> existingLocaleList) {
+ Set<LocaleStore.LocaleInfo> result = new HashSet<>(newLocaleList.size());
+ for (LocaleStore.LocaleInfo appLocaleInfo : newLocaleList) {
+ boolean same = false;
+ Locale appLocale = appLocaleInfo.getLocale();
+ for (LocaleStore.LocaleInfo localeInfo : existingLocaleList) {
+ Locale suggested = localeInfo.getLocale();
+ if (appLocale.getLanguage().equals(suggested.getLanguage())
+ && appLocale.getCountry().equals(suggested.getCountry())) {
+ same = true;
+ break;
+ }
+ }
+ if (!same) {
+ result.add(appLocaleInfo);
+ }
+ }
+ return result;
+ }
+
+ private boolean existsInAppOrIme(Locale locale) {
+ boolean existInApp = mAllAppActiveLocales.stream().anyMatch(
+ localeInfo -> localeInfo.getLocale().equals(locale));
+ if (existInApp) {
+ return true;
+ } else {
+ return mImeLocales.stream().anyMatch(
+ localeInfo -> localeInfo.getLocale().equals(locale));
+ }
+ }
+
+ private Set<LocaleStore.LocaleInfo> filterSupportedLocales(
+ Set<LocaleStore.LocaleInfo> suggestedLocales,
HashSet<Locale> appSupportedLocales) {
Set<LocaleStore.LocaleInfo> filteredList = new HashSet<>();
- for(LocaleStore.LocaleInfo li: systemLocaleList) {
+ for (LocaleStore.LocaleInfo li : suggestedLocales) {
if (appSupportedLocales.contains(li.getLocale())) {
filteredList.add(li);
} else {
- for(Locale l: appSupportedLocales) {
- if(LocaleList.matchesLanguageAndScript(li.getLocale(), l)) {
+ for (Locale l : appSupportedLocales) {
+ if (LocaleList.matchesLanguageAndScript(li.getLocale(), l)) {
filteredList.add(li);
break;
}
diff --git a/core/java/com/android/internal/app/AppLocaleStore.java b/core/java/com/android/internal/app/AppLocaleStore.java
index f3a322c..a450a05 100644
--- a/core/java/com/android/internal/app/AppLocaleStore.java
+++ b/core/java/com/android/internal/app/AppLocaleStore.java
@@ -30,7 +30,10 @@
import java.util.Locale;
import java.util.stream.Collectors;
-class AppLocaleStore {
+/**
+ * A class used to access an application's supporting locales.
+ */
+public class AppLocaleStore {
private static final String TAG = AppLocaleStore.class.getSimpleName();
public static AppLocaleResult getAppSupportedLocales(
@@ -148,8 +151,11 @@
return false;
}
- static class AppLocaleResult {
- enum LocaleStatus {
+ /**
+ * A class used to store an application's supporting locales.
+ */
+ public static class AppLocaleResult {
+ public enum LocaleStatus {
UNKNOWN_FAILURE,
NO_SUPPORTED_LANGUAGE_IN_APP,
ASSET_LOCALE_IS_EMPTY,
@@ -158,7 +164,7 @@
}
LocaleStatus mLocaleStatus;
- HashSet<Locale> mAppSupportedLocales;
+ public HashSet<Locale> mAppSupportedLocales;
public AppLocaleResult(LocaleStatus localeStatus, HashSet<Locale> appSupportedLocales) {
this.mLocaleStatus = localeStatus;
diff --git a/core/java/com/android/internal/app/LocaleStore.java b/core/java/com/android/internal/app/LocaleStore.java
index 689dec4..d2eee91 100644
--- a/core/java/com/android/internal/app/LocaleStore.java
+++ b/core/java/com/android/internal/app/LocaleStore.java
@@ -23,6 +23,7 @@
import android.provider.Settings;
import android.telephony.TelephonyManager;
import android.util.Log;
+import android.view.inputmethod.InputMethodSubtype;
import com.android.internal.annotations.VisibleForTesting;
@@ -45,10 +46,11 @@
@VisibleForTesting static final int SUGGESTION_TYPE_SIM = 1 << 0;
@VisibleForTesting static final int SUGGESTION_TYPE_CFG = 1 << 1;
// Only for per-app language picker
- private static final int SUGGESTION_TYPE_CURRENT = 1 << 2;
+ @VisibleForTesting static final int SUGGESTION_TYPE_CURRENT = 1 << 2;
// Only for per-app language picker
- private static final int SUGGESTION_TYPE_SYSTEM_LANGUAGE = 1 << 3;
-
+ @VisibleForTesting static final int SUGGESTION_TYPE_SYSTEM_LANGUAGE = 1 << 3;
+ // Only for per-app language picker
+ @VisibleForTesting static final int SUGGESTION_TYPE_OTHER_APP_LANGUAGE = 1 << 4;
private final Locale mLocale;
private final Locale mParent;
private final String mId;
@@ -259,7 +261,16 @@
}
}
- public static LocaleInfo getAppCurrentLocaleInfo(Context context, String appPackageName) {
+ /**
+ * Get the application's activated locale.
+ *
+ * @param context UI activity's context.
+ * @param appPackageName The application's package name.
+ * @param isAppSelected True if the application is selected in the UI; false otherwise.
+ * @return A LocaleInfo with the application's activated locale.
+ */
+ public static LocaleInfo getAppActivatedLocaleInfo(Context context, String appPackageName,
+ boolean isAppSelected) {
if (appPackageName == null) {
return null;
}
@@ -272,7 +283,11 @@
if (locale != null) {
LocaleInfo localeInfo = new LocaleInfo(locale);
- localeInfo.mSuggestionFlags |= LocaleInfo.SUGGESTION_TYPE_CURRENT;
+ if (isAppSelected) {
+ localeInfo.mSuggestionFlags |= LocaleInfo.SUGGESTION_TYPE_CURRENT;
+ } else {
+ localeInfo.mSuggestionFlags |= LocaleInfo.SUGGESTION_TYPE_OTHER_APP_LANGUAGE;
+ }
localeInfo.mIsTranslated = true;
return localeInfo;
}
@@ -283,6 +298,24 @@
}
/**
+ * Transform IME's language tag to LocaleInfo.
+ *
+ * @param list A list which includes IME's subtype.
+ * @return A LocaleInfo set which includes IME's language tags.
+ */
+ public static Set<LocaleInfo> transformImeLanguageTagToLocaleInfo(
+ List<InputMethodSubtype> list) {
+ Set<LocaleInfo> imeLocales = new HashSet<>();
+ for (InputMethodSubtype subtype : list) {
+ LocaleInfo localeInfo = new LocaleInfo(subtype.getLanguageTag());
+ localeInfo.mSuggestionFlags |= LocaleInfo.SUGGESTION_TYPE_OTHER_APP_LANGUAGE;
+ localeInfo.mIsTranslated = true;
+ imeLocales.add(localeInfo);
+ }
+ return imeLocales;
+ }
+
+ /**
* Returns a list of system languages with LocaleInfo
*/
public static List<LocaleInfo> getSystemCurrentLocaleInfo() {
@@ -458,4 +491,13 @@
}
return result;
}
+
+ /**
+ * API for testing.
+ */
+ @UnsupportedAppUsage
+ @VisibleForTesting
+ public static LocaleInfo fromLocale(Locale locale) {
+ return new LocaleInfo(locale);
+ }
}
diff --git a/core/java/com/android/internal/content/InstallLocationUtils.java b/core/java/com/android/internal/content/InstallLocationUtils.java
index c456cf3..4d9c09e 100644
--- a/core/java/com/android/internal/content/InstallLocationUtils.java
+++ b/core/java/com/android/internal/content/InstallLocationUtils.java
@@ -31,6 +31,7 @@
import android.os.IBinder;
import android.os.RemoteException;
import android.os.ServiceManager;
+import android.os.SystemProperties;
import android.os.storage.IStorageManager;
import android.os.storage.StorageManager;
import android.os.storage.StorageVolume;
@@ -261,12 +262,12 @@
// We're left with new installations with either preferring external or auto, so just pick
// volume with most space
+ String bestCandidate = !volumePaths.isEmpty() ? volumePaths.keyAt(0) : null;
if (volumePaths.size() == 1) {
if (checkFitOnVolume(storageManager, volumePaths.valueAt(0), params)) {
- return volumePaths.keyAt(0);
+ return bestCandidate;
}
} else {
- String bestCandidate = null;
long bestCandidateAvailBytes = Long.MIN_VALUE;
for (String vol : volumePaths.keySet()) {
final String volumePath = volumePaths.get(vol);
@@ -289,6 +290,14 @@
}
+ // For new installations of a predefined size, check property to let it through
+ // regardless of the actual free space.
+ if (bestCandidate != null && Integer.MAX_VALUE == params.sizeBytes
+ && SystemProperties.getBoolean("debug.pm.install_skip_size_check_for_maxint",
+ false)) {
+ return bestCandidate;
+ }
+
throw new IOException("No special requests, but no room on allowed volumes. "
+ " allow3rdPartyOnInternal? " + allow3rdPartyOnInternal);
}
diff --git a/core/jni/android_media_AudioSystem.cpp b/core/jni/android_media_AudioSystem.cpp
index 19cb30e..75f8402 100644
--- a/core/jni/android_media_AudioSystem.cpp
+++ b/core/jni/android_media_AudioSystem.cpp
@@ -2739,12 +2739,26 @@
}
static jint android_media_AudioSystem_removeDevicesRoleForStrategy(JNIEnv *env, jobject thiz,
- jint strategy, jint role) {
+ jint strategy, jint role,
+ jintArray jDeviceTypes,
+ jobjectArray jDeviceAddresses) {
+ AudioDeviceTypeAddrVector nDevices;
+ jint results = getVectorOfAudioDeviceTypeAddr(env, jDeviceTypes, jDeviceAddresses, nDevices);
+ if (results != NO_ERROR) {
+ return results;
+ }
+ int status = check_AudioSystem_Command(
+ AudioSystem::removeDevicesRoleForStrategy((product_strategy_t)strategy,
+ (device_role_t)role, nDevices));
+ return (jint)status;
+}
+
+static jint android_media_AudioSystem_clearDevicesRoleForStrategy(JNIEnv *env, jobject thiz,
+ jint strategy, jint role) {
return (jint)
- check_AudioSystem_Command(AudioSystem::removeDevicesRoleForStrategy((product_strategy_t)
- strategy,
- (device_role_t)
- role),
+ check_AudioSystem_Command(AudioSystem::clearDevicesRoleForStrategy((product_strategy_t)
+ strategy,
+ (device_role_t)role),
{NAME_NOT_FOUND});
}
@@ -3341,8 +3355,10 @@
(void *)android_media_AudioSystem_isCallScreeningModeSupported},
{"setDevicesRoleForStrategy", "(II[I[Ljava/lang/String;)I",
(void *)android_media_AudioSystem_setDevicesRoleForStrategy},
- {"removeDevicesRoleForStrategy", "(II)I",
+ {"removeDevicesRoleForStrategy", "(II[I[Ljava/lang/String;)I",
(void *)android_media_AudioSystem_removeDevicesRoleForStrategy},
+ {"clearDevicesRoleForStrategy", "(II)I",
+ (void *)android_media_AudioSystem_clearDevicesRoleForStrategy},
{"getDevicesForRoleAndStrategy", "(IILjava/util/List;)I",
(void *)android_media_AudioSystem_getDevicesForRoleAndStrategy},
{"setDevicesRoleForCapturePreset", "(II[I[Ljava/lang/String;)I",
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 3274e85..124d9e6 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -258,6 +258,7 @@
android:name="com.android.bluetooth.BluetoothMapContentObserver.action.MESSAGE_DELIVERY" />
<protected-broadcast
android:name="android.bluetooth.pan.profile.action.CONNECTION_STATE_CHANGED" />
+ <protected-broadcast android:name="android.bluetooth.action.HAP_CONNECTION_STATE_CHANGED" />
<protected-broadcast android:name="android.bluetooth.action.LE_AUDIO_CONNECTION_STATE_CHANGED" />
<protected-broadcast android:name="android.bluetooth.action.LE_AUDIO_ACTIVE_DEVICE_CHANGED" />
<protected-broadcast android:name="android.bluetooth.action.LE_AUDIO_CONF_CHANGED" />
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 35ff7e8..92b4ba1 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -2701,6 +2701,10 @@
<!-- Whether UI for multi user should be shown -->
<bool name="config_enableMultiUserUI">false</bool>
+ <!-- Whether multiple admins are allowed on the device. If set to true, new users can be created
+ with admin privileges and admin privileges can be granted/revoked from existing users. -->
+ <bool name="config_enableMultipleAdmins">false</bool>
+
<!-- Whether the new Auto Selection Network UI should be shown -->
<bool name="config_enableNewAutoSelectNetworkUI">false</bool>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index c73f2f4..f54335a 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -353,6 +353,7 @@
<java-symbol type="bool" name="config_speed_up_audio_on_mt_calls" />
<java-symbol type="bool" name="config_useFixedVolume" />
<java-symbol type="bool" name="config_enableMultiUserUI"/>
+ <java-symbol type="bool" name="config_enableMultipleAdmins"/>
<java-symbol type="bool" name="config_enableNewAutoSelectNetworkUI"/>
<java-symbol type="bool" name="config_disableUsbPermissionDialogs"/>
<java-symbol type="dimen" name="config_highResTaskSnapshotScale" />
diff --git a/data/keyboards/Vendor_054c_Product_0df2.kl b/data/keyboards/Vendor_054c_Product_0df2.kl
new file mode 100644
index 0000000..a47b310
--- /dev/null
+++ b/data/keyboards/Vendor_054c_Product_0df2.kl
@@ -0,0 +1,73 @@
+# Copyright (C) 2022 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.
+
+#
+# Sony Playstation(R) DualSense Edge Controller
+#
+
+# Only use this key layout if we have HID_PLAYSTATION!
+requires_kernel_config CONFIG_HID_PLAYSTATION
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+# Square
+key 0x134 BUTTON_X
+# Cross
+key 0x130 BUTTON_A
+# Circle
+key 0x131 BUTTON_B
+# Triangle
+key 0x133 BUTTON_Y
+
+key 0x136 BUTTON_L1
+key 0x137 BUTTON_R1
+key 0x138 BUTTON_L2
+key 0x139 BUTTON_R2
+
+# L2 axis
+axis 0x02 LTRIGGER
+# R2 axis
+axis 0x05 RTRIGGER
+
+# Left Analog Stick
+axis 0x00 X
+axis 0x01 Y
+# Right Analog Stick
+axis 0x03 Z
+axis 0x04 RZ
+
+# Left stick click
+key 0x13d BUTTON_THUMBL
+# Right stick click
+key 0x13e BUTTON_THUMBR
+
+# Hat
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Share / "half-sun"
+key 0x13a BUTTON_SELECT
+# Options / three horizontal lines
+key 0x13b BUTTON_START
+# PS key
+key 0x13c BUTTON_MODE
+
+# SENSORs
+sensor 0x00 ACCELEROMETER X
+sensor 0x01 ACCELEROMETER Y
+sensor 0x02 ACCELEROMETER Z
+sensor 0x03 GYROSCOPE X
+sensor 0x04 GYROSCOPE Y
+sensor 0x05 GYROSCOPE Z
diff --git a/data/keyboards/Vendor_054c_Product_0df2_fallback.kl b/data/keyboards/Vendor_054c_Product_0df2_fallback.kl
new file mode 100644
index 0000000..bfebb17
--- /dev/null
+++ b/data/keyboards/Vendor_054c_Product_0df2_fallback.kl
@@ -0,0 +1,75 @@
+# Copyright (C) 2022 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.
+
+#
+# Sony Playstation(R) DualSense Edge Controller
+#
+
+# Use this if HID_PLAYSTATION is not available
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+# Square
+key 304 BUTTON_X
+# Cross
+key 305 BUTTON_A
+# Circle
+key 306 BUTTON_B
+# Triangle
+key 307 BUTTON_Y
+
+key 308 BUTTON_L1
+key 309 BUTTON_R1
+key 310 BUTTON_L2
+key 311 BUTTON_R2
+
+# L2 axis
+axis 0x03 LTRIGGER
+# R2 axis
+axis 0x04 RTRIGGER
+
+# Left Analog Stick
+axis 0x00 X
+axis 0x01 Y
+# Right Analog Stick
+axis 0x02 Z
+axis 0x05 RZ
+
+# Left stick click
+key 314 BUTTON_THUMBL
+# Right stick click
+key 315 BUTTON_THUMBR
+
+# Hat
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Share / "half-sun"
+key 312 BUTTON_SELECT
+# Options / three horizontal lines
+key 313 BUTTON_START
+# PS key
+key 316 BUTTON_MODE
+
+# Touchpad press
+key 317 BUTTON_1
+
+# SENSORs
+sensor 0x00 ACCELEROMETER X
+sensor 0x01 ACCELEROMETER Y
+sensor 0x02 ACCELEROMETER Z
+sensor 0x03 GYROSCOPE X
+sensor 0x04 GYROSCOPE Y
+sensor 0x05 GYROSCOPE Z
diff --git a/libs/WindowManager/Shell/res/values/config.xml b/libs/WindowManager/Shell/res/values/config.xml
index 23db233..774f6c6 100644
--- a/libs/WindowManager/Shell/res/values/config.xml
+++ b/libs/WindowManager/Shell/res/values/config.xml
@@ -115,4 +115,10 @@
<!-- Components support to launch multiple instances into split-screen -->
<string-array name="config_appsSupportMultiInstancesSplit">
</string-array>
+
+ <!-- Whether the extended restart dialog is enabled -->
+ <bool name="config_letterboxIsRestartDialogEnabled">false</bool>
+
+ <!-- Whether the additional education about reachability is enabled -->
+ <bool name="config_letterboxIsReachabilityEducationEnabled">false</bool>
</resources>
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIConfiguration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIConfiguration.java
new file mode 100644
index 0000000..4f33a71
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIConfiguration.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2022 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 com.android.wm.shell.compatui;
+
+import android.content.Context;
+import android.provider.DeviceConfig;
+
+import androidx.annotation.NonNull;
+
+import com.android.wm.shell.R;
+import com.android.wm.shell.common.ShellExecutor;
+import com.android.wm.shell.common.annotations.ShellMainThread;
+import com.android.wm.shell.dagger.WMSingleton;
+
+import javax.inject.Inject;
+
+/**
+ * Configuration flags for the CompatUX implementation
+ */
+@WMSingleton
+public class CompatUIConfiguration implements DeviceConfig.OnPropertiesChangedListener {
+
+ static final String KEY_ENABLE_LETTERBOX_RESTART_DIALOG = "enable_letterbox_restart_dialog";
+
+ static final String KEY_ENABLE_LETTERBOX_REACHABILITY_EDUCATION =
+ "enable_letterbox_reachability_education";
+
+ // Whether the extended restart dialog is enabled
+ private boolean mIsRestartDialogEnabled;
+
+ // Whether the additional education about reachability is enabled
+ private boolean mIsReachabilityEducationEnabled;
+
+ // Whether the extended restart dialog is enabled
+ private boolean mIsRestartDialogOverrideEnabled;
+
+ // Whether the additional education about reachability is enabled
+ private boolean mIsReachabilityEducationOverrideEnabled;
+
+ // Whether the extended restart dialog is allowed from backend
+ private boolean mIsLetterboxRestartDialogAllowed;
+
+ // Whether the additional education about reachability is allowed from backend
+ private boolean mIsLetterboxReachabilityEducationAllowed;
+
+ @Inject
+ public CompatUIConfiguration(Context context, @ShellMainThread ShellExecutor mainExecutor) {
+ mIsRestartDialogEnabled = context.getResources().getBoolean(
+ R.bool.config_letterboxIsRestartDialogEnabled);
+ mIsReachabilityEducationEnabled = context.getResources().getBoolean(
+ R.bool.config_letterboxIsReachabilityEducationEnabled);
+ mIsLetterboxRestartDialogAllowed = DeviceConfig.getBoolean(
+ DeviceConfig.NAMESPACE_WINDOW_MANAGER, KEY_ENABLE_LETTERBOX_RESTART_DIALOG, false);
+ mIsLetterboxReachabilityEducationAllowed = DeviceConfig.getBoolean(
+ DeviceConfig.NAMESPACE_WINDOW_MANAGER, KEY_ENABLE_LETTERBOX_REACHABILITY_EDUCATION,
+ false);
+ DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_APP_COMPAT, mainExecutor,
+ this);
+ }
+
+ /**
+ * @return {@value true} if the restart dialog is enabled.
+ */
+ boolean isRestartDialogEnabled() {
+ return mIsRestartDialogOverrideEnabled || (mIsRestartDialogEnabled
+ && mIsLetterboxRestartDialogAllowed);
+ }
+
+ /**
+ * Enables/Disables the restart education dialog
+ */
+ void setIsRestartDialogOverrideEnabled(boolean enabled) {
+ mIsRestartDialogOverrideEnabled = enabled;
+ }
+
+ /**
+ * @return {@value true} if the reachability education is enabled.
+ */
+ boolean isReachabilityEducationEnabled() {
+ return mIsReachabilityEducationOverrideEnabled || (mIsReachabilityEducationEnabled
+ && mIsLetterboxReachabilityEducationAllowed);
+ }
+
+ /**
+ * Enables/Disables the reachability education
+ */
+ void setIsReachabilityEducationOverrideEnabled(boolean enabled) {
+ mIsReachabilityEducationOverrideEnabled = enabled;
+ }
+
+ @Override
+ public void onPropertiesChanged(@NonNull DeviceConfig.Properties properties) {
+ // TODO(b/263349751): Update flag and default value to true
+ if (properties.getKeyset().contains(KEY_ENABLE_LETTERBOX_RESTART_DIALOG)) {
+ mIsLetterboxRestartDialogAllowed = DeviceConfig.getBoolean(
+ DeviceConfig.NAMESPACE_WINDOW_MANAGER, KEY_ENABLE_LETTERBOX_RESTART_DIALOG,
+ false);
+ }
+ if (properties.getKeyset().contains(KEY_ENABLE_LETTERBOX_REACHABILITY_EDUCATION)) {
+ mIsLetterboxReachabilityEducationAllowed = DeviceConfig.getBoolean(
+ DeviceConfig.NAMESPACE_WINDOW_MANAGER,
+ KEY_ENABLE_LETTERBOX_REACHABILITY_EDUCATION, false);
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIShellCommandHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIShellCommandHandler.java
new file mode 100644
index 0000000..4fb18e2
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIShellCommandHandler.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2022 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 com.android.wm.shell.compatui;
+
+import com.android.wm.shell.dagger.WMSingleton;
+import com.android.wm.shell.sysui.ShellCommandHandler;
+
+import java.io.PrintWriter;
+import java.util.function.Consumer;
+
+import javax.inject.Inject;
+
+/**
+ * Handles the shell commands for the CompatUX.
+ *
+ * <p> Use with {@code adb shell dumpsys activity service SystemUIService WMShell compatui
+ * <command>}.
+ */
+@WMSingleton
+public final class CompatUIShellCommandHandler implements
+ ShellCommandHandler.ShellCommandActionHandler {
+
+ private final CompatUIConfiguration mCompatUIConfiguration;
+ private final ShellCommandHandler mShellCommandHandler;
+
+ @Inject
+ public CompatUIShellCommandHandler(ShellCommandHandler shellCommandHandler,
+ CompatUIConfiguration compatUIConfiguration) {
+ mShellCommandHandler = shellCommandHandler;
+ mCompatUIConfiguration = compatUIConfiguration;
+ }
+
+ void onInit() {
+ mShellCommandHandler.addCommandCallback("compatui", this, this);
+ }
+
+ @Override
+ public boolean onShellCommand(String[] args, PrintWriter pw) {
+ if (args.length != 2) {
+ pw.println("Invalid command: " + args[0]);
+ return false;
+ }
+ switch (args[0]) {
+ case "restartDialogEnabled":
+ return invokeOrError(args[1], pw,
+ mCompatUIConfiguration::setIsRestartDialogOverrideEnabled);
+ case "reachabilityEducationEnabled":
+ return invokeOrError(args[1], pw,
+ mCompatUIConfiguration::setIsReachabilityEducationOverrideEnabled);
+ default:
+ pw.println("Invalid command: " + args[0]);
+ return false;
+ }
+ }
+
+ @Override
+ public void printShellCommandHelp(PrintWriter pw, String prefix) {
+ pw.println(prefix + "restartDialogEnabled [0|false|1|true]");
+ pw.println(prefix + " Enable/Disable the restart education dialog for Size Compat Mode");
+ pw.println(prefix + "reachabilityEducationEnabled [0|false|1|true]");
+ pw.println(prefix
+ + " Enable/Disable the restart education dialog for letterbox reachability");
+ pw.println(prefix + " Disable the restart education dialog for letterbox reachability");
+ }
+
+ private static boolean invokeOrError(String input, PrintWriter pw,
+ Consumer<Boolean> setter) {
+ Boolean asBoolean = strToBoolean(input);
+ if (asBoolean == null) {
+ pw.println("Error: expected true, 1, false, 0.");
+ return false;
+ }
+ setter.accept(asBoolean);
+ return true;
+ }
+
+ // Converts a String to boolean if possible or it returns null otherwise
+ private static Boolean strToBoolean(String str) {
+ switch(str) {
+ case "1":
+ case "true":
+ return true;
+ case "0":
+ case "false":
+ return false;
+ }
+ return null;
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/DialogAnimationController.java
similarity index 85%
rename from libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduAnimationController.java
rename to libs/WindowManager/Shell/src/com/android/wm/shell/compatui/DialogAnimationController.java
index 3061eab..7475fea 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduAnimationController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/DialogAnimationController.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.wm.shell.compatui.letterboxedu;
+package com.android.wm.shell.compatui;
import static com.android.internal.R.styleable.WindowAnimation_windowEnterAnimation;
import static com.android.internal.R.styleable.WindowAnimation_windowExitAnimation;
@@ -38,10 +38,15 @@
import com.android.internal.policy.TransitionAnimation;
/**
- * Controls the enter/exit animations of the letterbox education.
+ * Controls the enter/exit a dialog.
+ *
+ * @param <T> The {@link DialogContainerSupplier} to use
*/
-class LetterboxEduAnimationController {
- private static final String TAG = "LetterboxEduAnimation";
+public class DialogAnimationController<T extends DialogContainerSupplier> {
+
+ // The alpha of a background is a number between 0 (fully transparent) to 255 (fully opaque).
+ // 204 is simply 255 * 0.8.
+ static final int BACKGROUND_DIM_ALPHA = 204;
// If shell transitions are enabled, startEnterAnimation will be called after all transitions
// have finished, and therefore the start delay should be shorter.
@@ -49,6 +54,7 @@
private final TransitionAnimation mTransitionAnimation;
private final String mPackageName;
+ private final String mTag;
@AnyRes
private final int mAnimStyleResId;
@@ -57,23 +63,24 @@
@Nullable
private Animator mBackgroundDimAnimator;
- LetterboxEduAnimationController(Context context) {
- mTransitionAnimation = new TransitionAnimation(context, /* debug= */ false, TAG);
+ public DialogAnimationController(Context context, String tag) {
+ mTransitionAnimation = new TransitionAnimation(context, /* debug= */ false, tag);
mAnimStyleResId = (new ContextThemeWrapper(context,
android.R.style.ThemeOverlay_Material_Dialog).getTheme()).obtainStyledAttributes(
com.android.internal.R.styleable.Window).getResourceId(
com.android.internal.R.styleable.Window_windowAnimationStyle, 0);
mPackageName = context.getPackageName();
+ mTag = tag;
}
/**
* Starts both background dim fade-in animation and the dialog enter animation.
*/
- void startEnterAnimation(@NonNull LetterboxEduDialogLayout layout, Runnable endCallback) {
+ public void startEnterAnimation(@NonNull T layout, Runnable endCallback) {
// Cancel any previous animation if it's still running.
cancelAnimation();
- final View dialogContainer = layout.getDialogContainer();
+ final View dialogContainer = layout.getDialogContainerView();
mDialogAnimation = loadAnimation(WindowAnimation_windowEnterAnimation);
if (mDialogAnimation == null) {
endCallback.run();
@@ -86,8 +93,8 @@
endCallback.run();
}));
- mBackgroundDimAnimator = getAlphaAnimator(layout.getBackgroundDim(),
- /* endAlpha= */ LetterboxEduDialogLayout.BACKGROUND_DIM_ALPHA,
+ mBackgroundDimAnimator = getAlphaAnimator(layout.getBackgroundDimDrawable(),
+ /* endAlpha= */ BACKGROUND_DIM_ALPHA,
mDialogAnimation.getDuration());
mBackgroundDimAnimator.addListener(getDimAnimatorListener());
@@ -101,11 +108,11 @@
/**
* Starts both the background dim fade-out animation and the dialog exit animation.
*/
- void startExitAnimation(@NonNull LetterboxEduDialogLayout layout, Runnable endCallback) {
+ public void startExitAnimation(@NonNull T layout, Runnable endCallback) {
// Cancel any previous animation if it's still running.
cancelAnimation();
- final View dialogContainer = layout.getDialogContainer();
+ final View dialogContainer = layout.getDialogContainerView();
mDialogAnimation = loadAnimation(WindowAnimation_windowExitAnimation);
if (mDialogAnimation == null) {
endCallback.run();
@@ -119,8 +126,8 @@
endCallback.run();
}));
- mBackgroundDimAnimator = getAlphaAnimator(layout.getBackgroundDim(), /* endAlpha= */ 0,
- mDialogAnimation.getDuration());
+ mBackgroundDimAnimator = getAlphaAnimator(layout.getBackgroundDimDrawable(),
+ /* endAlpha= */ 0, mDialogAnimation.getDuration());
mBackgroundDimAnimator.addListener(getDimAnimatorListener());
dialogContainer.startAnimation(mDialogAnimation);
@@ -130,7 +137,7 @@
/**
* Cancels all animations and resets the state of the controller.
*/
- void cancelAnimation() {
+ public void cancelAnimation() {
if (mDialogAnimation != null) {
mDialogAnimation.cancel();
mDialogAnimation = null;
@@ -145,7 +152,7 @@
Animation animation = mTransitionAnimation.loadAnimationAttr(mPackageName, mAnimStyleResId,
animAttr, /* translucent= */ false);
if (animation == null) {
- Log.e(TAG, "Failed to load animation " + animAttr);
+ Log.e(mTag, "Failed to load animation " + animAttr);
}
return animation;
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/DialogContainerSupplier.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/DialogContainerSupplier.java
new file mode 100644
index 0000000..7eea446
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/DialogContainerSupplier.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2022 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 com.android.wm.shell.compatui;
+
+import android.graphics.drawable.Drawable;
+import android.view.View;
+
+/**
+ * A component which can provide a {@link View} to use as a container for a Dialog
+ */
+public interface DialogContainerSupplier {
+
+ /**
+ * @return The {@link View} to use as a container for a Dialog
+ */
+ View getDialogContainerView();
+
+ /**
+ * @return The {@link Drawable} to use as background of the dialog.
+ */
+ Drawable getBackgroundDimDrawable();
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduDialogLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduDialogLayout.java
index 2e0b09e..9232f36 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduDialogLayout.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduDialogLayout.java
@@ -26,6 +26,7 @@
import androidx.constraintlayout.widget.ConstraintLayout;
import com.android.wm.shell.R;
+import com.android.wm.shell.compatui.DialogContainerSupplier;
/**
* Container for Letterbox Education Dialog and background dim.
@@ -33,11 +34,7 @@
* <p>This layout should fill the entire task and the background around the dialog acts as the
* background dim which dismisses the dialog when clicked.
*/
-class LetterboxEduDialogLayout extends ConstraintLayout {
-
- // The alpha of a background is a number between 0 (fully transparent) to 255 (fully opaque).
- // 204 is simply 255 * 0.8.
- static final int BACKGROUND_DIM_ALPHA = 204;
+class LetterboxEduDialogLayout extends ConstraintLayout implements DialogContainerSupplier {
private View mDialogContainer;
private TextView mDialogTitle;
@@ -60,18 +57,20 @@
super(context, attrs, defStyleAttr, defStyleRes);
}
- View getDialogContainer() {
+ @Override
+ public View getDialogContainerView() {
return mDialogContainer;
}
+ @Override
+ public Drawable getBackgroundDimDrawable() {
+ return mBackgroundDim;
+ }
+
TextView getDialogTitle() {
return mDialogTitle;
}
- Drawable getBackgroundDim() {
- return mBackgroundDim;
- }
-
/**
* Register a callback for the dismiss button and background dim.
*
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduWindowManager.java
index 867d0ef..c14c009 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduWindowManager.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduWindowManager.java
@@ -37,6 +37,7 @@
import com.android.wm.shell.common.DockStateReader;
import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.compatui.CompatUIWindowManagerAbstract;
+import com.android.wm.shell.compatui.DialogAnimationController;
import com.android.wm.shell.transition.Transitions;
/**
@@ -63,7 +64,7 @@
*/
private final SharedPreferences mSharedPreferences;
- private final LetterboxEduAnimationController mAnimationController;
+ private final DialogAnimationController<LetterboxEduDialogLayout> mAnimationController;
private final Transitions mTransitions;
@@ -96,14 +97,17 @@
DisplayLayout displayLayout, Transitions transitions,
Runnable onDismissCallback, DockStateReader dockStateReader) {
this(context, taskInfo, syncQueue, taskListener, displayLayout, transitions,
- onDismissCallback, new LetterboxEduAnimationController(context), dockStateReader);
+ onDismissCallback,
+ new DialogAnimationController<>(context, /* tag */ "LetterboxEduWindowManager"),
+ dockStateReader);
}
@VisibleForTesting
LetterboxEduWindowManager(Context context, TaskInfo taskInfo,
SyncTransactionQueue syncQueue, ShellTaskOrganizer.TaskListener taskListener,
DisplayLayout displayLayout, Transitions transitions, Runnable onDismissCallback,
- LetterboxEduAnimationController animationController, DockStateReader dockStateReader) {
+ DialogAnimationController<LetterboxEduDialogLayout> animationController,
+ DockStateReader dockStateReader) {
super(context, taskInfo, syncQueue, taskListener, displayLayout);
mTransitions = transitions;
mOnDismissCallback = onDismissCallback;
@@ -160,7 +164,7 @@
if (mLayout == null) {
return;
}
- final View dialogContainer = mLayout.getDialogContainer();
+ final View dialogContainer = mLayout.getDialogContainerView();
MarginLayoutParams marginParams = (MarginLayoutParams) dialogContainer.getLayoutParams();
final Rect taskBounds = getTaskBounds();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldAnimationController.java
index 6b59e31..d7cb490 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldAnimationController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldAnimationController.java
@@ -16,8 +16,6 @@
package com.android.wm.shell.unfold;
-import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
-
import android.annotation.NonNull;
import android.app.ActivityManager.RunningTaskInfo;
import android.app.TaskInfo;
@@ -56,6 +54,12 @@
private final SparseArray<SurfaceControl> mTaskSurfaces = new SparseArray<>();
private final SparseArray<UnfoldTaskAnimator> mAnimatorsByTaskId = new SparseArray<>();
+ /**
+ * Indicates whether we're in stage change process. This should be set to {@code true} in
+ * {@link #onStateChangeStarted()} and {@code false} in {@link #onStateChangeFinished()}.
+ */
+ private boolean mIsInStageChange;
+
public UnfoldAnimationController(
@NonNull ShellInit shellInit,
@NonNull TransactionPool transactionPool,
@@ -123,7 +127,7 @@
animator.onTaskChanged(taskInfo);
} else {
// Became inapplicable
- resetTask(animator, taskInfo);
+ maybeResetTask(animator, taskInfo);
animator.onTaskVanished(taskInfo);
mAnimatorsByTaskId.remove(taskInfo.taskId);
}
@@ -154,7 +158,7 @@
final boolean isCurrentlyApplicable = animator != null;
if (isCurrentlyApplicable) {
- resetTask(animator, taskInfo);
+ maybeResetTask(animator, taskInfo);
animator.onTaskVanished(taskInfo);
mAnimatorsByTaskId.remove(taskInfo.taskId);
}
@@ -166,6 +170,7 @@
return;
}
+ mIsInStageChange = true;
SurfaceControl.Transaction transaction = null;
for (int i = 0; i < mAnimators.size(); i++) {
final UnfoldTaskAnimator animator = mAnimators.get(i);
@@ -219,11 +224,12 @@
transaction.apply();
mTransactionPool.release(transaction);
+ mIsInStageChange = false;
}
- private void resetTask(UnfoldTaskAnimator animator, TaskInfo taskInfo) {
- if (taskInfo.getWindowingMode() == WINDOWING_MODE_PINNED) {
- // PiP task has its own cleanup path, ignore surface reset to avoid conflict.
+ private void maybeResetTask(UnfoldTaskAnimator animator, TaskInfo taskInfo) {
+ if (!mIsInStageChange) {
+ // No need to resetTask if there is no ongoing state change.
return;
}
final SurfaceControl.Transaction transaction = mTransactionPool.acquire();
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduDialogLayoutTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduDialogLayoutTest.java
index 1dee88c..a58620d 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduDialogLayoutTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduDialogLayoutTest.java
@@ -68,11 +68,11 @@
@Test
public void testOnFinishInflate() {
- assertEquals(mLayout.getDialogContainer(),
+ assertEquals(mLayout.getDialogContainerView(),
mLayout.findViewById(R.id.letterbox_education_dialog_container));
assertEquals(mLayout.getDialogTitle(),
mLayout.findViewById(R.id.letterbox_education_dialog_title));
- assertEquals(mLayout.getBackgroundDim(), mLayout.getBackground());
+ assertEquals(mLayout.getBackgroundDimDrawable(), mLayout.getBackground());
assertEquals(mLayout.getBackground().getAlpha(), 0);
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduWindowManagerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduWindowManagerTest.java
index 16517c0..14190f1 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduWindowManagerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduWindowManagerTest.java
@@ -56,6 +56,7 @@
import com.android.wm.shell.common.DisplayLayout;
import com.android.wm.shell.common.DockStateReader;
import com.android.wm.shell.common.SyncTransactionQueue;
+import com.android.wm.shell.compatui.DialogAnimationController;
import com.android.wm.shell.transition.Transitions;
import org.junit.After;
@@ -98,7 +99,7 @@
@Captor
private ArgumentCaptor<Runnable> mRunOnIdleCaptor;
- @Mock private LetterboxEduAnimationController mAnimationController;
+ @Mock private DialogAnimationController<LetterboxEduDialogLayout> mAnimationController;
@Mock private SyncTransactionQueue mSyncTransactionQueue;
@Mock private ShellTaskOrganizer.TaskListener mTaskListener;
@Mock private SurfaceControlViewHost mViewHost;
@@ -366,7 +367,7 @@
assertThat(params.width).isEqualTo(expectedWidth);
assertThat(params.height).isEqualTo(expectedHeight);
MarginLayoutParams dialogParams =
- (MarginLayoutParams) layout.getDialogContainer().getLayoutParams();
+ (MarginLayoutParams) layout.getDialogContainerView().getLayoutParams();
int verticalMargin = (int) mContext.getResources().getDimension(
R.dimen.letterbox_education_dialog_margin);
assertThat(dialogParams.topMargin).isEqualTo(verticalMargin + expectedExtraTopMargin);
diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java
index fdd6233..19610a93 100644
--- a/media/java/android/media/AudioManager.java
+++ b/media/java/android/media/AudioManager.java
@@ -1841,8 +1841,7 @@
* {@link #setPreferredDeviceForStrategy(AudioProductStrategy, AudioDeviceAttributes)}
* {@link #setPreferredDevicesForStrategy(AudioProductStrategy, List<AudioDeviceAttributes>)}
* @param strategy the strategy to query
- * @return the preferred device for that strategy, or null if none was ever set or if the
- * strategy is invalid
+ * @return list of the preferred devices for that strategy
*/
@SystemApi
@RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
@@ -1859,6 +1858,76 @@
/**
* @hide
+ * Set a device as non-default for a given strategy, i.e. the audio routing to be avoided by
+ * this audio strategy.
+ * <p>Use
+ * {@link #removeDeviceAsNonDefaultForStrategy(AudioProductStrategy, AudioDeviceAttributes)}
+ * to cancel setting this preference for this strategy.</p>
+ * @param strategy the audio strategy whose routing will be affected
+ * @param device the audio device to not route to when available
+ * @return true if the operation was successful, false otherwise
+ */
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
+ public boolean setDeviceAsNonDefaultForStrategy(@NonNull AudioProductStrategy strategy,
+ @NonNull AudioDeviceAttributes device) {
+ Objects.requireNonNull(strategy);
+ Objects.requireNonNull(device);
+ try {
+ final int status =
+ getService().setDeviceAsNonDefaultForStrategy(strategy.getId(), device);
+ return status == AudioSystem.SUCCESS;
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * @hide
+ * Removes the audio device(s) from the non-default device list previously set with
+ * {@link #setDeviceAsNonDefaultForStrategy(AudioProductStrategy, AudioDeviceAttributes)}
+ * @param strategy the audio strategy whose routing will be affected
+ * @param device the audio device to remove from the non-default device list
+ * @return true if the operation was successful, false otherwise (invalid strategy, or no
+ * device set for example)
+ */
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
+ public boolean removeDeviceAsNonDefaultForStrategy(@NonNull AudioProductStrategy strategy,
+ @NonNull AudioDeviceAttributes device) {
+ Objects.requireNonNull(strategy);
+ Objects.requireNonNull(device);
+ try {
+ final int status =
+ getService().removeDeviceAsNonDefaultForStrategy(strategy.getId(), device);
+ return status == AudioSystem.SUCCESS;
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * @hide
+ * Gets the audio device(s) from the non-default device list previously set with
+ * {@link #setDeviceAsNonDefaultForStrategy(AudioProductStrategy, AudioDeviceAttributes)}
+ * @param strategy the audio strategy to query
+ * @return list of non-default devices for the strategy
+ */
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
+ @NonNull
+ public List<AudioDeviceAttributes> getNonDefaultDevicesForStrategy(
+ @NonNull AudioProductStrategy strategy) {
+ Objects.requireNonNull(strategy);
+ try {
+ return getService().getNonDefaultDevicesForStrategy(strategy.getId());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * @hide
* Interface to be notified of changes in the preferred audio device set for a given audio
* strategy.
* <p>Note that this listener will only be invoked whenever
@@ -1892,9 +1961,11 @@
* Interface to be notified of changes in the preferred audio devices set for a given audio
* strategy.
* <p>Note that this listener will only be invoked whenever
- * {@link #setPreferredDeviceForStrategy(AudioProductStrategy, AudioDeviceAttributes)} or
- * {@link #setPreferredDevicesForStrategy(AudioProductStrategy, List<AudioDeviceAttributes>)}
- * {@link #removePreferredDeviceForStrategy(AudioProductStrategy)} causes a change in
+ * {@link #setPreferredDeviceForStrategy(AudioProductStrategy, AudioDeviceAttributes)},
+ * {@link #setPreferredDevicesForStrategy(AudioProductStrategy, List<AudioDeviceAttributes>)},
+ * {@link #setDeviceAsNonDefaultForStrategy(AudioProductStrategy, AudioDeviceAttributes)},
+ * {@link #removeDeviceAsNonDefaultForStrategy(AudioProductStrategy, AudioDeviceAttributes)}
+ * or {@link #removePreferredDeviceForStrategy(AudioProductStrategy)} causes a change in
* preferred device(s). It will not be invoked directly after registration with
* {@link #addOnPreferredDevicesForStrategyChangedListener(
* Executor, OnPreferredDevicesForStrategyChangedListener)}
@@ -1902,7 +1973,6 @@
* @see #setPreferredDeviceForStrategy(AudioProductStrategy, AudioDeviceAttributes)
* @see #setPreferredDevicesForStrategy(AudioProductStrategy, List)
* @see #removePreferredDeviceForStrategy(AudioProductStrategy)
- * @see #getPreferredDeviceForStrategy(AudioProductStrategy)
* @see #getPreferredDevicesForStrategy(AudioProductStrategy)
*/
@SystemApi
@@ -1966,30 +2036,9 @@
throws SecurityException {
Objects.requireNonNull(executor);
Objects.requireNonNull(listener);
- synchronized (mPrefDevListenerLock) {
- if (hasPrefDevListener(listener)) {
- throw new IllegalArgumentException(
- "attempt to call addOnPreferredDevicesForStrategyChangedListener() "
- + "on a previously registered listener");
- }
- // lazy initialization of the list of strategy-preferred device listener
- if (mPrefDevListeners == null) {
- mPrefDevListeners = new ArrayList<>();
- }
- final int oldCbCount = mPrefDevListeners.size();
- mPrefDevListeners.add(new PrefDevListenerInfo(listener, executor));
- if (oldCbCount == 0 && mPrefDevListeners.size() > 0) {
- // register binder for callbacks
- if (mPrefDevDispatcherStub == null) {
- mPrefDevDispatcherStub = new StrategyPreferredDevicesDispatcherStub();
- }
- try {
- getService().registerStrategyPreferredDevicesDispatcher(mPrefDevDispatcherStub);
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- }
- }
- }
+ mPrefDevListenerMgr.addListener(
+ executor, listener, "addOnPreferredDevicesForStrategyChangedListener",
+ () -> new StrategyPreferredDevicesDispatcherStub());
}
/**
@@ -2002,106 +2051,145 @@
public void removeOnPreferredDevicesForStrategyChangedListener(
@NonNull OnPreferredDevicesForStrategyChangedListener listener) {
Objects.requireNonNull(listener);
- synchronized (mPrefDevListenerLock) {
- if (!removePrefDevListener(listener)) {
- throw new IllegalArgumentException(
- "attempt to call removeOnPreferredDeviceForStrategyChangedListener() "
- + "on an unregistered listener");
- }
- if (mPrefDevListeners.size() == 0) {
- // unregister binder for callbacks
- try {
- getService().unregisterStrategyPreferredDevicesDispatcher(
- mPrefDevDispatcherStub);
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- } finally {
- mPrefDevDispatcherStub = null;
- mPrefDevListeners = null;
- }
- }
- }
+ mPrefDevListenerMgr.removeListener(
+ listener, "removeOnPreferredDevicesForStrategyChangedListener");
}
-
- private final Object mPrefDevListenerLock = new Object();
/**
- * List of listeners for preferred device for strategy and their associated Executor.
- * List is lazy-initialized on first registration
+ * @hide
+ * Interface to be notified of changes in the non-default audio devices set for a given audio
+ * strategy.
+ * <p>Note that this listener will only be invoked whenever
+ * {@link #setPreferredDeviceForStrategy(AudioProductStrategy, AudioDeviceAttributes)},
+ * {@link #setPreferredDevicesForStrategy(AudioProductStrategy, List<AudioDeviceAttributes>)},
+ * {@link #setDeviceAsNonDefaultForStrategy(AudioProductStrategy, AudioDeviceAttributes)},
+ * {@link #removeDeviceAsNonDefaultForStrategy(AudioProductStrategy, AudioDeviceAttributes)}
+ * or {@link #removePreferredDeviceForStrategy(AudioProductStrategy)} causes a change in
+ * non-default device(s). It will not be invoked directly after registration with
+ * {@link #addOnNonDefaultDevicesForStrategyChangedListener(
+ * Executor, OnNonDefaultDevicesForStrategyChangedListener)}
+ * to indicate which strategies had preferred devices at the time of registration.</p>
+ * @see #setDeviceAsNonDefaultForStrategy(AudioProductStrategy, AudioDeviceAttributes)
+ * @see #removeDeviceAsNonDefaultForStrategy(AudioProductStrategy, AudioDeviceAttributes)
*/
- @GuardedBy("mPrefDevListenerLock")
- private @Nullable ArrayList<PrefDevListenerInfo> mPrefDevListeners;
-
- private static class PrefDevListenerInfo {
- final @NonNull OnPreferredDevicesForStrategyChangedListener mListener;
- final @NonNull Executor mExecutor;
- PrefDevListenerInfo(OnPreferredDevicesForStrategyChangedListener listener, Executor exe) {
- mListener = listener;
- mExecutor = exe;
- }
+ @SystemApi
+ public interface OnNonDefaultDevicesForStrategyChangedListener {
+ /**
+ * Called on the listener to indicate that the non-default audio devices for the given
+ * strategy has changed.
+ * @param strategy the {@link AudioProductStrategy} whose non-default device changed
+ * @param devices a list of newly set non-default audio devices
+ */
+ void onNonDefaultDevicesForStrategyChanged(@NonNull AudioProductStrategy strategy,
+ @NonNull List<AudioDeviceAttributes> devices);
}
- @GuardedBy("mPrefDevListenerLock")
- private StrategyPreferredDevicesDispatcherStub mPrefDevDispatcherStub;
+ /**
+ * @hide
+ * Adds a listener for being notified of changes to the non-default audio devices for
+ * strategies.
+ * @param executor
+ * @param listener
+ * @throws SecurityException if the caller doesn't hold the required permission
+ */
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
+ public void addOnNonDefaultDevicesForStrategyChangedListener(
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull OnNonDefaultDevicesForStrategyChangedListener listener)
+ throws SecurityException {
+ Objects.requireNonNull(executor);
+ Objects.requireNonNull(listener);
+
+ mNonDefDevListenerMgr.addListener(
+ executor, listener, "addOnNonDefaultDevicesForStrategyChangedListener",
+ () -> new StrategyNonDefaultDevicesDispatcherStub());
+ }
+
+ /**
+ * @hide
+ * Removes a previously added listener of changes to the non-default audio device for
+ * strategies.
+ * @param listener
+ */
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
+ public void removeOnNonDefaultDevicesForStrategyChangedListener(
+ @NonNull OnNonDefaultDevicesForStrategyChangedListener listener) {
+ Objects.requireNonNull(listener);
+ mNonDefDevListenerMgr.removeListener(
+ listener, "removeOnNonDefaultDevicesForStrategyChangedListener");
+ }
+
+ /**
+ * Manages the OnPreferredDevicesForStrategyChangedListener listeners and the
+ * StrategyPreferredDevicesDispatcherStub
+ */
+ private final CallbackUtil.LazyListenerManager<OnPreferredDevicesForStrategyChangedListener>
+ mPrefDevListenerMgr = new CallbackUtil.LazyListenerManager();
+
+ /**
+ * Manages the OnNonDefaultDevicesForStrategyChangedListener listeners and the
+ * StrategyNonDefaultDevicesDispatcherStub
+ */
+ private final CallbackUtil.LazyListenerManager<OnNonDefaultDevicesForStrategyChangedListener>
+ mNonDefDevListenerMgr = new CallbackUtil.LazyListenerManager();
private final class StrategyPreferredDevicesDispatcherStub
- extends IStrategyPreferredDevicesDispatcher.Stub {
+ extends IStrategyPreferredDevicesDispatcher.Stub
+ implements CallbackUtil.DispatcherStub {
@Override
public void dispatchPrefDevicesChanged(int strategyId,
@NonNull List<AudioDeviceAttributes> devices) {
- // make a shallow copy of listeners so callback is not executed under lock
- final ArrayList<PrefDevListenerInfo> prefDevListeners;
- synchronized (mPrefDevListenerLock) {
- if (mPrefDevListeners == null || mPrefDevListeners.size() == 0) {
- return;
- }
- prefDevListeners = (ArrayList<PrefDevListenerInfo>) mPrefDevListeners.clone();
- }
final AudioProductStrategy strategy =
AudioProductStrategy.getAudioProductStrategyWithId(strategyId);
- final long ident = Binder.clearCallingIdentity();
+
+ mPrefDevListenerMgr.callListeners(
+ (listener) -> listener.onPreferredDevicesForStrategyChanged(strategy, devices));
+ }
+
+ @Override
+ public void register(boolean register) {
try {
- for (PrefDevListenerInfo info : prefDevListeners) {
- info.mExecutor.execute(() ->
- info.mListener.onPreferredDevicesForStrategyChanged(strategy, devices));
+ if (register) {
+ getService().registerStrategyPreferredDevicesDispatcher(this);
+ } else {
+ getService().unregisterStrategyPreferredDevicesDispatcher(this);
}
- } finally {
- Binder.restoreCallingIdentity(ident);
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
}
}
}
- @GuardedBy("mPrefDevListenerLock")
- private @Nullable PrefDevListenerInfo getPrefDevListenerInfo(
- OnPreferredDevicesForStrategyChangedListener listener) {
- if (mPrefDevListeners == null) {
- return null;
+ private final class StrategyNonDefaultDevicesDispatcherStub
+ extends IStrategyNonDefaultDevicesDispatcher.Stub
+ implements CallbackUtil.DispatcherStub {
+
+ @Override
+ public void dispatchNonDefDevicesChanged(int strategyId,
+ @NonNull List<AudioDeviceAttributes> devices) {
+ final AudioProductStrategy strategy =
+ AudioProductStrategy.getAudioProductStrategyWithId(strategyId);
+
+ mNonDefDevListenerMgr.callListeners(
+ (listener) -> listener.onNonDefaultDevicesForStrategyChanged(
+ strategy, devices));
}
- for (PrefDevListenerInfo info : mPrefDevListeners) {
- if (info.mListener == listener) {
- return info;
+
+ @Override
+ public void register(boolean register) {
+ try {
+ if (register) {
+ getService().registerStrategyNonDefaultDevicesDispatcher(this);
+ } else {
+ getService().unregisterStrategyNonDefaultDevicesDispatcher(this);
+ }
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
}
}
- return null;
- }
-
- @GuardedBy("mPrefDevListenerLock")
- private boolean hasPrefDevListener(OnPreferredDevicesForStrategyChangedListener listener) {
- return getPrefDevListenerInfo(listener) != null;
- }
-
- @GuardedBy("mPrefDevListenerLock")
- /**
- * @return true if the listener was removed from the list
- */
- private boolean removePrefDevListener(OnPreferredDevicesForStrategyChangedListener listener) {
- final PrefDevListenerInfo infoToRemove = getPrefDevListenerInfo(listener);
- if (infoToRemove != null) {
- mPrefDevListeners.remove(infoToRemove);
- return true;
- }
- return false;
}
//====================================================================
diff --git a/media/java/android/media/AudioSystem.java b/media/java/android/media/AudioSystem.java
index a743586..9339c3d 100644
--- a/media/java/android/media/AudioSystem.java
+++ b/media/java/android/media/AudioSystem.java
@@ -2063,12 +2063,46 @@
/**
* @hide
+ * Remove device as role for product strategy.
+ * @param strategy the id of the strategy to configure
+ * @param role the role of the devices
+ * @param devices the list of devices to be removed as role for the given strategy
+ * @return {@link #SUCCESS} if successfully set
+ */
+ public static int removeDevicesRoleForStrategy(
+ int strategy, int role, @NonNull List<AudioDeviceAttributes> devices) {
+ if (devices.isEmpty()) {
+ return BAD_VALUE;
+ }
+ int[] types = new int[devices.size()];
+ String[] addresses = new String[devices.size()];
+ for (int i = 0; i < devices.size(); ++i) {
+ types[i] = devices.get(i).getInternalType();
+ addresses[i] = devices.get(i).getAddress();
+ }
+ return removeDevicesRoleForStrategy(strategy, role, types, addresses);
+ }
+
+ /**
+ * @hide
* Remove devices as role for the strategy
* @param strategy the id of the strategy to configure
* @param role the role of the devices
+ * @param types all device types
+ * @param addresses all device addresses
+ * @return {@link #SUCCESS} if successfully removed
+ */
+ public static native int removeDevicesRoleForStrategy(
+ int strategy, int role, @NonNull int[] types, @NonNull String[] addresses);
+
+ /**
+ * @hide
+ * Remove all devices as role for the strategy
+ * @param strategy the id of the strategy to configure
+ * @param role the role of the devices
* @return {@link #SUCCESS} if successfully removed
*/
- public static native int removeDevicesRoleForStrategy(int strategy, int role);
+ public static native int clearDevicesRoleForStrategy(int strategy, int role);
/**
* @hide
diff --git a/media/java/android/media/CallbackUtil.java b/media/java/android/media/CallbackUtil.java
index 2b5fd25..f0280da 100644
--- a/media/java/android/media/CallbackUtil.java
+++ b/media/java/android/media/CallbackUtil.java
@@ -183,7 +183,7 @@
if (!removeListener(listener, listeners)) {
throw new IllegalArgumentException("attempt to call " + methodName
- + "on an unregistered listener");
+ + " on an unregistered listener");
}
if (listeners.size() == 0) {
unregisterStub.accept(dispatchStub);
diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl
index 0f63cc4..4b36237 100644
--- a/media/java/android/media/IAudioService.aidl
+++ b/media/java/android/media/IAudioService.aidl
@@ -42,6 +42,7 @@
import android.media.IRecordingConfigDispatcher;
import android.media.IRingtonePlayer;
import android.media.IStrategyPreferredDevicesDispatcher;
+import android.media.IStrategyNonDefaultDevicesDispatcher;
import android.media.ISpatializerCallback;
import android.media.ISpatializerHeadTrackerAvailableCallback;
import android.media.ISpatializerHeadTrackingModeCallback;
@@ -330,7 +331,8 @@
boolean isCallScreeningModeSupported();
- int setPreferredDevicesForStrategy(in int strategy, in List<AudioDeviceAttributes> device);
+ @EnforcePermission("MODIFY_AUDIO_ROUTING")
+ int setPreferredDevicesForStrategy(in int strategy, in List<AudioDeviceAttributes> devices);
@EnforcePermission("MODIFY_AUDIO_ROUTING")
int removePreferredDevicesForStrategy(in int strategy);
@@ -338,6 +340,15 @@
@EnforcePermission("MODIFY_AUDIO_ROUTING")
List<AudioDeviceAttributes> getPreferredDevicesForStrategy(in int strategy);
+ @EnforcePermission("MODIFY_AUDIO_ROUTING")
+ int setDeviceAsNonDefaultForStrategy(in int strategy, in AudioDeviceAttributes device);
+
+ @EnforcePermission("MODIFY_AUDIO_ROUTING")
+ int removeDeviceAsNonDefaultForStrategy(in int strategy, in AudioDeviceAttributes device);
+
+ @EnforcePermission("MODIFY_AUDIO_ROUTING")
+ List<AudioDeviceAttributes> getNonDefaultDevicesForStrategy(in int strategy);
+
List<AudioDeviceAttributes> getDevicesForAttributes(in AudioAttributes attributes);
List<AudioDeviceAttributes> getDevicesForAttributesUnprotected(in AudioAttributes attributes);
@@ -351,6 +362,12 @@
oneway void unregisterStrategyPreferredDevicesDispatcher(
IStrategyPreferredDevicesDispatcher dispatcher);
+ void registerStrategyNonDefaultDevicesDispatcher(
+ IStrategyNonDefaultDevicesDispatcher dispatcher);
+
+ oneway void unregisterStrategyNonDefaultDevicesDispatcher(
+ IStrategyNonDefaultDevicesDispatcher dispatcher);
+
oneway void setRttEnabled(in boolean rttEnabled);
@EnforcePermission("MODIFY_AUDIO_ROUTING")
diff --git a/media/java/android/media/IStrategyNonDefaultDevicesDispatcher.aidl b/media/java/android/media/IStrategyNonDefaultDevicesDispatcher.aidl
new file mode 100644
index 0000000..59239cb
--- /dev/null
+++ b/media/java/android/media/IStrategyNonDefaultDevicesDispatcher.aidl
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2022 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.media;
+
+import android.media.AudioDeviceAttributes;
+
+/**
+ * AIDL for AudioService to signal non-daefault devices updates for audio strategies.
+ *
+ * {@hide}
+ */
+oneway interface IStrategyNonDefaultDevicesDispatcher {
+
+ void dispatchNonDefDevicesChanged(int strategyId, in List<AudioDeviceAttributes> devices);
+
+}
+
diff --git a/media/java/android/media/RouteListingPreference.java b/media/java/android/media/RouteListingPreference.java
index d74df7a..6a5b290 100644
--- a/media/java/android/media/RouteListingPreference.java
+++ b/media/java/android/media/RouteListingPreference.java
@@ -17,6 +17,7 @@
package android.media;
import android.annotation.IntDef;
+import android.annotation.IntRange;
import android.annotation.NonNull;
import android.os.Parcel;
import android.os.Parcelable;
@@ -198,19 +199,22 @@
@NonNull private final String mRouteId;
@Flags private final int mFlags;
@DisableReason private final int mDisableReason;
+ private final int mSessionParticipantCount;
private Item(@NonNull Builder builder) {
mRouteId = builder.mRouteId;
mFlags = builder.mFlags;
mDisableReason = builder.mDisableReason;
+ mSessionParticipantCount = builder.mSessionParticipantCount;
}
private Item(Parcel in) {
- String routeId = in.readString();
- Preconditions.checkArgument(!TextUtils.isEmpty(routeId));
- mRouteId = routeId;
+ mRouteId = in.readString();
+ Preconditions.checkArgument(!TextUtils.isEmpty(mRouteId));
mFlags = in.readInt();
mDisableReason = in.readInt();
+ mSessionParticipantCount = in.readInt();
+ Preconditions.checkArgument(mSessionParticipantCount >= 0);
}
/** Returns the id of the route that corresponds to this route listing preference item. */
@@ -244,6 +248,17 @@
return mDisableReason;
}
+ /**
+ * Returns a non-negative number of participants in the ongoing session (if any) on the
+ * corresponding route.
+ *
+ * <p>The system ignores this value if zero, or if {@link #getFlags()} does not include
+ * {@link #FLAG_ONGOING_SESSION}.
+ */
+ public int getSessionParticipantCount() {
+ return mSessionParticipantCount;
+ }
+
// Item Parcelable implementation.
@Override
@@ -256,6 +271,7 @@
dest.writeString(mRouteId);
dest.writeInt(mFlags);
dest.writeInt(mDisableReason);
+ dest.writeInt(mSessionParticipantCount);
}
// Equals and hashCode.
@@ -271,12 +287,13 @@
Item item = (Item) other;
return mRouteId.equals(item.mRouteId)
&& mFlags == item.mFlags
- && mDisableReason == item.mDisableReason;
+ && mDisableReason == item.mDisableReason
+ && mSessionParticipantCount == item.mSessionParticipantCount;
}
@Override
public int hashCode() {
- return Objects.hash(mRouteId, mFlags, mDisableReason);
+ return Objects.hash(mRouteId, mFlags, mDisableReason, mSessionParticipantCount);
}
/** Builder for {@link Item}. */
@@ -285,6 +302,7 @@
private final String mRouteId;
private int mFlags;
private int mDisableReason;
+ private int mSessionParticipantCount;
/**
* Constructor.
@@ -311,6 +329,17 @@
return this;
}
+ /** See {@link Item#getSessionParticipantCount()}. */
+ @NonNull
+ public Builder setSessionParticipantCount(
+ @IntRange(from = 0) int sessionParticipantCount) {
+ Preconditions.checkArgument(
+ sessionParticipantCount >= 0,
+ "sessionParticipantCount must be non-negative.");
+ mSessionParticipantCount = sessionParticipantCount;
+ return this;
+ }
+
/** Creates and returns a new {@link Item} with the given parameters. */
@NonNull
public Item build() {
diff --git a/media/java/android/media/tv/tuner/filter/Filter.java b/media/java/android/media/tv/tuner/filter/Filter.java
index 8568c43..7e9443b 100644
--- a/media/java/android/media/tv/tuner/filter/Filter.java
+++ b/media/java/android/media/tv/tuner/filter/Filter.java
@@ -154,7 +154,8 @@
/** @hide */
@IntDef(prefix = "STATUS_",
- value = {STATUS_DATA_READY, STATUS_LOW_WATER, STATUS_HIGH_WATER, STATUS_OVERFLOW})
+ value = {STATUS_DATA_READY, STATUS_LOW_WATER, STATUS_HIGH_WATER, STATUS_OVERFLOW,
+ STATUS_NO_DATA})
@Retention(RetentionPolicy.SOURCE)
public @interface Status {}
@@ -183,6 +184,10 @@
* discarded.
*/
public static final int STATUS_OVERFLOW = DemuxFilterStatus.OVERFLOW;
+ /**
+ * The status of a filter that the filter buffer is empty and no filtered data is coming.
+ */
+ public static final int STATUS_NO_DATA = DemuxFilterStatus.NO_DATA;
/** @hide */
@IntDef(prefix = "SCRAMBLING_STATUS_",
diff --git a/packages/SettingsLib/src/com/android/settingslib/fuelgauge/BatterySaverUtils.java b/packages/SettingsLib/src/com/android/settingslib/fuelgauge/BatterySaverUtils.java
index 3e710e4..28353ab 100644
--- a/packages/SettingsLib/src/com/android/settingslib/fuelgauge/BatterySaverUtils.java
+++ b/packages/SettingsLib/src/com/android/settingslib/fuelgauge/BatterySaverUtils.java
@@ -22,6 +22,7 @@
import android.os.Bundle;
import android.os.PowerManager;
import android.os.UserHandle;
+import android.provider.Settings;
import android.provider.Settings.Global;
import android.provider.Settings.Secure;
import android.util.KeyValueListParser;
@@ -55,6 +56,10 @@
public static final String EXTRA_POWER_SAVE_MODE_TRIGGER_LEVEL =
"extra_power_save_mode_trigger_level";
+ /** Battery saver schedule keys. */
+ public static final String KEY_NO_SCHEDULE = "key_battery_saver_no_schedule";
+ public static final String KEY_PERCENTAGE = "key_battery_saver_percentage";
+
private BatterySaverUtils() {
}
@@ -108,7 +113,6 @@
* - If it's 4th time through 8th time, show the schedule suggestion notification.
*
* @param enable true to enable battery saver.
- *
* @return true if the request succeeded.
*/
public static synchronized boolean setPowerSaveMode(Context context,
@@ -154,10 +158,10 @@
* Shows the battery saver confirmation warning if it hasn't been acknowledged by the user in
* the past before. Various extras can be provided that will change the behavior of this
* notification as well as the ui for it.
- * @param context A valid context
- * @param extras Any extras to include in the intent to trigger this confirmation that will
- * help the system disambiguate what to show/do
*
+ * @param context A valid context
+ * @param extras Any extras to include in the intent to trigger this confirmation that will
+ * help the system disambiguate what to show/do
* @return True if it showed the notification because it has not been previously acknowledged.
* @see #EXTRA_CONFIRM_TEXT_ONLY
* @see #EXTRA_POWER_SAVE_MODE_TRIGGER
@@ -221,6 +225,7 @@
/**
* Reverts battery saver schedule mode to none if routine mode is selected.
+ *
* @param context a valid context
*/
public static void revertScheduleToNoneIfNeeded(Context context) {
@@ -233,4 +238,50 @@
PowerManager.POWER_SAVE_MODE_TRIGGER_PERCENTAGE);
}
}
+
+ /**
+ * Gets battery saver schedule mode.
+ *
+ * @param context a valid context
+ * @return battery saver schedule key
+ */
+ public static String getBatterySaverScheduleKey(Context context) {
+ final ContentResolver resolver = context.getContentResolver();
+ final int mode = Settings.Global.getInt(resolver, Global.AUTOMATIC_POWER_SAVE_MODE,
+ PowerManager.POWER_SAVE_MODE_TRIGGER_PERCENTAGE);
+ if (mode == PowerManager.POWER_SAVE_MODE_TRIGGER_PERCENTAGE) {
+ final int threshold =
+ Settings.Global.getInt(resolver, Global.LOW_POWER_MODE_TRIGGER_LEVEL, 0);
+ return threshold <= 0 ? KEY_NO_SCHEDULE : KEY_PERCENTAGE;
+ }
+ revertScheduleToNoneIfNeeded(context);
+ return KEY_NO_SCHEDULE;
+ }
+
+ /**
+ * Sets battery saver schedule mode.
+ *
+ * @param context a valid context
+ * @param scheduleKey {@link #KEY_NO_SCHEDULE} and {@link #KEY_PERCENTAGE}
+ * @param triggerLevel for automatic battery saver trigger level
+ */
+ public static void setBatterySaverScheduleMode(Context context, String scheduleKey,
+ int triggerLevel) {
+ final ContentResolver resolver = context.getContentResolver();
+ switch (scheduleKey) {
+ case KEY_NO_SCHEDULE:
+ Settings.Global.putInt(resolver, Global.AUTOMATIC_POWER_SAVE_MODE,
+ PowerManager.POWER_SAVE_MODE_TRIGGER_PERCENTAGE);
+ Settings.Global.putInt(resolver, Settings.Global.LOW_POWER_MODE_TRIGGER_LEVEL, 0);
+ break;
+ case KEY_PERCENTAGE:
+ Settings.Global.putInt(resolver, Global.AUTOMATIC_POWER_SAVE_MODE,
+ PowerManager.POWER_SAVE_MODE_TRIGGER_PERCENTAGE);
+ Settings.Global.putInt(resolver,
+ Settings.Global.LOW_POWER_MODE_TRIGGER_LEVEL, triggerLevel);
+ break;
+ default:
+ throw new IllegalStateException("Not a valid schedule key");
+ }
+ }
}
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/fuelgauge/BatterySaverUtilsTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/fuelgauge/BatterySaverUtilsTest.java
index 2bb3c2a..a15fe9f 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/fuelgauge/BatterySaverUtilsTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/fuelgauge/BatterySaverUtilsTest.java
@@ -16,6 +16,9 @@
package com.android.settingslib.fuelgauge;
+import static com.android.settingslib.fuelgauge.BatterySaverUtils.KEY_NO_SCHEDULE;
+import static com.android.settingslib.fuelgauge.BatterySaverUtils.KEY_PERCENTAGE;
+
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertEquals;
@@ -186,4 +189,46 @@
assertThat(Secure.getInt(mMockResolver, Secure.SUPPRESS_AUTO_BATTERY_SAVER_SUGGESTION, -1))
.isEqualTo(1);
}
+
+ @Test
+ public void testGetBatterySaverScheduleKey_returnExpectedKey() {
+ Global.putInt(mMockResolver, Global.LOW_POWER_MODE_TRIGGER_LEVEL, 0);
+ Global.putInt(mMockResolver, Global.AUTOMATIC_POWER_SAVE_MODE,
+ PowerManager.POWER_SAVE_MODE_TRIGGER_PERCENTAGE);
+
+ assertThat(BatterySaverUtils.getBatterySaverScheduleKey(mMockContext)).isEqualTo(
+ KEY_NO_SCHEDULE);
+
+ Global.putInt(mMockResolver, Global.LOW_POWER_MODE_TRIGGER_LEVEL, 20);
+ Global.putInt(mMockResolver, Global.AUTOMATIC_POWER_SAVE_MODE,
+ PowerManager.POWER_SAVE_MODE_TRIGGER_PERCENTAGE);
+
+ assertThat(BatterySaverUtils.getBatterySaverScheduleKey(mMockContext)).isEqualTo(
+ KEY_PERCENTAGE);
+
+ Global.putInt(mMockResolver, Global.LOW_POWER_MODE_TRIGGER_LEVEL, 20);
+ Global.putInt(mMockResolver, Global.AUTOMATIC_POWER_SAVE_MODE,
+ PowerManager.POWER_SAVE_MODE_TRIGGER_DYNAMIC);
+
+ assertThat(BatterySaverUtils.getBatterySaverScheduleKey(mMockContext)).isEqualTo(
+ KEY_NO_SCHEDULE);
+ }
+
+ @Test
+ public void testSetBatterySaverScheduleMode_setSchedule() {
+ BatterySaverUtils.setBatterySaverScheduleMode(mMockContext, KEY_NO_SCHEDULE, -1);
+
+ assertThat(Global.getInt(mMockResolver, Global.AUTOMATIC_POWER_SAVE_MODE, -1))
+ .isEqualTo(PowerManager.POWER_SAVE_MODE_TRIGGER_PERCENTAGE);
+ assertThat(Global.getInt(mMockResolver, Global.LOW_POWER_MODE_TRIGGER_LEVEL, -1))
+ .isEqualTo(0);
+
+ BatterySaverUtils.setBatterySaverScheduleMode(mMockContext, KEY_PERCENTAGE, 20);
+
+ assertThat(Global.getInt(mMockResolver, Global.AUTOMATIC_POWER_SAVE_MODE, -1))
+ .isEqualTo(PowerManager.POWER_SAVE_MODE_TRIGGER_PERCENTAGE);
+ assertThat(Global.getInt(mMockResolver, Global.LOW_POWER_MODE_TRIGGER_LEVEL, -1))
+ .isEqualTo(20);
+
+ }
}
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index e1000e0..220c16a 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -31,6 +31,52 @@
],
}
+// Opt-in configuration for code depending on Jetpack Compose.
+soong_config_module_type {
+ name: "systemui_compose_java_defaults",
+ module_type: "java_defaults",
+ config_namespace: "ANDROID",
+ bool_variables: ["SYSTEMUI_USE_COMPOSE"],
+ properties: [
+ "srcs",
+ "static_libs",
+ ],
+}
+
+systemui_compose_java_defaults {
+ name: "SystemUI_compose_defaults",
+ soong_config_variables: {
+ SYSTEMUI_USE_COMPOSE: {
+ // Because files in compose/features/ depend on SystemUI
+ // code, we compile those files when compiling SystemUI-core.
+ // We also compile the ComposeFacade in
+ // compose/facade/enabled/.
+ srcs: [
+ "compose/features/src/**/*.kt",
+ "compose/facade/enabled/src/**/*.kt",
+ ],
+
+ // The dependencies needed by SystemUIComposeFeatures,
+ // except for SystemUI-core.
+ // Copied from compose/features/Android.bp.
+ static_libs: [
+ "SystemUIComposeCore",
+
+ "androidx.compose.runtime_runtime",
+ "androidx.compose.material3_material3",
+ "androidx.activity_activity-compose",
+ ],
+
+ // By default, Compose is disabled and we compile the ComposeFacade
+ // in compose/facade/disabled/.
+ conditions_default: {
+ srcs: ["compose/facade/disabled/src/**/*.kt"],
+ static_libs: [],
+ },
+ },
+ },
+}
+
java_library {
name: "SystemUI-proto",
@@ -68,6 +114,9 @@
android_library {
name: "SystemUI-core",
+ defaults: [
+ "SystemUI_compose_defaults",
+ ],
srcs: [
"src/**/*.kt",
"src/**/*.java",
@@ -228,6 +277,9 @@
android_library {
name: "SystemUI-tests",
+ defaults: [
+ "SystemUI_compose_defaults",
+ ],
manifest: "tests/AndroidManifest-base.xml",
additional_manifests: ["tests/AndroidManifest.xml"],
srcs: [
diff --git a/packages/SystemUI/compose/core/src/com/android/systemui/compose/runtime/MovableContent.kt b/packages/SystemUI/compose/core/src/com/android/systemui/compose/runtime/MovableContent.kt
new file mode 100644
index 0000000..3f2f96b
--- /dev/null
+++ b/packages/SystemUI/compose/core/src/com/android/systemui/compose/runtime/MovableContent.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2022 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 com.android.systemui.compose.runtime
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.InternalComposeApi
+import androidx.compose.runtime.MovableContent
+import androidx.compose.runtime.currentComposer
+
+/**
+ * An overload of [androidx.compose.runtime.movableContentOf] with 5 parameters.
+ *
+ * @see androidx.compose.runtime.movableContentOf
+ */
+@OptIn(InternalComposeApi::class)
+fun <P1, P2, P3, P4, P5> movableContentOf(
+ content: @Composable (P1, P2, P3, P4, P5) -> Unit
+): @Composable (P1, P2, P3, P4, P5) -> Unit {
+ val movableContent =
+ MovableContent<Pair<Triple<P1, P2, P3>, Pair<P4, P5>>> {
+ content(
+ it.first.first,
+ it.first.second,
+ it.first.third,
+ it.second.first,
+ it.second.second,
+ )
+ }
+ return { p1, p2, p3, p4, p5 ->
+ currentComposer.insertMovableContent(movableContent, Triple(p1, p2, p3) to (p4 to p5))
+ }
+}
diff --git a/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt b/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt
new file mode 100644
index 0000000..6e728ce
--- /dev/null
+++ b/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2022 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 com.android.systemui.compose
+
+import androidx.activity.ComponentActivity
+import com.android.systemui.people.ui.viewmodel.PeopleViewModel
+
+/** The Compose facade, when Compose is *not* available. */
+object ComposeFacade : BaseComposeFacade {
+ override fun isComposeAvailable(): Boolean = false
+
+ override fun setPeopleSpaceActivityContent(
+ activity: ComponentActivity,
+ viewModel: PeopleViewModel,
+ onResult: (PeopleViewModel.Result) -> Unit,
+ ) {
+ throwComposeUnavailableError()
+ }
+
+ private fun throwComposeUnavailableError() {
+ error(
+ "Compose is not available. Make sure to check isComposeAvailable() before calling any" +
+ " other function on ComposeFacade."
+ )
+ }
+}
diff --git a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt
new file mode 100644
index 0000000..16294d9
--- /dev/null
+++ b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2022 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 com.android.systemui.compose
+
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import com.android.systemui.compose.theme.SystemUITheme
+import com.android.systemui.people.ui.compose.PeopleScreen
+import com.android.systemui.people.ui.viewmodel.PeopleViewModel
+
+/** The Compose facade, when Compose is available. */
+object ComposeFacade : BaseComposeFacade {
+ override fun isComposeAvailable(): Boolean = true
+
+ override fun setPeopleSpaceActivityContent(
+ activity: ComponentActivity,
+ viewModel: PeopleViewModel,
+ onResult: (PeopleViewModel.Result) -> Unit,
+ ) {
+ activity.setContent { SystemUITheme { PeopleScreen(viewModel, onResult) } }
+ }
+}
diff --git a/packages/SystemUI/compose/features/Android.bp b/packages/SystemUI/compose/features/Android.bp
index 325ede6..4533330 100644
--- a/packages/SystemUI/compose/features/Android.bp
+++ b/packages/SystemUI/compose/features/Android.bp
@@ -35,6 +35,7 @@
"androidx.compose.runtime_runtime",
"androidx.compose.material3_material3",
+ "androidx.activity_activity-compose",
],
kotlincflags: ["-Xjvm-default=all"],
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/people/ui/compose/PeopleScreen.kt b/packages/SystemUI/compose/features/src/com/android/systemui/people/ui/compose/PeopleScreen.kt
index 2aac46e..4a56b02 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/people/ui/compose/PeopleScreen.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/people/ui/compose/PeopleScreen.kt
@@ -139,11 +139,20 @@
bottom = PeopleSpacePadding,
start = 8.dp,
end = 8.dp,
- )
+ ),
) {
- ConversationList(R.string.priority_conversations, priorityTiles, onTileClicked)
- item { Spacer(Modifier.height(35.dp)) }
- ConversationList(R.string.recent_conversations, recentTiles, onTileClicked)
+ val hasPriorityConversations = priorityTiles.isNotEmpty()
+ if (hasPriorityConversations) {
+ ConversationList(R.string.priority_conversations, priorityTiles, onTileClicked)
+ }
+
+ if (recentTiles.isNotEmpty()) {
+ if (hasPriorityConversations) {
+ item { Spacer(Modifier.height(35.dp)) }
+ }
+
+ ConversationList(R.string.recent_conversations, recentTiles, onTileClicked)
+ }
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt b/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt
new file mode 100644
index 0000000..e5ec727
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2022 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 com.android.systemui.compose
+
+import androidx.activity.ComponentActivity
+import com.android.systemui.people.ui.viewmodel.PeopleViewModel
+
+/**
+ * A facade to interact with Compose, when it is available.
+ *
+ * You should access this facade by calling the static methods on
+ * [com.android.systemui.compose.ComposeFacade] directly.
+ */
+interface BaseComposeFacade {
+ /**
+ * Whether Compose is currently available. This function should be checked before calling any
+ * other functions on this facade.
+ *
+ * This value will never change at runtime.
+ */
+ fun isComposeAvailable(): Boolean
+
+ /** Bind the content of [activity] to [viewModel]. */
+ fun setPeopleSpaceActivityContent(
+ activity: ComponentActivity,
+ viewModel: PeopleViewModel,
+ onResult: (PeopleViewModel.Result) -> Unit,
+ )
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
index 96707f4..59f68f7 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
@@ -44,6 +44,7 @@
import com.android.systemui.statusbar.notification.fsi.FsiChromeRepo
import com.android.systemui.statusbar.notification.InstantAppNotifier
import com.android.systemui.statusbar.notification.fsi.FsiChromeViewModelFactory
+import com.android.systemui.statusbar.notification.fsi.FsiChromeViewBinder
import com.android.systemui.statusbar.phone.KeyguardLiftController
import com.android.systemui.stylus.StylusUsiPowerStartable
import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator
@@ -94,6 +95,12 @@
@ClassKey(FsiChromeViewModelFactory::class)
abstract fun bindFSIChromeWindowViewModel(sysui: FsiChromeViewModelFactory): CoreStartable
+ /** Inject into FsiChromeWindowBinder. */
+ @Binds
+ @IntoMap
+ @ClassKey(FsiChromeViewBinder::class)
+ abstract fun bindFsiChromeWindowBinder(sysui: FsiChromeViewBinder): CoreStartable
+
/** Inject into GarbageMonitor.Service. */
@Binds
@IntoMap
diff --git a/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceActivity.java b/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceActivity.java
index 7cc95a1..fba5f63 100644
--- a/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceActivity.java
+++ b/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceActivity.java
@@ -27,11 +27,15 @@
import androidx.activity.ComponentActivity;
import androidx.lifecycle.ViewModelProvider;
+import com.android.systemui.compose.ComposeFacade;
import com.android.systemui.people.ui.view.PeopleViewBinder;
import com.android.systemui.people.ui.viewmodel.PeopleViewModel;
import javax.inject.Inject;
+import kotlin.Unit;
+import kotlin.jvm.functions.Function1;
+
/** People Tile Widget configuration activity that shows the user their conversation tiles. */
public class PeopleSpaceActivity extends ComponentActivity {
@@ -58,13 +62,18 @@
int widgetId = getIntent().getIntExtra(EXTRA_APPWIDGET_ID, INVALID_APPWIDGET_ID);
viewModel.onWidgetIdChanged(widgetId);
- ViewGroup view = PeopleViewBinder.create(this);
- PeopleViewBinder.bind(view, viewModel, /* lifecycleOwner= */ this,
- (result) -> {
- finishActivity(result);
- return null;
- });
- setContentView(view);
+ Function1<PeopleViewModel.Result, Unit> onResult = (result) -> {
+ finishActivity(result);
+ return null;
+ };
+
+ if (ComposeFacade.INSTANCE.isComposeAvailable()) {
+ ComposeFacade.INSTANCE.setPeopleSpaceActivityContent(this, viewModel, onResult);
+ } else {
+ ViewGroup view = PeopleViewBinder.create(this);
+ PeopleViewBinder.bind(view, viewModel, /* lifecycleOwner= */ this, onResult);
+ setContentView(view);
+ }
}
private void finishActivity(PeopleViewModel.Result result) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/fsi/FsiChromeViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/fsi/FsiChromeViewBinder.kt
new file mode 100644
index 0000000..1a3927b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/fsi/FsiChromeViewBinder.kt
@@ -0,0 +1,99 @@
+package com.android.systemui.statusbar.notification.fsi
+
+import android.content.Context
+import android.view.LayoutInflater
+import android.view.WindowManager
+import com.android.systemui.CoreStartable
+import com.android.systemui.R
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.statusbar.notification.fsi.FsiDebug.Companion.log
+import com.android.systemui.statusbar.phone.CentralSurfaces
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+import java.util.concurrent.Executor
+import javax.inject.Inject
+
+@SysUISingleton
+class FsiChromeViewBinder
+@Inject
+constructor(
+ val context: Context,
+ val windowManager: WindowManager,
+ val viewModelFactory: FsiChromeViewModelFactory,
+ val layoutInflater: LayoutInflater,
+ val centralSurfaces: CentralSurfaces,
+ @Main val mainExecutor: Executor,
+ @Application val scope: CoroutineScope,
+) : CoreStartable {
+
+ companion object {
+ private const val classTag = "FsiChromeViewBinder"
+ }
+
+ private val fsiChromeView =
+ layoutInflater.inflate(R.layout.fsi_chrome_view, null /* root */, false /* attachToRoot */)
+ as FsiChromeView
+
+ var addedToWindowManager = false
+ var cornerRadius: Int = context.resources.getDimensionPixelSize(
+ R.dimen.notification_corner_radius)
+
+ override fun start() {
+ val methodTag = "start"
+ log("$classTag $methodTag ")
+
+ scope.launch {
+ log("$classTag $methodTag launch ")
+ viewModelFactory.viewModelFlow.collect { vm -> updateForViewModel(vm) }
+ }
+ }
+
+ private fun updateForViewModel(vm: FsiChromeViewModel?) {
+ val methodTag = "updateForViewModel"
+
+ if (vm == null) {
+ log("$classTag $methodTag viewModel is null, removing from window manager")
+
+ if (addedToWindowManager) {
+ windowManager.removeView(fsiChromeView)
+ addedToWindowManager = false
+ }
+ return
+ }
+
+ bindViewModel(vm, windowManager)
+
+ if (addedToWindowManager) {
+ log("$classTag $methodTag already addedToWindowManager")
+ } else {
+ windowManager.addView(fsiChromeView, FsiTaskViewConfig.getWmLayoutParams("PackageName"))
+ addedToWindowManager = true
+ }
+ }
+
+ private fun bindViewModel(
+ vm: FsiChromeViewModel,
+ windowManager: WindowManager,
+ ) {
+ log("$classTag bindViewModel")
+
+ fsiChromeView.appIconImageView.setImageDrawable(vm.appIcon)
+ fsiChromeView.appNameTextView.text = vm.appName
+
+ fsiChromeView.dismissButton.setOnClickListener { vm.onDismiss() }
+ fsiChromeView.fullscreenButton.setOnClickListener { vm.onFullscreen() }
+
+ vm.taskView.cornerRadius = cornerRadius.toFloat()
+ vm.taskView.startActivity(
+ vm.fsi,
+ FsiTaskViewConfig.getFillInIntent(),
+ FsiTaskViewConfig.getActivityOptions(context, windowManager),
+ FsiTaskViewConfig.getLaunchBounds(windowManager)
+ )
+
+ log("$classTag bindViewModel started taskview activity")
+ fsiChromeView.addView(vm.taskView)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/fsi/FsiTaskViewConfig.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/fsi/FsiTaskViewConfig.kt
new file mode 100644
index 0000000..034ab56
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/fsi/FsiTaskViewConfig.kt
@@ -0,0 +1,75 @@
+package com.android.systemui.statusbar.notification.fsi
+
+import android.app.ActivityOptions
+import android.content.Context
+import android.content.Intent
+import android.graphics.PixelFormat
+import android.graphics.Rect
+import android.os.Binder
+import android.view.ViewGroup
+import android.view.WindowManager
+
+/**
+ * Config for adding the FsiChromeView window to WindowManager and starting the FSI activity.
+ */
+class FsiTaskViewConfig {
+
+ companion object {
+
+ private const val classTag = "FsiTaskViewConfig"
+
+ fun getWmLayoutParams(packageName: String): WindowManager.LayoutParams {
+ val params: WindowManager.LayoutParams?
+ params =
+ WindowManager.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL,
+ WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE or
+ WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED or
+ WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER,
+ PixelFormat.TRANSLUCENT
+ )
+ params.setTrustedOverlay()
+ params.fitInsetsTypes = 0
+ params.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE
+ params.token = Binder()
+ params.packageName = packageName
+ params.layoutInDisplayCutoutMode =
+ WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
+ params.privateFlags =
+ params.privateFlags or WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS
+ return params
+ }
+
+ fun getFillInIntent(): Intent {
+ val fillInIntent = Intent()
+ fillInIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT)
+ fillInIntent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK)
+ // FLAG_ACTIVITY_NEW_TASK is auto-applied because
+ // we're starting the FSI activity from a non-Activity context
+ return fillInIntent
+ }
+
+ fun getLaunchBounds(windowManager: WindowManager): Rect {
+ // TODO(b/243421660) check this works for non-resizeable activity
+ return Rect()
+ }
+
+ fun getActivityOptions(context: Context, windowManager: WindowManager): ActivityOptions {
+ // Custom options so there is no activity transition animation
+ val options =
+ ActivityOptions.makeCustomAnimation(context, 0 /* enterResId */, 0 /* exitResId */)
+
+ options.taskAlwaysOnTop = true
+
+ options.pendingIntentLaunchFlags =
+ Intent.FLAG_ACTIVITY_NEW_DOCUMENT or
+ Intent.FLAG_ACTIVITY_MULTIPLE_TASK or
+ Intent.FLAG_ACTIVITY_NEW_TASK
+
+ options.launchBounds = getLaunchBounds(windowManager)
+ return options
+ }
+ }
+}
diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
index 4d173d6..cdd5471 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
@@ -22,9 +22,9 @@
import static android.view.WindowManager.LayoutParams.FLAG_SECURE;
import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS;
+import android.annotation.EnforcePermission;
import android.annotation.NonNull;
import android.annotation.Nullable;
-import android.annotation.RequiresPermission;
import android.annotation.StringRes;
import android.app.Activity;
import android.app.ActivityOptions;
@@ -62,6 +62,7 @@
import android.os.IBinder;
import android.os.LocaleList;
import android.os.Looper;
+import android.os.PermissionEnforcer;
import android.os.PowerManager;
import android.os.RemoteException;
import android.os.ResultReceiver;
@@ -198,6 +199,7 @@
IVirtualDeviceActivityListener activityListener,
Consumer<ArraySet<Integer>> runningAppsChangedCallback,
VirtualDeviceParams params) {
+ super(PermissionEnforcer.fromContext(context));
UserHandle ownerUserHandle = UserHandle.getUserHandleForUid(ownerUid);
mContext = context.createContextAsUser(ownerUserHandle, 0);
mAssociationInfo = associationInfo;
@@ -337,11 +339,9 @@
}
@Override // Binder call
+ @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public void close() {
- mContext.enforceCallingOrSelfPermission(
- android.Manifest.permission.CREATE_VIRTUAL_DEVICE,
- "Permission required to close the virtual device");
-
+ super.close_enforcePermission();
synchronized (mVirtualDeviceLock) {
if (!mPerDisplayWakelocks.isEmpty()) {
mPerDisplayWakelocks.forEach((displayId, wakeLock) -> {
@@ -389,14 +389,12 @@
return mWindowPolicyControllers;
}
- @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
@Override // Binder call
+ @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public void onAudioSessionStarting(int displayId,
@NonNull IAudioRoutingCallback routingCallback,
@Nullable IAudioConfigChangedCallback configChangedCallback) {
- mContext.enforceCallingOrSelfPermission(
- android.Manifest.permission.CREATE_VIRTUAL_DEVICE,
- "Permission required to start audio session");
+ super.onAudioSessionStarting_enforcePermission();
synchronized (mVirtualDeviceLock) {
if (!mVirtualDisplayIds.contains(displayId)) {
throw new SecurityException(
@@ -413,12 +411,10 @@
}
}
- @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
@Override // Binder call
+ @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public void onAudioSessionEnded() {
- mContext.enforceCallingOrSelfPermission(
- android.Manifest.permission.CREATE_VIRTUAL_DEVICE,
- "Permission required to stop audio session");
+ super.onAudioSessionEnded_enforcePermission();
synchronized (mVirtualDeviceLock) {
if (mVirtualAudioController != null) {
mVirtualAudioController.stopListening();
@@ -428,9 +424,9 @@
}
@Override // Binder call
+ @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public void createVirtualDpad(VirtualDpadConfig config, @NonNull IBinder deviceToken) {
- mContext.enforceCallingOrSelfPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE,
- "Permission required to create a virtual dpad");
+ super.createVirtualDpad_enforcePermission();
synchronized (mVirtualDeviceLock) {
if (!mVirtualDisplayIds.contains(config.getAssociatedDisplayId())) {
throw new SecurityException(
@@ -448,9 +444,9 @@
}
@Override // Binder call
+ @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public void createVirtualKeyboard(VirtualKeyboardConfig config, @NonNull IBinder deviceToken) {
- mContext.enforceCallingOrSelfPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE,
- "Permission required to create a virtual keyboard");
+ super.createVirtualKeyboard_enforcePermission();
synchronized (mVirtualDeviceLock) {
if (!mVirtualDisplayIds.contains(config.getAssociatedDisplayId())) {
throw new SecurityException(
@@ -470,9 +466,9 @@
}
@Override // Binder call
+ @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public void createVirtualMouse(VirtualMouseConfig config, @NonNull IBinder deviceToken) {
- mContext.enforceCallingOrSelfPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE,
- "Permission required to create a virtual mouse");
+ super.createVirtualMouse_enforcePermission();
synchronized (mVirtualDeviceLock) {
if (!mVirtualDisplayIds.contains(config.getAssociatedDisplayId())) {
throw new SecurityException(
@@ -490,10 +486,10 @@
}
@Override // Binder call
+ @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public void createVirtualTouchscreen(VirtualTouchscreenConfig config,
@NonNull IBinder deviceToken) {
- mContext.enforceCallingOrSelfPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE,
- "Permission required to create a virtual touchscreen");
+ super.createVirtualTouchscreen_enforcePermission();
synchronized (mVirtualDeviceLock) {
if (!mVirtualDisplayIds.contains(config.getAssociatedDisplayId())) {
throw new SecurityException(
@@ -501,30 +497,29 @@
+ "this virtual device");
}
}
- int screenHeightPixels = config.getHeightInPixels();
- int screenWidthPixels = config.getWidthInPixels();
- if (screenHeightPixels <= 0 || screenWidthPixels <= 0) {
+ int screenHeight = config.getHeight();
+ int screenWidth = config.getWidth();
+ if (screenHeight <= 0 || screenWidth <= 0) {
throw new IllegalArgumentException(
"Cannot create a virtual touchscreen, screen dimensions must be positive. Got: "
- + "(" + screenWidthPixels + ", " + screenHeightPixels + ")");
+ + "(" + screenWidth + ", " + screenHeight + ")");
}
final long ident = Binder.clearCallingIdentity();
try {
mInputController.createTouchscreen(config.getInputDeviceName(), config.getVendorId(),
config.getProductId(), deviceToken, config.getAssociatedDisplayId(),
- screenHeightPixels, screenWidthPixels);
+ screenHeight, screenWidth);
} finally {
Binder.restoreCallingIdentity(ident);
}
}
@Override // Binder call
+ @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public void createVirtualNavigationTouchpad(VirtualNavigationTouchpadConfig config,
@NonNull IBinder deviceToken) {
- mContext.enforceCallingOrSelfPermission(
- android.Manifest.permission.CREATE_VIRTUAL_DEVICE,
- "Permission required to create a virtual navigation touchpad");
+ super.createVirtualNavigationTouchpad_enforcePermission();
synchronized (mVirtualDeviceLock) {
if (!mVirtualDisplayIds.contains(config.getAssociatedDisplayId())) {
throw new SecurityException(
@@ -552,11 +547,9 @@
}
@Override // Binder call
+ @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public void unregisterInputDevice(IBinder token) {
- mContext.enforceCallingOrSelfPermission(
- android.Manifest.permission.CREATE_VIRTUAL_DEVICE,
- "Permission required to unregister this input device");
-
+ super.unregisterInputDevice_enforcePermission();
final long ident = Binder.clearCallingIdentity();
try {
mInputController.unregisterInputDevice(token);
@@ -577,7 +570,9 @@
@Override // Binder call
+ @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public boolean sendDpadKeyEvent(IBinder token, VirtualKeyEvent event) {
+ super.sendDpadKeyEvent_enforcePermission();
final long ident = Binder.clearCallingIdentity();
try {
return mInputController.sendDpadKeyEvent(token, event);
@@ -587,7 +582,9 @@
}
@Override // Binder call
+ @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public boolean sendKeyEvent(IBinder token, VirtualKeyEvent event) {
+ super.sendKeyEvent_enforcePermission();
final long ident = Binder.clearCallingIdentity();
try {
return mInputController.sendKeyEvent(token, event);
@@ -597,7 +594,9 @@
}
@Override // Binder call
+ @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public boolean sendButtonEvent(IBinder token, VirtualMouseButtonEvent event) {
+ super.sendButtonEvent_enforcePermission();
final long ident = Binder.clearCallingIdentity();
try {
return mInputController.sendButtonEvent(token, event);
@@ -607,7 +606,9 @@
}
@Override // Binder call
+ @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public boolean sendTouchEvent(IBinder token, VirtualTouchEvent event) {
+ super.sendTouchEvent_enforcePermission();
final long ident = Binder.clearCallingIdentity();
try {
return mInputController.sendTouchEvent(token, event);
@@ -617,7 +618,9 @@
}
@Override // Binder call
+ @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public boolean sendRelativeEvent(IBinder token, VirtualMouseRelativeEvent event) {
+ super.sendRelativeEvent_enforcePermission();
final long ident = Binder.clearCallingIdentity();
try {
return mInputController.sendRelativeEvent(token, event);
@@ -627,7 +630,9 @@
}
@Override // Binder call
+ @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public boolean sendScrollEvent(IBinder token, VirtualMouseScrollEvent event) {
+ super.sendScrollEvent_enforcePermission();
final long ident = Binder.clearCallingIdentity();
try {
return mInputController.sendScrollEvent(token, event);
@@ -647,11 +652,9 @@
}
@Override // Binder call
+ @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public void setShowPointerIcon(boolean showPointerIcon) {
- mContext.enforceCallingOrSelfPermission(
- android.Manifest.permission.CREATE_VIRTUAL_DEVICE,
- "Permission required to unregister this input device");
-
+ super.setShowPointerIcon_enforcePermission();
final long ident = Binder.clearCallingIdentity();
try {
synchronized (mVirtualDeviceLock) {
@@ -666,12 +669,11 @@
}
@Override // Binder call
+ @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public void createVirtualSensor(
@NonNull IBinder deviceToken,
@NonNull VirtualSensorConfig config) {
- mContext.enforceCallingOrSelfPermission(
- android.Manifest.permission.CREATE_VIRTUAL_DEVICE,
- "Permission required to create a virtual sensor");
+ super.createVirtualSensor_enforcePermission();
Objects.requireNonNull(config);
Objects.requireNonNull(deviceToken);
final long ident = Binder.clearCallingIdentity();
@@ -683,10 +685,9 @@
}
@Override // Binder call
+ @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public void unregisterSensor(@NonNull IBinder token) {
- mContext.enforceCallingOrSelfPermission(
- android.Manifest.permission.CREATE_VIRTUAL_DEVICE,
- "Permission required to unregister a virtual sensor");
+ super.unregisterSensor_enforcePermission();
final long ident = Binder.clearCallingIdentity();
try {
mSensorController.unregisterSensor(token);
@@ -696,10 +697,9 @@
}
@Override // Binder call
+ @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public boolean sendSensorEvent(@NonNull IBinder token, @NonNull VirtualSensorEvent event) {
- mContext.enforceCallingOrSelfPermission(
- android.Manifest.permission.CREATE_VIRTUAL_DEVICE,
- "Permission required to send a virtual sensor event");
+ super.sendSensorEvent_enforcePermission();
final long ident = Binder.clearCallingIdentity();
try {
return mSensorController.sendSensorEvent(token, event);
@@ -709,25 +709,23 @@
}
@Override // Binder call
+ @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public void registerIntentInterceptor(IVirtualDeviceIntentInterceptor intentInterceptor,
IntentFilter filter) {
+ super.registerIntentInterceptor_enforcePermission();
Objects.requireNonNull(intentInterceptor);
Objects.requireNonNull(filter);
- mContext.enforceCallingOrSelfPermission(
- android.Manifest.permission.CREATE_VIRTUAL_DEVICE,
- "Permission required to register intent interceptor");
synchronized (mVirtualDeviceLock) {
mIntentInterceptors.put(intentInterceptor.asBinder(), filter);
}
}
@Override // Binder call
+ @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public void unregisterIntentInterceptor(
@NonNull IVirtualDeviceIntentInterceptor intentInterceptor) {
+ super.unregisterIntentInterceptor_enforcePermission();
Objects.requireNonNull(intentInterceptor);
- mContext.enforceCallingOrSelfPermission(
- android.Manifest.permission.CREATE_VIRTUAL_DEVICE,
- "Permission required to unregister intent interceptor");
synchronized (mVirtualDeviceLock) {
mIntentInterceptors.remove(intentInterceptor.asBinder());
}
diff --git a/services/core/java/com/android/server/am/BroadcastProcessQueue.java b/services/core/java/com/android/server/am/BroadcastProcessQueue.java
index d53f8fb..b89084c 100644
--- a/services/core/java/com/android/server/am/BroadcastProcessQueue.java
+++ b/services/core/java/com/android/server/am/BroadcastProcessQueue.java
@@ -42,6 +42,7 @@
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayDeque;
import java.util.Iterator;
+import java.util.List;
import java.util.Objects;
/**
@@ -124,6 +125,12 @@
private final ArrayDeque<SomeArgs> mPendingOffload = new ArrayDeque<>(4);
/**
+ * List of all queues holding broadcasts that are waiting to be dispatched.
+ */
+ private final List<ArrayDeque<SomeArgs>> mPendingQueues = List.of(
+ mPendingUrgent, mPending, mPendingOffload);
+
+ /**
* Broadcast actively being dispatched to this process.
*/
private @Nullable BroadcastRecord mActive;
@@ -218,11 +225,11 @@
* given count of other receivers have reached a terminal state; typically
* used for ordered broadcasts and priority traunches.
*/
- public void enqueueOrReplaceBroadcast(@NonNull BroadcastRecord record, int recordIndex) {
+ public void enqueueOrReplaceBroadcast(@NonNull BroadcastRecord record, int recordIndex,
+ @NonNull BroadcastConsumer replacedBroadcastConsumer) {
if (record.isReplacePending()) {
- boolean didReplace = replaceBroadcastInQueue(mPending, record, recordIndex)
- || replaceBroadcastInQueue(mPendingUrgent, record, recordIndex)
- || replaceBroadcastInQueue(mPendingOffload, record, recordIndex);
+ final boolean didReplace = replaceBroadcast(record, recordIndex,
+ replacedBroadcastConsumer);
if (didReplace) {
return;
}
@@ -243,6 +250,26 @@
}
/**
+ * Searches from newest to oldest in the pending broadcast queues, and at the first matching
+ * pending broadcast it finds, replaces it in-place and returns -- does not attempt to handle
+ * "duplicate" broadcasts in the queue.
+ * <p>
+ * @return {@code true} if it found and replaced an existing record in the queue;
+ * {@code false} otherwise.
+ */
+ private boolean replaceBroadcast(@NonNull BroadcastRecord record, int recordIndex,
+ @NonNull BroadcastConsumer replacedBroadcastConsumer) {
+ final int count = mPendingQueues.size();
+ for (int i = 0; i < count; ++i) {
+ final ArrayDeque<SomeArgs> queue = mPendingQueues.get(i);
+ if (replaceBroadcastInQueue(queue, record, recordIndex, replacedBroadcastConsumer)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
* Searches from newest to oldest, and at the first matching pending broadcast
* it finds, replaces it in-place and returns -- does not attempt to handle
* "duplicate" broadcasts in the queue.
@@ -251,7 +278,8 @@
* {@code false} otherwise.
*/
private boolean replaceBroadcastInQueue(@NonNull ArrayDeque<SomeArgs> queue,
- @NonNull BroadcastRecord record, int recordIndex) {
+ @NonNull BroadcastRecord record, int recordIndex,
+ @NonNull BroadcastConsumer replacedBroadcastConsumer) {
final Iterator<SomeArgs> it = queue.descendingIterator();
final Object receiver = record.receivers.get(recordIndex);
while (it.hasNext()) {
@@ -262,12 +290,14 @@
if ((record.callingUid == testRecord.callingUid)
&& (record.userId == testRecord.userId)
&& record.intent.filterEquals(testRecord.intent)
- && isReceiverEquals(receiver, testReceiver)) {
+ && isReceiverEquals(receiver, testReceiver)
+ && testRecord.allReceiversPending()) {
// Exact match found; perform in-place swap
args.arg1 = record;
args.argi1 = recordIndex;
onBroadcastDequeued(testRecord, testRecordIndex);
onBroadcastEnqueued(record, recordIndex);
+ replacedBroadcastConsumer.accept(testRecord, testRecordIndex);
return true;
}
}
diff --git a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
index a850c8a..8f241b2 100644
--- a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
+++ b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
@@ -64,6 +64,7 @@
import android.os.SystemClock;
import android.os.UserHandle;
import android.text.format.DateUtils;
+import android.util.ArraySet;
import android.util.IndentingPrintWriter;
import android.util.MathUtils;
import android.util.Pair;
@@ -629,30 +630,26 @@
applyDeliveryGroupPolicy(r);
- if (r.isReplacePending()) {
- // Leave the skipped broadcasts intact in queue, so that we can
- // replace them at their current position during enqueue below
- forEachMatchingBroadcast(QUEUE_PREDICATE_ANY, (testRecord, testIndex) -> {
- // We only allow caller to replace broadcasts they enqueued
- return (r.callingUid == testRecord.callingUid)
- && (r.userId == testRecord.userId)
- && r.intent.filterEquals(testRecord.intent);
- }, mBroadcastConsumerSkipAndCanceled, false);
- }
-
r.enqueueTime = SystemClock.uptimeMillis();
r.enqueueRealTime = SystemClock.elapsedRealtime();
r.enqueueClockTime = System.currentTimeMillis();
+ final ArraySet<BroadcastRecord> replacedBroadcasts = new ArraySet<>();
+ final BroadcastConsumer replacedBroadcastConsumer =
+ (record, i) -> replacedBroadcasts.add(record);
for (int i = 0; i < r.receivers.size(); i++) {
final Object receiver = r.receivers.get(i);
final BroadcastProcessQueue queue = getOrCreateProcessQueue(
getReceiverProcessName(receiver), getReceiverUid(receiver));
- queue.enqueueOrReplaceBroadcast(r, i);
+ queue.enqueueOrReplaceBroadcast(r, i, replacedBroadcastConsumer);
updateRunnableList(queue);
enqueueUpdateRunningList();
}
+ // Skip any broadcasts that have been replaced by newer broadcasts with
+ // FLAG_RECEIVER_REPLACE_PENDING.
+ skipAndCancelReplacedBroadcasts(replacedBroadcasts);
+
// If nothing to dispatch, send any pending result immediately
if (r.receivers.isEmpty()) {
scheduleResultTo(r);
@@ -662,6 +659,17 @@
traceEnd(cookie);
}
+ private void skipAndCancelReplacedBroadcasts(ArraySet<BroadcastRecord> replacedBroadcasts) {
+ for (int i = 0; i < replacedBroadcasts.size(); ++i) {
+ final BroadcastRecord r = replacedBroadcasts.valueAt(i);
+ r.resultCode = Activity.RESULT_CANCELED;
+ r.resultData = null;
+ r.resultExtras = null;
+ scheduleResultTo(r);
+ notifyFinishBroadcast(r);
+ }
+ }
+
private void applyDeliveryGroupPolicy(@NonNull BroadcastRecord r) {
if (mService.shouldIgnoreDeliveryGroupPolicy(r.intent.getAction())) {
return;
diff --git a/services/core/java/com/android/server/am/BroadcastRecord.java b/services/core/java/com/android/server/am/BroadcastRecord.java
index 24cf3d2..37225d1 100644
--- a/services/core/java/com/android/server/am/BroadcastRecord.java
+++ b/services/core/java/com/android/server/am/BroadcastRecord.java
@@ -907,6 +907,17 @@
return record.options == null ? null : record.options.getDeliveryGroupMatchingFilter();
}
+ /**
+ * Returns {@code true} if all the receivers are still waiting to receive the broadcast.
+ * Otherwise {@code false}.
+ */
+ boolean allReceiversPending() {
+ // We could also count the number of receivers with deliver state DELIVERY_PENDING, but
+ // checking how many receivers have finished (either skipped or cancelled) and whether or
+ // not the dispatch has been started should be sufficient.
+ return (terminalCount == 0 && dispatchTime <= 0);
+ }
+
@Override
public String toString() {
if (mCachedToString == null) {
diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
index 418027f..9877ed3 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
@@ -36,6 +36,7 @@
import android.media.IAudioRoutesObserver;
import android.media.ICapturePresetDevicesRoleDispatcher;
import android.media.ICommunicationDeviceDispatcher;
+import android.media.IStrategyNonDefaultDevicesDispatcher;
import android.media.IStrategyPreferredDevicesDispatcher;
import android.media.MediaMetrics;
import android.media.audiopolicy.AudioProductStrategy;
@@ -871,6 +872,16 @@
return mDeviceInventory.removePreferredDevicesForStrategySync(strategy);
}
+ /*package*/ int setDeviceAsNonDefaultForStrategySync(int strategy,
+ @NonNull AudioDeviceAttributes device) {
+ return mDeviceInventory.setDeviceAsNonDefaultForStrategySync(strategy, device);
+ }
+
+ /*package*/ int removeDeviceAsNonDefaultForStrategySync(int strategy,
+ @NonNull AudioDeviceAttributes device) {
+ return mDeviceInventory.removeDeviceAsNonDefaultForStrategySync(strategy, device);
+ }
+
/*package*/ void registerStrategyPreferredDevicesDispatcher(
@NonNull IStrategyPreferredDevicesDispatcher dispatcher) {
mDeviceInventory.registerStrategyPreferredDevicesDispatcher(dispatcher);
@@ -881,6 +892,16 @@
mDeviceInventory.unregisterStrategyPreferredDevicesDispatcher(dispatcher);
}
+ /*package*/ void registerStrategyNonDefaultDevicesDispatcher(
+ @NonNull IStrategyNonDefaultDevicesDispatcher dispatcher) {
+ mDeviceInventory.registerStrategyNonDefaultDevicesDispatcher(dispatcher);
+ }
+
+ /*package*/ void unregisterStrategyNonDefaultDevicesDispatcher(
+ @NonNull IStrategyNonDefaultDevicesDispatcher dispatcher) {
+ mDeviceInventory.unregisterStrategyNonDefaultDevicesDispatcher(dispatcher);
+ }
+
/*package*/ int setPreferredDevicesForCapturePresetSync(int capturePreset,
@NonNull List<AudioDeviceAttributes> devices) {
return mDeviceInventory.setPreferredDevicesForCapturePresetSync(capturePreset, devices);
@@ -1039,6 +1060,17 @@
sendIMsgNoDelay(MSG_I_SAVE_REMOVE_PREF_DEVICES_FOR_STRATEGY, SENDMSG_QUEUE, strategy);
}
+ /*package*/ void postSaveSetDeviceAsNonDefaultForStrategy(
+ int strategy, AudioDeviceAttributes device) {
+ sendILMsgNoDelay(MSG_IL_SAVE_NDEF_DEVICE_FOR_STRATEGY, SENDMSG_QUEUE, strategy, device);
+ }
+
+ /*package*/ void postSaveRemoveDeviceAsNonDefaultForStrategy(
+ int strategy, AudioDeviceAttributes device) {
+ sendILMsgNoDelay(
+ MSG_IL_SAVE_REMOVE_NDEF_DEVICE_FOR_STRATEGY, SENDMSG_QUEUE, strategy, device);
+ }
+
/*package*/ void postSaveSetPreferredDevicesForCapturePreset(
int capturePreset, List<AudioDeviceAttributes> devices) {
sendILMsgNoDelay(
@@ -1508,6 +1540,16 @@
final int strategy = msg.arg1;
mDeviceInventory.onSaveRemovePreferredDevices(strategy);
} break;
+ case MSG_IL_SAVE_NDEF_DEVICE_FOR_STRATEGY: {
+ final int strategy = msg.arg1;
+ final AudioDeviceAttributes device = (AudioDeviceAttributes) msg.obj;
+ mDeviceInventory.onSaveSetDeviceAsNonDefault(strategy, device);
+ } break;
+ case MSG_IL_SAVE_REMOVE_NDEF_DEVICE_FOR_STRATEGY: {
+ final int strategy = msg.arg1;
+ final AudioDeviceAttributes device = (AudioDeviceAttributes) msg.obj;
+ mDeviceInventory.onSaveRemoveDeviceAsNonDefault(strategy, device);
+ } break;
case MSG_CHECK_MUTE_MUSIC:
checkMessagesMuteMusic(0);
break;
@@ -1593,6 +1635,9 @@
// process set volume for Le Audio, obj is BleVolumeInfo
private static final int MSG_II_SET_LE_AUDIO_OUT_VOLUME = 46;
+ private static final int MSG_IL_SAVE_NDEF_DEVICE_FOR_STRATEGY = 47;
+ private static final int MSG_IL_SAVE_REMOVE_NDEF_DEVICE_FOR_STRATEGY = 48;
+
private static boolean isMessageHandledUnderWakelock(int msgId) {
switch(msgId) {
case MSG_L_SET_WIRED_DEVICE_CONNECTION_STATE:
diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
index 34457b0..f9270c9 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
@@ -31,8 +31,11 @@
import android.media.AudioSystem;
import android.media.IAudioRoutesObserver;
import android.media.ICapturePresetDevicesRoleDispatcher;
+import android.media.IStrategyNonDefaultDevicesDispatcher;
import android.media.IStrategyPreferredDevicesDispatcher;
import android.media.MediaMetrics;
+import android.media.permission.ClearCallingIdentityContext;
+import android.media.permission.SafeCloseable;
import android.os.Binder;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
@@ -142,6 +145,10 @@
private final ArrayMap<Integer, List<AudioDeviceAttributes>> mPreferredDevices =
new ArrayMap<>();
+ // List of non-default devices for strategies
+ private final ArrayMap<Integer, List<AudioDeviceAttributes>> mNonDefaultDevices =
+ new ArrayMap<>();
+
// List of preferred devices of capture preset
private final ArrayMap<Integer, List<AudioDeviceAttributes>> mPreferredDevicesForCapturePreset =
new ArrayMap<>();
@@ -156,10 +163,14 @@
final RemoteCallbackList<IAudioRoutesObserver> mRoutesObservers =
new RemoteCallbackList<IAudioRoutesObserver>();
- // Monitoring of strategy-preferred device
+ // Monitoring of preferred device for strategies
final RemoteCallbackList<IStrategyPreferredDevicesDispatcher> mPrefDevDispatchers =
new RemoteCallbackList<IStrategyPreferredDevicesDispatcher>();
+ // Monitoring of non-default device for strategies
+ final RemoteCallbackList<IStrategyNonDefaultDevicesDispatcher> mNonDefDevDispatchers =
+ new RemoteCallbackList<IStrategyNonDefaultDevicesDispatcher>();
+
// Monitoring of devices for role and capture preset
final RemoteCallbackList<ICapturePresetDevicesRoleDispatcher> mDevRoleCapturePresetDispatchers =
new RemoteCallbackList<ICapturePresetDevicesRoleDispatcher>();
@@ -254,6 +265,9 @@
pw.println("\n" + prefix + "Preferred devices for strategy:");
mPreferredDevices.forEach((strategy, device) -> {
pw.println(" " + prefix + "strategy:" + strategy + " device:" + device); });
+ pw.println("\n" + prefix + "Non-default devices for strategy:");
+ mNonDefaultDevices.forEach((strategy, device) -> {
+ pw.println(" " + prefix + "strategy:" + strategy + " device:" + device); });
pw.println("\n" + prefix + "Connected devices:");
mConnectedDevices.forEach((key, deviceInfo) -> {
pw.println(" " + prefix + deviceInfo.toString()); });
@@ -291,6 +305,11 @@
mAudioSystem.setDevicesRoleForStrategy(
strategy, AudioSystem.DEVICE_ROLE_PREFERRED, devices); });
}
+ synchronized (mNonDefaultDevices) {
+ mNonDefaultDevices.forEach((strategy, devices) -> {
+ mAudioSystem.setDevicesRoleForStrategy(
+ strategy, AudioSystem.DEVICE_ROLE_DISABLED, devices); });
+ }
synchronized (mPreferredDevicesForCapturePreset) {
// TODO: call audiosystem to restore
}
@@ -608,6 +627,18 @@
/*package*/ void onSaveSetPreferredDevices(int strategy,
@NonNull List<AudioDeviceAttributes> devices) {
mPreferredDevices.put(strategy, devices);
+ List<AudioDeviceAttributes> nonDefaultDevices = mNonDefaultDevices.get(strategy);
+ if (nonDefaultDevices != null) {
+ nonDefaultDevices.removeAll(devices);
+
+ if (nonDefaultDevices.isEmpty()) {
+ mNonDefaultDevices.remove(strategy);
+ } else {
+ mNonDefaultDevices.put(strategy, nonDefaultDevices);
+ }
+ dispatchNonDefaultDevice(strategy, nonDefaultDevices);
+ }
+
dispatchPreferredDevice(strategy, devices);
}
@@ -616,6 +647,40 @@
dispatchPreferredDevice(strategy, new ArrayList<AudioDeviceAttributes>());
}
+ /*package*/ void onSaveSetDeviceAsNonDefault(int strategy,
+ @NonNull AudioDeviceAttributes device) {
+ List<AudioDeviceAttributes> nonDefaultDevices = mNonDefaultDevices.get(strategy);
+ if (nonDefaultDevices == null) {
+ nonDefaultDevices = new ArrayList<>();
+ }
+
+ if (!nonDefaultDevices.contains(device)) {
+ nonDefaultDevices.add(device);
+ }
+
+ mNonDefaultDevices.put(strategy, nonDefaultDevices);
+ dispatchNonDefaultDevice(strategy, nonDefaultDevices);
+
+ List<AudioDeviceAttributes> preferredDevices = mPreferredDevices.get(strategy);
+
+ if (preferredDevices != null) {
+ preferredDevices.remove(device);
+ mPreferredDevices.put(strategy, preferredDevices);
+
+ dispatchPreferredDevice(strategy, preferredDevices);
+ }
+ }
+
+ /*package*/ void onSaveRemoveDeviceAsNonDefault(int strategy,
+ @NonNull AudioDeviceAttributes device) {
+ List<AudioDeviceAttributes> nonDefaultDevices = mNonDefaultDevices.get(strategy);
+ if (nonDefaultDevices != null) {
+ nonDefaultDevices.remove(device);
+ mNonDefaultDevices.put(strategy, nonDefaultDevices);
+ dispatchNonDefaultDevice(strategy, nonDefaultDevices);
+ }
+ }
+
/*package*/ void onSaveSetPreferredDevicesForCapturePreset(
int capturePreset, @NonNull List<AudioDeviceAttributes> devices) {
mPreferredDevicesForCapturePreset.put(capturePreset, devices);
@@ -631,18 +696,19 @@
}
//------------------------------------------------------------
- // preferred device(s)
+ // preferred/non-default device(s)
/*package*/ int setPreferredDevicesForStrategySync(int strategy,
@NonNull List<AudioDeviceAttributes> devices) {
- final long identity = Binder.clearCallingIdentity();
+ int status = AudioSystem.ERROR;
- AudioService.sDeviceLogger.enqueue((new EventLogger.StringEvent(
- "setPreferredDevicesForStrategySync, strategy: " + strategy
- + " devices: " + devices)).printLog(TAG));
- final int status = mAudioSystem.setDevicesRoleForStrategy(
- strategy, AudioSystem.DEVICE_ROLE_PREFERRED, devices);
- Binder.restoreCallingIdentity(identity);
+ try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
+ AudioService.sDeviceLogger.enqueue((new EventLogger.StringEvent(
+ "setPreferredDevicesForStrategySync, strategy: " + strategy
+ + " devices: " + devices)).printLog(TAG));
+ status = mAudioSystem.setDevicesRoleForStrategy(
+ strategy, AudioSystem.DEVICE_ROLE_PREFERRED, devices);
+ }
if (status == AudioSystem.SUCCESS) {
mDeviceBroker.postSaveSetPreferredDevicesForStrategy(strategy, devices);
@@ -651,15 +717,16 @@
}
/*package*/ int removePreferredDevicesForStrategySync(int strategy) {
- final long identity = Binder.clearCallingIdentity();
+ int status = AudioSystem.ERROR;
- AudioService.sDeviceLogger.enqueue((new EventLogger.StringEvent(
- "removePreferredDevicesForStrategySync, strategy: "
- + strategy)).printLog(TAG));
+ try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
+ AudioService.sDeviceLogger.enqueue((new EventLogger.StringEvent(
+ "removePreferredDevicesForStrategySync, strategy: "
+ + strategy)).printLog(TAG));
- final int status = mAudioSystem.removeDevicesRoleForStrategy(
- strategy, AudioSystem.DEVICE_ROLE_PREFERRED);
- Binder.restoreCallingIdentity(identity);
+ status = mAudioSystem.clearDevicesRoleForStrategy(
+ strategy, AudioSystem.DEVICE_ROLE_PREFERRED);
+ }
if (status == AudioSystem.SUCCESS) {
mDeviceBroker.postSaveRemovePreferredDevicesForStrategy(strategy);
@@ -667,6 +734,50 @@
return status;
}
+ /*package*/ int setDeviceAsNonDefaultForStrategySync(int strategy,
+ @NonNull AudioDeviceAttributes device) {
+ int status = AudioSystem.ERROR;
+
+ try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
+ List<AudioDeviceAttributes> devices = new ArrayList<>();
+ devices.add(device);
+
+ AudioService.sDeviceLogger.enqueue((new EventLogger.StringEvent(
+ "setDeviceAsNonDefaultForStrategySync, strategy: " + strategy
+ + " device: " + device)).printLog(TAG));
+ status = mAudioSystem.setDevicesRoleForStrategy(
+ strategy, AudioSystem.DEVICE_ROLE_DISABLED, devices);
+ }
+
+ if (status == AudioSystem.SUCCESS) {
+ mDeviceBroker.postSaveSetDeviceAsNonDefaultForStrategy(strategy, device);
+ }
+ return status;
+ }
+
+ /*package*/ int removeDeviceAsNonDefaultForStrategySync(int strategy,
+ @NonNull AudioDeviceAttributes device) {
+ int status = AudioSystem.ERROR;
+
+ try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
+ List<AudioDeviceAttributes> devices = new ArrayList<>();
+ devices.add(device);
+
+ AudioService.sDeviceLogger.enqueue((new EventLogger.StringEvent(
+ "removeDeviceAsNonDefaultForStrategySync, strategy: "
+ + strategy + " devices: " + device)).printLog(TAG));
+
+ status = mAudioSystem.removeDevicesRoleForStrategy(
+ strategy, AudioSystem.DEVICE_ROLE_DISABLED, devices);
+ }
+
+ if (status == AudioSystem.SUCCESS) {
+ mDeviceBroker.postSaveRemoveDeviceAsNonDefaultForStrategy(strategy, device);
+ }
+ return status;
+ }
+
+
/*package*/ void registerStrategyPreferredDevicesDispatcher(
@NonNull IStrategyPreferredDevicesDispatcher dispatcher) {
mPrefDevDispatchers.register(dispatcher);
@@ -677,12 +788,24 @@
mPrefDevDispatchers.unregister(dispatcher);
}
+ /*package*/ void registerStrategyNonDefaultDevicesDispatcher(
+ @NonNull IStrategyNonDefaultDevicesDispatcher dispatcher) {
+ mNonDefDevDispatchers.register(dispatcher);
+ }
+
+ /*package*/ void unregisterStrategyNonDefaultDevicesDispatcher(
+ @NonNull IStrategyNonDefaultDevicesDispatcher dispatcher) {
+ mNonDefDevDispatchers.unregister(dispatcher);
+ }
+
/*package*/ int setPreferredDevicesForCapturePresetSync(
int capturePreset, @NonNull List<AudioDeviceAttributes> devices) {
- final long identity = Binder.clearCallingIdentity();
- final int status = mAudioSystem.setDevicesRoleForCapturePreset(
- capturePreset, AudioSystem.DEVICE_ROLE_PREFERRED, devices);
- Binder.restoreCallingIdentity(identity);
+ int status = AudioSystem.ERROR;
+
+ try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
+ status = mAudioSystem.setDevicesRoleForCapturePreset(
+ capturePreset, AudioSystem.DEVICE_ROLE_PREFERRED, devices);
+ }
if (status == AudioSystem.SUCCESS) {
mDeviceBroker.postSaveSetPreferredDevicesForCapturePreset(capturePreset, devices);
@@ -691,10 +814,12 @@
}
/*package*/ int clearPreferredDevicesForCapturePresetSync(int capturePreset) {
- final long identity = Binder.clearCallingIdentity();
- final int status = mAudioSystem.clearDevicesRoleForCapturePreset(
- capturePreset, AudioSystem.DEVICE_ROLE_PREFERRED);
- Binder.restoreCallingIdentity(identity);
+ int status = AudioSystem.ERROR;
+
+ try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
+ status = mAudioSystem.clearDevicesRoleForCapturePreset(
+ capturePreset, AudioSystem.DEVICE_ROLE_PREFERRED);
+ }
if (status == AudioSystem.SUCCESS) {
mDeviceBroker.postSaveClearPreferredDevicesForCapturePreset(capturePreset);
@@ -1523,6 +1648,19 @@
mPrefDevDispatchers.finishBroadcast();
}
+ private void dispatchNonDefaultDevice(int strategy,
+ @NonNull List<AudioDeviceAttributes> devices) {
+ final int nbDispatchers = mNonDefDevDispatchers.beginBroadcast();
+ for (int i = 0; i < nbDispatchers; i++) {
+ try {
+ mNonDefDevDispatchers.getBroadcastItem(i).dispatchNonDefDevicesChanged(
+ strategy, devices);
+ } catch (RemoteException e) {
+ }
+ }
+ mNonDefDevDispatchers.finishBroadcast();
+ }
+
private void dispatchDevicesRoleForCapturePreset(
int capturePreset, int role, @NonNull List<AudioDeviceAttributes> devices) {
final int nbDispatchers = mDevRoleCapturePresetDispatchers.beginBroadcast();
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index 43c8032d..24c7d2c 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -116,6 +116,7 @@
import android.media.ISpatializerHeadTrackerAvailableCallback;
import android.media.ISpatializerHeadTrackingModeCallback;
import android.media.ISpatializerOutputCallback;
+import android.media.IStrategyNonDefaultDevicesDispatcher;
import android.media.IStrategyPreferredDevicesDispatcher;
import android.media.IVolumeController;
import android.media.MediaMetrics;
@@ -2800,11 +2801,12 @@
* @see AudioManager#setPreferredDevicesForStrategy(AudioProductStrategy,
* List<AudioDeviceAttributes>)
*/
+ @android.annotation.EnforcePermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
public int setPreferredDevicesForStrategy(int strategy, List<AudioDeviceAttributes> devices) {
+ super.setPreferredDevicesForStrategy_enforcePermission();
if (devices == null) {
return AudioSystem.ERROR;
}
- enforceModifyAudioRoutingPermission();
final String logString = String.format(
"setPreferredDeviceForStrategy u/pid:%d/%d strat:%d dev:%s",
Binder.getCallingUid(), Binder.getCallingPid(), strategy,
@@ -2862,6 +2864,81 @@
}
}
+ /**
+ * @see AudioManager#setDeviceAsNonDefaultForStrategy(AudioProductStrategy,
+ * AudioDeviceAttributes)
+ * @see AudioManager#setDeviceAsNonDefaultForStrategy(AudioProductStrategy,
+ * List<AudioDeviceAttributes>)
+ */
+ @android.annotation.EnforcePermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
+ public int setDeviceAsNonDefaultForStrategy(int strategy,
+ @NonNull AudioDeviceAttributes device) {
+ super.setDeviceAsNonDefaultForStrategy_enforcePermission();
+ Objects.requireNonNull(device);
+ final String logString = String.format(
+ "setDeviceAsNonDefaultForStrategy u/pid:%d/%d strat:%d dev:%s",
+ Binder.getCallingUid(), Binder.getCallingPid(), strategy, device.toString());
+ sDeviceLogger.enqueue(new EventLogger.StringEvent(logString).printLog(TAG));
+ if (device.getRole() == AudioDeviceAttributes.ROLE_INPUT) {
+ Log.e(TAG, "Unsupported input routing in " + logString);
+ return AudioSystem.ERROR;
+ }
+
+ final int status = mDeviceBroker.setDeviceAsNonDefaultForStrategySync(strategy, device);
+ if (status != AudioSystem.SUCCESS) {
+ Log.e(TAG, String.format("Error %d in %s)", status, logString));
+ }
+
+ return status;
+ }
+
+ /**
+ * @see AudioManager#removeDeviceAsNonDefaultForStrategy(AudioProductStrategy,
+ * AudioDeviceAttributes)
+ */
+ @android.annotation.EnforcePermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
+ public int removeDeviceAsNonDefaultForStrategy(int strategy,
+ AudioDeviceAttributes device) {
+ super.removeDeviceAsNonDefaultForStrategy_enforcePermission();
+ Objects.requireNonNull(device);
+ final String logString = String.format(
+ "removeDeviceAsNonDefaultForStrategy strat:%d dev:%s", strategy, device.toString());
+ sDeviceLogger.enqueue(new EventLogger.StringEvent(logString).printLog(TAG));
+ if (device.getRole() == AudioDeviceAttributes.ROLE_INPUT) {
+ Log.e(TAG, "Unsupported input routing in " + logString);
+ return AudioSystem.ERROR;
+ }
+
+ final int status = mDeviceBroker.removeDeviceAsNonDefaultForStrategySync(strategy, device);
+ if (status != AudioSystem.SUCCESS) {
+ Log.e(TAG, String.format("Error %d in %s)", status, logString));
+ }
+ return status;
+ }
+
+ /**
+ * @see AudioManager#getNonDefaultDevicesForStrategy(AudioProductStrategy)
+ */
+ @android.annotation.EnforcePermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
+ public List<AudioDeviceAttributes> getNonDefaultDevicesForStrategy(int strategy) {
+ super.getNonDefaultDevicesForStrategy_enforcePermission();
+ List<AudioDeviceAttributes> devices = new ArrayList<>();
+ int status = AudioSystem.ERROR;
+
+ try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
+ status = AudioSystem.getDevicesForRoleAndStrategy(
+ strategy, AudioSystem.DEVICE_ROLE_DISABLED, devices);
+ }
+
+ if (status != AudioSystem.SUCCESS) {
+ Log.e(TAG, String.format("Error %d in getNonDefaultDeviceForStrategy(%d)",
+ status, strategy));
+ return new ArrayList<AudioDeviceAttributes>();
+ } else {
+ return devices;
+ }
+ }
+
/** @see AudioManager#addOnPreferredDevicesForStrategyChangedListener(
* Executor, AudioManager.OnPreferredDevicesForStrategyChangedListener)
*/
@@ -2886,6 +2963,30 @@
mDeviceBroker.unregisterStrategyPreferredDevicesDispatcher(dispatcher);
}
+ /** @see AudioManager#addOnNonDefaultDevicesForStrategyChangedListener(
+ * Executor, AudioManager.OnNonDefaultDevicesForStrategyChangedListener)
+ */
+ public void registerStrategyNonDefaultDevicesDispatcher(
+ @Nullable IStrategyNonDefaultDevicesDispatcher dispatcher) {
+ if (dispatcher == null) {
+ return;
+ }
+ enforceModifyAudioRoutingPermission();
+ mDeviceBroker.registerStrategyNonDefaultDevicesDispatcher(dispatcher);
+ }
+
+ /** @see AudioManager#removeOnNonDefaultDevicesForStrategyChangedListener(
+ * AudioManager.OnNonDefaultDevicesForStrategyChangedListener)
+ */
+ public void unregisterStrategyNonDefaultDevicesDispatcher(
+ @Nullable IStrategyNonDefaultDevicesDispatcher dispatcher) {
+ if (dispatcher == null) {
+ return;
+ }
+ enforceModifyAudioRoutingPermission();
+ mDeviceBroker.unregisterStrategyNonDefaultDevicesDispatcher(dispatcher);
+ }
+
/**
* @see AudioManager#setPreferredDeviceForCapturePreset(int, AudioDeviceAttributes)
*/
diff --git a/services/core/java/com/android/server/audio/AudioSystemAdapter.java b/services/core/java/com/android/server/audio/AudioSystemAdapter.java
index c176f29..7fefc55 100644
--- a/services/core/java/com/android/server/audio/AudioSystemAdapter.java
+++ b/services/core/java/com/android/server/audio/AudioSystemAdapter.java
@@ -279,14 +279,27 @@
}
/**
- * Same as {@link AudioSystem#removeDevicesRoleForStrategy(int, int)}
+ * Same as {@link AudioSystem#removeDevicesRoleForStrategy(int, int, List)}
+ * @param strategy
+ * @param role
+ * @param devices
+ * @return
+ */
+ public int removeDevicesRoleForStrategy(int strategy, int role,
+ @NonNull List<AudioDeviceAttributes> devices) {
+ invalidateRoutingCache();
+ return AudioSystem.removeDevicesRoleForStrategy(strategy, role, devices);
+ }
+
+ /**
+ * Same as {@link AudioSystem#clearDevicesRoleForStrategy(int, int)}
* @param strategy
* @param role
* @return
*/
- public int removeDevicesRoleForStrategy(int strategy, int role) {
+ public int clearDevicesRoleForStrategy(int strategy, int role) {
invalidateRoutingCache();
- return AudioSystem.removeDevicesRoleForStrategy(strategy, role);
+ return AudioSystem.clearDevicesRoleForStrategy(strategy, role);
}
/**
diff --git a/services/core/java/com/android/server/hdmi/Constants.java b/services/core/java/com/android/server/hdmi/Constants.java
index 5353092..6b5af88 100644
--- a/services/core/java/com/android/server/hdmi/Constants.java
+++ b/services/core/java/com/android/server/hdmi/Constants.java
@@ -619,6 +619,8 @@
})
@interface HpdSignalType {}
+ static final String DEVICE_CONFIG_FEATURE_FLAG_SOUNDBAR_MODE = "soundbar_mode";
+
private Constants() {
/* cannot be instantiated */
}
diff --git a/services/core/java/com/android/server/hdmi/HdmiControlService.java b/services/core/java/com/android/server/hdmi/HdmiControlService.java
index f66f8ea..85477b3 100644
--- a/services/core/java/com/android/server/hdmi/HdmiControlService.java
+++ b/services/core/java/com/android/server/hdmi/HdmiControlService.java
@@ -86,6 +86,7 @@
import android.os.SystemClock;
import android.os.SystemProperties;
import android.os.UserHandle;
+import android.provider.DeviceConfig;
import android.provider.Settings.Global;
import android.sysprop.HdmiProperties;
import android.text.TextUtils;
@@ -450,6 +451,9 @@
private boolean mWakeUpMessageReceived = false;
@ServiceThreadOnly
+ private boolean mSoundbarModeFeatureFlagEnabled = false;
+
+ @ServiceThreadOnly
private int mActivePortId = Constants.INVALID_PORT_ID;
// Set to true while the input change by MHL is allowed.
@@ -763,14 +767,6 @@
}
}
}, mServiceThreadExecutor);
- mHdmiCecConfig.registerChangeListener(HdmiControlManager.CEC_SETTING_NAME_SOUNDBAR_MODE,
- new HdmiCecConfig.SettingChangeListener() {
- @Override
- public void onChange(String setting) {
- setSoundbarMode(mHdmiCecConfig.getIntValue(
- HdmiControlManager.CEC_SETTING_NAME_SOUNDBAR_MODE));
- }
- }, mServiceThreadExecutor);
mHdmiCecConfig.registerChangeListener(
HdmiControlManager.CEC_SETTING_NAME_SYSTEM_AUDIO_CONTROL,
new HdmiCecConfig.SettingChangeListener() {
@@ -819,9 +815,39 @@
HdmiControlManager.SETTING_NAME_EARC_ENABLED);
setEarcEnabled(enabled);
}
+ },
+ mServiceThreadExecutor);
+
+ mSoundbarModeFeatureFlagEnabled = DeviceConfig.getBoolean(
+ DeviceConfig.NAMESPACE_HDMI_CONTROL,
+ Constants.DEVICE_CONFIG_FEATURE_FLAG_SOUNDBAR_MODE, false);
+
+ DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_HDMI_CONTROL,
+ getContext().getMainExecutor(), new DeviceConfig.OnPropertiesChangedListener() {
+ @Override
+ public void onPropertiesChanged(DeviceConfig.Properties properties) {
+ mSoundbarModeFeatureFlagEnabled = properties.getBoolean(
+ Constants.DEVICE_CONFIG_FEATURE_FLAG_SOUNDBAR_MODE,
+ false);
+ boolean soundbarModeSetting = mHdmiCecConfig.getIntValue(
+ HdmiControlManager.CEC_SETTING_NAME_SOUNDBAR_MODE)
+ == SOUNDBAR_MODE_ENABLED;
+ setSoundbarMode(soundbarModeSetting && mSoundbarModeFeatureFlagEnabled
+ ? SOUNDBAR_MODE_ENABLED : SOUNDBAR_MODE_DISABLED);
+ }
+ });
+ mHdmiCecConfig.registerChangeListener(HdmiControlManager.CEC_SETTING_NAME_SOUNDBAR_MODE,
+ new HdmiCecConfig.SettingChangeListener() {
+ @Override
+ public void onChange(String setting) {
+ boolean soundbarModeSetting = mHdmiCecConfig.getIntValue(
+ HdmiControlManager.CEC_SETTING_NAME_SOUNDBAR_MODE)
+ == SOUNDBAR_MODE_ENABLED;
+ setSoundbarMode(soundbarModeSetting && mSoundbarModeFeatureFlagEnabled
+ ? SOUNDBAR_MODE_ENABLED : SOUNDBAR_MODE_DISABLED);
+ }
}, mServiceThreadExecutor);
}
-
/** Returns true if the device screen is off */
boolean isScreenOff() {
return mDisplayManager.getDisplay(Display.DEFAULT_DISPLAY).getState() == Display.STATE_OFF;
@@ -937,6 +963,11 @@
return mPowerManagerInternal;
}
+ @VisibleForTesting
+ protected void enableAllFeatureFlags() {
+ mSoundbarModeFeatureFlagEnabled = true;
+ }
+
/**
* Triggers the address allocation that states the presence of a local device audio system in
* the network.
@@ -1151,7 +1182,8 @@
if (mHdmiCecConfig.getIntValue(HdmiControlManager.CEC_SETTING_NAME_SOUNDBAR_MODE)
== SOUNDBAR_MODE_ENABLED
&& !allLocalDeviceTypes.contains(HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM)
- && SystemProperties.getBoolean(Constants.PROPERTY_ARC_SUPPORT, true)) {
+ && SystemProperties.getBoolean(Constants.PROPERTY_ARC_SUPPORT, true)
+ && mSoundbarModeFeatureFlagEnabled) {
allLocalDeviceTypes.add(HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
}
return allLocalDeviceTypes;
diff --git a/services/core/java/com/android/server/pm/AppStateHelper.java b/services/core/java/com/android/server/pm/AppStateHelper.java
index 2ef193c..32479ee 100644
--- a/services/core/java/com/android/server/pm/AppStateHelper.java
+++ b/services/core/java/com/android/server/pm/AppStateHelper.java
@@ -23,6 +23,7 @@
import android.app.usage.NetworkStats;
import android.app.usage.NetworkStatsManager;
import android.content.Context;
+import android.content.pm.PackageManagerInternal;
import android.media.AudioManager;
import android.media.IAudioService;
import android.net.ConnectivityManager;
@@ -36,6 +37,7 @@
import com.android.server.LocalServices;
import java.util.ArrayList;
+import java.util.Collection;
import java.util.List;
import java.util.concurrent.TimeUnit;
@@ -157,6 +159,56 @@
return false;
}
+ private static boolean containsAny(Collection<String> list, Collection<String> which) {
+ if (list.isEmpty()) {
+ return false;
+ }
+ for (var element : which) {
+ if (list.contains(element)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private void addLibraryDependency(ArraySet<String> results, List<String> libPackageNames) {
+ var pmInternal = LocalServices.getService(PackageManagerInternal.class);
+
+ var libraryNames = new ArraySet<String>();
+ var staticSharedLibraryNames = new ArraySet<String>();
+ var sdkLibraryNames = new ArraySet<String>();
+ for (var packageName : libPackageNames) {
+ var pkg = pmInternal.getAndroidPackage(packageName);
+ if (pkg == null) {
+ continue;
+ }
+ libraryNames.addAll(pkg.getLibraryNames());
+ var libraryName = pkg.getStaticSharedLibraryName();
+ if (libraryName != null) {
+ staticSharedLibraryNames.add(libraryName);
+ }
+ libraryName = pkg.getSdkLibraryName();
+ if (libraryName != null) {
+ sdkLibraryNames.add(libraryName);
+ }
+ }
+
+ if (libraryNames.isEmpty()
+ && staticSharedLibraryNames.isEmpty()
+ && sdkLibraryNames.isEmpty()) {
+ return;
+ }
+
+ pmInternal.forEachPackage(pkg -> {
+ if (containsAny(pkg.getUsesLibraries(), libraryNames)
+ || containsAny(pkg.getUsesOptionalLibraries(), libraryNames)
+ || containsAny(pkg.getUsesStaticLibraries(), staticSharedLibraryNames)
+ || containsAny(pkg.getUsesSdkLibraries(), sdkLibraryNames)) {
+ results.add(pkg.getPackageName());
+ }
+ });
+ }
+
/**
* True if any app has sent or received network data over the past
* {@link #ACTIVE_NETWORK_DURATION_MILLIS} milliseconds.
@@ -225,6 +277,7 @@
*/
public List<String> getDependencyPackages(List<String> packageNames) {
var results = new ArraySet<String>();
+ // Include packages sharing the same process
var am = mContext.getSystemService(ActivityManager.class);
for (var info : am.getRunningAppProcesses()) {
for (var packageName : packageNames) {
@@ -236,10 +289,14 @@
}
}
}
+ // Include packages using bounded services
var amInternal = LocalServices.getService(ActivityManagerInternal.class);
for (var packageName : packageNames) {
results.addAll(amInternal.getClientPackages(packageName));
}
+ // Include packages using libraries
+ addLibraryDependency(results, packageNames);
+
return new ArrayList<>(results);
}
}
diff --git a/services/core/java/com/android/server/pm/Settings.java b/services/core/java/com/android/server/pm/Settings.java
index ff020eb..321c5c6 100644
--- a/services/core/java/com/android/server/pm/Settings.java
+++ b/services/core/java/com/android/server/pm/Settings.java
@@ -89,6 +89,7 @@
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.os.BackgroundThread;
+import com.android.internal.security.VerityUtils;
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.CollectionUtils;
import com.android.internal.util.IndentingPrintWriter;
@@ -370,6 +371,8 @@
// Current settings file.
private final File mSettingsFilename;
+ // Reserve copy of the current settings file.
+ private final File mSettingsReserveCopyFilename;
// Previous settings file.
// Removed when the current settings file successfully stored.
private final File mPreviousSettingsFilename;
@@ -640,6 +643,7 @@
mRuntimePermissionsPersistence = null;
mPermissionDataProvider = null;
mSettingsFilename = null;
+ mSettingsReserveCopyFilename = null;
mPreviousSettingsFilename = null;
mPackageListFilename = null;
mStoppedPackagesFilename = null;
@@ -711,6 +715,7 @@
|FileUtils.S_IROTH|FileUtils.S_IXOTH,
-1, -1);
mSettingsFilename = new File(mSystemDir, "packages.xml");
+ mSettingsReserveCopyFilename = new File(mSystemDir, "packages.xml.reservecopy");
mPreviousSettingsFilename = new File(mSystemDir, "packages-backup.xml");
mPackageListFilename = new File(mSystemDir, "packages.list");
FileUtils.setPermissions(mPackageListFilename, 0640, SYSTEM_UID, PACKAGE_INFO_GID);
@@ -752,6 +757,7 @@
mLock = null;
mRuntimePermissionsPersistence = r.mRuntimePermissionsPersistence;
mSettingsFilename = null;
+ mSettingsReserveCopyFilename = null;
mPreviousSettingsFilename = null;
mPackageListFilename = null;
mStoppedPackagesFilename = null;
@@ -2681,12 +2687,25 @@
// New settings successfully written, old ones are no longer needed.
mPreviousSettingsFilename.delete();
+ mSettingsReserveCopyFilename.delete();
FileUtils.setPermissions(mSettingsFilename.toString(),
- FileUtils.S_IRUSR|FileUtils.S_IWUSR
- |FileUtils.S_IRGRP|FileUtils.S_IWGRP,
+ FileUtils.S_IRUSR | FileUtils.S_IWUSR | FileUtils.S_IRGRP | FileUtils.S_IWGRP,
-1, -1);
+ try {
+ FileUtils.copy(mSettingsFilename, mSettingsReserveCopyFilename);
+ } catch (IOException e) {
+ Slog.e(TAG, "Failed to backup settings", e);
+ }
+
+ try {
+ VerityUtils.setUpFsverity(mSettingsFilename.getAbsolutePath());
+ VerityUtils.setUpFsverity(mSettingsReserveCopyFilename.getAbsolutePath());
+ } catch (IOException e) {
+ Slog.e(TAG, "Failed to verity-protect settings", e);
+ }
+
writeKernelMappingLPr();
writePackageListLPr();
writeAllUsersPackageRestrictionsLPr(sync);
@@ -3117,49 +3136,62 @@
}
}
- boolean readLPw(@NonNull Computer computer, @NonNull List<UserInfo> users) {
- FileInputStream str = null;
- if (mPreviousSettingsFilename.exists()) {
- try {
- str = new FileInputStream(mPreviousSettingsFilename);
- mReadMessages.append("Reading from backup settings file\n");
- PackageManagerService.reportSettingsProblem(Log.INFO,
- "Need to read from backup settings file");
- if (mSettingsFilename.exists()) {
- // If both the previous and current settings files exist,
- // we ignore the current since it might have been corrupted.
- Slog.w(PackageManagerService.TAG, "Cleaning up settings file "
- + mSettingsFilename);
- mSettingsFilename.delete();
- }
- } catch (java.io.IOException e) {
- // We'll try for the normal settings file.
- }
- }
-
+ boolean readSettingsLPw(@NonNull Computer computer, @NonNull List<UserInfo> users,
+ ArrayMap<String, Long> originalFirstInstallTimes) {
mPendingPackages.clear();
mPastSignatures.clear();
mKeySetRefs.clear();
mInstallerPackages.clear();
+ originalFirstInstallTimes.clear();
- // If any user state doesn't have a first install time, e.g., after an OTA,
- // use the pre OTA firstInstallTime timestamp. This is because we migrated from per package
- // firstInstallTime to per user-state. Without this, OTA can cause this info to be lost.
- final ArrayMap<String, Long> originalFirstInstallTimes = new ArrayMap<>();
+ File file = null;
+ FileInputStream str = null;
try {
- if (str == null) {
- if (!mSettingsFilename.exists()) {
- mReadMessages.append("No settings file found\n");
+ // Check if the previous write was incomplete.
+ if (mPreviousSettingsFilename.exists()) {
+ try {
+ file = mPreviousSettingsFilename;
+ str = new FileInputStream(file);
+ mReadMessages.append("Reading from backup settings file\n");
PackageManagerService.reportSettingsProblem(Log.INFO,
- "No settings file; creating initial state");
- // It's enough to just touch version details to create them
- // with default values
- findOrCreateVersion(StorageManager.UUID_PRIVATE_INTERNAL).forceCurrent();
- findOrCreateVersion(StorageManager.UUID_PRIMARY_PHYSICAL).forceCurrent();
- return false;
+ "Need to read from backup settings file");
+ if (mSettingsFilename.exists()) {
+ // If both the previous and current settings files exist,
+ // we ignore the current since it might have been corrupted.
+ Slog.w(PackageManagerService.TAG, "Cleaning up settings file "
+ + mSettingsFilename);
+ mSettingsFilename.delete();
+ }
+ // Ignore reserve copy as well.
+ mSettingsReserveCopyFilename.delete();
+ } catch (java.io.IOException e) {
+ // We'll try for the normal settings file.
}
- str = new FileInputStream(mSettingsFilename);
+ }
+ if (str == null) {
+ if (mSettingsFilename.exists()) {
+ // Using packages.xml.
+ file = mSettingsFilename;
+ str = new FileInputStream(file);
+ } else if (mSettingsReserveCopyFilename.exists()) {
+ // Using reserve copy.
+ file = mSettingsReserveCopyFilename;
+ str = new FileInputStream(file);
+ mReadMessages.append("Reading from reserve copy settings file\n");
+ PackageManagerService.reportSettingsProblem(Log.INFO,
+ "Need to read from reserve copy settings file");
+ }
+ }
+ if (str == null) {
+ // No available data sources.
+ mReadMessages.append("No settings file found\n");
+ PackageManagerService.reportSettingsProblem(Log.INFO,
+ "No settings file; creating initial state");
+ // Not necessary, but will avoid wtf-s in the "finally" section.
+ findOrCreateVersion(StorageManager.UUID_PRIVATE_INTERNAL).forceCurrent();
+ findOrCreateVersion(StorageManager.UUID_PRIMARY_PHYSICAL).forceCurrent();
+ return false;
}
final TypedXmlPullParser parser = Xml.resolvePullParser(str);
@@ -3280,6 +3312,33 @@
mReadMessages.append("Error reading: " + e.toString());
PackageManagerService.reportSettingsProblem(Log.ERROR, "Error reading settings: " + e);
Slog.wtf(PackageManagerService.TAG, "Error reading package manager settings", e);
+
+ // Remove corrupted file and retry.
+ Slog.e(TAG,
+ "Error reading package manager settings, removing " + file + " and retrying.",
+ e);
+ file.delete();
+
+ // Ignore the result to not mark this as a "first boot".
+ readSettingsLPw(computer, users, originalFirstInstallTimes);
+ }
+
+ return true;
+ }
+
+ /**
+ * @return false if settings file is missing (i.e. during first boot), true otherwise
+ */
+ boolean readLPw(@NonNull Computer computer, @NonNull List<UserInfo> users) {
+ // If any user state doesn't have a first install time, e.g., after an OTA,
+ // use the pre OTA firstInstallTime timestamp. This is because we migrated from per package
+ // firstInstallTime to per user-state. Without this, OTA can cause this info to be lost.
+ final ArrayMap<String, Long> originalFirstInstallTimes = new ArrayMap<>();
+
+ try {
+ if (!readSettingsLPw(computer, users, originalFirstInstallTimes)) {
+ return false;
+ }
} finally {
if (!mVersion.containsKey(StorageManager.UUID_PRIVATE_INTERNAL)) {
Slog.wtf(PackageManagerService.TAG,
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index 6b45ba2..5b9460a 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -1052,6 +1052,17 @@
private void startBootstrapServices(@NonNull TimingsTraceAndSlog t) {
t.traceBegin("startBootstrapServices");
+ t.traceBegin("ArtModuleServiceInitializer");
+ // This needs to happen before DexUseManagerLocal init. We do it here to avoid colliding
+ // with a GC. ArtModuleServiceInitializer is a class from a separate dex file
+ // "service-art.jar", so referencing it involves the class linker. The class linker and the
+ // GC are mutually exclusive (b/263486535). Therefore, we do this here to force trigger the
+ // class linker earlier. If we did this later, especially after PackageManagerService init,
+ // the class linker would be consistently blocked by a GC because PackageManagerService
+ // allocates a lot of memory and almost certainly triggers a GC.
+ ArtModuleServiceInitializer.setArtModuleServiceManager(new ArtModuleServiceManager());
+ t.traceEnd();
+
// Start the watchdog as early as possible so we can crash the system server
// if we deadlock during early boot
t.traceBegin("StartWatchdog");
@@ -1238,8 +1249,6 @@
t.traceBegin("DexUseManagerLocal");
// DexUseManagerLocal needs to be loaded after PackageManagerLocal has been registered, but
// before PackageManagerService starts processing binder calls to notifyDexLoad.
- // DexUseManagerLocal may also call artd, so ensure ArtModuleServiceManager is instantiated.
- ArtModuleServiceInitializer.setArtModuleServiceManager(new ArtModuleServiceManager());
LocalManagerRegistry.addManager(
DexUseManagerLocal.class, DexUseManagerLocal.createInstance());
t.traceEnd();
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerSettingsTests.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerSettingsTests.java
index 3727d66..b20e1dd 100644
--- a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerSettingsTests.java
+++ b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerSettingsTests.java
@@ -92,8 +92,11 @@
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
import java.security.PublicKey;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.PriorityQueue;
@@ -141,8 +144,7 @@
/** make sure our initialized KeySetManagerService metadata matches packages.xml */
@Test
- public void testReadKeySetSettings()
- throws ReflectiveOperationException, IllegalAccessException {
+ public void testReadKeySetSettings() throws Exception {
/* write out files and read */
writeOldFiles();
Settings settings = makeSettings();
@@ -150,6 +152,29 @@
verifyKeySetMetaData(settings);
}
+ // Same as above but use the reserve copy.
+ @Test
+ public void testReadReserveCopyKeySetSettings() throws Exception {
+ /* write out files and read */
+ writeReserveCopyOldFiles();
+ Settings settings = makeSettings();
+ assertThat(settings.readLPw(computer, createFakeUsers()), is(true));
+ verifyKeySetMetaData(settings);
+ }
+
+ // Same as above but packages.xml is malformed.
+ @Test
+ public void testReadMalformedPackagesXmlKeySetSettings() throws Exception {
+ // write out files
+ writeReserveCopyOldFiles();
+ // write corrupted packages.xml
+ writeCorruptedPackagesXml();
+
+ Settings settings = makeSettings();
+ assertThat(settings.readLPw(computer, createFakeUsers()), is(true));
+ verifyKeySetMetaData(settings);
+ }
+
/** read in data, write it out, and read it back in. Verify same. */
@Test
public void testWriteKeySetSettings()
@@ -165,6 +190,39 @@
verifyKeySetMetaData(settings);
}
+ // Same as above, but corrupt the primary.xml in process.
+ @Test
+ public void testWriteCorruptReadKeySetSettings() throws Exception {
+ // write out files and read
+ writeOldFiles();
+ Settings settings = makeSettings();
+ assertThat(settings.readLPw(computer, createFakeUsers()), is(true));
+
+ // write out
+ settings.writeLPr(computer, /*sync=*/true);
+
+ File filesDir = InstrumentationRegistry.getContext().getFilesDir();
+ File packageXml = new File(filesDir, "system/packages.xml");
+ File packagesReserveCopyXml = new File(filesDir, "system/packages.xml.reservecopy");
+ // Primary.
+ assertTrue(packageXml.exists());
+ // Reserve copy.
+ assertTrue(packagesReserveCopyXml.exists());
+ // Temporary backup.
+ assertFalse(new File(filesDir, "packages-backup.xml").exists());
+
+ // compare two copies, make sure they are the same
+ assertTrue(Arrays.equals(Files.readAllBytes(Path.of(packageXml.getAbsolutePath())),
+ Files.readAllBytes(Path.of(packagesReserveCopyXml.getAbsolutePath()))));
+
+ // write corrupted packages.xml
+ writeCorruptedPackagesXml();
+
+ // read back in and verify the same
+ assertThat(settings.readLPw(computer, createFakeUsers()), is(true));
+ verifyKeySetMetaData(settings);
+ }
+
@Test
public void testSettingsReadOld() {
// Write delegateshellthe package files and make sure they're parsed properly the first time
@@ -1572,9 +1630,19 @@
}
}
- private void writePackagesXml() {
+ private void writeCorruptedPackagesXml() {
writeFile(new File(InstrumentationRegistry.getContext().getFilesDir(), "system/packages.xml"),
("<?xml version='1.0' encoding='utf-8' standalone='yes' ?>"
+ + "<packages>"
+ + "<last-platform-version internal=\"15\" external=\"0\" />"
+ + "<permission-trees>"
+ + "<item name=\"com.google.android.permtree\""
+ ).getBytes());
+ }
+
+ private void writePackagesXml(String fileName) {
+ writeFile(new File(InstrumentationRegistry.getContext().getFilesDir(), fileName),
+ ("<?xml version='1.0' encoding='utf-8' standalone='yes' ?>"
+ "<packages>"
+ "<last-platform-version internal=\"15\" external=\"0\" fingerprint=\"foo\" />"
+ "<permission-trees>"
@@ -1715,7 +1783,14 @@
private void writeOldFiles() {
deleteSystemFolder();
- writePackagesXml();
+ writePackagesXml("system/packages.xml");
+ writeStoppedPackagesXml();
+ writePackagesList();
+ }
+
+ private void writeReserveCopyOldFiles() {
+ deleteSystemFolder();
+ writePackagesXml("system/packages.xml.reservecopy");
writeStoppedPackagesXml();
writePackagesList();
}
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java
index 8d78cd6..f56b0b4 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java
@@ -197,7 +197,7 @@
private void enqueueOrReplaceBroadcast(BroadcastProcessQueue queue,
BroadcastRecord record, int recordIndex, long enqueueTime) {
- queue.enqueueOrReplaceBroadcast(record, recordIndex);
+ queue.enqueueOrReplaceBroadcast(record, recordIndex, null /* replacedBroadcastConsumer */);
record.enqueueTime = enqueueTime;
}
@@ -327,7 +327,7 @@
final Intent airplane = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED);
final BroadcastRecord airplaneRecord = makeBroadcastRecord(airplane,
List.of(makeMockRegisteredReceiver()));
- queue.enqueueOrReplaceBroadcast(airplaneRecord, 0);
+ queue.enqueueOrReplaceBroadcast(airplaneRecord, 0, null /* replacedBroadcastConsumer */);
queue.setProcessCached(false);
final long notCachedRunnableAt = queue.getRunnableAt();
@@ -349,12 +349,12 @@
// enqueue a bg-priority broadcast then a fg-priority one
final Intent timezone = new Intent(Intent.ACTION_TIMEZONE_CHANGED);
final BroadcastRecord timezoneRecord = makeBroadcastRecord(timezone);
- queue.enqueueOrReplaceBroadcast(timezoneRecord, 0);
+ queue.enqueueOrReplaceBroadcast(timezoneRecord, 0, null /* replacedBroadcastConsumer */);
final Intent airplane = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED);
airplane.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
final BroadcastRecord airplaneRecord = makeBroadcastRecord(airplane);
- queue.enqueueOrReplaceBroadcast(airplaneRecord, 0);
+ queue.enqueueOrReplaceBroadcast(airplaneRecord, 0, null /* replacedBroadcastConsumer */);
// verify that:
// (a) the queue is immediately runnable by existence of a fg-priority broadcast
@@ -385,7 +385,7 @@
final BroadcastRecord airplaneRecord = makeBroadcastRecord(airplane, null,
List.of(withPriority(makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN), 10),
withPriority(makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN), 0)), true);
- queue.enqueueOrReplaceBroadcast(airplaneRecord, 1);
+ queue.enqueueOrReplaceBroadcast(airplaneRecord, 1, null /* replacedBroadcastConsumer */);
assertFalse(queue.isRunnable());
assertEquals(BroadcastProcessQueue.REASON_BLOCKED, queue.getRunnableAtReason());
@@ -408,7 +408,7 @@
final Intent airplane = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED);
final BroadcastRecord airplaneRecord = makeBroadcastRecord(airplane,
List.of(makeMockRegisteredReceiver()));
- queue.enqueueOrReplaceBroadcast(airplaneRecord, 0);
+ queue.enqueueOrReplaceBroadcast(airplaneRecord, 0, null /* replacedBroadcastConsumer */);
mConstants.MAX_PENDING_BROADCASTS = 128;
queue.invalidateRunnableAt();
@@ -434,11 +434,11 @@
new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED),
List.of(makeMockRegisteredReceiver()));
- queue.enqueueOrReplaceBroadcast(lazyRecord, 0);
+ queue.enqueueOrReplaceBroadcast(lazyRecord, 0, null /* replacedBroadcastConsumer */);
assertThat(queue.getRunnableAt()).isGreaterThan(lazyRecord.enqueueTime);
assertThat(queue.getRunnableAtReason()).isNotEqualTo(testRunnableAtReason);
- queue.enqueueOrReplaceBroadcast(testRecord, 0);
+ queue.enqueueOrReplaceBroadcast(testRecord, 0, null /* replacedBroadcastConsumer */);
assertThat(queue.getRunnableAt()).isAtMost(testRecord.enqueueTime);
assertThat(queue.getRunnableAtReason()).isEqualTo(testRunnableAtReason);
}
@@ -507,20 +507,26 @@
queue.enqueueOrReplaceBroadcast(
makeBroadcastRecord(new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED)
- .addFlags(Intent.FLAG_RECEIVER_OFFLOAD)), 0);
+ .addFlags(Intent.FLAG_RECEIVER_OFFLOAD)), 0,
+ null /* replacedBroadcastConsumer */);
queue.enqueueOrReplaceBroadcast(
- makeBroadcastRecord(new Intent(Intent.ACTION_TIMEZONE_CHANGED)), 0);
+ makeBroadcastRecord(new Intent(Intent.ACTION_TIMEZONE_CHANGED)), 0,
+ null /* replacedBroadcastConsumer */);
queue.enqueueOrReplaceBroadcast(
makeBroadcastRecord(new Intent(Intent.ACTION_LOCKED_BOOT_COMPLETED)
- .addFlags(Intent.FLAG_RECEIVER_FOREGROUND)), 0);
+ .addFlags(Intent.FLAG_RECEIVER_FOREGROUND)), 0,
+ null /* replacedBroadcastConsumer */);
queue.enqueueOrReplaceBroadcast(
makeBroadcastRecord(new Intent(Intent.ACTION_ALARM_CHANGED)
- .addFlags(Intent.FLAG_RECEIVER_OFFLOAD)), 0);
+ .addFlags(Intent.FLAG_RECEIVER_OFFLOAD)), 0,
+ null /* replacedBroadcastConsumer */);
queue.enqueueOrReplaceBroadcast(
- makeBroadcastRecord(new Intent(Intent.ACTION_TIME_TICK)), 0);
+ makeBroadcastRecord(new Intent(Intent.ACTION_TIME_TICK)), 0,
+ null /* replacedBroadcastConsumer */);
queue.enqueueOrReplaceBroadcast(
makeBroadcastRecord(new Intent(Intent.ACTION_LOCALE_CHANGED)
- .addFlags(Intent.FLAG_RECEIVER_FOREGROUND)), 0);
+ .addFlags(Intent.FLAG_RECEIVER_FOREGROUND)), 0,
+ null /* replacedBroadcastConsumer */);
queue.makeActiveNextPending();
assertEquals(Intent.ACTION_LOCKED_BOOT_COMPLETED, queue.getActive().intent.getAction());
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java
index d79c4d8..6800d52 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java
@@ -154,6 +154,7 @@
private ActivityManagerService mAms;
private BroadcastQueue mQueue;
+ BroadcastConstants mConstants;
/**
* Desired behavior of the next
@@ -277,10 +278,9 @@
}).when(mAms).getProcessRecordLocked(any(), anyInt());
doNothing().when(mAms).appNotResponding(any(), any());
- final BroadcastConstants constants = new BroadcastConstants(
- Settings.Global.BROADCAST_FG_CONSTANTS);
- constants.TIMEOUT = 100;
- constants.ALLOW_BG_ACTIVITY_START_TIMEOUT = 0;
+ mConstants = new BroadcastConstants(Settings.Global.BROADCAST_FG_CONSTANTS);
+ mConstants.TIMEOUT = 100;
+ mConstants.ALLOW_BG_ACTIVITY_START_TIMEOUT = 0;
final BroadcastSkipPolicy emptySkipPolicy = new BroadcastSkipPolicy(mAms) {
public boolean shouldSkip(BroadcastRecord r, Object o) {
// Ignored
@@ -291,7 +291,7 @@
return null;
}
};
- final BroadcastHistory emptyHistory = new BroadcastHistory(constants) {
+ final BroadcastHistory emptyHistory = new BroadcastHistory(mConstants) {
public void addBroadcastToHistoryLocked(BroadcastRecord original) {
// Ignored
}
@@ -299,13 +299,13 @@
if (mImpl == Impl.DEFAULT) {
var q = new BroadcastQueueImpl(mAms, mHandlerThread.getThreadHandler(), TAG,
- constants, emptySkipPolicy, emptyHistory, false,
+ mConstants, emptySkipPolicy, emptyHistory, false,
ProcessList.SCHED_GROUP_DEFAULT);
q.mReceiverBatch.mDeepReceiverCopy = true;
mQueue = q;
} else if (mImpl == Impl.MODERN) {
var q = new BroadcastQueueModernImpl(mAms, mHandlerThread.getThreadHandler(),
- constants, constants, emptySkipPolicy, emptyHistory);
+ mConstants, mConstants, emptySkipPolicy, emptyHistory);
q.mReceiverBatch.mDeepReceiverCopy = true;
mQueue = q;
} else {
@@ -1703,6 +1703,28 @@
}
@Test
+ public void testReplacePending_withPrioritizedBroadcasts() throws Exception {
+ mConstants.MAX_RUNNING_ACTIVE_BROADCASTS = 1;
+ final ProcessRecord callerApp = makeActiveProcessRecord(PACKAGE_GREEN);
+
+ final Intent userPresent = new Intent(Intent.ACTION_USER_PRESENT)
+ .addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING);
+
+ final List receivers = List.of(
+ withPriority(makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN), 100),
+ withPriority(makeManifestReceiver(PACKAGE_GREEN, CLASS_RED), 50),
+ withPriority(makeManifestReceiver(PACKAGE_GREEN, CLASS_YELLOW), 10),
+ withPriority(makeManifestReceiver(PACKAGE_GREEN, CLASS_BLUE), 0));
+
+ // Enqueue the broadcast a few times and verify that broadcast queues are not stuck
+ // and are emptied eventually.
+ for (int i = 0; i < 6; ++i) {
+ enqueueBroadcast(makeBroadcastRecord(userPresent, callerApp, receivers));
+ }
+ waitForIdle();
+ }
+
+ @Test
public void testIdleAndBarrier() throws Exception {
final ProcessRecord callerApp = makeActiveProcessRecord(PACKAGE_RED);
final ProcessRecord receiverApp = makeActiveProcessRecord(PACKAGE_GREEN);
diff --git a/services/tests/servicestests/src/com/android/server/audio/NoOpAudioSystemAdapter.java b/services/tests/servicestests/src/com/android/server/audio/NoOpAudioSystemAdapter.java
index ee9d59b..08a0878 100644
--- a/services/tests/servicestests/src/com/android/server/audio/NoOpAudioSystemAdapter.java
+++ b/services/tests/servicestests/src/com/android/server/audio/NoOpAudioSystemAdapter.java
@@ -75,7 +75,13 @@
}
@Override
- public int removeDevicesRoleForStrategy(int strategy, int role) {
+ public int removeDevicesRoleForStrategy(int strategy, int role,
+ @NonNull List<AudioDeviceAttributes> devices) {
+ return AudioSystem.AUDIO_STATUS_OK;
+ }
+
+ @Override
+ public int clearDevicesRoleForStrategy(int strategy, int role) {
return AudioSystem.AUDIO_STATUS_OK;
}
diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
index ac880ce..89eaa2c 100644
--- a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
@@ -35,7 +35,6 @@
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.nullable;
import static org.mockito.Mockito.argThat;
-import static org.mockito.Mockito.doCallRealMethod;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
@@ -97,8 +96,9 @@
import android.view.KeyEvent;
import android.view.WindowManager;
-import androidx.test.InstrumentationRegistry;
+import androidx.test.platform.app.InstrumentationRegistry;
+import com.android.compatibility.common.util.AdoptShellPermissionsRule;
import com.android.internal.app.BlockedAppStreamingActivity;
import com.android.server.LocalServices;
import com.android.server.input.InputManagerInternal;
@@ -107,6 +107,7 @@
import com.google.android.collect.Sets;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
@@ -177,17 +178,14 @@
.setAssociatedDisplayId(DISPLAY_ID_1)
.build();
private static final VirtualTouchscreenConfig TOUCHSCREEN_CONFIG =
- new VirtualTouchscreenConfig.Builder()
+ new VirtualTouchscreenConfig.Builder(WIDTH, HEIGHT)
.setVendorId(VENDOR_ID)
.setProductId(PRODUCT_ID)
.setInputDeviceName(DEVICE_NAME)
.setAssociatedDisplayId(DISPLAY_ID_1)
- .setWidthInPixels(WIDTH)
- .setHeightInPixels(HEIGHT)
.build();
private static final VirtualNavigationTouchpadConfig NAVIGATION_TOUCHPAD_CONFIG =
- new VirtualNavigationTouchpadConfig.Builder(
- /* touchpadHeight= */ HEIGHT, /* touchpadWidth= */ WIDTH)
+ new VirtualNavigationTouchpadConfig.Builder(WIDTH, HEIGHT)
.setVendorId(VENDOR_ID)
.setProductId(PRODUCT_ID)
.setInputDeviceName(DEVICE_NAME)
@@ -195,6 +193,11 @@
.build();
private static final String TEST_SITE = "http://test";
+ @Rule
+ public AdoptShellPermissionsRule mAdoptShellPermissionsRule = new AdoptShellPermissionsRule(
+ InstrumentationRegistry.getInstrumentation().getUiAutomation(),
+ Manifest.permission.CREATE_VIRTUAL_DEVICE);
+
private Context mContext;
private InputManagerMockHelper mInputManagerMockHelper;
private VirtualDeviceImpl mDeviceImpl;
@@ -304,10 +307,9 @@
LocalServices.removeServiceForTest(DisplayManagerInternal.class);
LocalServices.addService(DisplayManagerInternal.class, mDisplayManagerInternalMock);
- mContext = Mockito.spy(new ContextWrapper(InstrumentationRegistry.getTargetContext()));
+ mContext = Mockito.spy(new ContextWrapper(
+ InstrumentationRegistry.getInstrumentation().getTargetContext()));
doReturn(mContext).when(mContext).createContextAsUser(eq(Process.myUserHandle()), anyInt());
- doNothing().when(mContext).enforceCallingOrSelfPermission(
- eq(Manifest.permission.CREATE_VIRTUAL_DEVICE), anyString());
when(mContext.getSystemService(Context.DEVICE_POLICY_SERVICE)).thenReturn(
mDevicePolicyManagerMock);
@@ -726,48 +728,28 @@
@Test
public void createVirtualTouchscreen_zeroDisplayDimension_failsIllegalArgumentException() {
- mDeviceImpl.mVirtualDisplayIds.add(DISPLAY_ID_1);
- final VirtualTouchscreenConfig zeroConfig =
- new VirtualTouchscreenConfig.Builder()
- .setVendorId(VENDOR_ID)
- .setProductId(PRODUCT_ID)
- .setInputDeviceName(DEVICE_NAME)
- .setAssociatedDisplayId(DISPLAY_ID_1)
- .setWidthInPixels(0)
- .setHeightInPixels(0)
- .build();
assertThrows(IllegalArgumentException.class,
- () -> mDeviceImpl.createVirtualTouchscreen(zeroConfig, BINDER));
+ () -> new VirtualTouchscreenConfig.Builder(
+ /* touchscrenWidth= */ 0, /* touchscreenHeight= */ 0));
}
@Test
public void createVirtualTouchscreen_negativeDisplayDimension_failsIllegalArgumentException() {
- mDeviceImpl.mVirtualDisplayIds.add(DISPLAY_ID_1);
- final VirtualTouchscreenConfig negativeConfig =
- new VirtualTouchscreenConfig.Builder()
- .setVendorId(VENDOR_ID)
- .setProductId(PRODUCT_ID)
- .setInputDeviceName(DEVICE_NAME)
- .setAssociatedDisplayId(DISPLAY_ID_1)
- .setWidthInPixels(-100)
- .setHeightInPixels(-100)
- .build();
assertThrows(IllegalArgumentException.class,
- () -> mDeviceImpl.createVirtualTouchscreen(negativeConfig, BINDER));
-
+ () -> new VirtualTouchscreenConfig.Builder(
+ /* touchscrenWidth= */ -100, /* touchscreenHeight= */ -100));
}
@Test
public void createVirtualTouchscreen_positiveDisplayDimension_successful() {
mDeviceImpl.mVirtualDisplayIds.add(DISPLAY_ID_1);
VirtualTouchscreenConfig positiveConfig =
- new VirtualTouchscreenConfig.Builder()
+ new VirtualTouchscreenConfig.Builder(
+ /* touchscrenWidth= */ 600, /* touchscreenHeight= */ 800)
.setVendorId(VENDOR_ID)
.setProductId(PRODUCT_ID)
.setInputDeviceName(DEVICE_NAME)
.setAssociatedDisplayId(DISPLAY_ID_1)
- .setWidthInPixels(600)
- .setHeightInPixels(800)
.build();
mDeviceImpl.createVirtualTouchscreen(positiveConfig, BINDER);
assertWithMessage(
@@ -784,36 +766,16 @@
@Test
public void createVirtualNavigationTouchpad_zeroDisplayDimension_failsWithException() {
- mDeviceImpl.mVirtualDisplayIds.add(DISPLAY_ID_1);
assertThrows(IllegalArgumentException.class,
- () -> {
- final VirtualNavigationTouchpadConfig zeroConfig =
- new VirtualNavigationTouchpadConfig.Builder(
- /* touchpadHeight= */ 0, /* touchpadWidth= */ 0)
- .setVendorId(VENDOR_ID)
- .setProductId(PRODUCT_ID)
- .setInputDeviceName(DEVICE_NAME)
- .setAssociatedDisplayId(DISPLAY_ID_1)
- .build();
- mDeviceImpl.createVirtualNavigationTouchpad(zeroConfig, BINDER);
- });
+ () -> new VirtualNavigationTouchpadConfig.Builder(
+ /* touchpadHeight= */ 0, /* touchpadWidth= */ 0));
}
@Test
public void createVirtualNavigationTouchpad_negativeDisplayDimension_failsWithException() {
- mDeviceImpl.mVirtualDisplayIds.add(DISPLAY_ID_1);
assertThrows(IllegalArgumentException.class,
- () -> {
- final VirtualNavigationTouchpadConfig negativeConfig =
- new VirtualNavigationTouchpadConfig.Builder(
- /* touchpadHeight= */ -50, /* touchpadWidth= */ 50)
- .setVendorId(VENDOR_ID)
- .setProductId(PRODUCT_ID)
- .setInputDeviceName(DEVICE_NAME)
- .setAssociatedDisplayId(DISPLAY_ID_1)
- .build();
- mDeviceImpl.createVirtualNavigationTouchpad(negativeConfig, BINDER);
- });
+ () -> new VirtualNavigationTouchpadConfig.Builder(
+ /* touchpadHeight= */ -50, /* touchpadWidth= */ 50));
}
@Test
@@ -844,76 +806,76 @@
@Test
public void createVirtualDpad_noPermission_failsSecurityException() {
mDeviceImpl.mVirtualDisplayIds.add(DISPLAY_ID_1);
- doCallRealMethod().when(mContext).enforceCallingOrSelfPermission(
- eq(Manifest.permission.CREATE_VIRTUAL_DEVICE), anyString());
- assertThrows(SecurityException.class,
- () -> mDeviceImpl.createVirtualDpad(DPAD_CONFIG, BINDER));
+ try (DropShellPermissionsTemporarily drop = new DropShellPermissionsTemporarily()) {
+ assertThrows(SecurityException.class,
+ () -> mDeviceImpl.createVirtualDpad(DPAD_CONFIG, BINDER));
+ }
}
@Test
public void createVirtualKeyboard_noPermission_failsSecurityException() {
mDeviceImpl.mVirtualDisplayIds.add(DISPLAY_ID_1);
- doCallRealMethod().when(mContext).enforceCallingOrSelfPermission(
- eq(Manifest.permission.CREATE_VIRTUAL_DEVICE), anyString());
- assertThrows(SecurityException.class,
- () -> mDeviceImpl.createVirtualKeyboard(KEYBOARD_CONFIG, BINDER));
+ try (DropShellPermissionsTemporarily drop = new DropShellPermissionsTemporarily()) {
+ assertThrows(SecurityException.class,
+ () -> mDeviceImpl.createVirtualKeyboard(KEYBOARD_CONFIG, BINDER));
+ }
}
@Test
public void createVirtualMouse_noPermission_failsSecurityException() {
mDeviceImpl.mVirtualDisplayIds.add(DISPLAY_ID_1);
- doCallRealMethod().when(mContext).enforceCallingOrSelfPermission(
- eq(Manifest.permission.CREATE_VIRTUAL_DEVICE), anyString());
- assertThrows(SecurityException.class,
- () -> mDeviceImpl.createVirtualMouse(MOUSE_CONFIG, BINDER));
+ try (DropShellPermissionsTemporarily drop = new DropShellPermissionsTemporarily()) {
+ assertThrows(SecurityException.class,
+ () -> mDeviceImpl.createVirtualMouse(MOUSE_CONFIG, BINDER));
+ }
}
@Test
public void createVirtualTouchscreen_noPermission_failsSecurityException() {
mDeviceImpl.mVirtualDisplayIds.add(DISPLAY_ID_1);
- doCallRealMethod().when(mContext).enforceCallingOrSelfPermission(
- eq(Manifest.permission.CREATE_VIRTUAL_DEVICE), anyString());
- assertThrows(SecurityException.class,
- () -> mDeviceImpl.createVirtualTouchscreen(TOUCHSCREEN_CONFIG, BINDER));
+ try (DropShellPermissionsTemporarily drop = new DropShellPermissionsTemporarily()) {
+ assertThrows(SecurityException.class,
+ () -> mDeviceImpl.createVirtualTouchscreen(TOUCHSCREEN_CONFIG, BINDER));
+ }
}
@Test
public void createVirtualNavigationTouchpad_noPermission_failsSecurityException() {
mDeviceImpl.mVirtualDisplayIds.add(DISPLAY_ID_1);
- doCallRealMethod().when(mContext).enforceCallingOrSelfPermission(
- eq(Manifest.permission.CREATE_VIRTUAL_DEVICE), anyString());
- assertThrows(SecurityException.class,
- () -> mDeviceImpl.createVirtualNavigationTouchpad(NAVIGATION_TOUCHPAD_CONFIG,
- BINDER));
+ try (DropShellPermissionsTemporarily drop = new DropShellPermissionsTemporarily()) {
+ assertThrows(SecurityException.class,
+ () -> mDeviceImpl.createVirtualNavigationTouchpad(NAVIGATION_TOUCHPAD_CONFIG,
+ BINDER));
+ }
}
@Test
public void createVirtualSensor_noPermission_failsSecurityException() {
- doCallRealMethod().when(mContext).enforceCallingOrSelfPermission(
- eq(Manifest.permission.CREATE_VIRTUAL_DEVICE), anyString());
- assertThrows(
- SecurityException.class,
- () -> mDeviceImpl.createVirtualSensor(
- BINDER,
- new VirtualSensorConfig.Builder(
- Sensor.TYPE_ACCELEROMETER, DEVICE_NAME).build()));
+ try (DropShellPermissionsTemporarily drop = new DropShellPermissionsTemporarily()) {
+ assertThrows(
+ SecurityException.class,
+ () -> mDeviceImpl.createVirtualSensor(
+ BINDER,
+ new VirtualSensorConfig.Builder(
+ Sensor.TYPE_ACCELEROMETER, DEVICE_NAME).build()));
+ }
}
@Test
public void onAudioSessionStarting_noPermission_failsSecurityException() {
mDeviceImpl.mVirtualDisplayIds.add(DISPLAY_ID_1);
- doCallRealMethod().when(mContext).enforceCallingOrSelfPermission(
- eq(Manifest.permission.CREATE_VIRTUAL_DEVICE), anyString());
- assertThrows(SecurityException.class,
- () -> mDeviceImpl.onAudioSessionStarting(
- DISPLAY_ID_1, mRoutingCallback, mConfigChangedCallback));
+ try (DropShellPermissionsTemporarily drop = new DropShellPermissionsTemporarily()) {
+ assertThrows(SecurityException.class,
+ () -> mDeviceImpl.onAudioSessionStarting(
+ DISPLAY_ID_1, mRoutingCallback, mConfigChangedCallback));
+ }
}
@Test
public void onAudioSessionEnded_noPermission_failsSecurityException() {
- doCallRealMethod().when(mContext).enforceCallingOrSelfPermission(
- eq(Manifest.permission.CREATE_VIRTUAL_DEVICE), anyString());
- assertThrows(SecurityException.class, () -> mDeviceImpl.onAudioSessionEnded());
+ try (DropShellPermissionsTemporarily drop = new DropShellPermissionsTemporarily()) {
+ assertThrows(SecurityException.class, () -> mDeviceImpl.onAudioSessionEnded());
+ }
}
@Test
@@ -1593,4 +1555,18 @@
mVdms.addVirtualDevice(virtualDeviceImpl);
return virtualDeviceImpl;
}
+
+ /** Helper class to drop permissions temporarily and restore them at the end of a test. */
+ static final class DropShellPermissionsTemporarily implements AutoCloseable {
+ DropShellPermissionsTemporarily() {
+ InstrumentationRegistry.getInstrumentation().getUiAutomation()
+ .dropShellPermissionIdentity();
+ }
+
+ @Override
+ public void close() {
+ InstrumentationRegistry.getInstrumentation().getUiAutomation()
+ .adoptShellPermissionIdentity();
+ }
+ }
}
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/ActiveSourceActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/ActiveSourceActionTest.java
index 112db76..68ab671 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/ActiveSourceActionTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/ActiveSourceActionTest.java
@@ -94,6 +94,7 @@
mHdmiControlService.setCecController(hdmiCecController);
mHdmiControlService.setHdmiMhlController(HdmiMhlControllerStub.create(mHdmiControlService));
mHdmiControlService.initService();
+ mHdmiControlService.enableAllFeatureFlags();
mHdmiControlService.onBootPhase(PHASE_SYSTEM_SERVICES_READY);
mPowerManager = new FakePowerManagerWrapper(mContextSpy);
mHdmiControlService.setPowerManager(mPowerManager);
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/ArcInitiationActionFromAvrTest.java b/services/tests/servicestests/src/com/android/server/hdmi/ArcInitiationActionFromAvrTest.java
index e4eecc6..1b811ab 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/ArcInitiationActionFromAvrTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/ArcInitiationActionFromAvrTest.java
@@ -111,6 +111,7 @@
hdmiControlService.setCecController(hdmiCecController);
hdmiControlService.setHdmiMhlController(HdmiMhlControllerStub.create(hdmiControlService));
hdmiControlService.initService();
+ hdmiControlService.enableAllFeatureFlags();
mPowerManager = new FakePowerManagerWrapper(mContextSpy);
hdmiControlService.setPowerManager(mPowerManager);
mAction = new ArcInitiationActionFromAvr(mHdmiCecLocalDeviceAudioSystem);
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/ArcTerminationActionFromAvrTest.java b/services/tests/servicestests/src/com/android/server/hdmi/ArcTerminationActionFromAvrTest.java
index 2cb46da..53be896 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/ArcTerminationActionFromAvrTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/ArcTerminationActionFromAvrTest.java
@@ -107,6 +107,7 @@
hdmiControlService.setCecController(hdmiCecController);
hdmiControlService.setHdmiMhlController(HdmiMhlControllerStub.create(hdmiControlService));
hdmiControlService.initService();
+ hdmiControlService.enableAllFeatureFlags();
mPowerManager = new FakePowerManagerWrapper(mContextSpy);
hdmiControlService.setPowerManager(mPowerManager);
mHdmiCecLocalDeviceAudioSystem = new HdmiCecLocalDeviceAudioSystem(hdmiControlService) {
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/BaseAbsoluteVolumeControlTest.java b/services/tests/servicestests/src/com/android/server/hdmi/BaseAbsoluteVolumeControlTest.java
index 8ff87e3..0e1545e 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/BaseAbsoluteVolumeControlTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/BaseAbsoluteVolumeControlTest.java
@@ -165,6 +165,7 @@
mHdmiControlService.setHdmiMhlController(
HdmiMhlControllerStub.create(mHdmiControlService));
mHdmiControlService.initService();
+ mHdmiControlService.enableAllFeatureFlags();
mPowerManager = new FakePowerManagerWrapper(mContextSpy);
mHdmiControlService.setPowerManager(mPowerManager);
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/DevicePowerStatusActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/DevicePowerStatusActionTest.java
index 3a57db9..3045ea0 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/DevicePowerStatusActionTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/DevicePowerStatusActionTest.java
@@ -110,6 +110,7 @@
mHdmiControlService.setCecController(hdmiCecController);
mHdmiControlService.setHdmiMhlController(HdmiMhlControllerStub.create(mHdmiControlService));
mHdmiControlService.initService();
+ mHdmiControlService.enableAllFeatureFlags();
mHdmiControlService.onBootPhase(PHASE_SYSTEM_SERVICES_READY);
mPowerManager = new FakePowerManagerWrapper(mContextSpy);
mHdmiControlService.setPowerManager(mPowerManager);
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromPlaybackTest.java b/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromPlaybackTest.java
index 6a899e8..d610676 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromPlaybackTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromPlaybackTest.java
@@ -133,6 +133,7 @@
mHdmiControlService.setHdmiCecNetwork(mHdmiCecNetwork);
mHdmiControlService.initService();
+ mHdmiControlService.enableAllFeatureFlags();
mHdmiControlService.onBootPhase(PHASE_SYSTEM_SERVICES_READY);
mNativeWrapper.setPhysicalAddress(0x0000);
mPowerManager = new FakePowerManagerWrapper(context);
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromTvTest.java b/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromTvTest.java
index 0419768..14a83e0 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromTvTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromTvTest.java
@@ -143,6 +143,7 @@
true, false, false);
mNativeWrapper.setPortInfo(hdmiPortInfos);
mHdmiControlService.initService();
+ mHdmiControlService.enableAllFeatureFlags();
mHdmiControlService.onBootPhase(PHASE_SYSTEM_SERVICES_READY);
mPowerManager = new FakePowerManagerWrapper(context);
mHdmiControlService.setPowerManager(mPowerManager);
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecAtomLoggingTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecAtomLoggingTest.java
index d2fe6da..e6e479f 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecAtomLoggingTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecAtomLoggingTest.java
@@ -122,6 +122,7 @@
mNativeWrapper.setPortConnectionStatus(1, true);
mHdmiControlServiceSpy.initService();
+ mHdmiControlServiceSpy.enableAllFeatureFlags();
mPowerManager = new FakePowerManagerWrapper(mContextSpy);
mHdmiControlServiceSpy.setPowerManager(mPowerManager);
mHdmiControlServiceSpy.onBootPhase(SystemService.PHASE_SYSTEM_SERVICES_READY);
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecControllerTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecControllerTest.java
index 367f41d..9b2e227 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecControllerTest.java
@@ -127,6 +127,7 @@
localDevices.add(playbackDevice);
mHdmiControlServiceSpy.initService();
+ mHdmiControlServiceSpy.enableAllFeatureFlags();
mHdmiControlServiceSpy.allocateLogicalAddress(localDevices,
HdmiControlService.INITIATED_BY_ENABLE_CEC);
mHdmiControlServiceSpy.onBootPhase(SystemService.PHASE_SYSTEM_SERVICES_READY);
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystemTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystemTest.java
index de2c218..22f7c28 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystemTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystemTest.java
@@ -209,6 +209,7 @@
4, HdmiPortInfo.PORT_INPUT, HDMI_3_PHYSICAL_ADDRESS, true, false, false);
mNativeWrapper.setPortInfo(mHdmiPortInfo);
mHdmiControlService.initService();
+ mHdmiControlService.enableAllFeatureFlags();
mHdmiControlService.onBootPhase(PHASE_SYSTEM_SERVICES_READY);
mPowerManager = new FakePowerManagerWrapper(context);
mHdmiControlService.setPowerManager(mPowerManager);
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDevicePlaybackTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDevicePlaybackTest.java
index 3ed8983..c25d7c0 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDevicePlaybackTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDevicePlaybackTest.java
@@ -153,6 +153,7 @@
mNativeWrapper.setPortInfo(hdmiPortInfos);
mNativeWrapper.setPortConnectionStatus(1, true);
mHdmiControlService.initService();
+ mHdmiControlService.enableAllFeatureFlags();
mHdmiControlService.onBootPhase(PHASE_SYSTEM_SERVICES_READY);
mPowerManager = new FakePowerManagerWrapper(context);
mHdmiControlService.setPowerManager(mPowerManager);
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTest.java
index b30118c..4ff1b31 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTest.java
@@ -192,6 +192,7 @@
mNativeWrapper.setPortInfo(hdmiPortInfos);
mNativeWrapper.setPortConnectionStatus(1, true);
mHdmiControlService.initService();
+ mHdmiControlService.enableAllFeatureFlags();
mHdmiControlService.onBootPhase(PHASE_SYSTEM_SERVICES_READY);
mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
mNativeWrapper.setPhysicalAddress(0x2000);
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java
index 5dd29fd..c8f2afb 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java
@@ -192,6 +192,7 @@
new HdmiPortInfo(2, HdmiPortInfo.PORT_INPUT, 0x2000, true, false, true, true);
mNativeWrapper.setPortInfo(hdmiPortInfos);
mHdmiControlService.initService();
+ mHdmiControlService.enableAllFeatureFlags();
mHdmiControlService.onBootPhase(PHASE_SYSTEM_SERVICES_READY);
mPowerManager = new FakePowerManagerWrapper(context);
mHdmiControlService.setPowerManager(mPowerManager);
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecPowerStatusControllerTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecPowerStatusControllerTest.java
index 4e5336e..23edee5 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecPowerStatusControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecPowerStatusControllerTest.java
@@ -102,6 +102,7 @@
mNativeWrapper.setPortInfo(hdmiPortInfos);
mNativeWrapper.setPortConnectionStatus(1, true);
mHdmiControlService.initService();
+ mHdmiControlService.enableAllFeatureFlags();
mHdmiControlService.onBootPhase(PHASE_SYSTEM_SERVICES_READY);
mPowerManager = new FakePowerManagerWrapper(contextSpy);
mHdmiControlService.setPowerManager(mPowerManager);
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java
index aa49a62..b7cb347 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java
@@ -141,6 +141,7 @@
new HdmiPortInfo(4, HdmiPortInfo.PORT_INPUT, 0x3000, true, false, false, false);
mNativeWrapper.setPortInfo(mHdmiPortInfo);
mHdmiControlServiceSpy.initService();
+ mHdmiControlServiceSpy.enableAllFeatureFlags();
mPowerManager = new FakePowerManagerWrapper(mContextSpy);
mHdmiControlServiceSpy.setPowerManager(mPowerManager);
mHdmiControlServiceSpy.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
@@ -1203,6 +1204,7 @@
mTestLooper.dispatchAll();
Mockito.clearInvocations(mHdmiControlServiceSpy);
mHdmiControlServiceSpy.initService();
+ mHdmiControlServiceSpy.enableAllFeatureFlags();
mTestLooper.dispatchAll();
verify(mHdmiControlServiceSpy, times(1)).setEarcEnabledInHal(true, false);
verify(mHdmiControlServiceSpy, times(0)).setEarcEnabledInHal(eq(false), anyBoolean());
@@ -1216,6 +1218,7 @@
mTestLooper.dispatchAll();
Mockito.clearInvocations(mHdmiControlServiceSpy);
mHdmiControlServiceSpy.initService();
+ mHdmiControlServiceSpy.enableAllFeatureFlags();
mTestLooper.dispatchAll();
verify(mHdmiControlServiceSpy, times(1)).setEarcEnabledInHal(false, false);
verify(mHdmiControlServiceSpy, times(0)).setEarcEnabledInHal(eq(true), anyBoolean());
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiEarcLocalDeviceTxTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiEarcLocalDeviceTxTest.java
index bf44e09..ab95c1a 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiEarcLocalDeviceTxTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiEarcLocalDeviceTxTest.java
@@ -127,6 +127,7 @@
mHdmiControlService.setCecController(mHdmiCecController);
mHdmiControlService.setHdmiMhlController(HdmiMhlControllerStub.create(mHdmiControlService));
mHdmiControlService.initService();
+ mHdmiControlService.enableAllFeatureFlags();
mHdmiControlService.onBootPhase(PHASE_SYSTEM_SERVICES_READY);
mPowerManager = new FakePowerManagerWrapper(context);
mHdmiControlService.setPowerManager(mPowerManager);
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/PowerStatusMonitorActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/PowerStatusMonitorActionTest.java
index f72ac71..9d0faba 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/PowerStatusMonitorActionTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/PowerStatusMonitorActionTest.java
@@ -106,6 +106,7 @@
new HdmiPortInfo(2, HdmiPortInfo.PORT_INPUT, 0x2000, true, false, false);
mNativeWrapper.setPortInfo(hdmiPortInfo);
mHdmiControlService.initService();
+ mHdmiControlService.enableAllFeatureFlags();
mHdmiControlService.onBootPhase(PHASE_SYSTEM_SERVICES_READY);
mPowerManager = new FakePowerManagerWrapper(mContextSpy);
mHdmiControlService.setPowerManager(mPowerManager);
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/RequestSadActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/RequestSadActionTest.java
index f719ca1..a2bf3c0 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/RequestSadActionTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/RequestSadActionTest.java
@@ -122,6 +122,7 @@
mHdmiControlService.setCecController(mHdmiCecController);
mHdmiControlService.setHdmiMhlController(HdmiMhlControllerStub.create(mHdmiControlService));
mHdmiControlService.initService();
+ mHdmiControlService.enableAllFeatureFlags();
mHdmiControlService.onBootPhase(PHASE_SYSTEM_SERVICES_READY);
mPowerManager = new FakePowerManagerWrapper(context);
mHdmiControlService.setPowerManager(mPowerManager);
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/RoutingControlActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/RoutingControlActionTest.java
index be62df8..f78bddc 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/RoutingControlActionTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/RoutingControlActionTest.java
@@ -183,6 +183,7 @@
true, false, false);
mNativeWrapper.setPortInfo(hdmiPortInfos);
mHdmiControlService.initService();
+ mHdmiControlService.enableAllFeatureFlags();
mHdmiControlService.onBootPhase(PHASE_SYSTEM_SERVICES_READY);
mPowerManager = new FakePowerManagerWrapper(context);
mHdmiControlService.setPowerManager(mPowerManager);
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/SetAudioVolumeLevelDiscoveryActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/SetAudioVolumeLevelDiscoveryActionTest.java
index e3c8939..26f1517 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/SetAudioVolumeLevelDiscoveryActionTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/SetAudioVolumeLevelDiscoveryActionTest.java
@@ -99,6 +99,7 @@
mHdmiControlServiceSpy.setHdmiMhlController(
HdmiMhlControllerStub.create(mHdmiControlServiceSpy));
mHdmiControlServiceSpy.initService();
+ mHdmiControlServiceSpy.enableAllFeatureFlags();
mHdmiControlServiceSpy.onBootPhase(PHASE_SYSTEM_SERVICES_READY);
mPowerManager = new FakePowerManagerWrapper(mContextSpy);
mHdmiControlServiceSpy.setPowerManager(mPowerManager);
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/SystemAudioAutoInitiationActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/SystemAudioAutoInitiationActionTest.java
index e7557fe..c1c43ae 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/SystemAudioAutoInitiationActionTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/SystemAudioAutoInitiationActionTest.java
@@ -106,6 +106,7 @@
new HdmiPortInfo(2, HdmiPortInfo.PORT_INPUT, 0x2000, true, false, true);
mNativeWrapper.setPortInfo(hdmiPortInfos);
mHdmiControlService.initService();
+ mHdmiControlService.enableAllFeatureFlags();
mHdmiControlService.onBootPhase(PHASE_SYSTEM_SERVICES_READY);
mPowerManager = new FakePowerManagerWrapper(mContextSpy);
mHdmiControlService.setPowerManager(mPowerManager);
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/SystemAudioInitiationActionFromAvrTest.java b/services/tests/servicestests/src/com/android/server/hdmi/SystemAudioInitiationActionFromAvrTest.java
index c2f706a..a183d36 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/SystemAudioInitiationActionFromAvrTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/SystemAudioInitiationActionFromAvrTest.java
@@ -173,6 +173,7 @@
hdmiControlService, nativeWrapper, hdmiControlService.getAtomWriter());
hdmiControlService.setCecController(hdmiCecController);
hdmiControlService.initService();
+ hdmiControlService.enableAllFeatureFlags();
mPowerManager = new FakePowerManagerWrapper(context);
hdmiControlService.setPowerManager(mPowerManager);
mHdmiCecLocalDeviceAudioSystem =
diff --git a/tests/Internal/src/com/android/internal/app/AppLocaleCollectorTest.java b/tests/Internal/src/com/android/internal/app/AppLocaleCollectorTest.java
new file mode 100644
index 0000000..b24ac3c
--- /dev/null
+++ b/tests/Internal/src/com/android/internal/app/AppLocaleCollectorTest.java
@@ -0,0 +1,170 @@
+/*
+ * Copyright (C) 2022 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 com.android.internal.app;
+
+import static com.android.internal.app.AppLocaleStore.AppLocaleResult.LocaleStatus.GET_SUPPORTED_LANGUAGE_FROM_LOCAL_CONFIG;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.anyObject;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.spy;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.internal.app.LocaleStore.LocaleInfo;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+
+/**
+ * Unit tests for the {@link AppLocaleCollector}.
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class AppLocaleCollectorTest {
+ private static final String TAG = "AppLocaleCollectorTest";
+ private AppLocaleCollector mAppLocaleCollector;
+ private LocaleStore.LocaleInfo mAppCurrentLocale;
+ private Set<LocaleInfo> mAllAppActiveLocales;
+ private Set<LocaleInfo> mImeLocales;
+ private List<LocaleInfo> mSystemCurrentLocales;
+ private Set<LocaleInfo> mSystemSupportedLocales;
+ private AppLocaleStore.AppLocaleResult mResult;
+ private static final String PKG1 = "pkg1";
+ private static final int NONE = LocaleInfo.SUGGESTION_TYPE_NONE;
+ private static final int SIM = LocaleInfo.SUGGESTION_TYPE_SIM;
+ private static final int CFG = LocaleInfo.SUGGESTION_TYPE_CFG;
+ private static final int SIM_CFG = SIM | CFG;
+ private static final int CURRENT = LocaleInfo.SUGGESTION_TYPE_CURRENT;
+ private static final int SYSTEM = LocaleInfo.SUGGESTION_TYPE_SYSTEM_LANGUAGE;
+ private static final int OTHERAPP = LocaleInfo.SUGGESTION_TYPE_OTHER_APP_LANGUAGE;
+
+ @Before
+ public void setUp() throws Exception {
+ mAppLocaleCollector = spy(
+ new AppLocaleCollector(InstrumentationRegistry.getContext(), PKG1));
+
+ mAppCurrentLocale = createLocaleInfo("en-US", CURRENT);
+ mAllAppActiveLocales = initAllAppActivatedLocales();
+ mImeLocales = initImeLocales();
+ mSystemSupportedLocales = initSystemSupportedLocales();
+ mSystemCurrentLocales = initSystemCurrentLocales();
+ mResult = new AppLocaleStore.AppLocaleResult(GET_SUPPORTED_LANGUAGE_FROM_LOCAL_CONFIG,
+ initAppSupportedLocale());
+ }
+
+ @Test
+ public void testGetSupportedLocaleList() {
+ doReturn(mAppCurrentLocale).when(mAppLocaleCollector).getAppCurrentLocale();
+ doReturn(mResult).when(mAppLocaleCollector).getAppSupportedLocales();
+ doReturn(mAllAppActiveLocales).when(mAppLocaleCollector).getAllAppActiveLocales();
+ doReturn(mImeLocales).when(mAppLocaleCollector).getActiveImeLocales();
+ doReturn(mSystemSupportedLocales).when(mAppLocaleCollector).getSystemSupportedLocale(
+ anyObject(), eq(null), eq(true));
+ doReturn(mSystemCurrentLocales).when(mAppLocaleCollector).getSystemCurrentLocale();
+
+ Set<LocaleInfo> result = mAppLocaleCollector.getSupportedLocaleList(null, true, false);
+
+ HashMap<String, Integer> expectedResult = getExpectedResult();
+ assertEquals(result.size(), expectedResult.size());
+ for (LocaleInfo source : result) {
+ int suggestionFlags = expectedResult.getOrDefault(source.getId(), -1);
+ assertEquals(source.mSuggestionFlags, suggestionFlags);
+ }
+ }
+
+ private HashMap<String, Integer> getExpectedResult() {
+ HashMap<String, Integer> map = new HashMap<>();
+ map.put("en-US", CURRENT); // The locale current App activates.
+ map.put("fr", NONE); // The locale App and system support.
+ map.put("zu", NONE); // The locale App and system support.
+ map.put("en", NONE); // Use en because System supports en while APP supports en-CA, en-GB.
+ map.put("ko", NONE); // The locale App and system support.
+ map.put("en-AU", OTHERAPP); // The locale other App activates and current App supports.
+ map.put("en-CA", OTHERAPP); // The locale other App activates and current App supports.
+ map.put("ja-JP", OTHERAPP); // The locale other App activates and current App supports.
+ map.put("zh-Hant-TW", SIM); // The locale system activates.
+ map.put(createLocaleInfo("", SYSTEM).getId(), SYSTEM); // System language title
+ return map;
+ }
+
+ private Set<LocaleInfo> initSystemSupportedLocales() {
+ return Set.of(
+ createLocaleInfo("en", NONE),
+ createLocaleInfo("fr", NONE),
+ createLocaleInfo("zu", NONE),
+ createLocaleInfo("ko", NONE),
+ // will be filtered because current App doesn't support.
+ createLocaleInfo("es-US", SIM_CFG)
+ );
+ }
+
+ private List<LocaleInfo> initSystemCurrentLocales() {
+ return List.of(createLocaleInfo("zh-Hant-TW", SIM),
+ // will be filtered because current App activates this locale.
+ createLocaleInfo("en-US", SIM));
+ }
+
+ private Set<LocaleInfo> initAllAppActivatedLocales() {
+ return Set.of(
+ createLocaleInfo("en-CA", OTHERAPP),
+ createLocaleInfo("en-AU", OTHERAPP),
+ createLocaleInfo("ja-JP", OTHERAPP),
+ // will be filtered because current App activates this locale.
+ createLocaleInfo("en-US", OTHERAPP));
+ }
+
+ private Set<LocaleInfo> initImeLocales() {
+ return Set.of(
+ // will be filtered because system activates zh-Hant-TW.
+ createLocaleInfo("zh-TW", OTHERAPP),
+ // will be filtered because current App's activats this locale.
+ createLocaleInfo("en-US", OTHERAPP));
+ }
+
+ private HashSet<Locale> initAppSupportedLocale() {
+ HashSet<Locale> hs = new HashSet();
+ hs.add(Locale.forLanguageTag("en-US"));
+ hs.add(Locale.forLanguageTag("en-CA"));
+ hs.add(Locale.forLanguageTag("en-GB"));
+ hs.add(Locale.forLanguageTag("zh-TW"));
+ hs.add(Locale.forLanguageTag("ja"));
+ hs.add(Locale.forLanguageTag("fr"));
+ hs.add(Locale.forLanguageTag("zu"));
+ hs.add(Locale.forLanguageTag("ko"));
+ // will be filtered because it's not in the system language.
+ hs.add(Locale.forLanguageTag("mn"));
+ return hs;
+ }
+
+ private LocaleInfo createLocaleInfo(String languageTag, int suggestionFlag) {
+ LocaleInfo localeInfo = LocaleStore.fromLocale(Locale.forLanguageTag(languageTag));
+ localeInfo.mSuggestionFlags = suggestionFlag;
+ localeInfo.setTranslated(true);
+ return localeInfo;
+ }
+}
diff --git a/tests/Internal/src/com/android/internal/app/LocaleStoreTest.java b/tests/Internal/src/com/android/internal/app/LocaleStoreTest.java
new file mode 100644
index 0000000..bf6ece1
--- /dev/null
+++ b/tests/Internal/src/com/android/internal/app/LocaleStoreTest.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2022 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 com.android.internal.app;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.view.inputmethod.InputMethodSubtype;
+import android.view.inputmethod.InputMethodSubtype.InputMethodSubtypeBuilder;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.internal.app.LocaleStore.LocaleInfo;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Unit tests for the {@link LocaleStore}.
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class LocaleStoreTest {
+ @Before
+ public void setUp() {
+ }
+
+ @Test
+ public void testTransformImeLanguageTagToLocaleInfo() {
+ List<InputMethodSubtype> list = List.of(
+ new InputMethodSubtypeBuilder().setLanguageTag("en-US").build(),
+ new InputMethodSubtypeBuilder().setLanguageTag("zh-TW").build(),
+ new InputMethodSubtypeBuilder().setLanguageTag("ja-JP").build());
+
+ Set<LocaleInfo> localeSet = LocaleStore.transformImeLanguageTagToLocaleInfo(list);
+
+ Set<String> expectedLanguageTag = Set.of("en-US", "zh-TW", "ja-JP");
+ assertEquals(localeSet.size(), expectedLanguageTag.size());
+ for (LocaleInfo info : localeSet) {
+ assertEquals(info.mSuggestionFlags, LocaleInfo.SUGGESTION_TYPE_OTHER_APP_LANGUAGE);
+ assertTrue(expectedLanguageTag.contains(info.getId()));
+ }
+ }
+}