diff options
116 files changed, 3863 insertions, 2165 deletions
diff --git a/Android.bp b/Android.bp index 8afea341be70..933d1aff842b 100644 --- a/Android.bp +++ b/Android.bp @@ -591,7 +591,7 @@ stubs_defaults { libs: [ "art.module.public.api", "sdk_module-lib_current_framework-tethering", - "sdk_module-lib_current_framework-connectivity-tiramisu", + "sdk_module-lib_current_framework-connectivity-t", "sdk_public_current_framework-bluetooth", // There are a few classes from modules used by the core that // need to be resolved by metalava. We use a prebuilt stub of the diff --git a/StubLibraries.bp b/StubLibraries.bp index 726ab2a70a5e..94f4374594ec 100644 --- a/StubLibraries.bp +++ b/StubLibraries.bp @@ -258,7 +258,7 @@ java_library { srcs: [":module-lib-api-stubs-docs-non-updatable"], libs: [ "sdk_module-lib_current_framework-tethering", - "sdk_module-lib_current_framework-connectivity-tiramisu", + "sdk_module-lib_current_framework-connectivity-t", "sdk_public_current_framework-bluetooth", // NOTE: The below can be removed once the prebuilt stub contains bluetooth. "sdk_system_current_android", diff --git a/api/Android.bp b/api/Android.bp index 66c7823ed416..bbe26b73c721 100644 --- a/api/Android.bp +++ b/api/Android.bp @@ -113,7 +113,7 @@ combined_apis { "framework-appsearch", "framework-bluetooth", "framework-connectivity", - "framework-connectivity-tiramisu", + "framework-connectivity-t", "framework-graphics", "framework-media", "framework-mediaprovider", diff --git a/core/api/current.txt b/core/api/current.txt index ddfbb44e44a6..3a6fc590648e 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -14390,6 +14390,8 @@ package android.graphics { method @NonNull @Size(min=3) public abstract float[] fromXyz(@NonNull @Size(min=3) float[]); method @NonNull public static android.graphics.ColorSpace get(@NonNull android.graphics.ColorSpace.Named); method @IntRange(from=1, to=4) public int getComponentCount(); + method public int getDataSpace(); + method @Nullable public static android.graphics.ColorSpace getFromDataSpace(int); method @IntRange(from=android.graphics.ColorSpace.MIN_ID, to=android.graphics.ColorSpace.MAX_ID) public int getId(); method public abstract float getMaxValue(@IntRange(from=0, to=3) int); method public abstract float getMinValue(@IntRange(from=0, to=3) int); @@ -30684,7 +30686,7 @@ package android.os { public class BaseBundle { method public void clear(); method public boolean containsKey(String); - method @Nullable public Object get(String); + method @Deprecated @Nullable public Object get(String); method public boolean getBoolean(String); method public boolean getBoolean(String, boolean); method @Nullable public boolean[] getBooleanArray(@Nullable String); @@ -30926,16 +30928,21 @@ package android.os { method public float getFloat(String, float); method @Nullable public float[] getFloatArray(@Nullable String); method @Nullable public java.util.ArrayList<java.lang.Integer> getIntegerArrayList(@Nullable String); - method @Nullable public <T extends android.os.Parcelable> T getParcelable(@Nullable String); - method @Nullable public android.os.Parcelable[] getParcelableArray(@Nullable String); - method @Nullable public <T extends android.os.Parcelable> java.util.ArrayList<T> getParcelableArrayList(@Nullable String); - method @Nullable public java.io.Serializable getSerializable(@Nullable String); + method @Deprecated @Nullable public <T extends android.os.Parcelable> T getParcelable(@Nullable String); + method @Nullable public <T> T getParcelable(@Nullable String, @NonNull Class<T>); + method @Deprecated @Nullable public android.os.Parcelable[] getParcelableArray(@Nullable String); + method @Nullable public <T> T[] getParcelableArray(@Nullable String, @NonNull Class<T>); + method @Deprecated @Nullable public <T extends android.os.Parcelable> java.util.ArrayList<T> getParcelableArrayList(@Nullable String); + method @Nullable public <T> java.util.ArrayList<T> getParcelableArrayList(@Nullable String, @NonNull Class<T>); + method @Deprecated @Nullable public java.io.Serializable getSerializable(@Nullable String); + method @Nullable public <T extends java.io.Serializable> T getSerializable(@Nullable String, @NonNull Class<T>); method public short getShort(String); method public short getShort(String, short); method @Nullable public short[] getShortArray(@Nullable String); method @Nullable public android.util.Size getSize(@Nullable String); method @Nullable public android.util.SizeF getSizeF(@Nullable String); - method @Nullable public <T extends android.os.Parcelable> android.util.SparseArray<T> getSparseParcelableArray(@Nullable String); + method @Deprecated @Nullable public <T extends android.os.Parcelable> android.util.SparseArray<T> getSparseParcelableArray(@Nullable String); + method @Nullable public <T> android.util.SparseArray<T> getSparseParcelableArray(@Nullable String, @NonNull Class<T>); method @Nullable public java.util.ArrayList<java.lang.String> getStringArrayList(@Nullable String); method public boolean hasFileDescriptors(); method public void putAll(android.os.Bundle); @@ -49285,7 +49292,7 @@ package android.view { method @NonNull public android.view.SurfaceControl.Transaction setDataSpace(@NonNull android.view.SurfaceControl, int); method @NonNull public android.view.SurfaceControl.Transaction setFrameRate(@NonNull android.view.SurfaceControl, @FloatRange(from=0.0) float, int); method @NonNull public android.view.SurfaceControl.Transaction setFrameRate(@NonNull android.view.SurfaceControl, @FloatRange(from=0.0) float, int, int); - method @NonNull public android.view.SurfaceControl.Transaction setGeometry(@NonNull android.view.SurfaceControl, @Nullable android.graphics.Rect, @Nullable android.graphics.Rect, int); + method @Deprecated @NonNull public android.view.SurfaceControl.Transaction setGeometry(@NonNull android.view.SurfaceControl, @Nullable android.graphics.Rect, @Nullable android.graphics.Rect, int); method @NonNull public android.view.SurfaceControl.Transaction setLayer(@NonNull android.view.SurfaceControl, @IntRange(from=java.lang.Integer.MIN_VALUE, to=java.lang.Integer.MAX_VALUE) int); method @NonNull public android.view.SurfaceControl.Transaction setOpaque(@NonNull android.view.SurfaceControl, boolean); method @NonNull public android.view.SurfaceControl.Transaction setPosition(@NonNull android.view.SurfaceControl, float, float); diff --git a/core/java/android/hardware/input/InputManager.java b/core/java/android/hardware/input/InputManager.java index 2fd79cf980c7..c38a847dfb9f 100644 --- a/core/java/android/hardware/input/InputManager.java +++ b/core/java/android/hardware/input/InputManager.java @@ -1079,16 +1079,24 @@ public final class InputManager { } /** - * Gets the key code produced by the specified location on a US keyboard layout. - * Key code as defined in {@link android.view.KeyEvent}. - * This API is only functional for devices with {@link InputDevice#SOURCE_KEYBOARD} available - * which can alter their key mapping using country specific keyboard layouts. - * - * @param deviceId The input device id. - * @param locationKeyCode The location of a key on a US keyboard layout. - * @return The key code produced when pressing the key at the specified location, given the - * active keyboard layout. Returns {@link KeyEvent#KEYCODE_UNKNOWN} if the requested - * mapping could not be determined, or if an error occurred. + * Gets the {@link android.view.KeyEvent key code} produced by the given location on a reference + * QWERTY keyboard layout. + * <p> + * This API is useful for querying the physical location of keys that change the character + * produced based on the current locale and keyboard layout. + * <p> + * @see InputDevice#getKeyCodeForKeyLocation(int) for examples. + * + * @param locationKeyCode The location of a key specified as a key code on the QWERTY layout. + * This provides a consistent way of referring to the physical location of a key independently + * of the current keyboard layout. Also see the + * <a href="https://www.w3.org/TR/2017/CR-uievents-code-20170601/#key-alphanumeric-writing-system"> + * hypothetical keyboard</a> provided by the W3C, which may be helpful for identifying the + * physical location of a key. + * @return The key code produced by the key at the specified location, given the current + * keyboard layout. Returns {@link KeyEvent#KEYCODE_UNKNOWN} if the device does not specify + * {@link InputDevice#SOURCE_KEYBOARD} or the requested mapping cannot be determined. + * * @hide */ public int getKeyCodeForKeyLocation(int deviceId, int locationKeyCode) { diff --git a/core/java/android/os/BadTypeParcelableException.java b/core/java/android/os/BadTypeParcelableException.java new file mode 100644 index 000000000000..2ca3bd2adca1 --- /dev/null +++ b/core/java/android/os/BadTypeParcelableException.java @@ -0,0 +1,30 @@ +/* + * 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.os; + +/** Used by Parcel to signal that the type on the payload was not expected by the caller. */ +class BadTypeParcelableException extends BadParcelableException { + BadTypeParcelableException(String msg) { + super(msg); + } + BadTypeParcelableException(Exception cause) { + super(cause); + } + BadTypeParcelableException(String msg, Throwable cause) { + super(msg, cause); + } +} diff --git a/core/java/android/os/BaseBundle.java b/core/java/android/os/BaseBundle.java index 244335d3aa06..45812e551618 100644 --- a/core/java/android/os/BaseBundle.java +++ b/core/java/android/os/BaseBundle.java @@ -16,6 +16,8 @@ package android.os; +import static java.util.Objects.requireNonNull; + import android.annotation.NonNull; import android.annotation.Nullable; import android.compat.annotation.UnsupportedAppUsage; @@ -31,7 +33,7 @@ import com.android.internal.util.IndentingPrintWriter; import java.io.Serializable; import java.util.ArrayList; import java.util.Set; -import java.util.function.Function; +import java.util.function.BiFunction; /** * A mapping from String keys to values of various types. In most cases, you @@ -254,8 +256,8 @@ public class BaseBundle { } try { return getValueAt(0, String.class); - } catch (ClassCastException | BadParcelableException e) { - typeWarning("getPairValue()", /* value */ null, "String", e); + } catch (ClassCastException | BadTypeParcelableException e) { + typeWarning("getPairValue()", "String", e); return null; } } @@ -320,28 +322,46 @@ public class BaseBundle { * This call should always be made after {@link #unparcel()} or inside a lock after making sure * {@code mMap} is not null. * + * @deprecated Use {@link #getValue(String, Class, Class[])}. This method should only be used in + * other deprecated APIs. + * * @hide */ + @Deprecated + @Nullable final Object getValue(String key) { return getValue(key, /* clazz */ null); } + /** Same as {@link #getValue(String, Class, Class[])} with no item types. */ + @Nullable + final <T> T getValue(String key, @Nullable Class<T> clazz) { + // Avoids allocating Class[0] array + return getValue(key, clazz, (Class<?>[]) null); + } + /** - * Returns the value for key {@code key} for expected return type {@param clazz} (or {@code + * Returns the value for key {@code key} for expected return type {@code clazz} (or pass {@code * null} for no type check). * + * For {@code itemTypes}, see {@link Parcel#readValue(int, ClassLoader, Class, Class[])}. + * * This call should always be made after {@link #unparcel()} or inside a lock after making sure * {@code mMap} is not null. * * @hide */ - final <T> T getValue(String key, @Nullable Class<T> clazz) { + @Nullable + final <T> T getValue(String key, @Nullable Class<T> clazz, @Nullable Class<?>... itemTypes) { int i = mMap.indexOfKey(key); - return (i >= 0) ? getValueAt(i, clazz) : null; + return (i >= 0) ? getValueAt(i, clazz, itemTypes) : null; } /** - * Returns the value for a certain position in the array map. + * Returns the value for a certain position in the array map for expected return type {@code + * clazz} (or pass {@code null} for no type check). + * + * For {@code itemTypes}, see {@link Parcel#readValue(int, ClassLoader, Class, Class[])}. * * This call should always be made after {@link #unparcel()} or inside a lock after making sure * {@code mMap} is not null. @@ -349,11 +369,12 @@ public class BaseBundle { * @hide */ @SuppressWarnings("unchecked") - final <T> T getValueAt(int i, @Nullable Class<T> clazz) { + @Nullable + final <T> T getValueAt(int i, @Nullable Class<T> clazz, @Nullable Class<?>... itemTypes) { Object object = mMap.valueAt(i); - if (object instanceof Function<?, ?>) { + if (object instanceof BiFunction<?, ?, ?>) { try { - object = ((Function<Class<?>, ?>) object).apply(clazz); + object = ((BiFunction<Class<?>, Class<?>[], ?>) object).apply(clazz, itemTypes); } catch (BadParcelableException e) { if (sShouldDefuse) { Log.w(TAG, "Failed to parse item " + mMap.keyAt(i) + ", returning null.", e); @@ -615,7 +636,11 @@ public class BaseBundle { * * @param key a String key * @return an Object, or null + * + * @deprecated Use the type-safe specific APIs depending on the type of the item to be + * retrieved, eg. {@link #getString(String)}. */ + @Deprecated @Nullable public Object get(String key) { unparcel(); @@ -623,6 +648,32 @@ public class BaseBundle { } /** + * Returns the object of type {@code clazz} for the given {@code key}, or {@code null} if: + * <ul> + * <li>No mapping of the desired type exists for the given key. + * <li>A {@code null} value is explicitly associated with the key. + * <li>The object is not of type {@code clazz}. + * </ul> + * + * <p>Use the more specific APIs where possible, especially in the case of containers such as + * lists, since those APIs allow you to specify the type of the items. + * + * @param key String key + * @param clazz The type of the object expected + * @return an Object, or null + */ + @Nullable + <T> T get(@Nullable String key, @NonNull Class<T> clazz) { + unparcel(); + try { + return getValue(key, requireNonNull(clazz)); + } catch (ClassCastException | BadTypeParcelableException e) { + typeWarning(key, clazz.getCanonicalName(), e); + return null; + } + } + + /** * Removes any entry with the given key from the mapping of this Bundle. * * @param key a String key @@ -1006,7 +1057,7 @@ public class BaseBundle { sb.append(" but value was a "); sb.append(value.getClass().getName()); } else { - sb.append(" but value was of a different type "); + sb.append(" but value was of a different type"); } sb.append(". The default value "); sb.append(defaultValue); @@ -1019,6 +1070,10 @@ public class BaseBundle { typeWarning(key, value, className, "<null>", e); } + void typeWarning(String key, String className, RuntimeException e) { + typeWarning(key, /* value */ null, className, "<null>", e); + } + /** * Returns the value associated with the given key, or defaultValue if * no mapping of the desired type exists for the given key. @@ -1358,7 +1413,11 @@ public class BaseBundle { * * @param key a String, or null * @return a Serializable value, or null + * + * @deprecated Use {@link #getSerializable(String, Class)}. This method should only be used in + * other deprecated APIs. */ + @Deprecated @Nullable Serializable getSerializable(@Nullable String key) { unparcel(); @@ -1375,6 +1434,36 @@ public class BaseBundle { } /** + * Returns the value associated with the given key, or {@code null} if: + * <ul> + * <li>No mapping of the desired type exists for the given key. + * <li>A {@code null} value is explicitly associated with the key. + * <li>The object is not of type {@code clazz}. + * </ul> + * + * @param key a String, or null + * @param clazz The expected class of the returned type + * @return a Serializable value, or null + */ + @Nullable + <T extends Serializable> T getSerializable(@Nullable String key, @NonNull Class<T> clazz) { + return get(key, clazz); + } + + + @SuppressWarnings("unchecked") + @Nullable + <T> ArrayList<T> getArrayList(@Nullable String key, @NonNull Class<T> clazz) { + unparcel(); + try { + return getValue(key, ArrayList.class, requireNonNull(clazz)); + } catch (ClassCastException | BadTypeParcelableException e) { + typeWarning(key, "ArrayList<" + clazz.getCanonicalName() + ">", e); + return null; + } + } + + /** * Returns the value associated with the given key, or null if * no mapping of the desired type exists for the given key or a null * value is explicitly associated with the key. @@ -1384,17 +1473,7 @@ public class BaseBundle { */ @Nullable ArrayList<Integer> getIntegerArrayList(@Nullable String key) { - unparcel(); - Object o = getValue(key); - if (o == null) { - return null; - } - try { - return (ArrayList<Integer>) o; - } catch (ClassCastException e) { - typeWarning(key, o, "ArrayList<Integer>", e); - return null; - } + return getArrayList(key, Integer.class); } /** @@ -1407,17 +1486,7 @@ public class BaseBundle { */ @Nullable ArrayList<String> getStringArrayList(@Nullable String key) { - unparcel(); - Object o = getValue(key); - if (o == null) { - return null; - } - try { - return (ArrayList<String>) o; - } catch (ClassCastException e) { - typeWarning(key, o, "ArrayList<String>", e); - return null; - } + return getArrayList(key, String.class); } /** @@ -1430,17 +1499,7 @@ public class BaseBundle { */ @Nullable ArrayList<CharSequence> getCharSequenceArrayList(@Nullable String key) { - unparcel(); - Object o = getValue(key); - if (o == null) { - return null; - } - try { - return (ArrayList<CharSequence>) o; - } catch (ClassCastException e) { - typeWarning(key, o, "ArrayList<CharSequence>", e); - return null; - } + return getArrayList(key, CharSequence.class); } /** diff --git a/core/java/android/os/Bundle.java b/core/java/android/os/Bundle.java index 2b13f20ffe6d..edbbb59ac77d 100644 --- a/core/java/android/os/Bundle.java +++ b/core/java/android/os/Bundle.java @@ -20,6 +20,7 @@ import static java.util.Objects.requireNonNull; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.SuppressLint; import android.compat.annotation.UnsupportedAppUsage; import android.util.ArrayMap; import android.util.Size; @@ -876,7 +877,7 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { @Nullable public Bundle getBundle(@Nullable String key) { unparcel(); - Object o = getValue(key); + Object o = mMap.get(key); if (o == null) { return null; } @@ -899,7 +900,11 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { * * @param key a String, or {@code null} * @return a Parcelable value, or {@code null} + * + * @deprecated Use the type-safer {@link #getParcelable(String, Class)} starting from Android + * {@link Build.VERSION_CODES#TIRAMISU}. */ + @Deprecated @Nullable public <T extends Parcelable> T getParcelable(@Nullable String key) { unparcel(); @@ -916,30 +921,28 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { } /** - * Returns the value associated with the given key, or {@code null} if - * no mapping of the desired type exists for the given key or a {@code null} - * value is explicitly associated with the key. + * Returns the value associated with the given key or {@code null} if: + * <ul> + * <li>No mapping of the desired type exists for the given key. + * <li>A {@code null} value is explicitly associated with the key. + * <li>The object is not of type {@code clazz}. + * </ul> * * <p><b>Note: </b> if the expected value is not a class provided by the Android platform, * you must call {@link #setClassLoader(ClassLoader)} with the proper {@link ClassLoader} first. * Otherwise, this method might throw an exception or return {@code null}. * * @param key a String, or {@code null} - * @param clazz The type of the object expected or {@code null} for performing no checks. + * @param clazz The type of the object expected * @return a Parcelable value, or {@code null} - * - * @hide */ @SuppressWarnings("unchecked") @Nullable public <T> T getParcelable(@Nullable String key, @NonNull Class<T> clazz) { - unparcel(); - try { - return getValue(key, requireNonNull(clazz)); - } catch (ClassCastException | BadParcelableException e) { - typeWarning(key, /* value */ null, "Parcelable", e); - return null; - } + // The reason for not using <T extends Parcelable> is because the caller could provide a + // super class to restrict the children that doesn't implement Parcelable itself while the + // children do, more details at b/210800751 (same reasoning applies here). + return get(key, clazz); } /** @@ -953,7 +956,11 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { * * @param key a String, or {@code null} * @return a Parcelable[] value, or {@code null} + * + * @deprecated Use the type-safer {@link #getParcelableArray(String, Class)} starting from + * Android {@link Build.VERSION_CODES#TIRAMISU}. */ + @Deprecated @Nullable public Parcelable[] getParcelableArray(@Nullable String key) { unparcel(); @@ -970,6 +977,39 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { } /** + * Returns the value associated with the given key, or {@code null} if: + * <ul> + * <li>No mapping of the desired type exists for the given key. + * <li>A {@code null} value is explicitly associated with the key. + * <li>The object is not of type {@code clazz}. + * </ul> + * + * <p><b>Note: </b> if the expected value is not a class provided by the Android platform, + * you must call {@link #setClassLoader(ClassLoader)} with the proper {@link ClassLoader} first. + * Otherwise, this method might throw an exception or return {@code null}. + * + * @param key a String, or {@code null} + * @param clazz The type of the items inside the array + * @return a Parcelable[] value, or {@code null} + */ + @SuppressLint({"ArrayReturn", "NullableCollection"}) + @SuppressWarnings("unchecked") + @Nullable + public <T> T[] getParcelableArray(@Nullable String key, @NonNull Class<T> clazz) { + // The reason for not using <T extends Parcelable> is because the caller could provide a + // super class to restrict the children that doesn't implement Parcelable itself while the + // children do, more details at b/210800751 (same reasoning applies here). + unparcel(); + try { + // In Java 12, we can pass clazz.arrayType() instead of Parcelable[] and later casting. + return (T[]) getValue(key, Parcelable[].class, requireNonNull(clazz)); + } catch (ClassCastException | BadTypeParcelableException e) { + typeWarning(key, clazz.getCanonicalName() + "[]", e); + return null; + } + } + + /** * Returns the value associated with the given key, or {@code null} if * no mapping of the desired type exists for the given key or a {@code null} * value is explicitly associated with the key. @@ -980,7 +1020,11 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { * * @param key a String, or {@code null} * @return an ArrayList<T> value, or {@code null} + * + * @deprecated Use the type-safer {@link #getParcelable(String, Class)} starting from Android + * {@link Build.VERSION_CODES#TIRAMISU}. */ + @Deprecated @Nullable public <T extends Parcelable> ArrayList<T> getParcelableArrayList(@Nullable String key) { unparcel(); @@ -997,14 +1041,43 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { } /** + * Returns the value associated with the given key, or {@code null} if: + * <ul> + * <li>No mapping of the desired type exists for the given key. + * <li>A {@code null} value is explicitly associated with the key. + * <li>The object is not of type {@code clazz}. + * </ul> + * + * <p><b>Note: </b> if the expected value is not a class provided by the Android platform, + * you must call {@link #setClassLoader(ClassLoader)} with the proper {@link ClassLoader} first. + * Otherwise, this method might throw an exception or return {@code null}. + * + * @param key a String, or {@code null} + * @param clazz The type of the items inside the array list + * @return an ArrayList<T> value, or {@code null} + */ + @SuppressLint("NullableCollection") + @SuppressWarnings("unchecked") + @Nullable + public <T> ArrayList<T> getParcelableArrayList(@Nullable String key, @NonNull Class<T> clazz) { + // The reason for not using <T extends Parcelable> is because the caller could provide a + // super class to restrict the children that doesn't implement Parcelable itself while the + // children do, more details at b/210800751 (same reasoning applies here). + return getArrayList(key, clazz); + } + + /** * Returns the value associated with the given key, or null if * no mapping of the desired type exists for the given key or a null * value is explicitly associated with the key. * * @param key a String, or null - * * @return a SparseArray of T values, or null + * + * @deprecated Use the type-safer {@link #getSparseParcelableArray(String, Class)} starting from + * Android {@link Build.VERSION_CODES#TIRAMISU}. */ + @Deprecated @Nullable public <T extends Parcelable> SparseArray<T> getSparseParcelableArray(@Nullable String key) { unparcel(); @@ -1021,13 +1094,44 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { } /** + * Returns the value associated with the given key, or {@code null} if: + * <ul> + * <li>No mapping of the desired type exists for the given key. + * <li>A {@code null} value is explicitly associated with the key. + * <li>The object is not of type {@code clazz}. + * </ul> + * + * @param key a String, or null + * @return a SparseArray of T values, or null + */ + @SuppressWarnings("unchecked") + @Nullable + public <T> SparseArray<T> getSparseParcelableArray(@Nullable String key, + @NonNull Class<T> clazz) { + // The reason for not using <T extends Parcelable> is because the caller could provide a + // super class to restrict the children that doesn't implement Parcelable itself while the + // children do, more details at b/210800751 (same reasoning applies here). + unparcel(); + try { + return (SparseArray<T>) getValue(key, SparseArray.class, requireNonNull(clazz)); + } catch (ClassCastException | BadTypeParcelableException e) { + typeWarning(key, "SparseArray<" + clazz.getCanonicalName() + ">", e); + return null; + } + } + + /** * Returns the value associated with the given key, or null if * no mapping of the desired type exists for the given key or a null * value is explicitly associated with the key. * * @param key a String, or null * @return a Serializable value, or null + * + * @deprecated Use the type-safer {@link #getSerializable(String, Class)} starting from Android + * {@link Build.VERSION_CODES#TIRAMISU}. */ + @Deprecated @Override @Nullable public Serializable getSerializable(@Nullable String key) { @@ -1035,6 +1139,24 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { } /** + * Returns the value associated with the given key, or {@code null} if: + * <ul> + * <li>No mapping of the desired type exists for the given key. + * <li>A {@code null} value is explicitly associated with the key. + * <li>The object is not of type {@code clazz}. + * </ul> + * + * @param key a String, or null + * @param clazz The expected class of the returned type + * @return a Serializable value, or null + */ + @Nullable + public <T extends Serializable> T getSerializable(@Nullable String key, + @NonNull Class<T> clazz) { + return super.getSerializable(key, requireNonNull(clazz)); + } + + /** * Returns the value associated with the given key, or null if * no mapping of the desired type exists for the given key or a null * value is explicitly associated with the key. diff --git a/core/java/android/os/Parcel.java b/core/java/android/os/Parcel.java index ae923530b651..09cfb6e70cba 100644 --- a/core/java/android/os/Parcel.java +++ b/core/java/android/os/Parcel.java @@ -16,6 +16,8 @@ package android.os; +import static com.android.internal.util.Preconditions.checkArgument; + import static java.util.Objects.requireNonNull; import android.annotation.IntDef; @@ -65,6 +67,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.function.BiFunction; import java.util.function.Function; import java.util.function.IntFunction; import java.util.function.Supplier; @@ -287,26 +290,26 @@ public final class Parcel { private static final int VAL_NULL = -1; private static final int VAL_STRING = 0; private static final int VAL_INTEGER = 1; - private static final int VAL_MAP = 2; + private static final int VAL_MAP = 2; // length-prefixed private static final int VAL_BUNDLE = 3; - private static final int VAL_PARCELABLE = 4; + private static final int VAL_PARCELABLE = 4; // length-prefixed private static final int VAL_SHORT = 5; private static final int VAL_LONG = 6; private static final int VAL_FLOAT = 7; private static final int VAL_DOUBLE = 8; private static final int VAL_BOOLEAN = 9; private static final int VAL_CHARSEQUENCE = 10; - private static final int VAL_LIST = 11; - private static final int VAL_SPARSEARRAY = 12; + private static final int VAL_LIST = 11; // length-prefixed + private static final int VAL_SPARSEARRAY = 12; // length-prefixed private static final int VAL_BYTEARRAY = 13; private static final int VAL_STRINGARRAY = 14; private static final int VAL_IBINDER = 15; - private static final int VAL_PARCELABLEARRAY = 16; - private static final int VAL_OBJECTARRAY = 17; + private static final int VAL_PARCELABLEARRAY = 16; // length-prefixed + private static final int VAL_OBJECTARRAY = 17; // length-prefixed private static final int VAL_INTARRAY = 18; private static final int VAL_LONGARRAY = 19; private static final int VAL_BYTE = 20; - private static final int VAL_SERIALIZABLE = 21; + private static final int VAL_SERIALIZABLE = 21; // length-prefixed private static final int VAL_SPARSEBOOLEANARRAY = 22; private static final int VAL_BOOLEANARRAY = 23; private static final int VAL_CHARSEQUENCEARRAY = 24; @@ -3179,8 +3182,7 @@ public final class Parcel { */ @Deprecated public final void readMap(@NonNull Map outVal, @Nullable ClassLoader loader) { - int n = readInt(); - readMapInternal(outVal, n, loader, /* clazzKey */ null, /* clazzValue */ null); + readMapInternal(outVal, loader, /* clazzKey */ null, /* clazzValue */ null); } /** @@ -3195,8 +3197,7 @@ public final class Parcel { @NonNull Class<V> clazzValue) { Objects.requireNonNull(clazzKey); Objects.requireNonNull(clazzValue); - int n = readInt(); - readMapInternal(outVal, n, loader, clazzKey, clazzValue); + readMapInternal(outVal, loader, clazzKey, clazzValue); } /** @@ -3245,13 +3246,7 @@ public final class Parcel { @Deprecated @Nullable public HashMap readHashMap(@Nullable ClassLoader loader) { - int n = readInt(); - if (n < 0) { - return null; - } - HashMap m = new HashMap(n); - readMapInternal(m, n, loader, /* clazzKey */ null, /* clazzValue */ null); - return m; + return readHashMapInternal(loader, /* clazzKey */ null, /* clazzValue */ null); } /** @@ -3267,13 +3262,7 @@ public final class Parcel { @NonNull Class<? extends K> clazzKey, @NonNull Class<? extends V> clazzValue) { Objects.requireNonNull(clazzKey); Objects.requireNonNull(clazzValue); - int n = readInt(); - if (n < 0) { - return null; - } - HashMap<K, V> map = new HashMap<>(n); - readMapInternal(map, n, loader, clazzKey, clazzValue); - return map; + return readHashMapInternal(loader, clazzKey, clazzValue); } /** @@ -4296,16 +4285,17 @@ public final class Parcel { /** - * @param clazz The type of the object expected or {@code null} for performing no checks. + * @see #readValue(int, ClassLoader, Class, Class[]) */ @Nullable - private <T> T readValue(@Nullable ClassLoader loader, @Nullable Class<T> clazz) { + private <T> T readValue(@Nullable ClassLoader loader, @Nullable Class<T> clazz, + @Nullable Class<?>... itemTypes) { int type = readInt(); final T object; if (isLengthPrefixed(type)) { int length = readInt(); int start = dataPosition(); - object = readValue(type, loader, clazz); + object = readValue(type, loader, clazz, itemTypes); int actual = dataPosition() - start; if (actual != length) { Slog.wtfStack(TAG, @@ -4313,25 +4303,26 @@ public final class Parcel { + " consumed " + actual + " bytes, but " + length + " expected."); } } else { - object = readValue(type, loader, clazz); + object = readValue(type, loader, clazz, itemTypes); } return object; } /** - * This will return a {@link Function} for length-prefixed types that deserializes the object - * when {@link Function#apply} is called with the expected class of the return object (or {@code - * null} for no type check), for other types it will return the object itself. + * This will return a {@link BiFunction} for length-prefixed types that deserializes the object + * when {@link BiFunction#apply} is called (the arguments correspond to the ones of {@link + * #readValue(int, ClassLoader, Class, Class[])} after the class loader), for other types it + * will return the object itself. * - * <p>After calling {@link Function#apply(Object)} the parcel cursor will not change. Note that - * you shouldn't recycle the parcel, not at least until all objects have been retrieved. No + * <p>After calling {@link BiFunction#apply} the parcel cursor will not change. Note that you + * shouldn't recycle the parcel, not at least until all objects have been retrieved. No * synchronization attempts are made. * * </p>The function returned implements {@link #equals(Object)} and {@link #hashCode()}. Two * function objects are equal if either of the following is true: * <ul> - * <li>{@link Function#apply} has been called on both and both objects returned are equal. - * <li>{@link Function#apply} hasn't been called on either one and everything below is true: + * <li>{@link BiFunction#apply} has been called on both and both objects returned are equal. + * <li>{@link BiFunction#apply} hasn't been called on either one and everything below is true: * <ul> * <li>The {@code loader} parameters used to retrieve each are equal. * <li>They both have the same type. @@ -4358,7 +4349,7 @@ public final class Parcel { } - private static final class LazyValue implements Function<Class<?>, Object> { + private static final class LazyValue implements BiFunction<Class<?>, Class<?>[], Object> { /** * | 4B | 4B | * mSource = Parcel{... | type | length | object | ...} @@ -4390,7 +4381,7 @@ public final class Parcel { } @Override - public Object apply(@Nullable Class<?> clazz) { + public Object apply(@Nullable Class<?> clazz, @Nullable Class<?>[] itemTypes) { Parcel source = mSource; if (source != null) { synchronized (source) { @@ -4399,7 +4390,7 @@ public final class Parcel { int restore = source.dataPosition(); try { source.setDataPosition(mPosition); - mObject = source.readValue(mLoader, clazz); + mObject = source.readValue(mLoader, clazz, itemTypes); } finally { source.setDataPosition(restore); } @@ -4479,14 +4470,25 @@ public final class Parcel { } } + /** Same as {@link #readValue(ClassLoader, Class, Class[])} without any item types. */ + private <T> T readValue(int type, @Nullable ClassLoader loader, @Nullable Class<T> clazz) { + // Avoids allocating Class[0] array + return readValue(type, loader, clazz, (Class<?>[]) null); + } + /** * Reads a value from the parcel of type {@code type}. Does NOT read the int representing the * type first. + * * @param clazz The type of the object expected or {@code null} for performing no checks. + * @param itemTypes If the value is a container, these represent the item types (eg. for a list + * it's the item type, for a map, it's the key type, followed by the value + * type). */ @SuppressWarnings("unchecked") @Nullable - private <T> T readValue(int type, @Nullable ClassLoader loader, @Nullable Class<T> clazz) { + private <T> T readValue(int type, @Nullable ClassLoader loader, @Nullable Class<T> clazz, + @Nullable Class<?>... itemTypes) { final Object object; switch (type) { case VAL_NULL: @@ -4502,7 +4504,11 @@ public final class Parcel { break; case VAL_MAP: - object = readHashMap(loader); + checkTypeToUnparcel(clazz, HashMap.class); + Class<?> keyType = ArrayUtils.getOrNull(itemTypes, 0); + Class<?> valueType = ArrayUtils.getOrNull(itemTypes, 1); + checkArgument((keyType == null) == (valueType == null)); + object = readHashMapInternal(loader, keyType, valueType); break; case VAL_PARCELABLE: @@ -4533,10 +4539,12 @@ public final class Parcel { object = readCharSequence(); break; - case VAL_LIST: - object = readArrayList(loader); + case VAL_LIST: { + checkTypeToUnparcel(clazz, ArrayList.class); + Class<?> itemType = ArrayUtils.getOrNull(itemTypes, 0); + object = readArrayListInternal(loader, itemType); break; - + } case VAL_BOOLEANARRAY: object = createBooleanArray(); break; @@ -4557,10 +4565,12 @@ public final class Parcel { object = readStrongBinder(); break; - case VAL_OBJECTARRAY: - object = readArray(loader); + case VAL_OBJECTARRAY: { + Class<?> itemType = ArrayUtils.getOrNull(itemTypes, 0); + checkArrayTypeToUnparcel(clazz, (itemType != null) ? itemType : Object.class); + object = readArrayInternal(loader, itemType); break; - + } case VAL_INTARRAY: object = createIntArray(); break; @@ -4577,14 +4587,18 @@ public final class Parcel { object = readSerializableInternal(loader, clazz); break; - case VAL_PARCELABLEARRAY: - object = readParcelableArray(loader); + case VAL_PARCELABLEARRAY: { + Class<?> itemType = ArrayUtils.getOrNull(itemTypes, 0); + checkArrayTypeToUnparcel(clazz, (itemType != null) ? itemType : Parcelable.class); + object = readParcelableArrayInternal(loader, itemType); break; - - case VAL_SPARSEARRAY: - object = readSparseArray(loader); + } + case VAL_SPARSEARRAY: { + checkTypeToUnparcel(clazz, SparseArray.class); + Class<?> itemType = ArrayUtils.getOrNull(itemTypes, 0); + object = readSparseArrayInternal(loader, itemType); break; - + } case VAL_SPARSEBOOLEANARRAY: object = readSparseBooleanArray(); break; @@ -4632,7 +4646,7 @@ public final class Parcel { + " at offset " + off); } if (object != null && clazz != null && !clazz.isInstance(object)) { - throw new BadParcelableException("Unparcelled object " + object + throw new BadTypeParcelableException("Unparcelled object " + object + " is not an instance of required class " + clazz.getName() + " provided in the parameter"); } @@ -4659,6 +4673,38 @@ public final class Parcel { } /** + * Checks that an array of type T[], where T is {@code componentTypeToUnparcel}, is a subtype of + * {@code requiredArrayType}. + */ + private void checkArrayTypeToUnparcel(@Nullable Class<?> requiredArrayType, + Class<?> componentTypeToUnparcel) { + if (requiredArrayType != null) { + // In Java 12, we could use componentTypeToUnparcel.arrayType() for the check + Class<?> requiredComponentType = requiredArrayType.getComponentType(); + if (requiredComponentType == null) { + throw new BadTypeParcelableException( + "About to unparcel an array but type " + + requiredArrayType.getCanonicalName() + + " required by caller is not an array."); + } + checkTypeToUnparcel(requiredComponentType, componentTypeToUnparcel); + } + } + + /** + * Checks that {@code typeToUnparcel} is a subtype of {@code requiredType}, if {@code + * requiredType} is not {@code null}. + */ + private void checkTypeToUnparcel(@Nullable Class<?> requiredType, Class<?> typeToUnparcel) { + if (requiredType != null && !requiredType.isAssignableFrom(typeToUnparcel)) { + throw new BadTypeParcelableException( + "About to unparcel a " + typeToUnparcel.getCanonicalName() + + ", which is not a subtype of type " + requiredType.getCanonicalName() + + " required by caller."); + } + } + + /** * Read and return a new Parcelable from the parcel. The given class loader * will be used to load any enclosed Parcelables. If it is null, the default * class loader will be used. @@ -4788,7 +4834,7 @@ public final class Parcel { if (clazz != null) { Class<?> parcelableClass = creator.getClass().getEnclosingClass(); if (!clazz.isAssignableFrom(parcelableClass)) { - throw new BadParcelableException("Parcelable creator " + name + " is not " + throw new BadTypeParcelableException("Parcelable creator " + name + " is not " + "a subclass of required class " + clazz.getName() + " provided in the parameter"); } @@ -4811,7 +4857,7 @@ public final class Parcel { } if (clazz != null) { if (!clazz.isAssignableFrom(parcelableClass)) { - throw new BadParcelableException("Parcelable creator " + name + " is not " + throw new BadTypeParcelableException("Parcelable creator " + name + " is not " + "a subclass of required class " + clazz.getName() + " provided in the parameter"); } @@ -4872,15 +4918,7 @@ public final class Parcel { @Deprecated @Nullable public Parcelable[] readParcelableArray(@Nullable ClassLoader loader) { - int N = readInt(); - if (N < 0) { - return null; - } - Parcelable[] p = new Parcelable[N]; - for (int i = 0; i < N; i++) { - p[i] = readParcelable(loader); - } - return p; + return readParcelableArrayInternal(loader, /* clazz */ null); } /** @@ -4892,14 +4930,20 @@ public final class Parcel { * trying to instantiate an element. */ @SuppressLint({"ArrayReturn", "NullableCollection"}) - @SuppressWarnings("unchecked") @Nullable public <T> T[] readParcelableArray(@Nullable ClassLoader loader, @NonNull Class<T> clazz) { + return readParcelableArrayInternal(loader, requireNonNull(clazz)); + } + + @SuppressWarnings("unchecked") + @Nullable + private <T> T[] readParcelableArrayInternal(@Nullable ClassLoader loader, + @Nullable Class<T> clazz) { int n = readInt(); if (n < 0) { return null; } - T[] p = (T[]) Array.newInstance(clazz, n); + T[] p = (T[]) ((clazz == null) ? new Parcelable[n] : Array.newInstance(clazz, n)); for (int i = 0; i < n; i++) { p[i] = readParcelableInternal(loader, clazz); } @@ -4962,7 +5006,7 @@ public final class Parcel { // the class the same way as ObjectInputStream, using the provided classloader. Class<?> cl = Class.forName(name, false, loader); if (!clazz.isAssignableFrom(cl)) { - throw new BadParcelableException("Serializable object " + throw new BadTypeParcelableException("Serializable object " + cl.getName() + " is not a subclass of required class " + clazz.getName() + " provided in the parameter"); } @@ -4987,7 +5031,7 @@ public final class Parcel { // the deserialized object, as we cannot resolve the class the same way as // ObjectInputStream. if (!clazz.isAssignableFrom(object.getClass())) { - throw new BadParcelableException("Serializable object " + throw new BadTypeParcelableException("Serializable object " + object.getClass().getName() + " is not a subclass of required class " + clazz.getName() + " provided in the parameter"); } @@ -5097,7 +5141,26 @@ public final class Parcel { readMapInternal(outVal, n, loader, /* clazzKey */null, /* clazzValue */null); } - /* package */ <K, V> void readMapInternal(@NonNull Map<? super K, ? super V> outVal, int n, + @Nullable + private <K, V> HashMap<K, V> readHashMapInternal(@Nullable ClassLoader loader, + @NonNull Class<? extends K> clazzKey, @NonNull Class<? extends V> clazzValue) { + int n = readInt(); + if (n < 0) { + return null; + } + HashMap<K, V> map = new HashMap<>(n); + readMapInternal(map, n, loader, clazzKey, clazzValue); + return map; + } + + private <K, V> void readMapInternal(@NonNull Map<? super K, ? super V> outVal, + @Nullable ClassLoader loader, @Nullable Class<K> clazzKey, + @Nullable Class<V> clazzValue) { + int n = readInt(); + readMapInternal(outVal, n, loader, clazzKey, clazzValue); + } + + private <K, V> void readMapInternal(@NonNull Map<? super K, ? super V> outVal, int n, @Nullable ClassLoader loader, @Nullable Class<K> clazzKey, @Nullable Class<V> clazzValue) { while (n > 0) { @@ -5108,7 +5171,7 @@ public final class Parcel { } } - /* package */ void readArrayMapInternal(@NonNull ArrayMap<? super String, Object> outVal, + private void readArrayMapInternal(@NonNull ArrayMap<? super String, Object> outVal, int size, @Nullable ClassLoader loader) { readArrayMap(outVal, size, /* sorted */ true, /* lazy */ false, loader); } diff --git a/core/java/android/view/ImeFocusController.java b/core/java/android/view/ImeFocusController.java index 3fc9f6bcbdeb..b48b5258237f 100644 --- a/core/java/android/view/ImeFocusController.java +++ b/core/java/android/view/ImeFocusController.java @@ -54,13 +54,11 @@ public final class ImeFocusController { @NonNull private InputMethodManagerDelegate getImmDelegate() { - InputMethodManagerDelegate delegate = mDelegate; - if (delegate != null) { - return delegate; + if (mDelegate == null) { + mDelegate = mViewRootImpl.mContext.getSystemService( + InputMethodManager.class).getDelegate(); } - delegate = mViewRootImpl.mContext.getSystemService(InputMethodManager.class).getDelegate(); - mDelegate = delegate; - return delegate; + return mDelegate; } /** Called when the view root is moved to a different display. */ diff --git a/core/java/android/view/InputDevice.java b/core/java/android/view/InputDevice.java index 188d7459f9a7..7d5603994efa 100644 --- a/core/java/android/view/InputDevice.java +++ b/core/java/android/view/InputDevice.java @@ -737,15 +737,47 @@ public final class InputDevice implements Parcelable { } /** - * Gets the key code produced by the specified location on a US keyboard layout. - * Key code as defined in {@link android.view.KeyEvent}. - * This API is only functional for devices with {@link InputDevice#SOURCE_KEYBOARD} available - * which can alter their key mapping using country specific keyboard layouts. - * - * @param locationKeyCode The location of a key on a US keyboard layout. - * @return The key code produced when pressing the key at the specified location, given the - * active keyboard layout. Returns {@link KeyEvent#KEYCODE_UNKNOWN} if the requested - * mapping could not be determined, or if an error occurred. + * Gets the {@link android.view.KeyEvent key code} produced by the given location on a reference + * QWERTY keyboard layout. + * <p> + * This API is useful for querying the physical location of keys that change the character + * produced based on the current locale and keyboard layout. + * <p> + * The following table provides a non-exhaustive list of examples: + * <table border="2" width="85%" align="center" cellpadding="5"> + * <thead> + * <tr><th>Active Keyboard Layout</th> <th>Input Parameter</th> + * <th>Return Value</th></tr> + * </thead> + * + * <tbody> + * <tr> + * <td>French AZERTY</td> + * <td><code>{@link KeyEvent#KEYCODE_Q}</code></td> + * <td><code>{@link KeyEvent#KEYCODE_A}</code></td> + * </tr> + * <tr> + * <td>German QWERTZ</td> + * <td><code>{@link KeyEvent#KEYCODE_Y}</code></td> + * <td><code>{@link KeyEvent#KEYCODE_Z}</code></td> + * </tr> + * <tr> + * <td>US QWERTY</td> + * <td><code>{@link KeyEvent#KEYCODE_B}</code></td> + * <td><code>{@link KeyEvent#KEYCODE_B}</code></td> + * </tr> + * </tbody> + * </table> + * + * @param locationKeyCode The location of a key specified as a key code on the QWERTY layout. + * This provides a consistent way of referring to the physical location of a key independently + * of the current keyboard layout. Also see the + * <a href="https://www.w3.org/TR/2017/CR-uievents-code-20170601/#key-alphanumeric-writing-system"> + * hypothetical keyboard</a> provided by the W3C, which may be helpful for identifying the + * physical location of a key. + * @return The key code produced by the key at the specified location, given the current + * keyboard layout. Returns {@link KeyEvent#KEYCODE_UNKNOWN} if the device does not specify + * {@link InputDevice#SOURCE_KEYBOARD} or the requested mapping cannot be determined. */ public int getKeyCodeForKeyLocation(int locationKeyCode) { return InputManager.getInstance().getKeyCodeForKeyLocation(mId, locationKeyCode); diff --git a/core/java/android/view/SurfaceControl.java b/core/java/android/view/SurfaceControl.java index 98cef95885bd..632af2315bcd 100644 --- a/core/java/android/view/SurfaceControl.java +++ b/core/java/android/view/SurfaceControl.java @@ -3094,6 +3094,10 @@ public final class SurfaceControl implements Parcelable { * @param destFrame The destination rectangle in parent space. Or null for the source frame. * @param orientation The buffer rotation * @return This transaction object. + * @deprecated Use {@link #setCrop(SurfaceControl, Rect)}, + * {@link #setBufferTransform(SurfaceControl, int)}, + * {@link #setPosition(SurfaceControl, float, float)} and + * {@link #setScale(SurfaceControl, float, float)} instead. */ @NonNull public Transaction setGeometry(@NonNull SurfaceControl sc, @Nullable Rect sourceCrop, diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index 286b502791c8..34a13868f4d7 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -14173,7 +14173,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, && isAccessibilityPane()) { // If the pane isn't visible, content changed events are sufficient unless we're // reporting that the view just disappeared - if ((getVisibility() == VISIBLE) + if ((isAggregatedVisible()) || (changeType == AccessibilityEvent.CONTENT_CHANGE_TYPE_PANE_DISAPPEARED)) { final AccessibilityEvent event = AccessibilityEvent.obtain(); onInitializeAccessibilityEvent(event); diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java index 1c2704632e51..8ee3e432cbbb 100644 --- a/core/java/android/view/WindowManager.java +++ b/core/java/android/view/WindowManager.java @@ -3222,7 +3222,8 @@ public interface WindowManager extends ViewManager { /** * The window is allowed to extend into the {@link DisplayCutout} area, only if the - * {@link DisplayCutout} is fully contained within a system bar. Otherwise, the window is + * {@link DisplayCutout} is fully contained within a system bar or the {@link DisplayCutout} + * is not deeper than 16 dp, but this depends on the OEM choice. Otherwise, the window is * laid out such that it does not overlap with the {@link DisplayCutout} area. * * <p> @@ -3237,6 +3238,13 @@ public interface WindowManager extends ViewManager { * The usual precautions for not overlapping with the status and navigation bar are * sufficient for ensuring that no important content overlaps with the DisplayCutout. * + * <p> + * Note: OEMs can have an option to allow the window to always extend into the + * {@link DisplayCutout} area, no matter the cutout flag set, when the {@link DisplayCutout} + * is on the different side from system bars, only if the {@link DisplayCutout} overlaps at + * most 16dp with the windows. + * In such case, OEMs must provide an opt-in/out affordance for users. + * * @see DisplayCutout * @see WindowInsets * @see #layoutInDisplayCutoutMode @@ -3249,8 +3257,16 @@ public interface WindowManager extends ViewManager { * The window is always allowed to extend into the {@link DisplayCutout} areas on the short * edges of the screen. * + * <p> * The window will never extend into a {@link DisplayCutout} area on the long edges of the - * screen. + * screen, unless the {@link DisplayCutout} is not deeper than 16 dp, but this depends on + * the OEM choice. + * + * <p> + * Note: OEMs can have an option to allow the window to extend into the + * {@link DisplayCutout} area on the long edge side, only if the cutout overlaps at most + * 16dp with the windows. In such case, OEMs must provide an opt-in/out affordance for + * users. * * <p> * The window must make sure that no important content overlaps with the diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java index 3dfb4a5a084a..c207af53fab7 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -12774,7 +12774,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener /** * Called when a context menu option for the text view is selected. Currently * this will be one of {@link android.R.id#selectAll}, {@link android.R.id#cut}, - * {@link android.R.id#copy}, {@link android.R.id#paste} or {@link android.R.id#shareText}. + * {@link android.R.id#copy}, {@link android.R.id#paste}, + * {@link android.R.id#pasteAsPlainText} (starting at API level 23) or + * {@link android.R.id#shareText}. * * @return true if the context menu item action was performed. */ @@ -12965,6 +12967,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * method. The default actions can also be removed from the menu using * {@link android.view.Menu#removeItem(int)} and passing {@link android.R.id#selectAll}, * {@link android.R.id#cut}, {@link android.R.id#copy}, {@link android.R.id#paste}, + * {@link android.R.id#pasteAsPlainText} (starting at API level 23), * {@link android.R.id#replaceText} or {@link android.R.id#shareText} ids as parameters. * * <p>Returning false from @@ -13003,7 +13006,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * {@link android.view.ActionMode.Callback#onPrepareActionMode(android.view.ActionMode, * android.view.Menu)} method. The default actions can also be removed from the menu using * {@link android.view.Menu#removeItem(int)} and passing {@link android.R.id#selectAll}, - * {@link android.R.id#paste} or {@link android.R.id#replaceText} ids as parameters.</p> + * {@link android.R.id#paste}, {@link android.R.id#pasteAsPlainText} (starting at API + * level 23) or {@link android.R.id#replaceText} ids as parameters.</p> * * <p>Returning false from * {@link android.view.ActionMode.Callback#onCreateActionMode(android.view.ActionMode, diff --git a/core/java/com/android/internal/app/MediaRouteChooserDialog.java b/core/java/com/android/internal/app/MediaRouteChooserDialog.java index 7108d1443d3a..23d966fdbc0d 100644 --- a/core/java/com/android/internal/app/MediaRouteChooserDialog.java +++ b/core/java/com/android/internal/app/MediaRouteChooserDialog.java @@ -16,25 +16,26 @@ package com.android.internal.app; -import com.android.internal.R; - -import android.app.Dialog; +import android.app.AlertDialog; import android.content.Context; import android.media.MediaRouter; import android.media.MediaRouter.RouteInfo; import android.os.Bundle; import android.text.TextUtils; import android.util.TypedValue; +import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.view.Window; import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.Button; +import android.widget.LinearLayout; import android.widget.ListView; import android.widget.TextView; +import com.android.internal.R; + import java.util.Comparator; /** @@ -48,9 +49,10 @@ import java.util.Comparator; * * TODO: Move this back into the API, as in the support library media router. */ -public class MediaRouteChooserDialog extends Dialog { +public class MediaRouteChooserDialog extends AlertDialog { private final MediaRouter mRouter; private final MediaRouterCallback mCallback; + private final boolean mShowProgressBarWhenEmpty; private int mRouteTypes; private View.OnClickListener mExtendedSettingsClickListener; @@ -60,10 +62,15 @@ public class MediaRouteChooserDialog extends Dialog { private boolean mAttachedToWindow; public MediaRouteChooserDialog(Context context, int theme) { + this(context, theme, true); + } + + public MediaRouteChooserDialog(Context context, int theme, boolean showProgressBarWhenEmpty) { super(context, theme); mRouter = (MediaRouter) context.getSystemService(Context.MEDIA_ROUTER_SERVICE); mCallback = new MediaRouterCallback(); + mShowProgressBarWhenEmpty = showProgressBarWhenEmpty; } /** @@ -120,28 +127,38 @@ public class MediaRouteChooserDialog extends Dialog { @Override protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); + // Note: setView must be called before super.onCreate(). + setView(LayoutInflater.from(getContext()).inflate(R.layout.media_route_chooser_dialog, + null)); - getWindow().requestFeature(Window.FEATURE_LEFT_ICON); - - setContentView(R.layout.media_route_chooser_dialog); setTitle(mRouteTypes == MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY ? R.string.media_route_chooser_title_for_remote_display : R.string.media_route_chooser_title); - // Must be called after setContentView. - getWindow().setFeatureDrawableResource(Window.FEATURE_LEFT_ICON, - isLightTheme(getContext()) ? R.drawable.ic_media_route_off_holo_light - : R.drawable.ic_media_route_off_holo_dark); + setIcon(isLightTheme(getContext()) ? R.drawable.ic_media_route_off_holo_light + : R.drawable.ic_media_route_off_holo_dark); + + super.onCreate(savedInstanceState); + View emptyView = findViewById(android.R.id.empty); mAdapter = new RouteAdapter(getContext()); - mListView = (ListView)findViewById(R.id.media_route_list); + mListView = (ListView) findViewById(R.id.media_route_list); mListView.setAdapter(mAdapter); mListView.setOnItemClickListener(mAdapter); - mListView.setEmptyView(findViewById(android.R.id.empty)); + mListView.setEmptyView(emptyView); - mExtendedSettingsButton = (Button)findViewById(R.id.media_route_extended_settings_button); + mExtendedSettingsButton = (Button) findViewById(R.id.media_route_extended_settings_button); updateExtendedSettingsButton(); + + if (!mShowProgressBarWhenEmpty) { + findViewById(R.id.media_route_progress_bar).setVisibility(View.GONE); + + // Center the empty view when the progress bar is not shown. + LinearLayout.LayoutParams params = + (LinearLayout.LayoutParams) emptyView.getLayoutParams(); + params.gravity = Gravity.CENTER; + emptyView.setLayoutParams(params); + } } private void updateExtendedSettingsButton() { diff --git a/core/java/com/android/internal/app/MediaRouteDialogPresenter.java b/core/java/com/android/internal/app/MediaRouteDialogPresenter.java index bb2d7fab9cc1..5628b7ed9d15 100644 --- a/core/java/com/android/internal/app/MediaRouteDialogPresenter.java +++ b/core/java/com/android/internal/app/MediaRouteDialogPresenter.java @@ -66,24 +66,37 @@ public abstract class MediaRouteDialogPresenter { } } + /** Create a media route dialog as appropriate. */ public static Dialog createDialog(Context context, int routeTypes, View.OnClickListener extendedSettingsClickListener) { - final MediaRouter router = (MediaRouter)context.getSystemService( - Context.MEDIA_ROUTER_SERVICE); - int theme = MediaRouteChooserDialog.isLightTheme(context) ? android.R.style.Theme_DeviceDefault_Light_Dialog : android.R.style.Theme_DeviceDefault_Dialog; + return createDialog(context, routeTypes, extendedSettingsClickListener, theme); + } + + /** Create a media route dialog as appropriate. */ + public static Dialog createDialog(Context context, + int routeTypes, View.OnClickListener extendedSettingsClickListener, int theme) { + return createDialog(context, routeTypes, extendedSettingsClickListener, theme, + true /* showProgressBarWhenEmpty */); + } + + /** Create a media route dialog as appropriate. */ + public static Dialog createDialog(Context context, int routeTypes, + View.OnClickListener extendedSettingsClickListener, int theme, + boolean showProgressBarWhenEmpty) { + final MediaRouter router = context.getSystemService(MediaRouter.class); MediaRouter.RouteInfo route = router.getSelectedRoute(); if (route.isDefault() || !route.matchesTypes(routeTypes)) { - final MediaRouteChooserDialog d = new MediaRouteChooserDialog(context, theme); + final MediaRouteChooserDialog d = new MediaRouteChooserDialog(context, theme, + showProgressBarWhenEmpty); d.setRouteTypes(routeTypes); d.setExtendedSettingsClickListener(extendedSettingsClickListener); return d; } else { - MediaRouteControllerDialog d = new MediaRouteControllerDialog(context, theme); - return d; + return new MediaRouteControllerDialog(context, theme); } } } diff --git a/core/java/com/android/internal/util/ArrayUtils.java b/core/java/com/android/internal/util/ArrayUtils.java index 9bdcddf38c61..1fd04109ae45 100644 --- a/core/java/com/android/internal/util/ArrayUtils.java +++ b/core/java/com/android/internal/util/ArrayUtils.java @@ -888,6 +888,15 @@ public class ArrayUtils { } } + /** + * Returns the {@code i}-th item in {@code items}, if it exists and {@code items} is not {@code + * null}, otherwise returns {@code null}. + */ + @Nullable + public static <T> T getOrNull(@Nullable T[] items, int i) { + return (items != null && items.length > i) ? items[i] : null; + } + public static @Nullable <T> T firstOrNull(T[] items) { return items.length > 0 ? items[0] : null; } diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index d4c03e412fcb..80317b8f5305 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -6912,6 +6912,10 @@ android:permission="android.permission.BIND_JOB_SERVICE"> </service> + <service android:name="com.android.server.appsearch.contactsindexer.ContactsIndexerMaintenanceService" + android:permission="android.permission.BIND_JOB_SERVICE"> + </service> + <service android:name="com.android.server.pm.PackageManagerShellCommandDataLoader" android:exported="false"> <intent-filter> diff --git a/core/res/res/layout/media_route_chooser_dialog.xml b/core/res/res/layout/media_route_chooser_dialog.xml index cd1c74fd7d7d..bf73f4b26c9c 100644 --- a/core/res/res/layout/media_route_chooser_dialog.xml +++ b/core/res/res/layout/media_route_chooser_dialog.xml @@ -28,20 +28,21 @@ <!-- Content to show when list is empty. --> <LinearLayout android:id="@android:id/empty" - android:layout_width="match_parent" - android:layout_height="64dp" - android:orientation="horizontal" - android:paddingLeft="16dp" - android:paddingRight="16dp" - android:visibility="gone"> - <ProgressBar android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_gravity="center" /> + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="horizontal" + android:paddingLeft="16dp" + android:paddingRight="16dp" + android:visibility="gone"> + <ProgressBar android:id="@+id/media_route_progress_bar" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" /> <TextView android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_gravity="center" - android:paddingStart="16dp" - android:text="@string/media_route_chooser_searching" /> + android:layout_height="wrap_content" + android:layout_gravity="center" + android:paddingStart="16dp" + android:text="@string/media_route_chooser_searching" /> </LinearLayout> <!-- Settings button. --> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index e7eeecc2b516..d4513d0c4a3d 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -1676,6 +1676,7 @@ <java-symbol type="id" name="media_route_volume_slider" /> <java-symbol type="id" name="media_route_control_frame" /> <java-symbol type="id" name="media_route_extended_settings_button" /> + <java-symbol type="id" name="media_route_progress_bar" /> <java-symbol type="string" name="media_route_chooser_title" /> <java-symbol type="string" name="media_route_chooser_title_for_remote_display" /> <java-symbol type="string" name="media_route_controller_disconnect" /> diff --git a/graphics/java/android/graphics/ColorSpace.java b/graphics/java/android/graphics/ColorSpace.java index 2f978fc1fc2d..582488ff8de3 100644 --- a/graphics/java/android/graphics/ColorSpace.java +++ b/graphics/java/android/graphics/ColorSpace.java @@ -22,6 +22,10 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.Size; import android.annotation.SuppressAutoDoc; +import android.annotation.SuppressLint; +import android.hardware.DataSpace; +import android.hardware.DataSpace.NamedDataSpace; +import android.util.SparseIntArray; import libcore.util.NativeAllocationRegistry; @@ -207,6 +211,7 @@ public abstract class ColorSpace { // See static initialization block next to #get(Named) private static final ColorSpace[] sNamedColorSpaces = new ColorSpace[Named.values().length]; + private static final SparseIntArray sDataToColorSpaces = new SparseIntArray(); @NonNull private final String mName; @NonNull private final Model mModel; @@ -1389,6 +1394,47 @@ public abstract class ColorSpace { } /** + * Create a {@link ColorSpace} object using a {@link android.hardware.DataSpace DataSpace} + * value. + * + * <p>This function maps from a dataspace to a {@link Named} ColorSpace. + * If no {@link Named} ColorSpace object matching the {@code dataSpace} value can be created, + * {@code null} will return.</p> + * + * @param dataSpace The dataspace value + * @return the ColorSpace object or {@code null} if no matching colorspace can be found. + */ + @SuppressLint("MethodNameUnits") + @Nullable + public static ColorSpace getFromDataSpace(@NamedDataSpace int dataSpace) { + int index = sDataToColorSpaces.get(dataSpace, -1); + if (index != -1) { + return ColorSpace.get(index); + } else { + return null; + } + } + + /** + * Retrieve the {@link android.hardware.DataSpace DataSpace} value from a {@link ColorSpace} + * object. + * + * <p>If this {@link ColorSpace} object has no matching {@code dataSpace} value, + * {@link android.hardware.DataSpace#DATASPACE_UNKNOWN DATASPACE_UNKNOWN} will return.</p> + * + * @return the dataspace value. + */ + @SuppressLint("MethodNameUnits") + public @NamedDataSpace int getDataSpace() { + int index = sDataToColorSpaces.indexOfValue(getId()); + if (index != -1) { + return sDataToColorSpaces.keyAt(index); + } else { + return DataSpace.DATASPACE_UNKNOWN; + } + } + + /** * <p>Returns an instance of {@link ColorSpace} identified by the specified * name. The list of names provided in the {@link Named} enum gives access * to a variety of common RGB color spaces.</p> @@ -1445,6 +1491,7 @@ public abstract class ColorSpace { SRGB_TRANSFER_PARAMETERS, Named.SRGB.ordinal() ); + sDataToColorSpaces.put(DataSpace.DATASPACE_SRGB, Named.SRGB.ordinal()); sNamedColorSpaces[Named.LINEAR_SRGB.ordinal()] = new ColorSpace.Rgb( "sRGB IEC61966-2.1 (Linear)", SRGB_PRIMARIES, @@ -1453,6 +1500,7 @@ public abstract class ColorSpace { 0.0f, 1.0f, Named.LINEAR_SRGB.ordinal() ); + sDataToColorSpaces.put(DataSpace.DATASPACE_SRGB_LINEAR, Named.LINEAR_SRGB.ordinal()); sNamedColorSpaces[Named.EXTENDED_SRGB.ordinal()] = new ColorSpace.Rgb( "scRGB-nl IEC 61966-2-2:2003", SRGB_PRIMARIES, @@ -1464,6 +1512,7 @@ public abstract class ColorSpace { SRGB_TRANSFER_PARAMETERS, Named.EXTENDED_SRGB.ordinal() ); + sDataToColorSpaces.put(DataSpace.DATASPACE_SCRGB, Named.EXTENDED_SRGB.ordinal()); sNamedColorSpaces[Named.LINEAR_EXTENDED_SRGB.ordinal()] = new ColorSpace.Rgb( "scRGB IEC 61966-2-2:2003", SRGB_PRIMARIES, @@ -1472,6 +1521,8 @@ public abstract class ColorSpace { -0.5f, 7.499f, Named.LINEAR_EXTENDED_SRGB.ordinal() ); + sDataToColorSpaces.put( + DataSpace.DATASPACE_SCRGB_LINEAR, Named.LINEAR_EXTENDED_SRGB.ordinal()); sNamedColorSpaces[Named.BT709.ordinal()] = new ColorSpace.Rgb( "Rec. ITU-R BT.709-5", new float[] { 0.640f, 0.330f, 0.300f, 0.600f, 0.150f, 0.060f }, @@ -1480,6 +1531,7 @@ public abstract class ColorSpace { new Rgb.TransferParameters(1 / 1.099, 0.099 / 1.099, 1 / 4.5, 0.081, 1 / 0.45), Named.BT709.ordinal() ); + sDataToColorSpaces.put(DataSpace.DATASPACE_BT709, Named.BT709.ordinal()); sNamedColorSpaces[Named.BT2020.ordinal()] = new ColorSpace.Rgb( "Rec. ITU-R BT.2020-1", new float[] { 0.708f, 0.292f, 0.170f, 0.797f, 0.131f, 0.046f }, @@ -1488,6 +1540,7 @@ public abstract class ColorSpace { new Rgb.TransferParameters(1 / 1.0993, 0.0993 / 1.0993, 1 / 4.5, 0.08145, 1 / 0.45), Named.BT2020.ordinal() ); + sDataToColorSpaces.put(DataSpace.DATASPACE_BT2020, Named.BT2020.ordinal()); sNamedColorSpaces[Named.DCI_P3.ordinal()] = new ColorSpace.Rgb( "SMPTE RP 431-2-2007 DCI (P3)", new float[] { 0.680f, 0.320f, 0.265f, 0.690f, 0.150f, 0.060f }, @@ -1496,6 +1549,7 @@ public abstract class ColorSpace { 0.0f, 1.0f, Named.DCI_P3.ordinal() ); + sDataToColorSpaces.put(DataSpace.DATASPACE_DCI_P3, Named.DCI_P3.ordinal()); sNamedColorSpaces[Named.DISPLAY_P3.ordinal()] = new ColorSpace.Rgb( "Display P3", new float[] { 0.680f, 0.320f, 0.265f, 0.690f, 0.150f, 0.060f }, @@ -1504,6 +1558,7 @@ public abstract class ColorSpace { SRGB_TRANSFER_PARAMETERS, Named.DISPLAY_P3.ordinal() ); + sDataToColorSpaces.put(DataSpace.DATASPACE_DISPLAY_P3, Named.DISPLAY_P3.ordinal()); sNamedColorSpaces[Named.NTSC_1953.ordinal()] = new ColorSpace.Rgb( "NTSC (1953)", NTSC_1953_PRIMARIES, @@ -1528,6 +1583,7 @@ public abstract class ColorSpace { 0.0f, 1.0f, Named.ADOBE_RGB.ordinal() ); + sDataToColorSpaces.put(DataSpace.DATASPACE_ADOBE_RGB, Named.ADOBE_RGB.ordinal()); sNamedColorSpaces[Named.PRO_PHOTO_RGB.ordinal()] = new ColorSpace.Rgb( "ROMM RGB ISO 22028-2:2013", new float[] { 0.7347f, 0.2653f, 0.1596f, 0.8404f, 0.0366f, 0.0001f }, diff --git a/packages/CompanionDeviceManager/Android.bp b/packages/CompanionDeviceManager/Android.bp index 6ded16371e8a..9f5bfd40e7e3 100644 --- a/packages/CompanionDeviceManager/Android.bp +++ b/packages/CompanionDeviceManager/Android.bp @@ -34,6 +34,7 @@ license { android_app { name: "CompanionDeviceManager", defaults: ["platform_app_defaults"], + certificate: "platform", srcs: ["src/**/*.java"], static_libs: [ diff --git a/packages/CompanionDeviceManager/AndroidManifest.xml b/packages/CompanionDeviceManager/AndroidManifest.xml index 06f2d9d0f0c8..8b5d214f7a10 100644 --- a/packages/CompanionDeviceManager/AndroidManifest.xml +++ b/packages/CompanionDeviceManager/AndroidManifest.xml @@ -31,6 +31,7 @@ <uses-permission android:name="android.permission.RADIO_SCAN_WITHOUT_LOCATION"/> <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"/> <uses-permission android:name="android.permission.HIDE_NON_SYSTEM_OVERLAY_WINDOWS"/> + <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS"/> <application android:allowClearUserData="true" diff --git a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceActivity.java b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceActivity.java index b51d3103caec..a6a8fcf9af62 100644 --- a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceActivity.java +++ b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceActivity.java @@ -37,6 +37,7 @@ import android.companion.AssociationRequest; import android.companion.CompanionDeviceManager; import android.companion.IAssociationRequestCallback; import android.content.Intent; +import android.content.pm.PackageManager; import android.net.MacAddress; import android.os.Bundle; import android.os.Handler; @@ -71,6 +72,9 @@ public class CompanionDeviceActivity extends AppCompatActivity { private static final String EXTRA_ASSOCIATION_REQUEST = "association_request"; private static final String EXTRA_RESULT_RECEIVER = "result_receiver"; + // Activity result: Internal Error. + private static final int RESULT_INTERNAL_ERROR = 2; + // AssociationRequestsProcessor -> UI private static final int RESULT_CODE_ASSOCIATION_CREATED = 0; private static final String EXTRA_ASSOCIATION = "association"; @@ -191,6 +195,20 @@ public class CompanionDeviceActivity extends AppCompatActivity { private void initUI() { if (DEBUG) Log.d(TAG, "initUI(), request=" + mRequest); + final String packageName = mRequest.getPackageName(); + final int userId = mRequest.getUserId(); + final CharSequence appLabel; + + try { + appLabel = getApplicationLabel(this, packageName, userId); + } catch (PackageManager.NameNotFoundException e) { + Log.w(TAG, "Package u" + userId + "/" + packageName + " not found."); + + CompanionDeviceDiscoveryService.stop(this); + setResultAndFinish(null, RESULT_INTERNAL_ERROR); + return; + } + setContentView(R.layout.activity_confirmation); mTitle = findViewById(R.id.title); @@ -203,8 +221,6 @@ public class CompanionDeviceActivity extends AppCompatActivity { mButtonAllow.setOnClickListener(this::onPositiveButtonClick); findViewById(R.id.btn_negative).setOnClickListener(this::onNegativeButtonClick); - final CharSequence appLabel = getApplicationLabel(this, mRequest.getPackageName()); - if (mRequest.isSelfManaged()) { initUiForSelfManagedAssociation(appLabel); } else if (mRequest.isSingleDevice()) { @@ -257,7 +273,7 @@ public class CompanionDeviceActivity extends AppCompatActivity { if (DEBUG) Log.i(TAG, "onAssociationCreated(), association=" + association); // Don't need to notify the app, CdmService has already done that. Just finish. - setResultAndFinish(association); + setResultAndFinish(association, RESULT_OK); } private void cancel(boolean discoveryTimeout) { @@ -284,10 +300,10 @@ public class CompanionDeviceActivity extends AppCompatActivity { } // ... then set result and finish ("sending" onActivityResult()). - setResultAndFinish(null); + setResultAndFinish(null, RESULT_CANCELED); } - private void setResultAndFinish(@Nullable AssociationInfo association) { + private void setResultAndFinish(@Nullable AssociationInfo association, int resultCode) { if (DEBUG) Log.i(TAG, "setResultAndFinish(), association=" + association); final Intent data = new Intent(); @@ -297,7 +313,7 @@ public class CompanionDeviceActivity extends AppCompatActivity { data.putExtra(CompanionDeviceManager.EXTRA_DEVICE, mSelectedDevice.getDevice()); } } - setResult(association != null ? RESULT_OK : RESULT_CANCELED, data); + setResult(resultCode, data); finish(); } diff --git a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/Utils.java b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/Utils.java index eab421e48446..e3e563d56e8a 100644 --- a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/Utils.java +++ b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/Utils.java @@ -50,14 +50,13 @@ class Utils { } static @NonNull CharSequence getApplicationLabel( - @NonNull Context context, @NonNull String packageName) { + @NonNull Context context, @NonNull String packageName, int userId) + throws PackageManager.NameNotFoundException { final PackageManager packageManager = context.getPackageManager(); - final ApplicationInfo appInfo; - try { - appInfo = packageManager.getApplicationInfo(packageName, 0); - } catch (PackageManager.NameNotFoundException e) { - throw new RuntimeException(e); - } + + final ApplicationInfo appInfo = packageManager.getApplicationInfoAsUser( + packageName, PackageManager.ApplicationInfoFlags.of(0), userId); + return packageManager.getApplicationLabel(appInfo); } diff --git a/packages/ConnectivityT/framework-t/src/android/net/NetworkStats.java b/packages/ConnectivityT/framework-t/src/android/net/NetworkStats.java index 9175809d9c7c..f681ba1c3853 100644 --- a/packages/ConnectivityT/framework-t/src/android/net/NetworkStats.java +++ b/packages/ConnectivityT/framework-t/src/android/net/NetworkStats.java @@ -28,6 +28,7 @@ import android.os.Parcel; import android.os.Parcelable; import android.os.Process; import android.os.SystemClock; +import android.text.TextUtils; import android.util.SparseBooleanArray; import com.android.internal.annotations.VisibleForTesting; @@ -500,7 +501,7 @@ public final class NetworkStats implements Parcelable, Iterable<NetworkStats.Ent && roaming == e.roaming && defaultNetwork == e.defaultNetwork && rxBytes == e.rxBytes && rxPackets == e.rxPackets && txBytes == e.txBytes && txPackets == e.txPackets - && operations == e.operations && iface.equals(e.iface); + && operations == e.operations && TextUtils.equals(iface, e.iface); } return false; } diff --git a/packages/SettingsLib/src/com/android/settingslib/devicestate/AndroidSecureSettings.java b/packages/SettingsLib/src/com/android/settingslib/devicestate/AndroidSecureSettings.java new file mode 100644 index 000000000000..8aee576c3d04 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/devicestate/AndroidSecureSettings.java @@ -0,0 +1,54 @@ +/* + * 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.settingslib.devicestate; + +import android.content.ContentResolver; +import android.database.ContentObserver; +import android.provider.Settings; + +/** + * Implementation of {@link SecureSettings} that uses Android's {@link Settings.Secure} + * implementation. + */ +class AndroidSecureSettings implements SecureSettings { + + private final ContentResolver mContentResolver; + + AndroidSecureSettings(ContentResolver contentResolver) { + mContentResolver = contentResolver; + } + + @Override + public void putStringForUser(String name, String value, int userHandle) { + Settings.Secure.putStringForUser(mContentResolver, name, value, userHandle); + } + + @Override + public String getStringForUser(String name, int userHandle) { + return Settings.Secure.getStringForUser(mContentResolver, name, userHandle); + } + + @Override + public void registerContentObserver(String name, boolean notifyForDescendants, + ContentObserver observer, int userHandle) { + mContentResolver.registerContentObserver( + Settings.Secure.getUriFor(name), + notifyForDescendants, + observer, + userHandle); + } +} diff --git a/packages/SettingsLib/src/com/android/settingslib/devicestate/DeviceStateRotationLockSettingsManager.java b/packages/SettingsLib/src/com/android/settingslib/devicestate/DeviceStateRotationLockSettingsManager.java index afd3626ab889..961fab32fc2c 100644 --- a/packages/SettingsLib/src/com/android/settingslib/devicestate/DeviceStateRotationLockSettingsManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/devicestate/DeviceStateRotationLockSettingsManager.java @@ -24,6 +24,7 @@ import android.content.ContentResolver; import android.content.Context; import android.database.ContentObserver; import android.os.Handler; +import android.os.Looper; import android.os.UserHandle; import android.provider.Settings; import android.text.TextUtils; @@ -47,28 +48,33 @@ public final class DeviceStateRotationLockSettingsManager { private static DeviceStateRotationLockSettingsManager sSingleton; - private final ContentResolver mContentResolver; private final String[] mDeviceStateRotationLockDefaults; - private final Handler mMainHandler = Handler.getMain(); + private final Handler mMainHandler = new Handler(Looper.getMainLooper()); private final Set<DeviceStateRotationLockSettingsListener> mListeners = new HashSet<>(); + private final SecureSettings mSecureSettings; private SparseIntArray mDeviceStateRotationLockSettings; private SparseIntArray mDeviceStateRotationLockFallbackSettings; + private String mLastSettingValue; - private DeviceStateRotationLockSettingsManager(Context context) { - mContentResolver = context.getContentResolver(); + @VisibleForTesting + DeviceStateRotationLockSettingsManager(Context context, SecureSettings secureSettings) { + this.mSecureSettings = secureSettings; mDeviceStateRotationLockDefaults = context.getResources() .getStringArray(R.array.config_perDeviceStateRotationLockDefaults); loadDefaults(); initializeInMemoryMap(); - listenForSettingsChange(context); + listenForSettingsChange(); } /** Returns a singleton instance of this class */ public static synchronized DeviceStateRotationLockSettingsManager getInstance(Context context) { if (sSingleton == null) { + Context applicationContext = context.getApplicationContext(); + ContentResolver contentResolver = applicationContext.getContentResolver(); + SecureSettings secureSettings = new AndroidSecureSettings(contentResolver); sSingleton = - new DeviceStateRotationLockSettingsManager(context.getApplicationContext()); + new DeviceStateRotationLockSettingsManager(applicationContext, secureSettings); } return sSingleton; } @@ -81,11 +87,11 @@ public final class DeviceStateRotationLockSettingsManager { > 0; } - private void listenForSettingsChange(Context context) { - context.getContentResolver() + private void listenForSettingsChange() { + mSecureSettings .registerContentObserver( - Settings.Secure.getUriFor(Settings.Secure.DEVICE_STATE_ROTATION_LOCK), - /* notifyForDescendents= */ false, //NOTYPO + Settings.Secure.DEVICE_STATE_ROTATION_LOCK, + /* notifyForDescendants= */ false, new ContentObserver(mMainHandler) { @Override public void onChange(boolean selfChange) { @@ -182,8 +188,7 @@ public final class DeviceStateRotationLockSettingsManager { private void initializeInMemoryMap() { String serializedSetting = - Settings.Secure.getStringForUser( - mContentResolver, + mSecureSettings.getStringForUser( Settings.Secure.DEVICE_STATE_ROTATION_LOCK, UserHandle.USER_CURRENT); if (TextUtils.isEmpty(serializedSetting)) { @@ -222,11 +227,7 @@ public final class DeviceStateRotationLockSettingsManager { private void persistSettings() { if (mDeviceStateRotationLockSettings.size() == 0) { - Settings.Secure.putStringForUser( - mContentResolver, - Settings.Secure.DEVICE_STATE_ROTATION_LOCK, - /* value= */ "", - UserHandle.USER_CURRENT); + persistSettingIfChanged(/* newSettingValue= */ ""); return; } @@ -243,10 +244,17 @@ public final class DeviceStateRotationLockSettingsManager { .append(SEPARATOR_REGEX) .append(mDeviceStateRotationLockSettings.valueAt(i)); } - Settings.Secure.putStringForUser( - mContentResolver, + persistSettingIfChanged(stringBuilder.toString()); + } + + private void persistSettingIfChanged(String newSettingValue) { + if (TextUtils.equals(mLastSettingValue, newSettingValue)) { + return; + } + mLastSettingValue = newSettingValue; + mSecureSettings.putStringForUser( Settings.Secure.DEVICE_STATE_ROTATION_LOCK, - stringBuilder.toString(), + /* value= */ newSettingValue, UserHandle.USER_CURRENT); } diff --git a/packages/SettingsLib/src/com/android/settingslib/devicestate/SecureSettings.java b/packages/SettingsLib/src/com/android/settingslib/devicestate/SecureSettings.java new file mode 100644 index 000000000000..10528739b2b0 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/devicestate/SecureSettings.java @@ -0,0 +1,30 @@ +/* + * 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.settingslib.devicestate; + +import android.database.ContentObserver; + +/** Minimal wrapper interface around {@link android.provider.Settings.Secure} for easier testing. */ +interface SecureSettings { + + void putStringForUser(String name, String value, int userHandle); + + String getStringForUser(String name, int userHandle); + + void registerContentObserver(String name, boolean notifyForDescendants, + ContentObserver settingsObserver, int userHandle); +} diff --git a/packages/SettingsLib/src/com/android/settingslib/users/AvatarPickerActivity.java b/packages/SettingsLib/src/com/android/settingslib/users/AvatarPickerActivity.java index 93be66ad4882..1e1dfae9f7ac 100644 --- a/packages/SettingsLib/src/com/android/settingslib/users/AvatarPickerActivity.java +++ b/packages/SettingsLib/src/com/android/settingslib/users/AvatarPickerActivity.java @@ -81,6 +81,7 @@ public class AvatarPickerActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + setTheme(R.style.SudThemeGlifV3_DayNight); ThemeHelper.trySetDynamicColor(this); setContentView(R.layout.avatar_picker); setUpButtons(); diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/devicestate/DeviceStateRotationLockSettingsManagerTest.java b/packages/SettingsLib/tests/integ/src/com/android/settingslib/devicestate/DeviceStateRotationLockSettingsManagerTest.java new file mode 100644 index 000000000000..1a45384bc768 --- /dev/null +++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/devicestate/DeviceStateRotationLockSettingsManagerTest.java @@ -0,0 +1,97 @@ +/* + * 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.settingslib.devicestate; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.content.res.Resources; +import android.database.ContentObserver; +import android.os.UserHandle; +import android.provider.Settings; + +import androidx.test.InstrumentationRegistry; +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class DeviceStateRotationLockSettingsManagerTest { + + @Mock private Context mMockContext; + @Mock private Resources mMockResources; + + private DeviceStateRotationLockSettingsManager mManager; + private int mNumSettingsChanges = 0; + private final ContentObserver mContentObserver = new ContentObserver(null) { + @Override + public void onChange(boolean selfChange) { + mNumSettingsChanges++; + } + }; + private final FakeSecureSettings mFakeSecureSettings = new FakeSecureSettings(); + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + Context context = InstrumentationRegistry.getTargetContext(); + when(mMockContext.getApplicationContext()).thenReturn(mMockContext); + when(mMockContext.getResources()).thenReturn(mMockResources); + when(mMockContext.getContentResolver()).thenReturn(context.getContentResolver()); + mFakeSecureSettings.registerContentObserver( + Settings.Secure.DEVICE_STATE_ROTATION_LOCK, + /* notifyForDescendents= */ false, //NOTYPO + mContentObserver, + UserHandle.USER_CURRENT); + mManager = new DeviceStateRotationLockSettingsManager(context, mFakeSecureSettings); + } + + @Test + public void initialization_settingsAreChangedOnce() { + assertThat(mNumSettingsChanges).isEqualTo(1); + } + + @Test + public void updateSetting_multipleTimes_sameValue_settingsAreChangedOnlyOnce() { + mNumSettingsChanges = 0; + + mManager.updateSetting(/* deviceState= */ 1, /* rotationLocked= */ true); + mManager.updateSetting(/* deviceState= */ 1, /* rotationLocked= */ true); + mManager.updateSetting(/* deviceState= */ 1, /* rotationLocked= */ true); + + assertThat(mNumSettingsChanges).isEqualTo(1); + } + + @Test + public void updateSetting_multipleTimes_differentValues_settingsAreChangedMultipleTimes() { + mNumSettingsChanges = 0; + + mManager.updateSetting(/* deviceState= */ 1, /* rotationLocked= */ true); + mManager.updateSetting(/* deviceState= */ 1, /* rotationLocked= */ false); + mManager.updateSetting(/* deviceState= */ 1, /* rotationLocked= */ true); + + assertThat(mNumSettingsChanges).isEqualTo(3); + } +} diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/devicestate/FakeSecureSettings.java b/packages/SettingsLib/tests/integ/src/com/android/settingslib/devicestate/FakeSecureSettings.java new file mode 100644 index 000000000000..91baa68a1c49 --- /dev/null +++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/devicestate/FakeSecureSettings.java @@ -0,0 +1,60 @@ +/* + * 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.settingslib.devicestate; + +import android.database.ContentObserver; +import android.util.Pair; + +import com.google.common.collect.HashMultimap; +import com.google.common.collect.Multimap; + +import java.util.HashMap; +import java.util.Map; + +/** Fake implementation of {@link SecureSettings} that stores everything in memory. */ +class FakeSecureSettings implements SecureSettings { + + private final Map<SettingsKey, String> mValues = new HashMap<>(); + private final Multimap<SettingsKey, ContentObserver> mContentObservers = HashMultimap.create(); + + @Override + public void putStringForUser(String name, String value, int userHandle) { + SettingsKey settingsKey = new SettingsKey(userHandle, name); + mValues.put(settingsKey, value); + for (ContentObserver observer : mContentObservers.get(settingsKey)) { + observer.onChange(/* selfChange= */ false); + } + } + + @Override + public String getStringForUser(String name, int userHandle) { + return mValues.getOrDefault(new SettingsKey(userHandle, name), ""); + } + + @Override + public void registerContentObserver(String name, boolean notifyForDescendants, + ContentObserver settingsObserver, int userHandle) { + mContentObservers.put(new SettingsKey(userHandle, name), settingsObserver); + } + + private static class SettingsKey extends Pair<Integer, String> { + + SettingsKey(Integer userHandle, String settingName) { + super(userHandle, settingName); + } + } +} diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/devicestate/OWNERS b/packages/SettingsLib/tests/integ/src/com/android/settingslib/devicestate/OWNERS new file mode 100644 index 000000000000..98f41234feb1 --- /dev/null +++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/devicestate/OWNERS @@ -0,0 +1,3 @@ +# Default reviewers for this and subdirectories. +alexflo@google.com +chrisgollner@google.com diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt index c3f6a5d18b74..0da60f0b3d66 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt @@ -298,6 +298,9 @@ class ActivityLaunchAnimator( * * Important: The view must be attached to a [ViewGroup] when calling this function and * during the animation. For safety, this method will return null when it is not. + * + * Note: The background of [view] should be a (rounded) rectangle so that it can be + * properly animated. */ @JvmStatic fun fromView(view: View, cujType: Int? = null): Controller? { diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt index a3c5649e3fec..50178f470fd8 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt @@ -79,6 +79,9 @@ class DialogLaunchAnimator @JvmOverloads constructor( * If [animateBackgroundBoundsChange] is true, then the background of the dialog will be * animated when the dialog bounds change. * + * Note: The background of [view] should be a (rounded) rectangle so that it can be properly + * animated. + * * Caveats: When calling this function and [dialog] is not a fullscreen dialog, then it will be * made fullscreen and 2 views will be inserted between the dialog DecorView and its children. */ @@ -153,6 +156,9 @@ class DialogLaunchAnimator @JvmOverloads constructor( * activity started, when the dialog to app animation is done (or when it is cancelled). If this * method returns null, then the dialog won't be dismissed. * + * Note: The background of [view] should be a (rounded) rectangle so that it can be properly + * animated. + * * @param view any view inside the dialog to animate. */ @JvmOverloads diff --git a/packages/SystemUI/res/drawable/qs_dialog_btn_filled_large.xml b/packages/SystemUI/res/drawable/qs_dialog_btn_filled_large.xml new file mode 100644 index 000000000000..0544b871fa06 --- /dev/null +++ b/packages/SystemUI/res/drawable/qs_dialog_btn_filled_large.xml @@ -0,0 +1,32 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2021 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. + --> +<ripple xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" + android:color="?android:attr/colorControlHighlight"> + <item android:id="@android:id/mask"> + <shape android:shape="rectangle"> + <solid android:color="@android:color/white"/> + <corners android:radius="18dp"/> + </shape> + </item> + <item> + <shape android:shape="rectangle"> + <corners android:radius="18dp"/> + <solid android:color="?androidprv:attr/colorAccentPrimary"/> + </shape> + </item> +</ripple> diff --git a/packages/SystemUI/res/drawable/qs_footer_action_circle.xml b/packages/SystemUI/res/drawable/qs_footer_action_circle.xml index d057f5f32dc4..31a8c3bc2397 100644 --- a/packages/SystemUI/res/drawable/qs_footer_action_circle.xml +++ b/packages/SystemUI/res/drawable/qs_footer_action_circle.xml @@ -19,13 +19,17 @@ <ripple android:color="?android:attr/colorControlHighlight"> <item android:id="@android:id/mask"> - <shape android:shape="oval"> + <!-- We make this shape a rounded rectangle instead of a oval so that it can animate --> + <!-- properly into an app/dialog. --> + <shape android:shape="rectangle"> <solid android:color="@android:color/white"/> + <corners android:radius="@dimen/qs_footer_action_corner_radius"/> </shape> </item> <item> - <shape android:shape="oval"> + <shape android:shape="rectangle"> <solid android:color="?attr/offStateColor"/> + <corners android:radius="@dimen/qs_footer_action_corner_radius"/> </shape> </item> diff --git a/packages/SystemUI/res/drawable/qs_footer_action_circle_color.xml b/packages/SystemUI/res/drawable/qs_footer_action_circle_color.xml index 944061cc8e70..021a85f6a244 100644 --- a/packages/SystemUI/res/drawable/qs_footer_action_circle_color.xml +++ b/packages/SystemUI/res/drawable/qs_footer_action_circle_color.xml @@ -19,13 +19,17 @@ <ripple android:color="?android:attr/colorControlHighlight"> <item android:id="@android:id/mask"> - <shape android:shape="oval"> + <!-- We make this shape a rounded rectangle instead of a oval so that it can animate --> + <!-- properly into an app/dialog. --> + <shape android:shape="rectangle"> <solid android:color="@android:color/white"/> + <corners android:radius="@dimen/qs_footer_action_corner_radius"/> </shape> </item> <item> - <shape android:shape="oval"> + <shape android:shape="rectangle"> <solid android:color="?android:attr/colorAccent"/> + <corners android:radius="@dimen/qs_footer_action_corner_radius"/> </shape> </item> diff --git a/packages/SystemUI/res/layout/alert_dialog_button_bar_systemui.xml b/packages/SystemUI/res/layout/alert_dialog_button_bar_systemui.xml index a3e289a42d05..e06bfdc500da 100644 --- a/packages/SystemUI/res/layout/alert_dialog_button_bar_systemui.xml +++ b/packages/SystemUI/res/layout/alert_dialog_button_bar_systemui.xml @@ -22,10 +22,6 @@ android:scrollbarAlwaysDrawVerticalTrack="true" android:scrollIndicators="top|bottom" android:fillViewport="true" - android:paddingTop="@dimen/dialog_button_bar_top_padding" - android:paddingStart="@dimen/dialog_side_padding" - android:paddingEnd="@dimen/dialog_side_padding" - android:paddingBottom="@dimen/dialog_bottom_padding" style="?android:attr/buttonBarStyle"> <com.android.internal.widget.ButtonBarLayout android:layout_width="match_parent" diff --git a/packages/SystemUI/res/layout/alert_dialog_systemui.xml b/packages/SystemUI/res/layout/alert_dialog_systemui.xml index f280cbd16a0f..ca8fadd9c7da 100644 --- a/packages/SystemUI/res/layout/alert_dialog_systemui.xml +++ b/packages/SystemUI/res/layout/alert_dialog_systemui.xml @@ -83,9 +83,15 @@ android:layout_height="wrap_content" /> </FrameLayout> - <include + <FrameLayout android:layout_width="match_parent" android:layout_height="wrap_content" - layout="@layout/alert_dialog_button_bar_systemui" /> + android:paddingStart="@dimen/dialog_side_padding" + android:paddingEnd="@dimen/dialog_side_padding"> + <include + android:layout_width="match_parent" + android:layout_height="wrap_content" + layout="@layout/alert_dialog_button_bar_systemui" /> + </FrameLayout> </com.android.internal.widget.AlertDialogLayout>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout/dream_overlay_container.xml b/packages/SystemUI/res/layout/dream_overlay_container.xml index 330f515a29fc..8e83b4abe0dc 100644 --- a/packages/SystemUI/res/layout/dream_overlay_container.xml +++ b/packages/SystemUI/res/layout/dream_overlay_container.xml @@ -34,34 +34,5 @@ app:layout_constraintBottom_toBottomOf="parent" /> - <com.android.systemui.dreams.DreamOverlayStatusBarView - android:id="@+id/dream_overlay_status_bar" - android:layout_width="match_parent" - android:layout_height="@dimen/dream_overlay_status_bar_height" - android:paddingEnd="@dimen/dream_overlay_status_bar_margin" - android:paddingStart="@dimen/dream_overlay_status_bar_margin" - app:layout_constraintTop_toTopOf="parent"> - - <androidx.constraintlayout.widget.ConstraintLayout - android:id="@+id/dream_overlay_system_status" - android:layout_width="wrap_content" - android:layout_height="match_parent" - app:layout_constraintEnd_toEndOf="parent"> - - <com.android.systemui.statusbar.AlphaOptimizedImageView - android:id="@+id/dream_overlay_wifi_status" - android:layout_width="@dimen/status_bar_wifi_signal_size" - android:layout_height="match_parent" - android:layout_marginEnd="@dimen/dream_overlay_status_icon_margin" - android:visibility="gone" - app:layout_constraintEnd_toStartOf="@id/dream_overlay_battery" /> - - <com.android.systemui.battery.BatteryMeterView - android:id="@+id/dream_overlay_battery" - android:layout_width="wrap_content" - android:layout_height="match_parent" - app:layout_constraintEnd_toEndOf="parent" /> - - </androidx.constraintlayout.widget.ConstraintLayout> - </com.android.systemui.dreams.DreamOverlayStatusBarView> + <include layout="@layout/dream_overlay_status_bar_view" /> </com.android.systemui.dreams.DreamOverlayContainerView>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout/dream_overlay_status_bar_view.xml b/packages/SystemUI/res/layout/dream_overlay_status_bar_view.xml new file mode 100644 index 000000000000..813787e8f9d0 --- /dev/null +++ b/packages/SystemUI/res/layout/dream_overlay_status_bar_view.xml @@ -0,0 +1,96 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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. + --> +<com.android.systemui.dreams.DreamOverlayStatusBarView + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:id="@+id/dream_overlay_status_bar" + android:layout_width="match_parent" + android:layout_height="@dimen/dream_overlay_status_bar_height" + android:paddingEnd="@dimen/dream_overlay_status_bar_margin" + android:paddingStart="@dimen/dream_overlay_status_bar_margin" + app:layout_constraintTop_toTopOf="parent"> + + <com.android.systemui.dreams.DreamOverlayDotImageView + android:id="@+id/dream_overlay_notification_indicator" + android:layout_width="@dimen/dream_overlay_notification_indicator_size" + android:layout_height="@dimen/dream_overlay_notification_indicator_size" + android:visibility="gone" + android:contentDescription="@string/dream_overlay_status_bar_notification_indicator" + app:dotColor="@android:color/white" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintBottom_toBottomOf="parent" /> + + <LinearLayout + android:id="@+id/dream_overlay_system_status" + android:layout_width="wrap_content" + android:layout_height="match_parent" + android:orientation="horizontal" + app:layout_constraintEnd_toEndOf="parent"> + + <com.android.systemui.statusbar.AlphaOptimizedImageView + android:id="@+id/dream_overlay_assistant_guest_mode_enabled" + android:layout_width="@dimen/dream_overlay_status_bar_icon_size" + android:layout_height="match_parent" + android:layout_marginEnd="@dimen/dream_overlay_status_icon_margin" + android:src="@drawable/ic_account_circle" + android:tint="@android:color/white" + android:visibility="gone" + android:contentDescription= + "@string/dream_overlay_status_bar_assistant_guest_mode_enabled" /> + + <com.android.systemui.statusbar.AlphaOptimizedImageView + android:id="@+id/dream_overlay_alarm_set" + android:layout_width="@dimen/dream_overlay_status_bar_icon_size" + android:layout_height="match_parent" + android:layout_marginEnd="@dimen/dream_overlay_status_icon_margin" + android:src="@drawable/ic_alarm" + android:tint="@android:color/white" + android:visibility="gone" + android:contentDescription="@string/dream_overlay_status_bar_alarm_set" /> + + <com.android.systemui.statusbar.AlphaOptimizedImageView + android:id="@+id/dream_overlay_priority_mode" + android:layout_width="@dimen/dream_overlay_status_bar_icon_size" + android:layout_height="match_parent" + android:layout_marginEnd="@dimen/dream_overlay_status_icon_margin" + android:src="@drawable/ic_remove_circle" + android:tint="@android:color/white" + android:visibility="gone" + android:contentDescription="@string/dream_overlay_status_bar_priority_mode" /> + + <com.android.systemui.statusbar.AlphaOptimizedImageView + android:id="@+id/dream_overlay_wifi_status" + android:layout_width="@dimen/dream_overlay_status_bar_icon_size" + android:layout_height="match_parent" + android:layout_marginEnd="@dimen/dream_overlay_status_icon_margin" + android:src="@drawable/ic_signal_wifi_off" + android:visibility="gone" + android:contentDescription="@string/dream_overlay_status_bar_wifi_off" /> + + <com.android.systemui.dreams.DreamOverlayDotImageView + android:id="@+id/dream_overlay_camera_mic_off" + android:layout_width="@dimen/dream_overlay_camera_mic_off_indicator_size" + android:layout_height="@dimen/dream_overlay_camera_mic_off_indicator_size" + android:layout_gravity="center_vertical" + android:layout_marginEnd="@dimen/dream_overlay_status_icon_margin" + android:visibility="gone" + android:contentDescription="@string/dream_overlay_status_bar_camera_mic_off" + app:dotColor="@color/dream_overlay_camera_mic_off_dot_color" /> + + </LinearLayout> +</com.android.systemui.dreams.DreamOverlayStatusBarView> diff --git a/packages/SystemUI/res/values/attrs.xml b/packages/SystemUI/res/values/attrs.xml index 2992859b1ce7..c5e005c556b2 100644 --- a/packages/SystemUI/res/values/attrs.xml +++ b/packages/SystemUI/res/values/attrs.xml @@ -199,5 +199,9 @@ </declare-styleable> <attr name="overlayButtonTextColor" format="color" /> + + <declare-styleable name="DreamOverlayDotImageView"> + <attr name="dotColor" format="color" /> + </declare-styleable> </resources> diff --git a/packages/SystemUI/res/values/colors.xml b/packages/SystemUI/res/values/colors.xml index f4e7cf3fcf40..dc7470081da2 100644 --- a/packages/SystemUI/res/values/colors.xml +++ b/packages/SystemUI/res/values/colors.xml @@ -225,4 +225,6 @@ <color name="settingslib_track_off_color">@color/settingslib_track_off</color> <color name="connected_network_primary_color">#191C18</color> <color name="connected_network_secondary_color">#41493D</color> + + <color name="dream_overlay_camera_mic_off_dot_color">#FCBE03</color> </resources> diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 370413400766..fcf60bf43c62 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -1332,12 +1332,16 @@ <dimen name="fgs_manager_min_width_minor">100%</dimen> <!-- Dream overlay related dimensions --> - <dimen name="dream_overlay_status_bar_height">80dp</dimen> + <dimen name="dream_overlay_status_bar_height">60dp</dimen> <dimen name="dream_overlay_status_bar_margin">40dp</dimen> <dimen name="dream_overlay_status_icon_margin">8dp</dimen> + <dimen name="dream_overlay_status_bar_icon_size"> + @*android:dimen/status_bar_system_icon_size</dimen> <!-- Height of the area at the top of the dream overlay to allow dragging down the notifications shade. --> <dimen name="dream_overlay_notifications_drag_area_height">100dp</dimen> + <dimen name="dream_overlay_camera_mic_off_indicator_size">8dp</dimen> + <dimen name="dream_overlay_notification_indicator_size">6dp</dimen> <!-- Dream overlay complications related dimensions --> <dimen name="dream_overlay_complication_clock_time_text_size">72sp</dimen> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 23b252906056..df16b0d45228 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -2410,4 +2410,17 @@ <!-- Toast shown when a notification does not support dragging to split [CHAR LIMIT=NONE] --> <string name="drag_split_not_supported">This notification does not support dragging to Splitscreen.</string> + + <!-- Content description for the Wi-Fi off icon in the dream overlay status bar [CHAR LIMIT=NONE] --> + <string name="dream_overlay_status_bar_wifi_off">Wi\u2011Fi unavailable</string> + <!-- Content description for the priority mode icon in the dream overlay status bar [CHAR LIMIT=NONE] --> + <string name="dream_overlay_status_bar_priority_mode">Priority mode</string> + <!-- Content description for the alarm set icon in the dream overlay status bar [CHAR LIMIT=NONE] --> + <string name="dream_overlay_status_bar_alarm_set">Alarm set</string> + <!-- Content description for the assistant guest mode enabled icon in the dream overlay status bar [CHAR LIMIT=NONE] --> + <string name="dream_overlay_status_bar_assistant_guest_mode_enabled">Assistant guest mode enabled</string> + <!-- Content description for the camera and mic off icon in the dream overlay status bar [CHAR LIMIT=NONE] --> + <string name="dream_overlay_status_bar_camera_mic_off">Camera and mic are off</string> + <!-- Content description for the camera and mic off icon in the dream overlay status bar [CHAR LIMIT=NONE] --> + <string name="dream_overlay_status_bar_notification_indicator">There are notifications</string> </resources> diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml index 9d65c3895827..f2eaa75496e8 100644 --- a/packages/SystemUI/res/values/styles.xml +++ b/packages/SystemUI/res/values/styles.xml @@ -381,12 +381,19 @@ <item name="android:buttonBarNeutralButtonStyle">@style/Widget.Dialog.Button.BorderButton</item> <item name="android:colorBackground">?androidprv:attr/colorSurface</item> <item name="android:alertDialogStyle">@style/AlertDialogStyle</item> + <item name="android:buttonBarStyle">@style/ButtonBarStyle</item> + <item name="android:buttonBarButtonStyle">@style/Widget.Dialog.Button.Large</item> </style> <style name="AlertDialogStyle" parent="@androidprv:style/AlertDialog.DeviceDefault"> <item name="android:layout">@layout/alert_dialog_systemui</item> </style> + <style name="ButtonBarStyle" parent="@androidprv:style/DeviceDefault.ButtonBar.AlertDialog"> + <item name="android:paddingTop">@dimen/dialog_button_bar_top_padding</item> + <item name="android:paddingBottom">@dimen/dialog_bottom_padding</item> + </style> + <style name="Theme.SystemUI.Dialog.Alert" parent="@*android:style/Theme.DeviceDefault.Light.Dialog.Alert" /> <style name="Theme.SystemUI.Dialog.GlobalActions" parent="@android:style/Theme.DeviceDefault.Light.NoActionBar.Fullscreen"> @@ -962,6 +969,11 @@ <item name="android:textColor">?android:attr/textColorPrimary</item> </style> + <style name="Widget.Dialog.Button.Large"> + <item name="android:background">@drawable/qs_dialog_btn_filled_large</item> + <item name="android:minHeight">56dp</item> + </style> + <style name="MainSwitch.Settingslib" parent="@android:style/Theme.DeviceDefault"> <item name="android:switchMinWidth">@dimen/settingslib_min_switch_width</item> </style> diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java index 08b4d3f68a87..2b1c47f60070 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java @@ -103,8 +103,8 @@ public class QuickStepContract { // enabled (since it's used to navigate back within the bubbled app, or to collapse the bubble // stack. public static final int SYSUI_STATE_BUBBLES_EXPANDED = 1 << 14; - // The global actions dialog is showing - public static final int SYSUI_STATE_GLOBAL_ACTIONS_SHOWING = 1 << 15; + // A SysUI dialog is showing. + public static final int SYSUI_STATE_DIALOG_SHOWING = 1 << 15; // The one-handed mode is active public static final int SYSUI_STATE_ONE_HANDED_ACTIVE = 1 << 16; // Allow system gesture no matter the system bar(s) is visible or not @@ -140,7 +140,7 @@ public class QuickStepContract { SYSUI_STATE_TRACING_ENABLED, SYSUI_STATE_ASSIST_GESTURE_CONSTRAINED, SYSUI_STATE_BUBBLES_EXPANDED, - SYSUI_STATE_GLOBAL_ACTIONS_SHOWING, + SYSUI_STATE_DIALOG_SHOWING, SYSUI_STATE_ONE_HANDED_ACTIVE, SYSUI_STATE_ALLOW_GESTURE_IGNORING_BAR_VISIBILITY, SYSUI_STATE_IME_SHOWING, @@ -166,7 +166,7 @@ public class QuickStepContract { str.add((flags & SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING_OCCLUDED) != 0 ? "keygrd_occluded" : ""); str.add((flags & SYSUI_STATE_BOUNCER_SHOWING) != 0 ? "bouncer_visible" : ""); - str.add((flags & SYSUI_STATE_GLOBAL_ACTIONS_SHOWING) != 0 ? "global_actions" : ""); + str.add((flags & SYSUI_STATE_DIALOG_SHOWING) != 0 ? "dialog_showing" : ""); str.add((flags & SYSUI_STATE_A11Y_BUTTON_CLICKABLE) != 0 ? "a11y_click" : ""); str.add((flags & SYSUI_STATE_A11Y_BUTTON_LONG_CLICKABLE) != 0 ? "a11y_long_click" : ""); str.add((flags & SYSUI_STATE_TRACING_ENABLED) != 0 ? "tracing" : ""); @@ -256,7 +256,7 @@ public class QuickStepContract { public static boolean isBackGestureDisabled(int sysuiStateFlags) { // Always allow when the bouncer/global actions is showing (even on top of the keyguard) if ((sysuiStateFlags & SYSUI_STATE_BOUNCER_SHOWING) != 0 - || (sysuiStateFlags & SYSUI_STATE_GLOBAL_ACTIONS_SHOWING) != 0) { + || (sysuiStateFlags & SYSUI_STATE_DIALOG_SHOWING) != 0) { return false; } if ((sysuiStateFlags & SYSUI_STATE_ALLOW_GESTURE_IGNORING_BAR_VISIBILITY) != 0) { diff --git a/packages/SystemUI/src/com/android/systemui/Dependency.java b/packages/SystemUI/src/com/android/systemui/Dependency.java index b32c2b639f16..84b6ace17ab9 100644 --- a/packages/SystemUI/src/com/android/systemui/Dependency.java +++ b/packages/SystemUI/src/com/android/systemui/Dependency.java @@ -38,6 +38,7 @@ import com.android.settingslib.bluetooth.LocalBluetoothManager; import com.android.systemui.accessibility.AccessibilityButtonModeObserver; import com.android.systemui.accessibility.AccessibilityButtonTargetsObserver; import com.android.systemui.accessibility.floatingmenu.AccessibilityFloatingMenuController; +import com.android.systemui.animation.DialogLaunchAnimator; import com.android.systemui.appops.AppOpsController; import com.android.systemui.assist.AssistManager; import com.android.systemui.broadcast.BroadcastDispatcher; @@ -107,6 +108,7 @@ import com.android.systemui.statusbar.phone.ScreenOffAnimationController; import com.android.systemui.statusbar.phone.ShadeController; import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider; import com.android.systemui.statusbar.phone.StatusBarIconController; +import com.android.systemui.statusbar.phone.SystemUIDialogManager; import com.android.systemui.statusbar.policy.AccessibilityController; import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper; import com.android.systemui.statusbar.policy.BatteryController; @@ -372,6 +374,8 @@ public class Dependency { @Inject Lazy<AmbientState> mAmbientStateLazy; @Inject Lazy<GroupMembershipManager> mGroupMembershipManagerLazy; @Inject Lazy<GroupExpansionManager> mGroupExpansionManagerLazy; + @Inject Lazy<SystemUIDialogManager> mSystemUIDialogManagerLazy; + @Inject Lazy<DialogLaunchAnimator> mDialogLaunchAnimatorLazy; @Inject public Dependency() { @@ -592,6 +596,8 @@ public class Dependency { mProviders.put(AmbientState.class, mAmbientStateLazy::get); mProviders.put(GroupMembershipManager.class, mGroupMembershipManagerLazy::get); mProviders.put(GroupExpansionManager.class, mGroupExpansionManagerLazy::get); + mProviders.put(SystemUIDialogManager.class, mSystemUIDialogManagerLazy::get); + mProviders.put(DialogLaunchAnimator.class, mDialogLaunchAnimatorLazy::get); Dependency.setInstance(this); } diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayDotImageView.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayDotImageView.java new file mode 100644 index 000000000000..02a8b39a106a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayDotImageView.java @@ -0,0 +1,128 @@ +/* + * 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.dreams; + +import android.annotation.ColorInt; +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.ColorFilter; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.systemui.R; +import com.android.systemui.statusbar.AlphaOptimizedImageView; + +/** + * An {@link AlphaOptimizedImageView} that is responsible for rendering a dot. Used by + * {@link DreamOverlayStatusBarView}. + */ +public class DreamOverlayDotImageView extends AlphaOptimizedImageView { + private final @ColorInt int mDotColor; + + public DreamOverlayDotImageView(Context context) { + this(context, null); + } + + public DreamOverlayDotImageView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public DreamOverlayDotImageView(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public DreamOverlayDotImageView(Context context, AttributeSet attrs, int defStyleAttr, + int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + TypedArray a = context.getTheme().obtainStyledAttributes(attrs, + R.styleable.DreamOverlayDotImageView, 0, 0); + + try { + mDotColor = a.getColor(R.styleable.DreamOverlayDotImageView_dotColor, Color.WHITE); + } finally { + a.recycle(); + } + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + setImageDrawable(new DotDrawable(mDotColor)); + } + + private static class DotDrawable extends Drawable { + private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + private Bitmap mDotBitmap; + private final Rect mBounds = new Rect(); + private final @ColorInt int mDotColor; + + DotDrawable(@ColorInt int color) { + mDotColor = color; + } + + @Override + public void draw(@NonNull Canvas canvas) { + if (mBounds.isEmpty()) { + return; + } + + if (mDotBitmap == null) { + mDotBitmap = createBitmap(mBounds.width(), mBounds.height()); + } + + canvas.drawBitmap(mDotBitmap, null, mBounds, mPaint); + } + + @Override + protected void onBoundsChange(Rect bounds) { + super.onBoundsChange(bounds); + mBounds.set(bounds.left, bounds.top, bounds.right, bounds.bottom); + // Make sure to regenerate the dot bitmap when the bounds change. + mDotBitmap = null; + } + + @Override + public void setAlpha(int alpha) { + } + + @Override + public void setColorFilter(@Nullable ColorFilter colorFilter) { + } + + @Override + public int getOpacity() { + return 0; + } + + private Bitmap createBitmap(int width, int height) { + Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); + paint.setColor(mDotColor); + canvas.drawCircle(width / 2.f, height / 2.f, Math.min(width, height) / 2.f, paint); + return bitmap; + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarView.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarView.java index 9847ef633bc1..2d969206b468 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarView.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarView.java @@ -25,17 +25,13 @@ import androidx.constraintlayout.widget.ConstraintLayout; import com.android.internal.util.Preconditions; import com.android.systemui.R; -import com.android.systemui.battery.BatteryMeterView; -import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChangeCallback; /** * {@link DreamOverlayStatusBarView} is the view responsible for displaying the status bar in a - * dream. The status bar includes status icons such as battery and wifi. + * dream. The status bar displays conditional status icons such as "priority mode" and "no wifi". */ -public class DreamOverlayStatusBarView extends ConstraintLayout implements - BatteryStateChangeCallback { +public class DreamOverlayStatusBarView extends ConstraintLayout { - private BatteryMeterView mBatteryView; private ImageView mWifiStatusView; public DreamOverlayStatusBarView(Context context) { @@ -59,20 +55,8 @@ public class DreamOverlayStatusBarView extends ConstraintLayout implements protected void onFinishInflate() { super.onFinishInflate(); - mBatteryView = Preconditions.checkNotNull(findViewById(R.id.dream_overlay_battery), - "R.id.dream_overlay_battery must not be null"); mWifiStatusView = Preconditions.checkNotNull(findViewById(R.id.dream_overlay_wifi_status), "R.id.dream_overlay_wifi_status must not be null"); - - mWifiStatusView.setImageDrawable(getContext().getDrawable(R.drawable.ic_signal_wifi_off)); - } - - /** - * Whether to show the battery percent text next to the battery status icons. - * @param show True if the battery percent text should be shown. - */ - void showBatteryPercentText(boolean show) { - mBatteryView.setForceShowPercent(show); } /** diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarViewController.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarViewController.java index 5674b9f3f9fd..ed82ab0e308f 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarViewController.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarViewController.java @@ -17,24 +17,19 @@ package com.android.systemui.dreams; import android.annotation.IntDef; -import android.content.Context; import android.net.ConnectivityManager; import android.net.ConnectivityManager.NetworkCallback; import android.net.Network; import android.net.NetworkCapabilities; import android.net.NetworkRequest; -import com.android.systemui.battery.BatteryMeterViewController; import com.android.systemui.dreams.dagger.DreamOverlayComponent; -import com.android.systemui.dreams.dagger.DreamOverlayModule; -import com.android.systemui.statusbar.policy.BatteryController; import com.android.systemui.util.ViewController; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import javax.inject.Inject; -import javax.inject.Named; /** * View controller for {@link DreamOverlayStatusBarView}. @@ -52,21 +47,7 @@ public class DreamOverlayStatusBarViewController extends ViewController<DreamOve private static final int WIFI_STATUS_UNAVAILABLE = 1; private static final int WIFI_STATUS_AVAILABLE = 2; - @Retention(RetentionPolicy.SOURCE) - @IntDef(prefix = { "BATTERY_STATUS_" }, value = { - BATTERY_STATUS_UNKNOWN, - BATTERY_STATUS_NOT_CHARGING, - BATTERY_STATUS_CHARGING - }) - private @interface BatteryStatus {} - private static final int BATTERY_STATUS_UNKNOWN = 0; - private static final int BATTERY_STATUS_NOT_CHARGING = 1; - private static final int BATTERY_STATUS_CHARGING = 2; - - private final BatteryController mBatteryController; - private final BatteryMeterViewController mBatteryMeterViewController; private final ConnectivityManager mConnectivityManager; - private final boolean mShowPercentAvailable; private final NetworkRequest mNetworkRequest = new NetworkRequest.Builder() .clearCapabilities() @@ -91,43 +72,18 @@ public class DreamOverlayStatusBarViewController extends ViewController<DreamOve } }; - private final BatteryController.BatteryStateChangeCallback mBatteryStateChangeCallback = - new BatteryController.BatteryStateChangeCallback() { - @Override - public void onBatteryLevelChanged(int level, boolean pluggedIn, boolean charging) { - DreamOverlayStatusBarViewController.this.onBatteryLevelChanged(charging); - } - }; - private @WifiStatus int mWifiStatus = WIFI_STATUS_UNKNOWN; - private @BatteryStatus int mBatteryStatus = BATTERY_STATUS_UNKNOWN; @Inject public DreamOverlayStatusBarViewController( - Context context, DreamOverlayStatusBarView view, - BatteryController batteryController, - @Named(DreamOverlayModule.DREAM_OVERLAY_BATTERY_CONTROLLER) - BatteryMeterViewController batteryMeterViewController, ConnectivityManager connectivityManager) { super(view); - mBatteryController = batteryController; - mBatteryMeterViewController = batteryMeterViewController; mConnectivityManager = connectivityManager; - - mShowPercentAvailable = context.getResources().getBoolean( - com.android.internal.R.bool.config_battery_percentage_setting_available); - } - - @Override - protected void onInit() { - super.onInit(); - mBatteryMeterViewController.init(); } @Override protected void onViewAttached() { - mBatteryController.addCallback(mBatteryStateChangeCallback); mConnectivityManager.registerNetworkCallback(mNetworkRequest, mNetworkCallback); NetworkCapabilities capabilities = @@ -140,7 +96,6 @@ public class DreamOverlayStatusBarViewController extends ViewController<DreamOve @Override protected void onViewDetached() { - mBatteryController.removeCallback(mBatteryStateChangeCallback); mConnectivityManager.unregisterNetworkCallback(mNetworkCallback); } @@ -155,18 +110,4 @@ public class DreamOverlayStatusBarViewController extends ViewController<DreamOve mView.showWifiStatus(mWifiStatus == WIFI_STATUS_UNAVAILABLE); } } - - /** - * The battery level has changed. Update the battery status icon as appropriate. - * @param charging Whether the battery is currently charging. - */ - private void onBatteryLevelChanged(boolean charging) { - final int newBatteryStatus = - charging ? BATTERY_STATUS_CHARGING : BATTERY_STATUS_NOT_CHARGING; - if (mBatteryStatus != newBatteryStatus) { - mBatteryStatus = newBatteryStatus; - mView.showBatteryPercentText( - mBatteryStatus == BATTERY_STATUS_CHARGING && mShowPercentAvailable); - } - } } diff --git a/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamOverlayModule.java b/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamOverlayModule.java index 4eb5cb97607a..839a05e6e78f 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamOverlayModule.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamOverlayModule.java @@ -16,9 +16,7 @@ package com.android.systemui.dreams.dagger; -import android.content.ContentResolver; import android.content.res.Resources; -import android.os.Handler; import android.view.LayoutInflater; import android.view.ViewGroup; @@ -28,15 +26,9 @@ import androidx.lifecycle.LifecycleRegistry; import com.android.internal.util.Preconditions; import com.android.systemui.R; -import com.android.systemui.battery.BatteryMeterView; -import com.android.systemui.battery.BatteryMeterViewController; -import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dreams.DreamOverlayContainerView; import com.android.systemui.dreams.DreamOverlayStatusBarView; -import com.android.systemui.statusbar.policy.BatteryController; -import com.android.systemui.statusbar.policy.ConfigurationController; -import com.android.systemui.tuner.TunerService; import javax.inject.Named; @@ -47,9 +39,6 @@ import dagger.Provides; /** Dagger module for {@link DreamOverlayComponent}. */ @Module public abstract class DreamOverlayModule { - private static final String DREAM_OVERLAY_BATTERY_VIEW = "dream_overlay_battery_view"; - public static final String DREAM_OVERLAY_BATTERY_CONTROLLER = - "dream_overlay_battery_controller"; public static final String DREAM_OVERLAY_CONTENT_VIEW = "dream_overlay_content_view"; public static final String MAX_BURN_IN_OFFSET = "max_burn_in_offset"; public static final String BURN_IN_PROTECTION_UPDATE_INTERVAL = @@ -86,37 +75,6 @@ public abstract class DreamOverlayModule { /** */ @Provides @DreamOverlayComponent.DreamOverlayScope - @Named(DREAM_OVERLAY_BATTERY_VIEW) - static BatteryMeterView providesBatteryMeterView(DreamOverlayContainerView view) { - return Preconditions.checkNotNull(view.findViewById(R.id.dream_overlay_battery), - "R.id.battery must not be null"); - } - - /** */ - @Provides - @DreamOverlayComponent.DreamOverlayScope - @Named(DREAM_OVERLAY_BATTERY_CONTROLLER) - static BatteryMeterViewController providesBatteryMeterViewController( - @Named(DREAM_OVERLAY_BATTERY_VIEW) BatteryMeterView batteryMeterView, - ConfigurationController configurationController, - TunerService tunerService, - BroadcastDispatcher broadcastDispatcher, - @Main Handler mainHandler, - ContentResolver contentResolver, - BatteryController batteryController) { - return new BatteryMeterViewController( - batteryMeterView, - configurationController, - tunerService, - broadcastDispatcher, - mainHandler, - contentResolver, - batteryController); - } - - /** */ - @Provides - @DreamOverlayComponent.DreamOverlayScope @Named(MAX_BURN_IN_OFFSET) static int providesMaxBurnInOffset(@Main Resources resources) { return resources.getDimensionPixelSize(R.dimen.default_burn_in_prevention_offset); diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java index 84fa6a6772fe..e3886cd80a42 100644 --- a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java +++ b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java @@ -27,7 +27,6 @@ import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_2BUTTON; import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_USER_REQUEST; import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_NOT_REQUIRED; import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN; -import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_GLOBAL_ACTIONS_SHOWING; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; @@ -117,7 +116,6 @@ import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.colorextraction.SysuiColorExtractor; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.Main; -import com.android.systemui.model.SysUiState; import com.android.systemui.plugins.GlobalActions.GlobalActionsManager; import com.android.systemui.plugins.GlobalActionsPanelPlugin; import com.android.systemui.scrim.ScrimDrawable; @@ -125,7 +123,6 @@ import com.android.systemui.statusbar.NotificationShadeWindowController; import com.android.systemui.statusbar.VibratorHelper; import com.android.systemui.statusbar.phone.StatusBar; import com.android.systemui.statusbar.phone.SystemUIDialog; -import com.android.systemui.statusbar.phone.SystemUIDialogManager; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.telephony.TelephonyListenerManager; @@ -200,7 +197,6 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene private final TelecomManager mTelecomManager; private final MetricsLogger mMetricsLogger; private final UiEventLogger mUiEventLogger; - private final SysUiState mSysUiState; // Used for RingerModeTracker private final LifecycleRegistry mLifecycle = new LifecycleRegistry(this); @@ -241,7 +237,6 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene protected Handler mMainHandler; private int mSmallestScreenWidthDp; private final Optional<StatusBar> mStatusBarOptional; - private final SystemUIDialogManager mDialogManager; private final KeyguardUpdateMonitor mKeyguardUpdateMonitor; private final DialogLaunchAnimator mDialogLaunchAnimator; @@ -347,13 +342,11 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene @Background Executor backgroundExecutor, UiEventLogger uiEventLogger, RingerModeTracker ringerModeTracker, - SysUiState sysUiState, @Main Handler handler, PackageManager packageManager, Optional<StatusBar> statusBarOptional, KeyguardUpdateMonitor keyguardUpdateMonitor, - DialogLaunchAnimator dialogLaunchAnimator, - SystemUIDialogManager dialogManager) { + DialogLaunchAnimator dialogLaunchAnimator) { mContext = context; mWindowManagerFuncs = windowManagerFuncs; mAudioManager = audioManager; @@ -379,13 +372,11 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene mIWindowManager = iWindowManager; mBackgroundExecutor = backgroundExecutor; mRingerModeTracker = ringerModeTracker; - mSysUiState = sysUiState; mMainHandler = handler; mSmallestScreenWidthDp = resources.getConfiguration().smallestScreenWidthDp; mStatusBarOptional = statusBarOptional; mKeyguardUpdateMonitor = keyguardUpdateMonitor; mDialogLaunchAnimator = dialogLaunchAnimator; - mDialogManager = dialogManager; // receive broadcasts IntentFilter filter = new IntentFilter(); @@ -682,11 +673,10 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene ActionsDialogLite dialog = new ActionsDialogLite(mContext, com.android.systemui.R.style.Theme_SystemUI_Dialog_GlobalActionsLite, - mAdapter, mOverflowAdapter, mSysuiColorExtractor, - mStatusBarService, mNotificationShadeWindowController, - mSysUiState, this::onRefresh, mKeyguardShowing, mPowerAdapter, mUiEventLogger, - mStatusBarOptional, mKeyguardUpdateMonitor, mLockPatternUtils, - mDialogManager); + mAdapter, mOverflowAdapter, mSysuiColorExtractor, mStatusBarService, + mNotificationShadeWindowController, this::onRefresh, mKeyguardShowing, + mPowerAdapter, mUiEventLogger, mStatusBarOptional, mKeyguardUpdateMonitor, + mLockPatternUtils); dialog.setOnDismissListener(this); dialog.setOnShowListener(this); @@ -2165,7 +2155,6 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene private boolean mKeyguardShowing; protected float mScrimAlpha; protected final NotificationShadeWindowController mNotificationShadeWindowController; - protected final SysUiState mSysUiState; private ListPopupWindow mOverflowPopup; private Dialog mPowerOptionsDialog; protected final Runnable mOnRefreshCallback; @@ -2226,15 +2215,13 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene MyOverflowAdapter overflowAdapter, SysuiColorExtractor sysuiColorExtractor, IStatusBarService statusBarService, NotificationShadeWindowController notificationShadeWindowController, - SysUiState sysuiState, Runnable onRefreshCallback, boolean keyguardShowing, + Runnable onRefreshCallback, boolean keyguardShowing, MyPowerOptionsAdapter powerAdapter, UiEventLogger uiEventLogger, - Optional<StatusBar> statusBarOptional, - KeyguardUpdateMonitor keyguardUpdateMonitor, LockPatternUtils lockPatternUtils, - SystemUIDialogManager systemUiDialogManager) { + Optional<StatusBar> statusBarOptional, KeyguardUpdateMonitor keyguardUpdateMonitor, + LockPatternUtils lockPatternUtils) { // We set dismissOnDeviceLock to false because we have a custom broadcast receiver to // dismiss this dialog when the device is locked. - super(context, themeRes, false /* dismissOnDeviceLock */, - systemUiDialogManager); + super(context, themeRes, false /* dismissOnDeviceLock */); mContext = context; mAdapter = adapter; mOverflowAdapter = overflowAdapter; @@ -2242,7 +2229,6 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene mColorExtractor = sysuiColorExtractor; mStatusBarService = statusBarService; mNotificationShadeWindowController = notificationShadeWindowController; - mSysUiState = sysuiState; mOnRefreshCallback = onRefreshCallback; mKeyguardShowing = keyguardShowing; mUiEventLogger = uiEventLogger; @@ -2463,8 +2449,6 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene public void show() { super.show(); mNotificationShadeWindowController.setRequestTopUi(true, TAG); - mSysUiState.setFlag(SYSUI_STATE_GLOBAL_ACTIONS_SHOWING, true) - .commitUpdate(mContext.getDisplayId()); // By default this dialog windowAnimationStyle is null, and therefore windowAnimations // should be equal to 0 which means we need to animate the dialog in-window. If it's not @@ -2563,9 +2547,6 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene dismissPowerOptions(); mNotificationShadeWindowController.setRequestTopUi(false, TAG); - mSysUiState.setFlag(SYSUI_STATE_GLOBAL_ACTIONS_SHOWING, false) - .commitUpdate(mContext.getDisplayId()); - super.dismiss(); } diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java index 7bb5454112c4..04a324b87382 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java @@ -56,7 +56,6 @@ import androidx.recyclerview.widget.RecyclerView; import com.android.systemui.R; import com.android.systemui.statusbar.phone.SystemUIDialog; -import com.android.systemui.statusbar.phone.SystemUIDialogManager; /** * Base dialog for media output UI @@ -99,9 +98,8 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog implements } }; - public MediaOutputBaseDialog(Context context, MediaOutputController mediaOutputController, - SystemUIDialogManager dialogManager) { - super(context, R.style.Theme_SystemUI_Dialog_Media, dialogManager); + public MediaOutputBaseDialog(Context context, MediaOutputController mediaOutputController) { + super(context, R.style.Theme_SystemUI_Dialog_Media); // Save the context that is wrapped with our theme. mContext = getContext(); diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java index 7bc0f5202c91..e929b5e21053 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java @@ -66,7 +66,6 @@ import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection; import com.android.systemui.statusbar.phone.ShadeController; -import com.android.systemui.statusbar.phone.SystemUIDialogManager; import java.util.ArrayList; import java.util.Collection; @@ -91,7 +90,6 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback { private final ShadeController mShadeController; private final ActivityStarter mActivityStarter; private final DialogLaunchAnimator mDialogLaunchAnimator; - private final SystemUIDialogManager mDialogManager; private final List<MediaDevice> mGroupMediaDevices = new CopyOnWriteArrayList<>(); private final boolean mAboveStatusbar; private final boolean mVolumeAdjustmentForRemoteGroupSessions; @@ -119,7 +117,7 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback { boolean aboveStatusbar, MediaSessionManager mediaSessionManager, LocalBluetoothManager lbm, ShadeController shadeController, ActivityStarter starter, CommonNotifCollection notifCollection, UiEventLogger uiEventLogger, - DialogLaunchAnimator dialogLaunchAnimator, SystemUIDialogManager dialogManager) { + DialogLaunchAnimator dialogLaunchAnimator) { mContext = context; mPackageName = packageName; mMediaSessionManager = mediaSessionManager; @@ -135,7 +133,6 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback { mDialogLaunchAnimator = dialogLaunchAnimator; mVolumeAdjustmentForRemoteGroupSessions = mContext.getResources().getBoolean( com.android.internal.R.bool.config_volumeAdjustmentForRemoteGroupSessions); - mDialogManager = dialogManager; mColorActiveItem = Utils.getColorStateListDefaultColor(mContext, R.color.media_dialog_active_item_main_content); mColorInactiveItem = Utils.getColorStateListDefaultColor(mContext, @@ -610,10 +607,9 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback { // We show the output group dialog from the output dialog. MediaOutputController controller = new MediaOutputController(mContext, mPackageName, mAboveStatusbar, mMediaSessionManager, mLocalBluetoothManager, mShadeController, - mActivityStarter, mNotifCollection, mUiEventLogger, mDialogLaunchAnimator, - mDialogManager); + mActivityStarter, mNotifCollection, mUiEventLogger, mDialogLaunchAnimator); MediaOutputGroupDialog dialog = new MediaOutputGroupDialog(mContext, mAboveStatusbar, - controller, mDialogManager); + controller); mDialogLaunchAnimator.showFromView(dialog, mediaOutputDialog); } diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java index 4e9da55ffbcb..7696a1f63c01 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java @@ -29,7 +29,6 @@ import com.android.internal.logging.UiEvent; import com.android.internal.logging.UiEventLogger; import com.android.systemui.R; import com.android.systemui.dagger.SysUISingleton; -import com.android.systemui.statusbar.phone.SystemUIDialogManager; /** * Dialog for media output transferring. @@ -39,9 +38,8 @@ public class MediaOutputDialog extends MediaOutputBaseDialog { final UiEventLogger mUiEventLogger; MediaOutputDialog(Context context, boolean aboveStatusbar, MediaOutputController - mediaOutputController, UiEventLogger uiEventLogger, - SystemUIDialogManager dialogManager) { - super(context, mediaOutputController, dialogManager); + mediaOutputController, UiEventLogger uiEventLogger) { + super(context, mediaOutputController); mUiEventLogger = uiEventLogger; mAdapter = new MediaOutputAdapter(mMediaOutputController, this); if (!aboveStatusbar) { diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogFactory.kt b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogFactory.kt index e1e7fa3ebbe0..9e252ea1eddc 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogFactory.kt +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogFactory.kt @@ -25,7 +25,6 @@ import com.android.systemui.animation.DialogLaunchAnimator import com.android.systemui.plugins.ActivityStarter import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection import com.android.systemui.statusbar.phone.ShadeController -import com.android.systemui.statusbar.phone.SystemUIDialogManager import javax.inject.Inject /** @@ -39,8 +38,7 @@ class MediaOutputDialogFactory @Inject constructor( private val starter: ActivityStarter, private val notifCollection: CommonNotifCollection, private val uiEventLogger: UiEventLogger, - private val dialogLaunchAnimator: DialogLaunchAnimator, - private val dialogManager: SystemUIDialogManager + private val dialogLaunchAnimator: DialogLaunchAnimator ) { companion object { var mediaOutputDialog: MediaOutputDialog? = null @@ -53,9 +51,8 @@ class MediaOutputDialogFactory @Inject constructor( val controller = MediaOutputController(context, packageName, aboveStatusBar, mediaSessionManager, lbm, shadeController, starter, notifCollection, - uiEventLogger, dialogLaunchAnimator, dialogManager) - val dialog = MediaOutputDialog(context, aboveStatusBar, controller, uiEventLogger, - dialogManager) + uiEventLogger, dialogLaunchAnimator) + val dialog = MediaOutputDialog(context, aboveStatusBar, controller, uiEventLogger) mediaOutputDialog = dialog // Show the dialog. diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputGroupDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputGroupDialog.java index 9f752b92a85e..f1c66016a49a 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputGroupDialog.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputGroupDialog.java @@ -25,7 +25,6 @@ import android.view.WindowManager; import androidx.core.graphics.drawable.IconCompat; import com.android.systemui.R; -import com.android.systemui.statusbar.phone.SystemUIDialogManager; /** * Dialog for media output group. @@ -34,8 +33,8 @@ import com.android.systemui.statusbar.phone.SystemUIDialogManager; public class MediaOutputGroupDialog extends MediaOutputBaseDialog { MediaOutputGroupDialog(Context context, boolean aboveStatusbar, MediaOutputController - mediaOutputController, SystemUIDialogManager dialogManager) { - super(context, mediaOutputController, dialogManager); + mediaOutputController) { + super(context, mediaOutputController); mMediaOutputController.resetGroupMediaDevices(); mAdapter = new MediaOutputGroupAdapter(mMediaOutputController); if (!aboveStatusbar) { diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java index e088f548a356..5d6bbae1f14a 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java @@ -202,11 +202,12 @@ public class CastTile extends QSTileImpl<BooleanState> { mActivityStarter .postStartActivityDismissingKeyguard(getLongClickIntent(), 0, controller); - }); + }, R.style.Theme_SystemUI_Dialog, false /* showProgressBarWhenEmpty */); holder.init(dialog); SystemUIDialog.setShowForAllUsers(dialog, true); SystemUIDialog.registerDismissListener(dialog); SystemUIDialog.setWindowOnTop(dialog, mKeyguard.isShowing()); + SystemUIDialog.setDialogSize(dialog); mUiHandler.post(() -> { if (view != null) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java index 97225284f208..79d646cdbd13 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java @@ -42,7 +42,10 @@ import androidx.annotation.Nullable; import com.android.systemui.Dependency; import com.android.systemui.R; +import com.android.systemui.animation.DialogLaunchAnimator; import com.android.systemui.broadcast.BroadcastDispatcher; +import com.android.systemui.model.SysUiState; +import com.android.systemui.shared.system.QuickStepContract; import java.util.ArrayList; import java.util.List; @@ -64,7 +67,8 @@ public class SystemUIDialog extends AlertDialog implements ViewRootImpl.ConfigCh private final Context mContext; @Nullable private final DismissReceiver mDismissReceiver; private final Handler mHandler = new Handler(); - @Nullable private final SystemUIDialogManager mDialogManager; + private final SystemUIDialogManager mDialogManager; + private final SysUiState mSysUiState; private int mLastWidth = Integer.MIN_VALUE; private int mLastHeight = Integer.MIN_VALUE; @@ -77,24 +81,11 @@ public class SystemUIDialog extends AlertDialog implements ViewRootImpl.ConfigCh this(context, R.style.Theme_SystemUI_Dialog); } - public SystemUIDialog(Context context, SystemUIDialogManager dialogManager) { - this(context, R.style.Theme_SystemUI_Dialog, true, dialogManager); - } - public SystemUIDialog(Context context, int theme) { this(context, theme, true /* dismissOnDeviceLock */); } - public SystemUIDialog(Context context, int theme, SystemUIDialogManager dialogManager) { - this(context, theme, true /* dismissOnDeviceLock */, dialogManager); - } - public SystemUIDialog(Context context, int theme, boolean dismissOnDeviceLock) { - this(context, theme, dismissOnDeviceLock, null); - } - - public SystemUIDialog(Context context, int theme, boolean dismissOnDeviceLock, - @Nullable SystemUIDialogManager dialogManager) { super(context, theme); mContext = context; @@ -104,7 +95,12 @@ public class SystemUIDialog extends AlertDialog implements ViewRootImpl.ConfigCh getWindow().setAttributes(attrs); mDismissReceiver = dismissOnDeviceLock ? new DismissReceiver(this) : null; - mDialogManager = dialogManager; + + // TODO(b/219008720): Remove those calls to Dependency.get by introducing a + // SystemUIDialogFactory and make all other dialogs create a SystemUIDialog to which we set + // the content and attach listeners. + mDialogManager = Dependency.get(SystemUIDialogManager.class); + mSysUiState = Dependency.get(SysUiState.class); } @Override @@ -174,13 +170,11 @@ public class SystemUIDialog extends AlertDialog implements ViewRootImpl.ConfigCh mDismissReceiver.register(); } - if (mDialogManager != null) { - mDialogManager.setShowing(this, true); - } - // Listen for configuration changes to resize this dialog window. This is mostly necessary // for foldables that often go from large <=> small screen when folding/unfolding. ViewRootImpl.addConfigCallback(this); + mDialogManager.setShowing(this, true); + mSysUiState.setFlag(QuickStepContract.SYSUI_STATE_DIALOG_SHOWING, true); } @Override @@ -191,11 +185,9 @@ public class SystemUIDialog extends AlertDialog implements ViewRootImpl.ConfigCh mDismissReceiver.unregister(); } - if (mDialogManager != null) { - mDialogManager.setShowing(this, false); - } - ViewRootImpl.removeConfigCallback(this); + mDialogManager.setShowing(this, false); + mSysUiState.setFlag(QuickStepContract.SYSUI_STATE_DIALOG_SHOWING, false); } public void setShowForAllUsers(boolean show) { @@ -401,10 +393,13 @@ public class SystemUIDialog extends AlertDialog implements ViewRootImpl.ConfigCh private final Dialog mDialog; private boolean mRegistered; private final BroadcastDispatcher mBroadcastDispatcher; + private final DialogLaunchAnimator mDialogLaunchAnimator; DismissReceiver(Dialog dialog) { mDialog = dialog; + // TODO(b/219008720): Remove those calls to Dependency.get. mBroadcastDispatcher = Dependency.get(BroadcastDispatcher.class); + mDialogLaunchAnimator = Dependency.get(DialogLaunchAnimator.class); } void register() { @@ -421,6 +416,10 @@ public class SystemUIDialog extends AlertDialog implements ViewRootImpl.ConfigCh @Override public void onReceive(Context context, Intent intent) { + // These broadcast are usually received when locking the device, swiping up to home + // (which collapses the shade), etc. In those cases, we usually don't want to animate + // back into the view. + mDialogLaunchAnimator.disableAllCurrentDialogsExitAnimations(); mDialog.dismiss(); } } diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java index 6fefce2be19b..b2a79b01fb74 100644 --- a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java +++ b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java @@ -21,7 +21,7 @@ import static android.view.Display.DEFAULT_DISPLAY; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BOUNCER_SHOWING; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BUBBLES_EXPANDED; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BUBBLES_MANAGE_MENU_EXPANDED; -import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_GLOBAL_ACTIONS_SHOWING; +import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_DIALOG_SHOWING; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_ONE_HANDED_ACTIVE; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_QUICK_SETTINGS_EXPANDED; @@ -97,7 +97,7 @@ public final class WMShell extends CoreStartable implements CommandQueue.Callbacks, ProtoTraceable<SystemUiTraceProto> { private static final String TAG = WMShell.class.getName(); private static final int INVALID_SYSUI_STATE_MASK = - SYSUI_STATE_GLOBAL_ACTIONS_SHOWING + SYSUI_STATE_DIALOG_SHOWING | SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING | SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING_OCCLUDED | SYSUI_STATE_BOUNCER_SHOWING diff --git a/packages/SystemUI/tests/src/com/android/systemui/SysuiTestCase.java b/packages/SystemUI/tests/src/com/android/systemui/SysuiTestCase.java index 40632a85d722..7a0db1fd975c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/SysuiTestCase.java +++ b/packages/SystemUI/tests/src/com/android/systemui/SysuiTestCase.java @@ -42,6 +42,7 @@ import com.android.systemui.dump.DumpManager; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.settings.UserTracker; import com.android.systemui.statusbar.SmartReplyController; +import com.android.systemui.statusbar.phone.SystemUIDialogManager; import org.junit.After; import org.junit.AfterClass; @@ -112,6 +113,11 @@ public abstract class SysuiTestCase { // KeyguardUpdateMonitor to be created (injected). // TODO(b/1531701009) Clean up NotificationContentView creation to prevent this mDependency.injectMockDependency(SmartReplyController.class); + + // Make sure that all tests on any SystemUIDialog does not crash because this dependency + // is missing (constructing the actual one would throw). + // TODO(b/219008720): Remove this. + mDependency.injectMockDependency(SystemUIDialogManager.class); } @After diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStatusBarViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStatusBarViewControllerTest.java index 7f72dda441d2..658702929d7b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStatusBarViewControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStatusBarViewControllerTest.java @@ -29,8 +29,6 @@ import android.testing.AndroidTestingRunner; import androidx.test.filters.SmallTest; import com.android.systemui.SysuiTestCase; -import com.android.systemui.battery.BatteryMeterViewController; -import com.android.systemui.statusbar.policy.BatteryController; import org.junit.Before; import org.junit.Test; @@ -46,10 +44,6 @@ public class DreamOverlayStatusBarViewControllerTest extends SysuiTestCase { @Mock DreamOverlayStatusBarView mView; @Mock - BatteryController mBatteryController; - @Mock - BatteryMeterViewController mBatteryMeterViewController; - @Mock ConnectivityManager mConnectivityManager; @Mock NetworkCapabilities mNetworkCapabilities; @@ -61,22 +55,7 @@ public class DreamOverlayStatusBarViewControllerTest extends SysuiTestCase { @Before public void setup() { MockitoAnnotations.initMocks(this); - mController = new DreamOverlayStatusBarViewController( - mContext, mView, mBatteryController, mBatteryMeterViewController, - mConnectivityManager); - } - - @Test - public void testOnInitInitializesControllers() { - mController.onInit(); - verify(mBatteryMeterViewController).init(); - } - - @Test - public void testOnViewAttachedAddsBatteryControllerCallback() { - mController.onViewAttached(); - verify(mBatteryController) - .addCallback(any(BatteryController.BatteryStateChangeCallback.class)); + mController = new DreamOverlayStatusBarViewController(mView, mConnectivityManager); } @Test @@ -113,13 +92,6 @@ public class DreamOverlayStatusBarViewControllerTest extends SysuiTestCase { } @Test - public void testOnViewDetachedRemovesBatteryControllerCallback() { - mController.onViewDetached(); - verify(mBatteryController) - .removeCallback(any(BatteryController.BatteryStateChangeCallback.class)); - } - - @Test public void testOnViewDetachedUnregistersNetworkCallback() { mController.onViewDetached(); verify(mConnectivityManager) @@ -127,26 +99,6 @@ public class DreamOverlayStatusBarViewControllerTest extends SysuiTestCase { } @Test - public void testBatteryPercentTextShownWhenBatteryLevelChangesWhileCharging() { - final ArgumentCaptor<BatteryController.BatteryStateChangeCallback> callbackCapture = - ArgumentCaptor.forClass(BatteryController.BatteryStateChangeCallback.class); - mController.onViewAttached(); - verify(mBatteryController).addCallback(callbackCapture.capture()); - callbackCapture.getValue().onBatteryLevelChanged(1, true, true); - verify(mView).showBatteryPercentText(true); - } - - @Test - public void testBatteryPercentTextHiddenWhenBatteryLevelChangesWhileNotCharging() { - final ArgumentCaptor<BatteryController.BatteryStateChangeCallback> callbackCapture = - ArgumentCaptor.forClass(BatteryController.BatteryStateChangeCallback.class); - mController.onViewAttached(); - verify(mBatteryController).addCallback(callbackCapture.capture()); - callbackCapture.getValue().onBatteryLevelChanged(1, true, false); - verify(mView).showBatteryPercentText(false); - } - - @Test public void testWifiStatusHiddenWhenWifiBecomesAvailable() { // Make sure wifi starts out unavailable when onViewAttached is called. when(mNetworkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) diff --git a/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogLiteTest.java b/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogLiteTest.java index 71fc8ee6cce8..953be7d6f002 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogLiteTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogLiteTest.java @@ -19,7 +19,6 @@ package com.android.systemui.globalactions; import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doReturn; @@ -57,13 +56,11 @@ import com.android.systemui.SysuiTestCase; import com.android.systemui.animation.DialogLaunchAnimator; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.colorextraction.SysuiColorExtractor; -import com.android.systemui.model.SysUiState; import com.android.systemui.plugins.GlobalActions; import com.android.systemui.settings.UserContextProvider; import com.android.systemui.statusbar.NotificationShadeWindowController; import com.android.systemui.statusbar.VibratorHelper; import com.android.systemui.statusbar.phone.StatusBar; -import com.android.systemui.statusbar.phone.SystemUIDialogManager; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.telephony.TelephonyListenerManager; @@ -112,7 +109,6 @@ public class GlobalActionsDialogLiteTest extends SysuiTestCase { @Mock private UiEventLogger mUiEventLogger; @Mock private RingerModeTracker mRingerModeTracker; @Mock private RingerModeLiveData mRingerModeLiveData; - @Mock private SysUiState mSysUiState; @Mock private PackageManager mPackageManager; @Mock private Handler mHandler; @Mock private UserContextProvider mUserContextProvider; @@ -120,7 +116,6 @@ public class GlobalActionsDialogLiteTest extends SysuiTestCase { @Mock private StatusBar mStatusBar; @Mock private KeyguardUpdateMonitor mKeyguardUpdateMonitor; @Mock private DialogLaunchAnimator mDialogLaunchAnimator; - @Mock private SystemUIDialogManager mDialogManager; private TestableLooper mTestableLooper; @@ -161,19 +156,16 @@ public class GlobalActionsDialogLiteTest extends SysuiTestCase { mBackgroundExecutor, mUiEventLogger, mRingerModeTracker, - mSysUiState, mHandler, mPackageManager, Optional.of(mStatusBar), mKeyguardUpdateMonitor, - mDialogLaunchAnimator, - mDialogManager); + mDialogLaunchAnimator); mGlobalActionsDialogLite.setZeroDialogPressDelayForTesting(); ColorExtractor.GradientColors backdropColors = new ColorExtractor.GradientColors(); backdropColors.setMainColor(Color.BLACK); when(mColorExtractor.getNeutralColors()).thenReturn(backdropColors); - when(mSysUiState.setFlag(anyInt(), anyBoolean())).thenReturn(mSysUiState); } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java index c5c4d79e043d..2be30b39763e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java @@ -45,7 +45,6 @@ import com.android.systemui.animation.DialogLaunchAnimator; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.statusbar.notification.NotificationEntryManager; import com.android.systemui.statusbar.phone.ShadeController; -import com.android.systemui.statusbar.phone.SystemUIDialogManager; import org.junit.Before; import org.junit.Test; @@ -68,7 +67,6 @@ public class MediaOutputBaseDialogTest extends SysuiTestCase { mock(NotificationEntryManager.class); private final UiEventLogger mUiEventLogger = mock(UiEventLogger.class); private final DialogLaunchAnimator mDialogLaunchAnimator = mock(DialogLaunchAnimator.class); - private final SystemUIDialogManager mDialogManager = mock(SystemUIDialogManager.class); private MediaOutputBaseDialogImpl mMediaOutputBaseDialogImpl; private MediaOutputController mMediaOutputController; @@ -82,7 +80,7 @@ public class MediaOutputBaseDialogTest extends SysuiTestCase { public void setUp() { mMediaOutputController = new MediaOutputController(mContext, TEST_PACKAGE, false, mMediaSessionManager, mLocalBluetoothManager, mShadeController, mStarter, - mNotificationEntryManager, mUiEventLogger, mDialogLaunchAnimator, mDialogManager); + mNotificationEntryManager, mUiEventLogger, mDialogLaunchAnimator); mMediaOutputBaseDialogImpl = new MediaOutputBaseDialogImpl(mContext, mMediaOutputController); mMediaOutputBaseDialogImpl.onCreate(new Bundle()); @@ -175,7 +173,7 @@ public class MediaOutputBaseDialogTest extends SysuiTestCase { class MediaOutputBaseDialogImpl extends MediaOutputBaseDialog { MediaOutputBaseDialogImpl(Context context, MediaOutputController mediaOutputController) { - super(context, mediaOutputController, mDialogManager); + super(context, mediaOutputController); mAdapter = mMediaOutputBaseAdapter; } diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java index bdc311725880..789822e262d5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java @@ -55,7 +55,6 @@ import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection; import com.android.systemui.statusbar.phone.ShadeController; -import com.android.systemui.statusbar.phone.SystemUIDialogManager; import org.junit.Before; import org.junit.Test; @@ -94,7 +93,6 @@ public class MediaOutputControllerTest extends SysuiTestCase { private CommonNotifCollection mNotifCollection = mock(CommonNotifCollection.class); private final UiEventLogger mUiEventLogger = mock(UiEventLogger.class); private final DialogLaunchAnimator mDialogLaunchAnimator = mock(DialogLaunchAnimator.class); - private final SystemUIDialogManager mDialogManager = mock(SystemUIDialogManager.class); private Context mSpyContext; private MediaOutputController mMediaOutputController; @@ -117,7 +115,7 @@ public class MediaOutputControllerTest extends SysuiTestCase { mMediaOutputController = new MediaOutputController(mSpyContext, TEST_PACKAGE_NAME, false, mMediaSessionManager, mLocalBluetoothManager, mShadeController, mStarter, - mNotifCollection, mUiEventLogger, mDialogLaunchAnimator, mDialogManager); + mNotifCollection, mUiEventLogger, mDialogLaunchAnimator); mLocalMediaManager = spy(mMediaOutputController.mLocalMediaManager); mMediaOutputController.mLocalMediaManager = mLocalMediaManager; MediaDescription.Builder builder = new MediaDescription.Builder(); @@ -161,7 +159,7 @@ public class MediaOutputControllerTest extends SysuiTestCase { public void start_withoutPackageName_verifyMediaControllerInit() { mMediaOutputController = new MediaOutputController(mSpyContext, null, false, mMediaSessionManager, mLocalBluetoothManager, mShadeController, mStarter, - mNotifCollection, mUiEventLogger, mDialogLaunchAnimator, mDialogManager); + mNotifCollection, mUiEventLogger, mDialogLaunchAnimator); mMediaOutputController.start(mCb); @@ -182,7 +180,7 @@ public class MediaOutputControllerTest extends SysuiTestCase { public void stop_withoutPackageName_verifyMediaControllerDeinit() { mMediaOutputController = new MediaOutputController(mSpyContext, null, false, mMediaSessionManager, mLocalBluetoothManager, mShadeController, mStarter, - mNotifCollection, mUiEventLogger, mDialogLaunchAnimator, mDialogManager); + mNotifCollection, mUiEventLogger, mDialogLaunchAnimator); mMediaOutputController.start(mCb); @@ -453,7 +451,7 @@ public class MediaOutputControllerTest extends SysuiTestCase { public void getNotificationLargeIcon_withoutPackageName_returnsNull() { mMediaOutputController = new MediaOutputController(mSpyContext, null, false, mMediaSessionManager, mLocalBluetoothManager, mShadeController, mStarter, - mNotifCollection, mUiEventLogger, mDialogLaunchAnimator, mDialogManager); + mNotifCollection, mUiEventLogger, mDialogLaunchAnimator); assertThat(mMediaOutputController.getNotificationIcon()).isNull(); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java index ada8d3592012..8a3ea562269d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java @@ -40,7 +40,6 @@ import com.android.systemui.animation.DialogLaunchAnimator; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.statusbar.notification.NotificationEntryManager; import com.android.systemui.statusbar.phone.ShadeController; -import com.android.systemui.statusbar.phone.SystemUIDialogManager; import org.junit.After; import org.junit.Before; @@ -68,7 +67,6 @@ public class MediaOutputDialogTest extends SysuiTestCase { mock(NotificationEntryManager.class); private final UiEventLogger mUiEventLogger = mock(UiEventLogger.class); private final DialogLaunchAnimator mDialogLaunchAnimator = mock(DialogLaunchAnimator.class); - private final SystemUIDialogManager mDialogManager = mock(SystemUIDialogManager.class); private MediaOutputDialog mMediaOutputDialog; private MediaOutputController mMediaOutputController; @@ -78,10 +76,10 @@ public class MediaOutputDialogTest extends SysuiTestCase { public void setUp() { mMediaOutputController = new MediaOutputController(mContext, TEST_PACKAGE, false, mMediaSessionManager, mLocalBluetoothManager, mShadeController, mStarter, - mNotificationEntryManager, mUiEventLogger, mDialogLaunchAnimator, mDialogManager); + mNotificationEntryManager, mUiEventLogger, mDialogLaunchAnimator); mMediaOutputController.mLocalMediaManager = mLocalMediaManager; mMediaOutputDialog = new MediaOutputDialog(mContext, false, - mMediaOutputController, mUiEventLogger, mDialogManager); + mMediaOutputController, mUiEventLogger); mMediaOutputDialog.show(); when(mLocalMediaManager.getCurrentConnectedDevice()).thenReturn(mMediaDevice); @@ -127,7 +125,7 @@ public class MediaOutputDialogTest extends SysuiTestCase { // and verify if the calling times increases. public void onCreate_ShouldLogVisibility() { MediaOutputDialog testDialog = new MediaOutputDialog(mContext, false, - mMediaOutputController, mUiEventLogger, mDialogManager); + mMediaOutputController, mUiEventLogger); testDialog.show(); testDialog.dismissDialog(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputGroupDialogTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputGroupDialogTest.java index b114452facc3..e8cd6c88956d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputGroupDialogTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputGroupDialogTest.java @@ -38,7 +38,6 @@ import com.android.systemui.animation.DialogLaunchAnimator; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.statusbar.notification.NotificationEntryManager; import com.android.systemui.statusbar.phone.ShadeController; -import com.android.systemui.statusbar.phone.SystemUIDialogManager; import org.junit.After; import org.junit.Before; @@ -67,7 +66,6 @@ public class MediaOutputGroupDialogTest extends SysuiTestCase { mock(NotificationEntryManager.class); private final UiEventLogger mUiEventLogger = mock(UiEventLogger.class); private final DialogLaunchAnimator mDialogLaunchAnimator = mock(DialogLaunchAnimator.class); - private final SystemUIDialogManager mDialogManager = mock(SystemUIDialogManager.class); private MediaOutputGroupDialog mMediaOutputGroupDialog; private MediaOutputController mMediaOutputController; @@ -77,10 +75,10 @@ public class MediaOutputGroupDialogTest extends SysuiTestCase { public void setUp() { mMediaOutputController = new MediaOutputController(mContext, TEST_PACKAGE, false, mMediaSessionManager, mLocalBluetoothManager, mShadeController, mStarter, - mNotificationEntryManager, mUiEventLogger, mDialogLaunchAnimator, mDialogManager); + mNotificationEntryManager, mUiEventLogger, mDialogLaunchAnimator); mMediaOutputController.mLocalMediaManager = mLocalMediaManager; mMediaOutputGroupDialog = new MediaOutputGroupDialog(mContext, false, - mMediaOutputController, mDialogManager); + mMediaOutputController); mMediaOutputGroupDialog.show(); when(mLocalMediaManager.getSelectedMediaDevice()).thenReturn(mMediaDevices); } diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java index 62da981bd9a7..0e9926590511 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java @@ -1363,8 +1363,18 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub * </p> * * @param displayId The logical display id - * @param region the new magnified region, may be empty if - * magnification is not enabled (e.g. scale is 1) + * @param region The magnification region. + * If the config mode is + * {@link MagnificationConfig#MAGNIFICATION_MODE_FULLSCREEN}, + * it is the region of the screen currently active for magnification. + * the returned region will be empty if the magnification is not active + * (e.g. scale is 1. And the magnification is active if magnification + * gestures are enabled or if a service is running that can control + * magnification. + * If the config mode is + * {@link MagnificationConfig#MAGNIFICATION_MODE_WINDOW}, + * it is the region of screen projected on the magnification window. + * The region will be empty if magnification is not activated. * @param config The magnification config. That has magnification mode, the new scale and the * new screen-relative center position */ diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationController.java b/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationController.java index fe97a46e12f9..a95820966926 100644 --- a/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationController.java +++ b/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationController.java @@ -50,7 +50,6 @@ import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.function.pooled.PooledLambda; import com.android.server.LocalServices; -import com.android.server.accessibility.AccessibilityManagerService; import com.android.server.accessibility.AccessibilityTraceManager; import com.android.server.wm.WindowManagerInternal; @@ -374,9 +373,8 @@ public class FullScreenMagnificationController implements .setScale(getScale()) .setCenterX(getCenterX()) .setCenterY(getCenterY()).build(); - mControllerCtx.getAms().notifyMagnificationChanged(mDisplayId, - mMagnificationRegion, - config); + mMagnificationInfoChangedCallback.onFullScreenMagnificationChanged(mDisplayId, + mMagnificationRegion, config); if (mUnregisterPending && !isMagnifying()) { unregister(mDeleteAfterUnregister); } @@ -665,10 +663,10 @@ public class FullScreenMagnificationController implements * FullScreenMagnificationController Constructor */ public FullScreenMagnificationController(@NonNull Context context, - @NonNull AccessibilityManagerService ams, @NonNull Object lock, + @NonNull AccessibilityTraceManager traceManager, @NonNull Object lock, @NonNull MagnificationInfoChangedCallback magnificationInfoChangedCallback, @NonNull MagnificationScaleProvider scaleProvider) { - this(new ControllerContext(context, ams, + this(new ControllerContext(context, traceManager, LocalServices.getService(WindowManagerInternal.class), new Handler(context.getMainLooper()), context.getResources().getInteger(R.integer.config_longAnimTime)), lock, @@ -1521,7 +1519,6 @@ public class FullScreenMagnificationController implements @VisibleForTesting public static class ControllerContext { private final Context mContext; - private final AccessibilityManagerService mAms; private final AccessibilityTraceManager mTrace; private final WindowManagerInternal mWindowManager; private final Handler mHandler; @@ -1531,13 +1528,12 @@ public class FullScreenMagnificationController implements * Constructor for ControllerContext. */ public ControllerContext(@NonNull Context context, - @NonNull AccessibilityManagerService ams, + @NonNull AccessibilityTraceManager traceManager, @NonNull WindowManagerInternal windowManager, @NonNull Handler handler, long animationDuration) { mContext = context; - mAms = ams; - mTrace = ams.getTraceManager(); + mTrace = traceManager; mWindowManager = windowManager; mHandler = handler; mAnimationDuration = animationDuration; @@ -1552,14 +1548,6 @@ public class FullScreenMagnificationController implements } /** - * @return AccessibilityManagerService - */ - @NonNull - public AccessibilityManagerService getAms() { - return mAms; - } - - /** * @return AccessibilityTraceManager */ @NonNull @@ -1632,5 +1620,17 @@ public class FullScreenMagnificationController implements * hidden. */ void onImeWindowVisibilityChanged(boolean shown); + + /** + * Called when the magnification spec changed. + * + * @param displayId The logical display id + * @param region The region of the screen currently active for magnification. + * The returned region will be empty if the magnification is not active. + * @param config The magnification config. That has magnification mode, the new scale and + * the new screen-relative center position + */ + void onFullScreenMagnificationChanged(int displayId, @NonNull Region region, + @NonNull MagnificationConfig config); } } diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationController.java b/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationController.java index c376bf873dc8..09e82c787c90 100644 --- a/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationController.java +++ b/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationController.java @@ -405,6 +405,12 @@ public class MagnificationController implements WindowMagnificationManager.Callb mAms.notifyMagnificationChanged(displayId, new Region(bounds), config); } + @Override + public void onFullScreenMagnificationChanged(int displayId, @NonNull Region region, + @NonNull MagnificationConfig config) { + mAms.notifyMagnificationChanged(displayId, region, config); + } + private void disableFullScreenMagnificationIfNeeded(int displayId) { final FullScreenMagnificationController fullScreenMagnificationController = getFullScreenMagnificationController(); @@ -590,7 +596,7 @@ public class MagnificationController implements WindowMagnificationManager.Callb synchronized (mLock) { if (mFullScreenMagnificationController == null) { mFullScreenMagnificationController = new FullScreenMagnificationController(mContext, - mAms, mLock, this, mScaleProvider); + mAms.getTraceManager(), mLock, this, mScaleProvider); } } return mFullScreenMagnificationController; diff --git a/services/backup/backuplib/java/com/android/server/backup/transport/BackupTransportClient.java b/services/backup/backuplib/java/com/android/server/backup/transport/BackupTransportClient.java index 89633373b152..7e3ede15aa04 100644 --- a/services/backup/backuplib/java/com/android/server/backup/transport/BackupTransportClient.java +++ b/services/backup/backuplib/java/com/android/server/backup/transport/BackupTransportClient.java @@ -22,20 +22,18 @@ import android.app.backup.RestoreDescription; import android.app.backup.RestoreSet; import android.content.Intent; import android.content.pm.PackageInfo; -import android.os.Binder; import android.os.ParcelFileDescriptor; import android.os.RemoteException; import android.util.Slog; -import com.android.internal.annotations.GuardedBy; import com.android.internal.backup.IBackupTransport; import com.android.internal.infra.AndroidFuture; import java.util.ArrayDeque; -import java.util.Deque; +import java.util.HashSet; import java.util.List; import java.util.Queue; -import java.util.concurrent.ArrayBlockingQueue; +import java.util.Set; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; @@ -49,17 +47,19 @@ public class BackupTransportClient { private final IBackupTransport mTransportBinder; private final TransportStatusCallbackPool mCallbackPool; + private final TransportFutures mTransportFutures; BackupTransportClient(IBackupTransport transportBinder) { mTransportBinder = transportBinder; mCallbackPool = new TransportStatusCallbackPool(); + mTransportFutures = new TransportFutures(); } /** * See {@link IBackupTransport#name()}. */ public String name() throws RemoteException { - AndroidFuture<String> resultFuture = new AndroidFuture<>(); + AndroidFuture<String> resultFuture = mTransportFutures.newFuture(); mTransportBinder.name(resultFuture); return getFutureResult(resultFuture); } @@ -68,7 +68,7 @@ public class BackupTransportClient { * See {@link IBackupTransport#configurationIntent()} */ public Intent configurationIntent() throws RemoteException { - AndroidFuture<Intent> resultFuture = new AndroidFuture<>(); + AndroidFuture<Intent> resultFuture = mTransportFutures.newFuture(); mTransportBinder.configurationIntent(resultFuture); return getFutureResult(resultFuture); } @@ -77,7 +77,7 @@ public class BackupTransportClient { * See {@link IBackupTransport#currentDestinationString()} */ public String currentDestinationString() throws RemoteException { - AndroidFuture<String> resultFuture = new AndroidFuture<>(); + AndroidFuture<String> resultFuture = mTransportFutures.newFuture(); mTransportBinder.currentDestinationString(resultFuture); return getFutureResult(resultFuture); } @@ -86,7 +86,7 @@ public class BackupTransportClient { * See {@link IBackupTransport#dataManagementIntent()} */ public Intent dataManagementIntent() throws RemoteException { - AndroidFuture<Intent> resultFuture = new AndroidFuture<>(); + AndroidFuture<Intent> resultFuture = mTransportFutures.newFuture(); mTransportBinder.dataManagementIntent(resultFuture); return getFutureResult(resultFuture); } @@ -96,7 +96,7 @@ public class BackupTransportClient { */ @Nullable public CharSequence dataManagementIntentLabel() throws RemoteException { - AndroidFuture<CharSequence> resultFuture = new AndroidFuture<>(); + AndroidFuture<CharSequence> resultFuture = mTransportFutures.newFuture(); mTransportBinder.dataManagementIntentLabel(resultFuture); return getFutureResult(resultFuture); } @@ -105,7 +105,7 @@ public class BackupTransportClient { * See {@link IBackupTransport#transportDirName()} */ public String transportDirName() throws RemoteException { - AndroidFuture<String> resultFuture = new AndroidFuture<>(); + AndroidFuture<String> resultFuture = mTransportFutures.newFuture(); mTransportBinder.transportDirName(resultFuture); return getFutureResult(resultFuture); } @@ -153,7 +153,7 @@ public class BackupTransportClient { * See {@link IBackupTransport#requestBackupTime()} */ public long requestBackupTime() throws RemoteException { - AndroidFuture<Long> resultFuture = new AndroidFuture<>(); + AndroidFuture<Long> resultFuture = mTransportFutures.newFuture(); mTransportBinder.requestBackupTime(resultFuture); Long result = getFutureResult(resultFuture); return result == null ? BackupTransport.TRANSPORT_ERROR : result; @@ -177,7 +177,7 @@ public class BackupTransportClient { * See {@link IBackupTransport#getAvailableRestoreSets()} */ public RestoreSet[] getAvailableRestoreSets() throws RemoteException { - AndroidFuture<List<RestoreSet>> resultFuture = new AndroidFuture<>(); + AndroidFuture<List<RestoreSet>> resultFuture = mTransportFutures.newFuture(); mTransportBinder.getAvailableRestoreSets(resultFuture); List<RestoreSet> result = getFutureResult(resultFuture); return result == null ? null : result.toArray(new RestoreSet[] {}); @@ -187,7 +187,7 @@ public class BackupTransportClient { * See {@link IBackupTransport#getCurrentRestoreSet()} */ public long getCurrentRestoreSet() throws RemoteException { - AndroidFuture<Long> resultFuture = new AndroidFuture<>(); + AndroidFuture<Long> resultFuture = mTransportFutures.newFuture(); mTransportBinder.getCurrentRestoreSet(resultFuture); Long result = getFutureResult(resultFuture); return result == null ? BackupTransport.TRANSPORT_ERROR : result; @@ -210,7 +210,7 @@ public class BackupTransportClient { * See {@link IBackupTransport#nextRestorePackage()} */ public RestoreDescription nextRestorePackage() throws RemoteException { - AndroidFuture<RestoreDescription> resultFuture = new AndroidFuture<>(); + AndroidFuture<RestoreDescription> resultFuture = mTransportFutures.newFuture(); mTransportBinder.nextRestorePackage(resultFuture); return getFutureResult(resultFuture); } @@ -245,7 +245,7 @@ public class BackupTransportClient { * See {@link IBackupTransport#requestFullBackupTime()} */ public long requestFullBackupTime() throws RemoteException { - AndroidFuture<Long> resultFuture = new AndroidFuture<>(); + AndroidFuture<Long> resultFuture = mTransportFutures.newFuture(); mTransportBinder.requestFullBackupTime(resultFuture); Long result = getFutureResult(resultFuture); return result == null ? BackupTransport.TRANSPORT_ERROR : result; @@ -309,7 +309,7 @@ public class BackupTransportClient { */ public boolean isAppEligibleForBackup(PackageInfo targetPackage, boolean isFullBackup) throws RemoteException { - AndroidFuture<Boolean> resultFuture = new AndroidFuture<>(); + AndroidFuture<Boolean> resultFuture = mTransportFutures.newFuture(); mTransportBinder.isAppEligibleForBackup(targetPackage, isFullBackup, resultFuture); Boolean result = getFutureResult(resultFuture); return result != null && result; @@ -319,7 +319,7 @@ public class BackupTransportClient { * See {@link IBackupTransport#getBackupQuota(String, boolean)} */ public long getBackupQuota(String packageName, boolean isFullBackup) throws RemoteException { - AndroidFuture<Long> resultFuture = new AndroidFuture<>(); + AndroidFuture<Long> resultFuture = mTransportFutures.newFuture(); mTransportBinder.getBackupQuota(packageName, isFullBackup, resultFuture); Long result = getFutureResult(resultFuture); return result == null ? BackupTransport.TRANSPORT_ERROR : result; @@ -355,18 +355,58 @@ public class BackupTransportClient { * See {@link IBackupTransport#getTransportFlags()} */ public int getTransportFlags() throws RemoteException { - AndroidFuture<Integer> resultFuture = new AndroidFuture<>(); + AndroidFuture<Integer> resultFuture = mTransportFutures.newFuture(); mTransportBinder.getTransportFlags(resultFuture); Integer result = getFutureResult(resultFuture); return result == null ? BackupTransport.TRANSPORT_ERROR : result; } + /** + * Allows the {@link TransportConnection} to notify this client + * if the underlying transport has become unusable. If that happens + * we want to cancel all active futures or callbacks. + */ + void onBecomingUnusable() { + mCallbackPool.cancelActiveCallbacks(); + mTransportFutures.cancelActiveFutures(); + } + private <T> T getFutureResult(AndroidFuture<T> future) { try { return future.get(600, TimeUnit.SECONDS); } catch (InterruptedException | ExecutionException | TimeoutException e) { Slog.w(TAG, "Failed to get result from transport:", e); return null; + } finally { + mTransportFutures.remove(future); + } + } + + private static class TransportFutures { + private final Object mActiveFuturesLock = new Object(); + private final Set<AndroidFuture<?>> mActiveFutures = new HashSet<>(); + + <T> AndroidFuture<T> newFuture() { + AndroidFuture<T> future = new AndroidFuture<>(); + synchronized (mActiveFuturesLock) { + mActiveFutures.add(future); + } + return future; + } + + <T> void remove(AndroidFuture<T> future) { + synchronized (mActiveFuturesLock) { + mActiveFutures.remove(future); + } + } + + void cancelActiveFutures() { + synchronized (mActiveFuturesLock) { + for (AndroidFuture<?> future : mActiveFutures) { + future.cancel(true); + } + mActiveFutures.clear(); + } } } @@ -375,27 +415,47 @@ public class BackupTransportClient { private final Object mPoolLock = new Object(); private final Queue<TransportStatusCallback> mCallbackPool = new ArrayDeque<>(); + private final Set<TransportStatusCallback> mActiveCallbacks = new HashSet<>(); TransportStatusCallback acquire() { synchronized (mPoolLock) { - if (mCallbackPool.isEmpty()) { - return new TransportStatusCallback(); - } else { - return mCallbackPool.poll(); + TransportStatusCallback callback = mCallbackPool.poll(); + if (callback == null) { + callback = new TransportStatusCallback(); } + callback.reset(); + mActiveCallbacks.add(callback); + return callback; } } void recycle(TransportStatusCallback callback) { synchronized (mPoolLock) { + mActiveCallbacks.remove(callback); if (mCallbackPool.size() > MAX_POOL_SIZE) { Slog.d(TAG, "TransportStatusCallback pool size exceeded"); return; } - - callback.reset(); mCallbackPool.add(callback); } } + + void cancelActiveCallbacks() { + synchronized (mPoolLock) { + for (TransportStatusCallback callback : mActiveCallbacks) { + try { + callback.onOperationCompleteWithStatus(BackupTransport.TRANSPORT_ERROR); + // This waits for status to propagate before the callback is reset. + callback.getOperationStatus(); + } catch (RemoteException ex) { + // Nothing we can do. + } + if (mCallbackPool.size() < MAX_POOL_SIZE) { + mCallbackPool.add(callback); + } + } + mActiveCallbacks.clear(); + } + } } } diff --git a/services/backup/backuplib/java/com/android/server/backup/transport/TransportConnection.java b/services/backup/backuplib/java/com/android/server/backup/transport/TransportConnection.java index f9a3c36a3220..1009787bebe5 100644 --- a/services/backup/backuplib/java/com/android/server/backup/transport/TransportConnection.java +++ b/services/backup/backuplib/java/com/android/server/backup/transport/TransportConnection.java @@ -449,6 +449,9 @@ public class TransportConnection { private void onServiceDisconnected() { synchronized (mStateLock) { log(Priority.ERROR, "Service disconnected: client UNUSABLE"); + if (mTransport != null) { + mTransport.onBecomingUnusable(); + } setStateLocked(State.UNUSABLE, null); try { // After unbindService() no calls back to mConnection @@ -473,6 +476,9 @@ public class TransportConnection { checkStateIntegrityLocked(); log(Priority.ERROR, "Binding died: client UNUSABLE"); + if (mTransport != null) { + mTransport.onBecomingUnusable(); + } // After unbindService() no calls back to mConnection switch (mState) { case State.UNUSABLE: diff --git a/services/backup/backuplib/java/com/android/server/backup/transport/TransportStatusCallback.java b/services/backup/backuplib/java/com/android/server/backup/transport/TransportStatusCallback.java index a55178c27eef..bc5cb0250d56 100644 --- a/services/backup/backuplib/java/com/android/server/backup/transport/TransportStatusCallback.java +++ b/services/backup/backuplib/java/com/android/server/backup/transport/TransportStatusCallback.java @@ -75,13 +75,11 @@ public class TransportStatusCallback extends ITransportStatusCallback.Stub { } Slog.w(TAG, "Couldn't get operation status from transport"); - return BackupTransport.TRANSPORT_ERROR; } catch (InterruptedException e) { Slog.w(TAG, "Couldn't get operation status from transport: ", e); - return BackupTransport.TRANSPORT_ERROR; - } finally { - reset(); } + + return BackupTransport.TRANSPORT_ERROR; } synchronized void reset() { diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index 6813f3f83135..1c10304f7223 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -15623,24 +15623,20 @@ public class ActivityManagerService extends IActivityManager.Stub } for (int i = 0, size = processes.size(); i < size; i++) { ProcessRecord app = processes.get(i); - pw.println(String.format("------ DUMP RESOURCES %s (%s) ------", + pw.println(String.format("Resources History for %s (%s)", app.processName, app.info.packageName)); pw.flush(); try { - TransferPipe tp = new TransferPipe(); + TransferPipe tp = new TransferPipe(" "); try { IApplicationThread thread = app.getThread(); if (thread != null) { app.getThread().dumpResources(tp.getWriteFd(), null); tp.go(fd.getFileDescriptor(), 2000); - pw.println(String.format("------ END DUMP RESOURCES %s (%s) ------", - app.processName, - app.info.packageName)); - pw.flush(); } else { pw.println(String.format( - "------ DUMP RESOURCES %s (%s) failed, no thread ------", + " Resources history for %s (%s) failed, no thread", app.processName, app.info.packageName)); } @@ -15648,11 +15644,7 @@ public class ActivityManagerService extends IActivityManager.Stub tp.kill(); } } catch (IOException e) { - pw.println(String.format( - "------ EXCEPTION DUMPING RESOURCES for %s (%s): %s ------", - app.processName, - app.info.packageName, - e.getMessage())); + pw.println(" " + e.getMessage()); pw.flush(); } diff --git a/services/core/java/com/android/server/audio/BtHelper.java b/services/core/java/com/android/server/audio/BtHelper.java index 3491cd59ebb7..49a935ebadc9 100644 --- a/services/core/java/com/android/server/audio/BtHelper.java +++ b/services/core/java/com/android/server/audio/BtHelper.java @@ -479,6 +479,8 @@ public class BtHelper { } if (profile == BluetoothProfile.A2DP) { mA2dp = (BluetoothA2dp) proxy; + } else if (profile == BluetoothProfile.HEARING_AID) { + mHearingAid = (BluetoothHearingAid) proxy; } else if (profile == BluetoothProfile.LE_AUDIO) { mLeAudio = (BluetoothLeAudio) proxy; } diff --git a/services/core/java/com/android/server/connectivity/Vpn.java b/services/core/java/com/android/server/connectivity/Vpn.java index ea054a5b280c..682f0dfe067c 100644 --- a/services/core/java/com/android/server/connectivity/Vpn.java +++ b/services/core/java/com/android/server/connectivity/Vpn.java @@ -2020,7 +2020,8 @@ public class Vpn { .setCategory(Notification.CATEGORY_SYSTEM) .setVisibility(Notification.VISIBILITY_PUBLIC) .setOngoing(true) - .setColor(mContext.getColor(R.color.system_notification_accent_color)); + .setColor(mContext.getColor( + android.R.color.system_notification_accent_color)); notificationManager.notify(TAG, SystemMessage.NOTE_VPN_DISCONNECTED, builder.build()); } finally { Binder.restoreCallingIdentity(token); diff --git a/services/core/java/com/android/server/inputmethod/InputMethodBindingController.java b/services/core/java/com/android/server/inputmethod/InputMethodBindingController.java index b2f500a59ba9..d2d80ffde532 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodBindingController.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodBindingController.java @@ -408,6 +408,11 @@ final class InputMethodBindingController { @GuardedBy("ImfLock.class") @NonNull InputBindResult bindCurrentMethod() { + if (mSelectedMethodId == null) { + Slog.e(TAG, "mSelectedMethodId is null!"); + return InputBindResult.NO_IME; + } + InputMethodInfo info = mMethodMap.get(mSelectedMethodId); if (info == null) { throw new IllegalArgumentException("Unknown id: " + mSelectedMethodId); diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubServiceUtil.java b/services/core/java/com/android/server/location/contexthub/ContextHubServiceUtil.java index 8fdde2486401..e9bf90f1a82e 100644 --- a/services/core/java/com/android/server/location/contexthub/ContextHubServiceUtil.java +++ b/services/core/java/com/android/server/location/contexthub/ContextHubServiceUtil.java @@ -16,8 +16,6 @@ package com.android.server.location.contexthub; -import static android.content.pm.PackageManager.PERMISSION_GRANTED; - import android.Manifest; import android.content.Context; import android.hardware.contexthub.V1_0.AsyncEventType; @@ -297,19 +295,14 @@ import java.util.List; } /** - * Checks for location hardware permissions. + * Checks for ACCESS_CONTEXT_HUB permissions. * * @param context the context of the service */ /* package */ static void checkPermissions(Context context) { - boolean hasAccessContextHubPermission = (context.checkCallingPermission( - CONTEXT_HUB_PERMISSION) == PERMISSION_GRANTED); - - if (!hasAccessContextHubPermission) { - throw new SecurityException( - "ACCESS_CONTEXT_HUB permission required to use Context Hub"); - } + context.enforceCallingOrSelfPermission(CONTEXT_HUB_PERMISSION, + "ACCESS_CONTEXT_HUB permission required to use Context Hub"); } /** diff --git a/services/core/java/com/android/server/media/MediaSessionRecord.java b/services/core/java/com/android/server/media/MediaSessionRecord.java index 96391ac62530..074d891a9974 100644 --- a/services/core/java/com/android/server/media/MediaSessionRecord.java +++ b/services/core/java/com/android/server/media/MediaSessionRecord.java @@ -647,18 +647,17 @@ public class MediaSessionRecord implements IBinder.DeathRecipient, MediaSessionR if (mDestroyed) { return; } - toSend = new ArrayList<>(); - if (mQueue != null) { - toSend.ensureCapacity(mQueue.size()); - toSend.addAll(mQueue); - } + toSend = mQueue == null ? null : new ArrayList<>(mQueue); } Collection<ISessionControllerCallbackHolder> deadCallbackHolders = null; for (ISessionControllerCallbackHolder holder : mControllerCallbackHolders) { - ParceledListSlice<QueueItem> parcelableQueue = new ParceledListSlice<>(toSend); - // Limit the size of initial Parcel to prevent binder buffer overflow - // as onQueueChanged is an async binder call. - parcelableQueue.setInlineCountLimit(1); + ParceledListSlice<QueueItem> parcelableQueue = null; + if (toSend != null) { + parcelableQueue = new ParceledListSlice<>(toSend); + // Limit the size of initial Parcel to prevent binder buffer overflow + // as onQueueChanged is an async binder call. + parcelableQueue.setInlineCountLimit(1); + } try { holder.mCallback.onQueueChanged(parcelableQueue); diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java index c09c904ff931..db0b0c58b046 100644 --- a/services/core/java/com/android/server/pm/InstallPackageHelper.java +++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java @@ -183,7 +183,6 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Set; import java.util.concurrent.ExecutorService; @@ -1596,18 +1595,16 @@ final class InstallPackageHelper { parsedPackage.setRestrictUpdateHash(oldPackage.getRestrictUpdateHash()); } - // Check for shared user id changes - if (!Objects.equals(oldPackage.getSharedUserId(), - parsedPackage.getSharedUserId()) - // Don't mark as invalid if the app is trying to - // leave a sharedUserId - && parsedPackage.getSharedUserId() != null) { + // APK should not change its sharedUserId declarations + final var oldSharedUid = oldPackage.getSharedUserId() != null + ? oldPackage.getSharedUserId() : "<nothing>"; + final var newSharedUid = parsedPackage.getSharedUserId() != null + ? parsedPackage.getSharedUserId() : "<nothing>"; + if (!oldSharedUid.equals(newSharedUid)) { throw new PrepareFailure(INSTALL_FAILED_UID_CHANGED, "Package " + parsedPackage.getPackageName() + " shared user changed from " - + (oldPackage.getSharedUserId() != null - ? oldPackage.getSharedUserId() : "<nothing>") - + " to " + parsedPackage.getSharedUserId()); + + oldSharedUid + " to " + newSharedUid); } // In case of rollback, remember per-user/profile install state @@ -3696,10 +3693,13 @@ final class InstallPackageHelper { } disabledPkgSetting = mPm.mSettings.getDisabledSystemPkgLPr( parsedPackage.getPackageName()); - sharedUserSetting = (parsedPackage.getSharedUserId() != null) - ? mPm.mSettings.getSharedUserLPw(parsedPackage.getSharedUserId(), - 0 /*pkgFlags*/, 0 /*pkgPrivateFlags*/, true) - : null; + if (parsedPackage.getSharedUserId() != null && !parsedPackage.isLeavingSharedUid()) { + sharedUserSetting = mPm.mSettings.getSharedUserLPw( + parsedPackage.getSharedUserId(), + 0 /*pkgFlags*/, 0 /*pkgPrivateFlags*/, true /*create*/); + } else { + sharedUserSetting = null; + } if (DEBUG_PACKAGE_SCANNING && (parseFlags & ParsingPackageUtils.PARSE_CHATTY) != 0 && sharedUserSetting != null) { diff --git a/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackage.java b/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackage.java index cdc2b1245b1e..f6f9faf98c40 100644 --- a/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackage.java +++ b/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackage.java @@ -290,6 +290,9 @@ public interface ParsingPackage extends ParsingPackageRead { /** @see R#styleable.AndroidManifest_inheritKeyStoreKeys */ ParsingPackage setInheritKeyStoreKeys(boolean inheritKeyStoreKeys); + /** @see R#styleable.AndroidManifest_sharedUserMaxSdkVersion */ + ParsingPackage setLeavingSharedUid(boolean leavingSharedUid); + ParsingPackage setLabelRes(int labelRes); ParsingPackage setLargestWidthLimitDp(int largestWidthLimitDp); diff --git a/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackageImpl.java b/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackageImpl.java index 177eaca8e06f..67670272ef8b 100644 --- a/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackageImpl.java +++ b/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackageImpl.java @@ -549,6 +549,7 @@ public class ParsingPackageImpl implements ParsingPackage, ParsingPackageHidden, private static final long SDK_LIBRARY = 1L << 49; private static final long INHERIT_KEYSTORE_KEYS = 1L << 50; private static final long ENABLE_ON_BACK_INVOKED_CALLBACK = 1L << 51; + private static final long LEAVING_SHARED_UID = 1L << 52; } private ParsingPackageImpl setBoolean(@Booleans.Values long flag, boolean value) { @@ -2403,6 +2404,11 @@ public class ParsingPackageImpl implements ParsingPackage, ParsingPackageHidden, } @Override + public boolean isLeavingSharedUid() { + return getBoolean(Booleans.LEAVING_SHARED_UID); + } + + @Override public ParsingPackageImpl setBaseRevisionCode(int value) { baseRevisionCode = value; return this; @@ -2551,6 +2557,11 @@ public class ParsingPackageImpl implements ParsingPackage, ParsingPackageHidden, } @Override + public ParsingPackageImpl setLeavingSharedUid(boolean value) { + return setBoolean(Booleans.LEAVING_SHARED_UID, value); + } + + @Override public ParsingPackageImpl setLabelRes(int value) { labelRes = value; return this; diff --git a/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackageRead.java b/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackageRead.java index 428374fa21a8..50033f652bfd 100644 --- a/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackageRead.java +++ b/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackageRead.java @@ -360,4 +360,11 @@ public interface ParsingPackageRead extends PkgWithoutStateAppInfo, PkgWithoutSt * @see R.styleable.AndroidManifestApplication_enableOnBackInvokedCallback */ boolean isOnBackInvokedCallbackEnabled(); + + /** + * Returns true if R.styleable#AndroidManifest_sharedUserMaxSdkVersion is set to a value + * smaller than the current SDK version. + * @see R.styleable#AndroidManifest_sharedUserMaxSdkVersion + */ + boolean isLeavingSharedUid(); } diff --git a/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackageUtils.java b/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackageUtils.java index f30daa930e6c..ed1ab01e1d12 100644 --- a/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackageUtils.java +++ b/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackageUtils.java @@ -1032,11 +1032,6 @@ public class ParsingPackageUtils { private static ParseResult<ParsingPackage> parseSharedUser(ParseInput input, ParsingPackage pkg, TypedArray sa) { - int maxSdkVersion = anInteger(0, R.styleable.AndroidManifest_sharedUserMaxSdkVersion, sa); - if ((maxSdkVersion != 0) && maxSdkVersion < Build.VERSION.RESOURCES_SDK_INT) { - return input.success(pkg); - } - String str = nonConfigString(0, R.styleable.AndroidManifest_sharedUserId, sa); if (TextUtils.isEmpty(str)) { return input.success(pkg); @@ -1052,7 +1047,11 @@ public class ParsingPackageUtils { } } + int maxSdkVersion = anInteger(0, R.styleable.AndroidManifest_sharedUserMaxSdkVersion, sa); + boolean leaving = (maxSdkVersion != 0) && (maxSdkVersion < Build.VERSION.RESOURCES_SDK_INT); + return input.success(pkg + .setLeavingSharedUid(leaving) .setSharedUserId(str.intern()) .setSharedUserLabel(resId(R.styleable.AndroidManifest_sharedUserLabel, sa))); } diff --git a/services/core/java/com/android/server/vibrator/AbstractVibratorStep.java b/services/core/java/com/android/server/vibrator/AbstractVibratorStep.java new file mode 100644 index 000000000000..3550bda282dd --- /dev/null +++ b/services/core/java/com/android/server/vibrator/AbstractVibratorStep.java @@ -0,0 +1,148 @@ +/* + * 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.server.vibrator; + +import android.os.SystemClock; +import android.os.VibrationEffect; +import android.util.Slog; + +import java.util.Arrays; +import java.util.List; + +/** + * Represent a step on a single vibrator that plays one or more segments from a + * {@link VibrationEffect.Composed} effect. + */ +abstract class AbstractVibratorStep extends Step { + public final VibratorController controller; + public final VibrationEffect.Composed effect; + public final int segmentIndex; + public final long previousStepVibratorOffTimeout; + + long mVibratorOnResult; + boolean mVibratorCompleteCallbackReceived; + + /** + * @param conductor The VibrationStepConductor for these steps. + * @param startTime The time to schedule this step in the + * {@link VibrationStepConductor}. + * @param controller The vibrator that is playing the effect. + * @param effect The effect being played in this step. + * @param index The index of the next segment to be played by this step + * @param previousStepVibratorOffTimeout The time the vibrator is expected to complete any + * previous vibration and turn off. This is used to allow this step to + * be triggered when the completion callback is received, and can + * be used to play effects back-to-back. + */ + AbstractVibratorStep(VibrationStepConductor conductor, long startTime, + VibratorController controller, VibrationEffect.Composed effect, int index, + long previousStepVibratorOffTimeout) { + super(conductor, startTime); + this.controller = controller; + this.effect = effect; + this.segmentIndex = index; + this.previousStepVibratorOffTimeout = previousStepVibratorOffTimeout; + } + + public int getVibratorId() { + return controller.getVibratorInfo().getId(); + } + + @Override + public long getVibratorOnDuration() { + return mVibratorOnResult; + } + + @Override + public boolean acceptVibratorCompleteCallback(int vibratorId) { + boolean isSameVibrator = controller.getVibratorInfo().getId() == vibratorId; + mVibratorCompleteCallbackReceived |= isSameVibrator; + // Only activate this step if a timeout was set to wait for the vibration to complete, + // otherwise we are waiting for the correct time to play the next step. + return isSameVibrator && (previousStepVibratorOffTimeout > SystemClock.uptimeMillis()); + } + + @Override + public List<Step> cancel() { + return Arrays.asList(new CompleteEffectVibratorStep(conductor, SystemClock.uptimeMillis(), + /* cancelled= */ true, controller, previousStepVibratorOffTimeout)); + } + + @Override + public void cancelImmediately() { + if (previousStepVibratorOffTimeout > SystemClock.uptimeMillis()) { + // Vibrator might be running from previous steps, so turn it off while canceling. + stopVibrating(); + } + } + + protected void stopVibrating() { + if (VibrationThread.DEBUG) { + Slog.d(VibrationThread.TAG, + "Turning off vibrator " + getVibratorId()); + } + controller.off(); + } + + protected void changeAmplitude(float amplitude) { + if (VibrationThread.DEBUG) { + Slog.d(VibrationThread.TAG, + "Amplitude changed on vibrator " + getVibratorId() + " to " + amplitude); + } + controller.setAmplitude(amplitude); + } + + /** + * Return the {@link VibrationStepConductor#nextVibrateStep} with same timings, only jumping + * the segments. + */ + protected List<Step> skipToNextSteps(int segmentsSkipped) { + return nextSteps(startTime, previousStepVibratorOffTimeout, segmentsSkipped); + } + + /** + * Return the {@link VibrationStepConductor#nextVibrateStep} with same start and off timings + * calculated from {@link #getVibratorOnDuration()}, jumping all played segments. + * + * <p>This method has same behavior as {@link #skipToNextSteps(int)} when the vibrator + * result is non-positive, meaning the vibrator has either ignored or failed to turn on. + */ + protected List<Step> nextSteps(int segmentsPlayed) { + if (mVibratorOnResult <= 0) { + // Vibration was not started, so just skip the played segments and keep timings. + return skipToNextSteps(segmentsPlayed); + } + long nextStartTime = SystemClock.uptimeMillis() + mVibratorOnResult; + long nextVibratorOffTimeout = + nextStartTime + VibrationStepConductor.CALLBACKS_EXTRA_TIMEOUT; + return nextSteps(nextStartTime, nextVibratorOffTimeout, segmentsPlayed); + } + + /** + * Return the {@link VibrationStepConductor#nextVibrateStep} with given start and off timings, + * which might be calculated independently, jumping all played segments. + * + * <p>This should be used when the vibrator on/off state is not responsible for the steps + * execution timings, e.g. while playing the vibrator amplitudes. + */ + protected List<Step> nextSteps(long nextStartTime, long vibratorOffTimeout, + int segmentsPlayed) { + Step nextStep = conductor.nextVibrateStep(nextStartTime, controller, effect, + segmentIndex + segmentsPlayed, vibratorOffTimeout); + return nextStep == null ? VibrationStepConductor.EMPTY_STEP_LIST : Arrays.asList(nextStep); + } +} diff --git a/services/core/java/com/android/server/vibrator/CompleteEffectVibratorStep.java b/services/core/java/com/android/server/vibrator/CompleteEffectVibratorStep.java new file mode 100644 index 000000000000..8585e3473ef3 --- /dev/null +++ b/services/core/java/com/android/server/vibrator/CompleteEffectVibratorStep.java @@ -0,0 +1,114 @@ +/* + * 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.server.vibrator; + +import android.os.SystemClock; +import android.os.Trace; +import android.os.VibrationEffect; +import android.util.Slog; + +import java.util.Arrays; +import java.util.List; + +/** + * Represents a step to complete a {@link VibrationEffect}. + * + * <p>This runs right at the time the vibration is considered to end and will update the pending + * vibrators count. This can turn off the vibrator or slowly ramp it down to zero amplitude. + */ +final class CompleteEffectVibratorStep extends AbstractVibratorStep { + private final boolean mCancelled; + + CompleteEffectVibratorStep(VibrationStepConductor conductor, long startTime, boolean cancelled, + VibratorController controller, long previousStepVibratorOffTimeout) { + super(conductor, startTime, controller, /* effect= */ null, /* index= */ -1, + previousStepVibratorOffTimeout); + mCancelled = cancelled; + } + + @Override + public boolean isCleanUp() { + // If the vibration was cancelled then this is just a clean up to ramp off the vibrator. + // Otherwise this step is part of the vibration. + return mCancelled; + } + + @Override + public List<Step> cancel() { + if (mCancelled) { + // Double cancelling will just turn off the vibrator right away. + return Arrays.asList( + new TurnOffVibratorStep(conductor, SystemClock.uptimeMillis(), controller)); + } + return super.cancel(); + } + + @Override + public List<Step> play() { + Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "CompleteEffectVibratorStep"); + try { + if (VibrationThread.DEBUG) { + Slog.d(VibrationThread.TAG, + "Running " + (mCancelled ? "cancel" : "complete") + " vibration" + + " step on vibrator " + controller.getVibratorInfo().getId()); + } + if (mVibratorCompleteCallbackReceived) { + // Vibration completion callback was received by this step, just turn if off + // and skip any clean-up. + stopVibrating(); + return VibrationStepConductor.EMPTY_STEP_LIST; + } + + float currentAmplitude = controller.getCurrentAmplitude(); + long remainingOnDuration = + previousStepVibratorOffTimeout - VibrationStepConductor.CALLBACKS_EXTRA_TIMEOUT + - SystemClock.uptimeMillis(); + long rampDownDuration = + Math.min(remainingOnDuration, + conductor.vibrationSettings.getRampDownDuration()); + long stepDownDuration = conductor.vibrationSettings.getRampStepDuration(); + if (currentAmplitude < VibrationStepConductor.RAMP_OFF_AMPLITUDE_MIN + || rampDownDuration <= stepDownDuration) { + // No need to ramp down the amplitude, just wait to turn it off. + if (mCancelled) { + // Vibration is completing because it was cancelled, turn off right away. + stopVibrating(); + return VibrationStepConductor.EMPTY_STEP_LIST; + } else { + return Arrays.asList(new TurnOffVibratorStep( + conductor, previousStepVibratorOffTimeout, controller)); + } + } + + if (VibrationThread.DEBUG) { + Slog.d(VibrationThread.TAG, + "Ramping down vibrator " + controller.getVibratorInfo().getId() + + " from amplitude " + currentAmplitude + + " for " + rampDownDuration + "ms"); + } + float amplitudeDelta = currentAmplitude / (rampDownDuration / stepDownDuration); + float amplitudeTarget = currentAmplitude - amplitudeDelta; + long newVibratorOffTimeout = + mCancelled ? rampDownDuration : previousStepVibratorOffTimeout; + return Arrays.asList( + new RampOffVibratorStep(conductor, startTime, amplitudeTarget, amplitudeDelta, + controller, newVibratorOffTimeout)); + } finally { + Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR); + } + } +} diff --git a/services/core/java/com/android/server/vibrator/ComposePrimitivesVibratorStep.java b/services/core/java/com/android/server/vibrator/ComposePrimitivesVibratorStep.java new file mode 100644 index 000000000000..d1ea80557419 --- /dev/null +++ b/services/core/java/com/android/server/vibrator/ComposePrimitivesVibratorStep.java @@ -0,0 +1,84 @@ +/* + * 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.server.vibrator; + +import android.os.Trace; +import android.os.VibrationEffect; +import android.os.vibrator.PrimitiveSegment; +import android.os.vibrator.VibrationEffectSegment; +import android.util.Slog; + +import java.util.ArrayList; +import java.util.List; + +/** + * Represents a step to turn the vibrator on using a composition of primitives. + * + * <p>This step will use the maximum supported number of consecutive segments of type + * {@link PrimitiveSegment} starting at the current index. + */ +final class ComposePrimitivesVibratorStep extends AbstractVibratorStep { + + ComposePrimitivesVibratorStep(VibrationStepConductor conductor, long startTime, + VibratorController controller, VibrationEffect.Composed effect, int index, + long previousStepVibratorOffTimeout) { + // This step should wait for the last vibration to finish (with the timeout) and for the + // intended step start time (to respect the effect delays). + super(conductor, Math.max(startTime, previousStepVibratorOffTimeout), controller, effect, + index, previousStepVibratorOffTimeout); + } + + @Override + public List<Step> play() { + Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "ComposePrimitivesStep"); + try { + // Load the next PrimitiveSegments to create a single compose call to the vibrator, + // limited to the vibrator composition maximum size. + int limit = controller.getVibratorInfo().getCompositionSizeMax(); + int segmentCount = limit > 0 + ? Math.min(effect.getSegments().size(), segmentIndex + limit) + : effect.getSegments().size(); + List<PrimitiveSegment> primitives = new ArrayList<>(); + for (int i = segmentIndex; i < segmentCount; i++) { + VibrationEffectSegment segment = effect.getSegments().get(i); + if (segment instanceof PrimitiveSegment) { + primitives.add((PrimitiveSegment) segment); + } else { + break; + } + } + + if (primitives.isEmpty()) { + Slog.w(VibrationThread.TAG, "Ignoring wrong segment for a ComposePrimitivesStep: " + + effect.getSegments().get(segmentIndex)); + return skipToNextSteps(/* segmentsSkipped= */ 1); + } + + if (VibrationThread.DEBUG) { + Slog.d(VibrationThread.TAG, "Compose " + primitives + " primitives on vibrator " + + controller.getVibratorInfo().getId()); + } + mVibratorOnResult = controller.on( + primitives.toArray(new PrimitiveSegment[primitives.size()]), + getVibration().id); + + return nextSteps(/* segmentsPlayed= */ primitives.size()); + } finally { + Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR); + } + } +} diff --git a/services/core/java/com/android/server/vibrator/ComposePwleVibratorStep.java b/services/core/java/com/android/server/vibrator/ComposePwleVibratorStep.java new file mode 100644 index 000000000000..73bf933f8bc9 --- /dev/null +++ b/services/core/java/com/android/server/vibrator/ComposePwleVibratorStep.java @@ -0,0 +1,84 @@ +/* + * 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.server.vibrator; + +import android.os.Trace; +import android.os.VibrationEffect; +import android.os.vibrator.RampSegment; +import android.os.vibrator.StepSegment; +import android.os.vibrator.VibrationEffectSegment; +import android.util.Slog; + +import java.util.ArrayList; +import java.util.List; + +/** + * Represents a step to turn the vibrator on using a composition of PWLE segments. + * + * <p>This step will use the maximum supported number of consecutive segments of type + * {@link StepSegment} or {@link RampSegment} starting at the current index. + */ +final class ComposePwleVibratorStep extends AbstractVibratorStep { + + ComposePwleVibratorStep(VibrationStepConductor conductor, long startTime, + VibratorController controller, VibrationEffect.Composed effect, int index, + long previousStepVibratorOffTimeout) { + // This step should wait for the last vibration to finish (with the timeout) and for the + // intended step start time (to respect the effect delays). + super(conductor, Math.max(startTime, previousStepVibratorOffTimeout), controller, effect, + index, previousStepVibratorOffTimeout); + } + + @Override + public List<Step> play() { + Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "ComposePwleStep"); + try { + // Load the next RampSegments to create a single composePwle call to the vibrator, + // limited to the vibrator PWLE maximum size. + int limit = controller.getVibratorInfo().getPwleSizeMax(); + int segmentCount = limit > 0 + ? Math.min(effect.getSegments().size(), segmentIndex + limit) + : effect.getSegments().size(); + List<RampSegment> pwles = new ArrayList<>(); + for (int i = segmentIndex; i < segmentCount; i++) { + VibrationEffectSegment segment = effect.getSegments().get(i); + if (segment instanceof RampSegment) { + pwles.add((RampSegment) segment); + } else { + break; + } + } + + if (pwles.isEmpty()) { + Slog.w(VibrationThread.TAG, "Ignoring wrong segment for a ComposePwleStep: " + + effect.getSegments().get(segmentIndex)); + return skipToNextSteps(/* segmentsSkipped= */ 1); + } + + if (VibrationThread.DEBUG) { + Slog.d(VibrationThread.TAG, "Compose " + pwles + " PWLEs on vibrator " + + controller.getVibratorInfo().getId()); + } + mVibratorOnResult = controller.on(pwles.toArray(new RampSegment[pwles.size()]), + getVibration().id); + + return nextSteps(/* segmentsPlayed= */ pwles.size()); + } finally { + Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR); + } + } +} diff --git a/services/core/java/com/android/server/vibrator/FinishSequentialEffectStep.java b/services/core/java/com/android/server/vibrator/FinishSequentialEffectStep.java new file mode 100644 index 000000000000..bbbca024214f --- /dev/null +++ b/services/core/java/com/android/server/vibrator/FinishSequentialEffectStep.java @@ -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. + */ + +package com.android.server.vibrator; + +import android.os.Trace; +import android.util.Slog; + +import java.util.Arrays; +import java.util.List; + +/** + * Finish a sync vibration started by a {@link StartSequentialEffectStep}. + * + * <p>This only plays after all active vibrators steps have finished, and adds a {@link + * StartSequentialEffectStep} to the queue if the sequential effect isn't finished yet. + */ +final class FinishSequentialEffectStep extends Step { + public final StartSequentialEffectStep startedStep; + + FinishSequentialEffectStep(StartSequentialEffectStep startedStep) { + // No predefined startTime, just wait for all steps in the queue. + super(startedStep.conductor, Long.MAX_VALUE); + this.startedStep = startedStep; + } + + @Override + public boolean isCleanUp() { + // This step only notes that all the vibrators has been turned off. + return true; + } + + @Override + public List<Step> play() { + Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "FinishSequentialEffectStep"); + try { + if (VibrationThread.DEBUG) { + Slog.d(VibrationThread.TAG, + "FinishSequentialEffectStep for effect #" + startedStep.currentIndex); + } + conductor.vibratorManagerHooks.noteVibratorOff(conductor.getVibration().uid); + Step nextStep = startedStep.nextStep(); + return nextStep == null ? VibrationStepConductor.EMPTY_STEP_LIST + : Arrays.asList(nextStep); + } finally { + Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR); + } + } + + @Override + public List<Step> cancel() { + cancelImmediately(); + return VibrationStepConductor.EMPTY_STEP_LIST; + } + + @Override + public void cancelImmediately() { + conductor.vibratorManagerHooks.noteVibratorOff(conductor.getVibration().uid); + } +} diff --git a/services/core/java/com/android/server/vibrator/PerformPrebakedVibratorStep.java b/services/core/java/com/android/server/vibrator/PerformPrebakedVibratorStep.java new file mode 100644 index 000000000000..601ae978f637 --- /dev/null +++ b/services/core/java/com/android/server/vibrator/PerformPrebakedVibratorStep.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.server.vibrator; + +import android.os.Trace; +import android.os.VibrationEffect; +import android.os.vibrator.PrebakedSegment; +import android.os.vibrator.VibrationEffectSegment; +import android.util.Slog; + +import java.util.ArrayList; +import java.util.List; + +/** + * Represents a step to turn the vibrator on with a single prebaked effect. + * + * <p>This step automatically falls back by replacing the prebaked segment with + * {@link VibrationSettings#getFallbackEffect(int)}, if available. + */ +final class PerformPrebakedVibratorStep extends AbstractVibratorStep { + + PerformPrebakedVibratorStep(VibrationStepConductor conductor, long startTime, + VibratorController controller, VibrationEffect.Composed effect, int index, + long previousStepVibratorOffTimeout) { + // This step should wait for the last vibration to finish (with the timeout) and for the + // intended step start time (to respect the effect delays). + super(conductor, Math.max(startTime, previousStepVibratorOffTimeout), controller, effect, + index, previousStepVibratorOffTimeout); + } + + @Override + public List<Step> play() { + Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "PerformPrebakedVibratorStep"); + try { + VibrationEffectSegment segment = effect.getSegments().get(segmentIndex); + if (!(segment instanceof PrebakedSegment)) { + Slog.w(VibrationThread.TAG, "Ignoring wrong segment for a " + + "PerformPrebakedVibratorStep: " + segment); + return skipToNextSteps(/* segmentsSkipped= */ 1); + } + + PrebakedSegment prebaked = (PrebakedSegment) segment; + if (VibrationThread.DEBUG) { + Slog.d(VibrationThread.TAG, "Perform " + VibrationEffect.effectIdToString( + prebaked.getEffectId()) + " on vibrator " + + controller.getVibratorInfo().getId()); + } + + VibrationEffect fallback = getVibration().getFallback(prebaked.getEffectId()); + mVibratorOnResult = controller.on(prebaked, getVibration().id); + + if (mVibratorOnResult == 0 && prebaked.shouldFallback() + && (fallback instanceof VibrationEffect.Composed)) { + if (VibrationThread.DEBUG) { + Slog.d(VibrationThread.TAG, "Playing fallback for effect " + + VibrationEffect.effectIdToString(prebaked.getEffectId())); + } + AbstractVibratorStep fallbackStep = conductor.nextVibrateStep(startTime, controller, + replaceCurrentSegment((VibrationEffect.Composed) fallback), + segmentIndex, previousStepVibratorOffTimeout); + List<Step> fallbackResult = fallbackStep.play(); + // Update the result with the fallback result so this step is seamlessly + // replaced by the fallback to any outer application of this. + mVibratorOnResult = fallbackStep.getVibratorOnDuration(); + return fallbackResult; + } + + return nextSteps(/* segmentsPlayed= */ 1); + } finally { + Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR); + } + } + + /** + * Replace segment at {@link #segmentIndex} in {@link #effect} with given fallback segments. + * + * @return a copy of {@link #effect} with replaced segment. + */ + private VibrationEffect.Composed replaceCurrentSegment(VibrationEffect.Composed fallback) { + List<VibrationEffectSegment> newSegments = new ArrayList<>(effect.getSegments()); + int newRepeatIndex = effect.getRepeatIndex(); + newSegments.remove(segmentIndex); + newSegments.addAll(segmentIndex, fallback.getSegments()); + if (segmentIndex < effect.getRepeatIndex()) { + newRepeatIndex += fallback.getSegments().size() - 1; + } + return new VibrationEffect.Composed(newSegments, newRepeatIndex); + } +} diff --git a/services/core/java/com/android/server/vibrator/RampOffVibratorStep.java b/services/core/java/com/android/server/vibrator/RampOffVibratorStep.java new file mode 100644 index 000000000000..8cf5fb394d9d --- /dev/null +++ b/services/core/java/com/android/server/vibrator/RampOffVibratorStep.java @@ -0,0 +1,84 @@ +/* + * 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.server.vibrator; + +import android.os.SystemClock; +import android.os.Trace; +import android.util.Slog; + +import java.util.Arrays; +import java.util.List; + +/** Represents a step to ramp down the vibrator amplitude before turning it off. */ +final class RampOffVibratorStep extends AbstractVibratorStep { + private final float mAmplitudeTarget; + private final float mAmplitudeDelta; + + RampOffVibratorStep(VibrationStepConductor conductor, long startTime, float amplitudeTarget, + float amplitudeDelta, VibratorController controller, + long previousStepVibratorOffTimeout) { + super(conductor, startTime, controller, /* effect= */ null, /* index= */ -1, + previousStepVibratorOffTimeout); + mAmplitudeTarget = amplitudeTarget; + mAmplitudeDelta = amplitudeDelta; + } + + @Override + public boolean isCleanUp() { + return true; + } + + @Override + public List<Step> cancel() { + return Arrays.asList( + new TurnOffVibratorStep(conductor, SystemClock.uptimeMillis(), controller)); + } + + @Override + public List<Step> play() { + Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "RampOffVibratorStep"); + try { + if (VibrationThread.DEBUG) { + long latency = SystemClock.uptimeMillis() - startTime; + Slog.d(VibrationThread.TAG, "Ramp down the vibrator amplitude, step with " + + latency + "ms latency."); + } + if (mVibratorCompleteCallbackReceived) { + // Vibration completion callback was received by this step, just turn if off + // and skip the rest of the steps to ramp down the vibrator amplitude. + stopVibrating(); + return VibrationStepConductor.EMPTY_STEP_LIST; + } + + changeAmplitude(mAmplitudeTarget); + + float newAmplitudeTarget = mAmplitudeTarget - mAmplitudeDelta; + if (newAmplitudeTarget < VibrationStepConductor.RAMP_OFF_AMPLITUDE_MIN) { + // Vibrator amplitude cannot go further down, just turn it off. + return Arrays.asList(new TurnOffVibratorStep( + conductor, previousStepVibratorOffTimeout, controller)); + } + return Arrays.asList(new RampOffVibratorStep( + conductor, + startTime + conductor.vibrationSettings.getRampStepDuration(), + newAmplitudeTarget, mAmplitudeDelta, controller, + previousStepVibratorOffTimeout)); + } finally { + Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR); + } + } +} diff --git a/services/core/java/com/android/server/vibrator/SetAmplitudeVibratorStep.java b/services/core/java/com/android/server/vibrator/SetAmplitudeVibratorStep.java new file mode 100644 index 000000000000..d5c11161bdfa --- /dev/null +++ b/services/core/java/com/android/server/vibrator/SetAmplitudeVibratorStep.java @@ -0,0 +1,186 @@ +/* + * 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.server.vibrator; + +import android.os.SystemClock; +import android.os.Trace; +import android.os.VibrationEffect; +import android.os.vibrator.StepSegment; +import android.os.vibrator.VibrationEffectSegment; +import android.util.Slog; + +import java.util.Arrays; +import java.util.List; + +/** + * Represents a step to turn the vibrator on and change its amplitude. + * + * <p>This step ignores vibration completion callbacks and control the vibrator on/off state + * and amplitude to simulate waveforms represented by a sequence of {@link StepSegment}. + */ +final class SetAmplitudeVibratorStep extends AbstractVibratorStep { + private long mNextOffTime; + + SetAmplitudeVibratorStep(VibrationStepConductor conductor, long startTime, + VibratorController controller, VibrationEffect.Composed effect, int index, + long previousStepVibratorOffTimeout) { + // This step has a fixed startTime coming from the timings of the waveform it's playing. + super(conductor, startTime, controller, effect, index, previousStepVibratorOffTimeout); + mNextOffTime = previousStepVibratorOffTimeout; + } + + @Override + public boolean acceptVibratorCompleteCallback(int vibratorId) { + if (controller.getVibratorInfo().getId() == vibratorId) { + mVibratorCompleteCallbackReceived = true; + mNextOffTime = SystemClock.uptimeMillis(); + } + // Timings are tightly controlled here, so only trigger this step if the vibrator was + // supposed to be ON but has completed prematurely, to turn it back on as soon as + // possible. + return mNextOffTime < startTime && controller.getCurrentAmplitude() > 0; + } + + @Override + public List<Step> play() { + // TODO: consider separating the "on" steps at the start into a separate Step. + // TODO: consider instantiating the step with the required amplitude, rather than + // needing to dig into the effect. + Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "SetAmplitudeVibratorStep"); + try { + long now = SystemClock.uptimeMillis(); + long latency = now - startTime; + if (VibrationThread.DEBUG) { + Slog.d(VibrationThread.TAG, + "Running amplitude step with " + latency + "ms latency."); + } + + if (mVibratorCompleteCallbackReceived && latency < 0) { + // This step was run early because the vibrator turned off prematurely. + // Turn it back on and return this same step to run at the exact right time. + mNextOffTime = turnVibratorBackOn(/* remainingDuration= */ -latency); + return Arrays.asList(new SetAmplitudeVibratorStep(conductor, startTime, controller, + effect, segmentIndex, mNextOffTime)); + } + + VibrationEffectSegment segment = effect.getSegments().get(segmentIndex); + if (!(segment instanceof StepSegment)) { + Slog.w(VibrationThread.TAG, + "Ignoring wrong segment for a SetAmplitudeVibratorStep: " + segment); + return skipToNextSteps(/* segmentsSkipped= */ 1); + } + + StepSegment stepSegment = (StepSegment) segment; + if (stepSegment.getDuration() == 0) { + // Skip waveform entries with zero timing. + return skipToNextSteps(/* segmentsSkipped= */ 1); + } + + float amplitude = stepSegment.getAmplitude(); + if (amplitude == 0) { + if (previousStepVibratorOffTimeout > now) { + // Amplitude cannot be set to zero, so stop the vibrator. + stopVibrating(); + mNextOffTime = now; + } + } else { + if (startTime >= mNextOffTime) { + // Vibrator is OFF. Turn vibrator back on for the duration of another + // cycle before setting the amplitude. + long onDuration = getVibratorOnDuration(effect, segmentIndex); + if (onDuration > 0) { + mVibratorOnResult = startVibrating(onDuration); + mNextOffTime = now + onDuration + + VibrationStepConductor.CALLBACKS_EXTRA_TIMEOUT; + } + } + changeAmplitude(amplitude); + } + + // Use original startTime to avoid propagating latencies to the waveform. + long nextStartTime = startTime + segment.getDuration(); + return nextSteps(nextStartTime, mNextOffTime, /* segmentsPlayed= */ 1); + } finally { + Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR); + } + } + + private long turnVibratorBackOn(long remainingDuration) { + long onDuration = getVibratorOnDuration(effect, segmentIndex); + if (onDuration <= 0) { + // Vibrator is supposed to go back off when this step starts, so just leave it off. + return previousStepVibratorOffTimeout; + } + onDuration += remainingDuration; + float expectedAmplitude = controller.getCurrentAmplitude(); + mVibratorOnResult = startVibrating(onDuration); + if (mVibratorOnResult > 0) { + // Set the amplitude back to the value it was supposed to be playing at. + changeAmplitude(expectedAmplitude); + } + return SystemClock.uptimeMillis() + onDuration + + VibrationStepConductor.CALLBACKS_EXTRA_TIMEOUT; + } + + private long startVibrating(long duration) { + if (VibrationThread.DEBUG) { + Slog.d(VibrationThread.TAG, + "Turning on vibrator " + controller.getVibratorInfo().getId() + " for " + + duration + "ms"); + } + return controller.on(duration, getVibration().id); + } + + /** + * Get the duration the vibrator will be on for a waveform, starting at {@code startIndex} + * until the next time it's vibrating amplitude is zero or a different type of segment is + * found. + */ + private long getVibratorOnDuration(VibrationEffect.Composed effect, int startIndex) { + List<VibrationEffectSegment> segments = effect.getSegments(); + int segmentCount = segments.size(); + int repeatIndex = effect.getRepeatIndex(); + int i = startIndex; + long timing = 0; + while (i < segmentCount) { + VibrationEffectSegment segment = segments.get(i); + if (!(segment instanceof StepSegment) + || ((StepSegment) segment).getAmplitude() == 0) { + break; + } + timing += segment.getDuration(); + i++; + if (i == segmentCount && repeatIndex >= 0) { + i = repeatIndex; + // prevent infinite loop + repeatIndex = -1; + } + if (i == startIndex) { + // The repeating waveform keeps the vibrator ON all the time. Use a minimum + // of 1s duration to prevent short patterns from turning the vibrator ON too + // frequently. + return Math.max(timing, 1000); + } + } + if (i == segmentCount && effect.getRepeatIndex() < 0) { + // Vibration ending at non-zero amplitude, add extra timings to ramp down after + // vibration is complete. + timing += conductor.vibrationSettings.getRampDownDuration(); + } + return timing; + } +} diff --git a/services/core/java/com/android/server/vibrator/StartSequentialEffectStep.java b/services/core/java/com/android/server/vibrator/StartSequentialEffectStep.java new file mode 100644 index 000000000000..b8885e81dbe2 --- /dev/null +++ b/services/core/java/com/android/server/vibrator/StartSequentialEffectStep.java @@ -0,0 +1,369 @@ +/* + * 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.server.vibrator; + +import android.annotation.Nullable; +import android.hardware.vibrator.IVibratorManager; +import android.os.CombinedVibration; +import android.os.SystemClock; +import android.os.Trace; +import android.os.VibrationEffect; +import android.os.VibratorInfo; +import android.os.vibrator.PrebakedSegment; +import android.os.vibrator.PrimitiveSegment; +import android.os.vibrator.StepSegment; +import android.os.vibrator.VibrationEffectSegment; +import android.util.Slog; +import android.util.SparseArray; + +import java.util.ArrayList; +import java.util.List; + +/** + * Starts a sync vibration. + * + * <p>If this step has successfully started playing a vibration on any vibrator, it will always + * add a {@link FinishSequentialEffectStep} to the queue, to be played after all vibrators + * have finished all their individual steps. + * + * <p>If this step does not start any vibrator, it will add a {@link StartSequentialEffectStep} if + * the sequential effect isn't finished yet. + * + * <p>TODO: this step actually does several things: multiple HAL calls to sync the vibrators, + * as well as dispatching the underlying vibrator instruction calls (which need to be done before + * triggering the synced effects). This role/encapsulation could probably be improved to split up + * the grouped HAL calls here, as well as to clarify the role of dispatching VibratorSteps between + * this class and the controller. + */ +final class StartSequentialEffectStep extends Step { + public final CombinedVibration.Sequential sequentialEffect; + public final int currentIndex; + + private long mVibratorsOnMaxDuration; + + StartSequentialEffectStep(VibrationStepConductor conductor, + CombinedVibration.Sequential effect) { + this(conductor, SystemClock.uptimeMillis() + effect.getDelays().get(0), effect, + /* index= */ 0); + } + + StartSequentialEffectStep(VibrationStepConductor conductor, long startTime, + CombinedVibration.Sequential effect, int index) { + super(conductor, startTime); + sequentialEffect = effect; + currentIndex = index; + } + + @Override + public long getVibratorOnDuration() { + return mVibratorsOnMaxDuration; + } + + @Override + public List<Step> play() { + Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "StartSequentialEffectStep"); + List<Step> nextSteps = new ArrayList<>(); + mVibratorsOnMaxDuration = -1; + try { + if (VibrationThread.DEBUG) { + Slog.d(VibrationThread.TAG, + "StartSequentialEffectStep for effect #" + currentIndex); + } + CombinedVibration effect = sequentialEffect.getEffects().get(currentIndex); + DeviceEffectMap effectMapping = createEffectToVibratorMapping(effect); + if (effectMapping == null) { + // Unable to map effects to vibrators, ignore this step. + return nextSteps; + } + + mVibratorsOnMaxDuration = startVibrating(effectMapping, nextSteps); + if (mVibratorsOnMaxDuration > 0) { + conductor.vibratorManagerHooks.noteVibratorOn(conductor.getVibration().uid, + mVibratorsOnMaxDuration); + } + } finally { + if (mVibratorsOnMaxDuration >= 0) { + // It least one vibrator was started then add a finish step to wait for all + // active vibrators to finish their individual steps before going to the next. + // Otherwise this step was ignored so just go to the next one. + Step nextStep = + mVibratorsOnMaxDuration > 0 ? new FinishSequentialEffectStep(this) + : nextStep(); + if (nextStep != null) { + nextSteps.add(nextStep); + } + } + Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR); + } + return nextSteps; + } + + @Override + public List<Step> cancel() { + return VibrationStepConductor.EMPTY_STEP_LIST; + } + + @Override + public void cancelImmediately() { + } + + /** + * Create the next {@link StartSequentialEffectStep} to play this sequential effect, starting at + * the + * time this method is called, or null if sequence is complete. + */ + @Nullable + Step nextStep() { + int nextIndex = currentIndex + 1; + if (nextIndex >= sequentialEffect.getEffects().size()) { + return null; + } + long nextEffectDelay = sequentialEffect.getDelays().get(nextIndex); + long nextStartTime = SystemClock.uptimeMillis() + nextEffectDelay; + return new StartSequentialEffectStep(conductor, nextStartTime, sequentialEffect, + nextIndex); + } + + /** Create a mapping of individual {@link VibrationEffect} to available vibrators. */ + @Nullable + private DeviceEffectMap createEffectToVibratorMapping( + CombinedVibration effect) { + if (effect instanceof CombinedVibration.Mono) { + return new DeviceEffectMap((CombinedVibration.Mono) effect); + } + if (effect instanceof CombinedVibration.Stereo) { + return new DeviceEffectMap((CombinedVibration.Stereo) effect); + } + return null; + } + + /** + * Starts playing effects on designated vibrators, in sync. + * + * @param effectMapping The {@link CombinedVibration} mapped to this device vibrators + * @param nextSteps An output list to accumulate the future {@link Step + * Steps} created + * by this method, typically one for each vibrator that has + * successfully started vibrating on this step. + * @return The duration, in millis, of the {@link CombinedVibration}. Repeating + * waveforms return {@link Long#MAX_VALUE}. Zero or negative values indicate the vibrators + * have ignored all effects. + */ + private long startVibrating( + DeviceEffectMap effectMapping, List<Step> nextSteps) { + int vibratorCount = effectMapping.size(); + if (vibratorCount == 0) { + // No effect was mapped to any available vibrator. + return 0; + } + + AbstractVibratorStep[] steps = new AbstractVibratorStep[vibratorCount]; + long vibrationStartTime = SystemClock.uptimeMillis(); + for (int i = 0; i < vibratorCount; i++) { + steps[i] = conductor.nextVibrateStep(vibrationStartTime, + conductor.getVibrators().get(effectMapping.vibratorIdAt(i)), + effectMapping.effectAt(i), + /* segmentIndex= */ 0, /* vibratorOffTimeout= */ 0); + } + + if (steps.length == 1) { + // No need to prepare and trigger sync effects on a single vibrator. + return startVibrating(steps[0], nextSteps); + } + + // This synchronization of vibrators should be executed one at a time, even if we are + // vibrating different sets of vibrators in parallel. The manager can only prepareSynced + // one set of vibrators at a time. + // This property is guaranteed by there only being one thread (VibrationThread) executing + // one Step at a time, so there's no need to hold the state lock. Callbacks will be + // delivered asynchronously but enqueued until the step processing is finished. + boolean hasPrepared = false; + boolean hasTriggered = false; + long maxDuration = 0; + try { + hasPrepared = conductor.vibratorManagerHooks.prepareSyncedVibration( + effectMapping.getRequiredSyncCapabilities(), + effectMapping.getVibratorIds()); + + for (AbstractVibratorStep step : steps) { + long duration = startVibrating(step, nextSteps); + if (duration < 0) { + // One vibrator has failed, fail this entire sync attempt. + return maxDuration = -1; + } + maxDuration = Math.max(maxDuration, duration); + } + + // Check if sync was prepared and if any step was accepted by a vibrator, + // otherwise there is nothing to trigger here. + if (hasPrepared && maxDuration > 0) { + hasTriggered = conductor.vibratorManagerHooks.triggerSyncedVibration( + getVibration().id); + } + return maxDuration; + } finally { + if (hasPrepared && !hasTriggered) { + // Trigger has failed or all steps were ignored by the vibrators. + conductor.vibratorManagerHooks.cancelSyncedVibration(); + nextSteps.clear(); + } else if (maxDuration < 0) { + // Some vibrator failed without being prepared so other vibrators might be + // active. Cancel and remove every pending step from output list. + for (int i = nextSteps.size() - 1; i >= 0; i--) { + nextSteps.remove(i).cancelImmediately(); + } + } + } + } + + private long startVibrating(AbstractVibratorStep step, List<Step> nextSteps) { + nextSteps.addAll(step.play()); + long stepDuration = step.getVibratorOnDuration(); + if (stepDuration < 0) { + // Step failed, so return negative duration to propagate failure. + return stepDuration; + } + // Return the longest estimation for the entire effect. + return Math.max(stepDuration, step.effect.getDuration()); + } + + /** + * Map a {@link CombinedVibration} to the vibrators available on the device. + * + * <p>This contains the logic to find the capabilities required from {@link IVibratorManager} to + * play all of the effects in sync. + */ + final class DeviceEffectMap { + private final SparseArray<VibrationEffect.Composed> mVibratorEffects; + private final int[] mVibratorIds; + private final long mRequiredSyncCapabilities; + + DeviceEffectMap(CombinedVibration.Mono mono) { + SparseArray<VibratorController> vibrators = conductor.getVibrators(); + mVibratorEffects = new SparseArray<>(vibrators.size()); + mVibratorIds = new int[vibrators.size()]; + for (int i = 0; i < vibrators.size(); i++) { + int vibratorId = vibrators.keyAt(i); + VibratorInfo vibratorInfo = vibrators.valueAt(i).getVibratorInfo(); + VibrationEffect effect = conductor.deviceEffectAdapter.apply( + mono.getEffect(), vibratorInfo); + if (effect instanceof VibrationEffect.Composed) { + mVibratorEffects.put(vibratorId, (VibrationEffect.Composed) effect); + mVibratorIds[i] = vibratorId; + } + } + mRequiredSyncCapabilities = calculateRequiredSyncCapabilities(mVibratorEffects); + } + + DeviceEffectMap(CombinedVibration.Stereo stereo) { + SparseArray<VibratorController> vibrators = conductor.getVibrators(); + SparseArray<VibrationEffect> stereoEffects = stereo.getEffects(); + mVibratorEffects = new SparseArray<>(); + for (int i = 0; i < stereoEffects.size(); i++) { + int vibratorId = stereoEffects.keyAt(i); + if (vibrators.contains(vibratorId)) { + VibratorInfo vibratorInfo = vibrators.valueAt(i).getVibratorInfo(); + VibrationEffect effect = conductor.deviceEffectAdapter.apply( + stereoEffects.valueAt(i), vibratorInfo); + if (effect instanceof VibrationEffect.Composed) { + mVibratorEffects.put(vibratorId, (VibrationEffect.Composed) effect); + } + } + } + mVibratorIds = new int[mVibratorEffects.size()]; + for (int i = 0; i < mVibratorEffects.size(); i++) { + mVibratorIds[i] = mVibratorEffects.keyAt(i); + } + mRequiredSyncCapabilities = calculateRequiredSyncCapabilities(mVibratorEffects); + } + + /** + * Return the number of vibrators mapped to play the {@link CombinedVibration} on this + * device. + */ + public int size() { + return mVibratorIds.length; + } + + /** + * Return all capabilities required to play the {@link CombinedVibration} in + * between calls to {@link IVibratorManager#prepareSynced} and + * {@link IVibratorManager#triggerSynced}. + */ + public long getRequiredSyncCapabilities() { + return mRequiredSyncCapabilities; + } + + /** Return all vibrator ids mapped to play the {@link CombinedVibration}. */ + public int[] getVibratorIds() { + return mVibratorIds; + } + + /** Return the id of the vibrator at given index. */ + public int vibratorIdAt(int index) { + return mVibratorEffects.keyAt(index); + } + + /** Return the {@link VibrationEffect} at given index. */ + public VibrationEffect.Composed effectAt(int index) { + return mVibratorEffects.valueAt(index); + } + + /** + * Return all capabilities required from the {@link IVibratorManager} to prepare and + * trigger all given effects in sync. + * + * @return {@link IVibratorManager#CAP_SYNC} together with all required + * IVibratorManager.CAP_PREPARE_* and IVibratorManager.CAP_MIXED_TRIGGER_* capabilities. + */ + private long calculateRequiredSyncCapabilities( + SparseArray<VibrationEffect.Composed> effects) { + long prepareCap = 0; + for (int i = 0; i < effects.size(); i++) { + VibrationEffectSegment firstSegment = effects.valueAt(i).getSegments().get(0); + if (firstSegment instanceof StepSegment) { + prepareCap |= IVibratorManager.CAP_PREPARE_ON; + } else if (firstSegment instanceof PrebakedSegment) { + prepareCap |= IVibratorManager.CAP_PREPARE_PERFORM; + } else if (firstSegment instanceof PrimitiveSegment) { + prepareCap |= IVibratorManager.CAP_PREPARE_COMPOSE; + } + } + int triggerCap = 0; + if (requireMixedTriggerCapability(prepareCap, IVibratorManager.CAP_PREPARE_ON)) { + triggerCap |= IVibratorManager.CAP_MIXED_TRIGGER_ON; + } + if (requireMixedTriggerCapability(prepareCap, IVibratorManager.CAP_PREPARE_PERFORM)) { + triggerCap |= IVibratorManager.CAP_MIXED_TRIGGER_PERFORM; + } + if (requireMixedTriggerCapability(prepareCap, IVibratorManager.CAP_PREPARE_COMPOSE)) { + triggerCap |= IVibratorManager.CAP_MIXED_TRIGGER_COMPOSE; + } + return IVibratorManager.CAP_SYNC | prepareCap | triggerCap; + } + + /** + * Return true if {@code prepareCapabilities} contains this {@code capability} mixed with + * different ones, requiring a mixed trigger capability from the vibrator manager for + * syncing all effects. + */ + private boolean requireMixedTriggerCapability(long prepareCapabilities, long capability) { + return (prepareCapabilities & capability) != 0 + && (prepareCapabilities & ~capability) != 0; + } + } +} diff --git a/services/core/java/com/android/server/vibrator/Step.java b/services/core/java/com/android/server/vibrator/Step.java new file mode 100644 index 000000000000..042e8a0e6ad5 --- /dev/null +++ b/services/core/java/com/android/server/vibrator/Step.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.server.vibrator; + +import android.annotation.NonNull; +import android.os.CombinedVibration; +import android.os.SystemClock; +import android.os.VibrationEffect; + +import java.util.List; + +/** + * Represent a single step for playing a vibration. + * + * <p>Every step has a start time, which can be used to apply delays between steps while + * executing them in sequence. + */ +abstract class Step implements Comparable<Step> { + public final VibrationStepConductor conductor; + public final long startTime; + + Step(VibrationStepConductor conductor, long startTime) { + this.conductor = conductor; + this.startTime = startTime; + } + + protected Vibration getVibration() { + return conductor.getVibration(); + } + + /** + * Returns true if this step is a clean up step and not part of a {@link VibrationEffect} or + * {@link CombinedVibration}. + */ + public boolean isCleanUp() { + return false; + } + + /** Play this step, returning a (possibly empty) list of next steps. */ + @NonNull + public abstract List<Step> play(); + + /** + * Cancel this pending step and return a (possibly empty) list of clean-up steps that should + * be played to gracefully cancel this step. + */ + @NonNull + public abstract List<Step> cancel(); + + /** Cancel this pending step immediately, skipping any clean-up. */ + public abstract void cancelImmediately(); + + /** + * Return the duration the vibrator was turned on when this step was played. + * + * @return A positive duration that the vibrator was turned on for by this step; + * Zero if the segment is not supported, the step was not played yet or vibrator was never + * turned on by this step; A negative value if the vibrator call has failed. + */ + public long getVibratorOnDuration() { + return 0; + } + + /** + * Return true to run this step right after a vibrator has notified vibration completed, + * used to resume steps waiting on vibrator callbacks with a timeout. + */ + public boolean acceptVibratorCompleteCallback(int vibratorId) { + return false; + } + + /** + * Returns the time in millis to wait before playing this step. This is performed + * while holding the queue lock, so should not rely on potentially slow operations. + */ + public long calculateWaitTime() { + if (startTime == Long.MAX_VALUE) { + // This step don't have a predefined start time, it's just marked to be executed + // after all other steps have finished. + return 0; + } + return Math.max(0, startTime - SystemClock.uptimeMillis()); + } + + @Override + public int compareTo(Step o) { + return Long.compare(startTime, o.startTime); + } +} diff --git a/services/core/java/com/android/server/vibrator/TurnOffVibratorStep.java b/services/core/java/com/android/server/vibrator/TurnOffVibratorStep.java new file mode 100644 index 000000000000..297ef5614e84 --- /dev/null +++ b/services/core/java/com/android/server/vibrator/TurnOffVibratorStep.java @@ -0,0 +1,66 @@ +/* + * 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.server.vibrator; + +import android.os.SystemClock; +import android.os.Trace; + +import java.util.Arrays; +import java.util.List; + +/** + * Represents a step to turn the vibrator off. + * + * <p>This runs after a timeout on the expected time the vibrator should have finished playing, + * and can be brought forward by vibrator complete callbacks. The step shouldn't be skipped, even + * if the vibrator-complete callback was received, as some implementations still rely on the + * "off" call to actually stop. + */ +final class TurnOffVibratorStep extends AbstractVibratorStep { + + TurnOffVibratorStep(VibrationStepConductor conductor, long startTime, + VibratorController controller) { + super(conductor, startTime, controller, /* effect= */ null, /* index= */ -1, startTime); + } + + @Override + public boolean isCleanUp() { + return true; + } + + @Override + public List<Step> cancel() { + return Arrays.asList( + new TurnOffVibratorStep(conductor, SystemClock.uptimeMillis(), controller)); + } + + @Override + public void cancelImmediately() { + stopVibrating(); + } + + @Override + public List<Step> play() { + Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "TurnOffVibratorStep"); + try { + stopVibrating(); + return VibrationStepConductor.EMPTY_STEP_LIST; + } finally { + Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR); + } + } +} diff --git a/services/core/java/com/android/server/vibrator/VibrationStepConductor.java b/services/core/java/com/android/server/vibrator/VibrationStepConductor.java new file mode 100644 index 000000000000..51691fbcdf5a --- /dev/null +++ b/services/core/java/com/android/server/vibrator/VibrationStepConductor.java @@ -0,0 +1,355 @@ +/* + * 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.server.vibrator; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.CombinedVibration; +import android.os.VibrationEffect; +import android.os.WorkSource; +import android.os.vibrator.PrebakedSegment; +import android.os.vibrator.PrimitiveSegment; +import android.os.vibrator.RampSegment; +import android.os.vibrator.VibrationEffectSegment; +import android.util.Slog; +import android.util.SparseArray; + +import com.android.internal.annotations.GuardedBy; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.PriorityQueue; +import java.util.Queue; + +/** + * Creates and manages a queue of steps for performing a VibrationEffect, as well as coordinating + * dispatch of callbacks. + */ +final class VibrationStepConductor { + /** + * Extra timeout added to the end of each vibration step to ensure it finishes even when + * vibrator callbacks are lost. + */ + static final long CALLBACKS_EXTRA_TIMEOUT = 1_000; + /** Threshold to prevent the ramp off steps from trying to set extremely low amplitudes. */ + static final float RAMP_OFF_AMPLITUDE_MIN = 1e-3f; + static final List<Step> EMPTY_STEP_LIST = new ArrayList<>(); + + final Object mLock = new Object(); + + // Used within steps. + public final VibrationSettings vibrationSettings; + public final DeviceVibrationEffectAdapter deviceEffectAdapter; + public final VibrationThread.VibratorManagerHooks vibratorManagerHooks; + + private final WorkSource mWorkSource; + private final Vibration mVibration; + private final SparseArray<VibratorController> mVibrators = new SparseArray<>(); + + @GuardedBy("mLock") + private final PriorityQueue<Step> mNextSteps = new PriorityQueue<>(); + @GuardedBy("mLock") + private final Queue<Step> mPendingOnVibratorCompleteSteps = new LinkedList<>(); + @GuardedBy("mLock") + private final Queue<Integer> mCompletionNotifiedVibrators = new LinkedList<>(); + + @GuardedBy("mLock") + private int mPendingVibrateSteps; + @GuardedBy("mLock") + private int mConsumedStartVibrateSteps; + @GuardedBy("mLock") + private int mSuccessfulVibratorOnSteps; + @GuardedBy("mLock") + private boolean mWaitToProcessVibratorCompleteCallbacks; + + VibrationStepConductor(Vibration vib, VibrationSettings vibrationSettings, + DeviceVibrationEffectAdapter effectAdapter, + SparseArray<VibratorController> availableVibrators, + VibrationThread.VibratorManagerHooks vibratorManagerHooks) { + this.mVibration = vib; + this.vibrationSettings = vibrationSettings; + this.deviceEffectAdapter = effectAdapter; + this.vibratorManagerHooks = vibratorManagerHooks; + this.mWorkSource = new WorkSource(mVibration.uid); + + CombinedVibration effect = vib.getEffect(); + for (int i = 0; i < availableVibrators.size(); i++) { + if (effect.hasVibrator(availableVibrators.keyAt(i))) { + mVibrators.put(availableVibrators.keyAt(i), availableVibrators.valueAt(i)); + } + } + } + + @Nullable + AbstractVibratorStep nextVibrateStep(long startTime, VibratorController controller, + VibrationEffect.Composed effect, int segmentIndex, + long previousStepVibratorOffTimeout) { + if (segmentIndex >= effect.getSegments().size()) { + segmentIndex = effect.getRepeatIndex(); + } + if (segmentIndex < 0) { + // No more segments to play, last step is to complete the vibration on this vibrator. + return new CompleteEffectVibratorStep(this, startTime, /* cancelled= */ false, + controller, previousStepVibratorOffTimeout); + } + + VibrationEffectSegment segment = effect.getSegments().get(segmentIndex); + if (segment instanceof PrebakedSegment) { + return new PerformPrebakedVibratorStep(this, startTime, controller, effect, + segmentIndex, previousStepVibratorOffTimeout); + } + if (segment instanceof PrimitiveSegment) { + return new ComposePrimitivesVibratorStep(this, startTime, controller, effect, + segmentIndex, previousStepVibratorOffTimeout); + } + if (segment instanceof RampSegment) { + return new ComposePwleVibratorStep(this, startTime, controller, effect, segmentIndex, + previousStepVibratorOffTimeout); + } + return new SetAmplitudeVibratorStep(this, startTime, controller, effect, segmentIndex, + previousStepVibratorOffTimeout); + } + + public void initializeForEffect(@NonNull CombinedVibration.Sequential vibration) { + synchronized (mLock) { + mPendingVibrateSteps++; + mNextSteps.offer(new StartSequentialEffectStep(this, vibration)); + } + } + + public Vibration getVibration() { + return mVibration; + } + + public WorkSource getWorkSource() { + return mWorkSource; + } + + SparseArray<VibratorController> getVibrators() { + return mVibrators; + } + + public boolean isFinished() { + synchronized (mLock) { + return mPendingOnVibratorCompleteSteps.isEmpty() && mNextSteps.isEmpty(); + } + } + + /** + * Calculate the {@link Vibration.Status} based on the current queue state and the expected + * number of {@link StartSequentialEffectStep} to be played. + */ + public Vibration.Status calculateVibrationStatus(int expectedStartVibrateSteps) { + synchronized (mLock) { + if (mPendingVibrateSteps > 0 + || mConsumedStartVibrateSteps < expectedStartVibrateSteps) { + return Vibration.Status.RUNNING; + } + if (mSuccessfulVibratorOnSteps > 0) { + return Vibration.Status.FINISHED; + } + // If no step was able to turn the vibrator ON successfully. + return Vibration.Status.IGNORED_UNSUPPORTED; + } + } + + /** Returns the time in millis to wait before calling {@link #runNextStep()}. */ + @GuardedBy("mLock") + public long getWaitMillisBeforeNextStepLocked() { + if (!mPendingOnVibratorCompleteSteps.isEmpty()) { + // Steps resumed by vibrator complete callback should be played right away. + return 0; + } + Step nextStep = mNextSteps.peek(); + return nextStep == null ? 0 : nextStep.calculateWaitTime(); + } + + /** + * Play and remove the step at the top of this queue, and also adds the next steps generated + * to be played next. + */ + public void runNextStep() { + // Vibrator callbacks should wait until the polled step is played and the next steps are + // added back to the queue, so they can handle the callback. + markWaitToProcessVibratorCallbacks(); + try { + Step nextStep = pollNext(); + if (nextStep != null) { + // This might turn on the vibrator and have a HAL latency. Execute this outside + // any lock to avoid blocking other interactions with the thread. + List<Step> nextSteps = nextStep.play(); + synchronized (mLock) { + if (nextStep.getVibratorOnDuration() > 0) { + mSuccessfulVibratorOnSteps++; + } + if (nextStep instanceof StartSequentialEffectStep) { + mConsumedStartVibrateSteps++; + } + if (!nextStep.isCleanUp()) { + mPendingVibrateSteps--; + } + for (int i = 0; i < nextSteps.size(); i++) { + mPendingVibrateSteps += nextSteps.get(i).isCleanUp() ? 0 : 1; + } + mNextSteps.addAll(nextSteps); + } + } + } finally { + synchronized (mLock) { + processVibratorCompleteCallbacksLocked(); + } + } + } + + /** + * Notify the vibrator completion. + * + * <p>This is a lightweight method that do not trigger any operation from {@link + * VibratorController}, so it can be called directly from a native callback. + */ + @GuardedBy("mLock") + private void notifyVibratorCompleteLocked(int vibratorId) { + mCompletionNotifiedVibrators.offer(vibratorId); + if (!mWaitToProcessVibratorCompleteCallbacks) { + // No step is being played or cancelled now, process the callback right away. + processVibratorCompleteCallbacksLocked(); + } + } + + public void notifyVibratorComplete(int vibratorId) { + synchronized (mLock) { + if (VibrationThread.DEBUG) { + Slog.d(VibrationThread.TAG, + "Vibration complete reported by vibrator " + vibratorId); + } + notifyVibratorCompleteLocked(vibratorId); + mLock.notify(); + } + } + + public void notifySyncedVibrationComplete() { + synchronized (mLock) { + if (VibrationThread.DEBUG) { + Slog.d(VibrationThread.TAG, + "Synced vibration complete reported by vibrator manager"); + } + for (int i = 0; i < mVibrators.size(); i++) { + notifyVibratorCompleteLocked(mVibrators.keyAt(i)); + } + mLock.notify(); + } + } + + /** + * Cancel the current queue, replacing all remaining steps with respective clean-up steps. + * + * <p>This will remove all steps and replace them with respective + * {@link Step#cancel()}. + */ + public void cancel() { + // Vibrator callbacks should wait until all steps from the queue are properly cancelled + // and clean up steps are added back to the queue, so they can handle the callback. + markWaitToProcessVibratorCallbacks(); + try { + List<Step> cleanUpSteps = new ArrayList<>(); + Step step; + while ((step = pollNext()) != null) { + cleanUpSteps.addAll(step.cancel()); + } + synchronized (mLock) { + // All steps generated by Step.cancel() should be clean-up steps. + mPendingVibrateSteps = 0; + mNextSteps.addAll(cleanUpSteps); + } + } finally { + synchronized (mLock) { + processVibratorCompleteCallbacksLocked(); + } + } + } + + /** + * Cancel the current queue immediately, clearing all remaining steps and skipping clean-up. + * + * <p>This will remove and trigger {@link Step#cancelImmediately()} in all steps, in order. + */ + public void cancelImmediately() { + // Vibrator callbacks should wait until all steps from the queue are properly cancelled. + markWaitToProcessVibratorCallbacks(); + try { + Step step; + while ((step = pollNext()) != null) { + // This might turn off the vibrator and have a HAL latency. Execute this outside + // any lock to avoid blocking other interactions with the thread. + step.cancelImmediately(); + } + synchronized (mLock) { + mPendingVibrateSteps = 0; + } + } finally { + synchronized (mLock) { + processVibratorCompleteCallbacksLocked(); + } + } + } + + @Nullable + private Step pollNext() { + synchronized (mLock) { + // Prioritize the steps resumed by a vibrator complete callback. + if (!mPendingOnVibratorCompleteSteps.isEmpty()) { + return mPendingOnVibratorCompleteSteps.poll(); + } + return mNextSteps.poll(); + } + } + + private void markWaitToProcessVibratorCallbacks() { + synchronized (mLock) { + mWaitToProcessVibratorCompleteCallbacks = true; + } + } + + /** + * Notify the step in this queue that should be resumed by the vibrator completion + * callback and keep it separate to be consumed by {@link #runNextStep()}. + * + * <p>This is a lightweight method that do not trigger any operation from {@link + * VibratorController}, so it can be called directly from a native callback. + * + * <p>This assumes only one of the next steps is waiting on this given vibrator, so the + * first step found will be resumed by this method, in no particular order. + */ + @GuardedBy("mLock") + private void processVibratorCompleteCallbacksLocked() { + mWaitToProcessVibratorCompleteCallbacks = false; + while (!mCompletionNotifiedVibrators.isEmpty()) { + int vibratorId = mCompletionNotifiedVibrators.poll(); + Iterator<Step> it = mNextSteps.iterator(); + while (it.hasNext()) { + Step step = it.next(); + if (step.acceptVibratorCompleteCallback(vibratorId)) { + it.remove(); + mPendingOnVibratorCompleteSteps.offer(step); + break; + } + } + } + } +} diff --git a/services/core/java/com/android/server/vibrator/VibrationThread.java b/services/core/java/com/android/server/vibrator/VibrationThread.java index 1f1f40b8121d..f2cd8c3ec3f8 100644 --- a/services/core/java/com/android/server/vibrator/VibrationThread.java +++ b/services/core/java/com/android/server/vibrator/VibrationThread.java @@ -16,59 +16,23 @@ package com.android.server.vibrator; -import android.annotation.NonNull; -import android.annotation.Nullable; -import android.hardware.vibrator.IVibratorManager; import android.os.CombinedVibration; import android.os.IBinder; import android.os.PowerManager; import android.os.Process; import android.os.RemoteException; -import android.os.SystemClock; import android.os.Trace; -import android.os.VibrationEffect; -import android.os.VibratorInfo; -import android.os.WorkSource; -import android.os.vibrator.PrebakedSegment; -import android.os.vibrator.PrimitiveSegment; -import android.os.vibrator.RampSegment; -import android.os.vibrator.StepSegment; -import android.os.vibrator.VibrationEffectSegment; import android.util.Slog; import android.util.SparseArray; -import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.app.IBatteryStats; -import com.android.internal.util.FrameworkStatsLog; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Iterator; -import java.util.LinkedList; -import java.util.List; import java.util.NoSuchElementException; -import java.util.PriorityQueue; -import java.util.Queue; /** Plays a {@link Vibration} in dedicated thread. */ final class VibrationThread extends Thread implements IBinder.DeathRecipient { - private static final String TAG = "VibrationThread"; - private static final boolean DEBUG = false; - - /** - * Extra timeout added to the end of each vibration step to ensure it finishes even when - * vibrator callbacks are lost. - */ - private static final long CALLBACKS_EXTRA_TIMEOUT = 1_000; - - /** Threshold to prevent the ramp off steps from trying to set extremely low amplitudes. */ - private static final float RAMP_OFF_AMPLITUDE_MIN = 1e-3f; - - /** Fixed large duration used to note repeating vibrations to {@link IBatteryStats}. */ - private static final long BATTERY_STATS_REPEATING_VIBRATION_DURATION = 5_000; - - private static final List<Step> EMPTY_STEP_LIST = new ArrayList<>(); + static final String TAG = "VibrationThread"; + static final boolean DEBUG = false; /** Calls into VibratorManager functionality needed for playing a {@link Vibration}. */ interface VibratorManagerHooks { @@ -94,6 +58,15 @@ final class VibrationThread extends Thread implements IBinder.DeathRecipient { void cancelSyncedVibration(); /** + * Record that a vibrator was turned on, and may remain on for the specified duration, + * on behalf of the given uid. + */ + void noteVibratorOn(int uid, long duration); + + /** Record that a vibrator was turned off, on behalf of the given uid. */ + void noteVibratorOff(int uid); + + /** * Tell the manager that the currently active vibration has completed its vibration, from * the perspective of the Effect. However, the VibrationThread may still be continuing with * cleanup tasks, and should not be given new work until {@link #onVibrationThreadReleased} @@ -108,16 +81,10 @@ final class VibrationThread extends Thread implements IBinder.DeathRecipient { void onVibrationThreadReleased(); } - private final Object mLock = new Object(); - private final WorkSource mWorkSource; private final PowerManager.WakeLock mWakeLock; - private final IBatteryStats mBatteryStatsService; - private final VibrationSettings mVibrationSettings; - private final DeviceVibrationEffectAdapter mDeviceEffectAdapter; - private final Vibration mVibration; - private final VibratorManagerHooks mVibratorManagerHooks; - private final SparseArray<VibratorController> mVibrators = new SparseArray<>(); - private final StepQueue mStepQueue = new StepQueue(); + private final VibrationThread.VibratorManagerHooks mVibratorManagerHooks; + + private final VibrationStepConductor mStepConductor; private volatile boolean mStop; private volatile boolean mForceStop; @@ -127,30 +94,20 @@ final class VibrationThread extends Thread implements IBinder.DeathRecipient { VibrationThread(Vibration vib, VibrationSettings vibrationSettings, DeviceVibrationEffectAdapter effectAdapter, SparseArray<VibratorController> availableVibrators, PowerManager.WakeLock wakeLock, - IBatteryStats batteryStatsService, VibratorManagerHooks vibratorManagerHooks) { - mVibration = vib; - mVibrationSettings = vibrationSettings; - mDeviceEffectAdapter = effectAdapter; + VibratorManagerHooks vibratorManagerHooks) { mVibratorManagerHooks = vibratorManagerHooks; - mWorkSource = new WorkSource(mVibration.uid); mWakeLock = wakeLock; - mBatteryStatsService = batteryStatsService; - - CombinedVibration effect = vib.getEffect(); - for (int i = 0; i < availableVibrators.size(); i++) { - if (effect.hasVibrator(availableVibrators.keyAt(i))) { - mVibrators.put(availableVibrators.keyAt(i), availableVibrators.valueAt(i)); - } - } + mStepConductor = new VibrationStepConductor(vib, vibrationSettings, effectAdapter, + availableVibrators, vibratorManagerHooks); } Vibration getVibration() { - return mVibration; + return mStepConductor.getVibration(); } @VisibleForTesting SparseArray<VibratorController> getVibrators() { - return mVibrators; + return mStepConductor.getVibrators(); } @Override @@ -179,7 +136,7 @@ final class VibrationThread extends Thread implements IBinder.DeathRecipient { /** Runs the VibrationThread ensuring that the wake lock is acquired and released. */ private void runWithWakeLock() { - mWakeLock.setWorkSource(mWorkSource); + mWakeLock.setWorkSource(mStepConductor.getWorkSource()); mWakeLock.acquire(); try { runWithWakeLockAndDeathLink(); @@ -193,8 +150,9 @@ final class VibrationThread extends Thread implements IBinder.DeathRecipient { * Called from within runWithWakeLock. */ private void runWithWakeLockAndDeathLink() { + IBinder vibrationBinderToken = mStepConductor.getVibration().token; try { - mVibration.token.linkToDeath(this, 0); + vibrationBinderToken.linkToDeath(this, 0); } catch (RemoteException e) { Slog.e(TAG, "Error linking vibration to token death", e); clientVibrationCompleteIfNotAlready(Vibration.Status.IGNORED_ERROR_TOKEN); @@ -206,7 +164,7 @@ final class VibrationThread extends Thread implements IBinder.DeathRecipient { playVibration(); } finally { try { - mVibration.token.unlinkToDeath(this, 0); + vibrationBinderToken.unlinkToDeath(this, 0); } catch (NoSuchElementException e) { Slog.wtf(TAG, "Failed to unlink token", e); } @@ -220,11 +178,11 @@ final class VibrationThread extends Thread implements IBinder.DeathRecipient { return; } mStop = true; - synchronized (mLock) { + synchronized (mStepConductor.mLock) { if (DEBUG) { Slog.d(TAG, "Vibration cancelled"); } - mLock.notify(); + mStepConductor.mLock.notify(); } } @@ -235,36 +193,22 @@ final class VibrationThread extends Thread implements IBinder.DeathRecipient { return; } mStop = mForceStop = true; - synchronized (mLock) { + synchronized (mStepConductor.mLock) { if (DEBUG) { Slog.d(TAG, "Vibration cancelled immediately"); } - mLock.notify(); + mStepConductor.mLock.notify(); } } /** Notify current vibration that a synced step has completed. */ public void syncedVibrationComplete() { - synchronized (mLock) { - if (DEBUG) { - Slog.d(TAG, "Synced vibration complete reported by vibrator manager"); - } - for (int i = 0; i < mVibrators.size(); i++) { - mStepQueue.notifyVibratorComplete(mVibrators.keyAt(i)); - } - mLock.notify(); - } + mStepConductor.notifySyncedVibrationComplete(); } /** Notify current vibration that a step has completed on given vibrator. */ public void vibratorComplete(int vibratorId) { - synchronized (mLock) { - if (DEBUG) { - Slog.d(TAG, "Vibration complete reported by vibrator " + vibratorId); - } - mStepQueue.notifyVibratorComplete(vibratorId); - mLock.notify(); - } + mStepConductor.notifyVibratorComplete(vibratorId); } // Indicate that the vibration is complete. This can be called multiple times only for @@ -273,52 +217,55 @@ final class VibrationThread extends Thread implements IBinder.DeathRecipient { private void clientVibrationCompleteIfNotAlready(Vibration.Status completedStatus) { if (!mCalledVibrationCompleteCallback) { mCalledVibrationCompleteCallback = true; - mVibratorManagerHooks.onVibrationCompleted(mVibration.id, completedStatus); + mVibratorManagerHooks.onVibrationCompleted( + mStepConductor.getVibration().id, completedStatus); } } private void playVibration() { Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "playVibration"); try { - CombinedVibration.Sequential sequentialEffect = toSequential(mVibration.getEffect()); + CombinedVibration.Sequential sequentialEffect = + toSequential(mStepConductor.getVibration().getEffect()); final int sequentialEffectSize = sequentialEffect.getEffects().size(); - mStepQueue.initializeForEffect(sequentialEffect); + mStepConductor.initializeForEffect(sequentialEffect); - while (!mStepQueue.isFinished()) { + while (!mStepConductor.isFinished()) { long waitMillisBeforeNextStep; - synchronized (mLock) { - waitMillisBeforeNextStep = mStepQueue.getWaitMillisBeforeNextStep(); + synchronized (mStepConductor.mLock) { + waitMillisBeforeNextStep = mStepConductor.getWaitMillisBeforeNextStepLocked(); if (waitMillisBeforeNextStep > 0) { try { - mLock.wait(waitMillisBeforeNextStep); + mStepConductor.mLock.wait(waitMillisBeforeNextStep); } catch (InterruptedException e) { } } } // Only run the next vibration step if we didn't have to wait in this loop. - // If we waited then the queue may have changed, so loop again to re-evaluate - // the scheduling of the queue top element. + // If we waited then the queue may have changed or the wait could have been + // interrupted by a cancel call, so loop again to re-evaluate the scheduling of + // the queue top element. if (waitMillisBeforeNextStep <= 0) { if (DEBUG) { Slog.d(TAG, "Play vibration consuming next step..."); } // Run the step without holding the main lock, to avoid HAL interactions from // blocking the thread. - mStepQueue.runNextStep(); + mStepConductor.runNextStep(); } Vibration.Status status = mStop ? Vibration.Status.CANCELLED - : mStepQueue.calculateVibrationStatus(sequentialEffectSize); + : mStepConductor.calculateVibrationStatus(sequentialEffectSize); if (status != Vibration.Status.RUNNING && !mCalledVibrationCompleteCallback) { // First time vibration stopped running, start clean-up tasks and notify // callback immediately. clientVibrationCompleteIfNotAlready(status); if (status == Vibration.Status.CANCELLED) { - mStepQueue.cancel(); + mStepConductor.cancel(); } } if (mForceStop) { // Cancel every step and stop playing them right away, even clean-up steps. - mStepQueue.cancelImmediately(); + mStepConductor.cancelImmediately(); clientVibrationCompleteIfNotAlready(Vibration.Status.CANCELLED); break; } @@ -328,61 +275,6 @@ final class VibrationThread extends Thread implements IBinder.DeathRecipient { } } - private void noteVibratorOn(long duration) { - try { - if (duration <= 0) { - return; - } - if (duration == Long.MAX_VALUE) { - // Repeating duration has started. Report a fixed duration here, noteVibratorOff - // should be called when this is cancelled. - duration = BATTERY_STATS_REPEATING_VIBRATION_DURATION; - } - mBatteryStatsService.noteVibratorOn(mVibration.uid, duration); - FrameworkStatsLog.write_non_chained(FrameworkStatsLog.VIBRATOR_STATE_CHANGED, - mVibration.uid, null, FrameworkStatsLog.VIBRATOR_STATE_CHANGED__STATE__ON, - duration); - } catch (RemoteException e) { - } - } - - private void noteVibratorOff() { - try { - mBatteryStatsService.noteVibratorOff(mVibration.uid); - FrameworkStatsLog.write_non_chained(FrameworkStatsLog.VIBRATOR_STATE_CHANGED, - mVibration.uid, null, FrameworkStatsLog.VIBRATOR_STATE_CHANGED__STATE__OFF, - /* duration= */ 0); - } catch (RemoteException e) { - } - } - - @Nullable - private SingleVibratorStep nextVibrateStep(long startTime, VibratorController controller, - VibrationEffect.Composed effect, int segmentIndex, long vibratorOffTimeout) { - if (segmentIndex >= effect.getSegments().size()) { - segmentIndex = effect.getRepeatIndex(); - } - if (segmentIndex < 0) { - // No more segments to play, last step is to complete the vibration on this vibrator. - return new EffectCompleteStep(startTime, /* cancelled= */ false, controller, - vibratorOffTimeout); - } - - VibrationEffectSegment segment = effect.getSegments().get(segmentIndex); - if (segment instanceof PrebakedSegment) { - return new PerformStep(startTime, controller, effect, segmentIndex, vibratorOffTimeout); - } - if (segment instanceof PrimitiveSegment) { - return new ComposePrimitivesStep(startTime, controller, effect, segmentIndex, - vibratorOffTimeout); - } - if (segment instanceof RampSegment) { - return new ComposePwleStep(startTime, controller, effect, segmentIndex, - vibratorOffTimeout); - } - return new AmplitudeStep(startTime, controller, effect, segmentIndex, vibratorOffTimeout); - } - private static CombinedVibration.Sequential toSequential(CombinedVibration effect) { if (effect instanceof CombinedVibration.Sequential) { return (CombinedVibration.Sequential) effect; @@ -392,1268 +284,4 @@ final class VibrationThread extends Thread implements IBinder.DeathRecipient { .combine(); } - /** Queue for {@link Step Steps}, sorted by their start time. */ - private final class StepQueue { - @GuardedBy("mLock") - private final PriorityQueue<Step> mNextSteps = new PriorityQueue<>(); - @GuardedBy("mLock") - private final Queue<Step> mPendingOnVibratorCompleteSteps = new LinkedList<>(); - @GuardedBy("mLock") - private final Queue<Integer> mCompletionNotifiedVibrators = new LinkedList<>(); - - @GuardedBy("mLock") - private int mPendingVibrateSteps; - @GuardedBy("mLock") - private int mConsumedStartVibrateSteps; - @GuardedBy("mLock") - private int mSuccessfulVibratorOnSteps; - @GuardedBy("mLock") - private boolean mWaitToProcessVibratorCompleteCallbacks; - - public void initializeForEffect(@NonNull CombinedVibration.Sequential vibration) { - synchronized (mLock) { - mPendingVibrateSteps++; - mNextSteps.offer(new StartVibrateStep(vibration)); - } - } - - public boolean isFinished() { - synchronized (mLock) { - return mPendingOnVibratorCompleteSteps.isEmpty() && mNextSteps.isEmpty(); - } - } - - /** - * Calculate the {@link Vibration.Status} based on the current queue state and the expected - * number of {@link StartVibrateStep} to be played. - */ - public Vibration.Status calculateVibrationStatus(int expectedStartVibrateSteps) { - synchronized (mLock) { - if (mPendingVibrateSteps > 0 - || mConsumedStartVibrateSteps < expectedStartVibrateSteps) { - return Vibration.Status.RUNNING; - } - if (mSuccessfulVibratorOnSteps > 0) { - return Vibration.Status.FINISHED; - } - // If no step was able to turn the vibrator ON successfully. - return Vibration.Status.IGNORED_UNSUPPORTED; - } - } - - /** Returns the time in millis to wait before calling {@link #runNextStep()}. */ - @GuardedBy("VibrationThread.this.mLock") - public long getWaitMillisBeforeNextStep() { - if (!mPendingOnVibratorCompleteSteps.isEmpty()) { - // Steps resumed by vibrator complete callback should be played right away. - return 0; - } - Step nextStep = mNextSteps.peek(); - return nextStep == null ? 0 : nextStep.calculateWaitTime(); - } - - /** - * Play and remove the step at the top of this queue, and also adds the next steps generated - * to be played next. - */ - public void runNextStep() { - // Vibrator callbacks should wait until the polled step is played and the next steps are - // added back to the queue, so they can handle the callback. - markWaitToProcessVibratorCallbacks(); - try { - Step nextStep = pollNext(); - if (nextStep != null) { - // This might turn on the vibrator and have a HAL latency. Execute this outside - // any lock to avoid blocking other interactions with the thread. - List<Step> nextSteps = nextStep.play(); - synchronized (mLock) { - if (nextStep.getVibratorOnDuration() > 0) { - mSuccessfulVibratorOnSteps++; - } - if (nextStep instanceof StartVibrateStep) { - mConsumedStartVibrateSteps++; - } - if (!nextStep.isCleanUp()) { - mPendingVibrateSteps--; - } - for (int i = 0; i < nextSteps.size(); i++) { - mPendingVibrateSteps += nextSteps.get(i).isCleanUp() ? 0 : 1; - } - mNextSteps.addAll(nextSteps); - } - } - } finally { - synchronized (mLock) { - processVibratorCompleteCallbacks(); - } - } - } - - /** - * Notify the vibrator completion. - * - * <p>This is a lightweight method that do not trigger any operation from {@link - * VibratorController}, so it can be called directly from a native callback. - */ - @GuardedBy("mLock") - public void notifyVibratorComplete(int vibratorId) { - mCompletionNotifiedVibrators.offer(vibratorId); - if (!mWaitToProcessVibratorCompleteCallbacks) { - // No step is being played or cancelled now, process the callback right away. - processVibratorCompleteCallbacks(); - } - } - - /** - * Cancel the current queue, replacing all remaining steps with respective clean-up steps. - * - * <p>This will remove all steps and replace them with respective - * {@link Step#cancel()}. - */ - public void cancel() { - // Vibrator callbacks should wait until all steps from the queue are properly cancelled - // and clean up steps are added back to the queue, so they can handle the callback. - markWaitToProcessVibratorCallbacks(); - try { - List<Step> cleanUpSteps = new ArrayList<>(); - Step step; - while ((step = pollNext()) != null) { - cleanUpSteps.addAll(step.cancel()); - } - synchronized (mLock) { - // All steps generated by Step.cancel() should be clean-up steps. - mPendingVibrateSteps = 0; - mNextSteps.addAll(cleanUpSteps); - } - } finally { - synchronized (mLock) { - processVibratorCompleteCallbacks(); - } - } - } - - /** - * Cancel the current queue immediately, clearing all remaining steps and skipping clean-up. - * - * <p>This will remove and trigger {@link Step#cancelImmediately()} in all steps, in order. - */ - public void cancelImmediately() { - // Vibrator callbacks should wait until all steps from the queue are properly cancelled. - markWaitToProcessVibratorCallbacks(); - try { - Step step; - while ((step = pollNext()) != null) { - // This might turn off the vibrator and have a HAL latency. Execute this outside - // any lock to avoid blocking other interactions with the thread. - step.cancelImmediately(); - } - synchronized (mLock) { - mPendingVibrateSteps = 0; - } - } finally { - synchronized (mLock) { - processVibratorCompleteCallbacks(); - } - } - } - - @Nullable - private Step pollNext() { - synchronized (mLock) { - // Prioritize the steps resumed by a vibrator complete callback. - if (!mPendingOnVibratorCompleteSteps.isEmpty()) { - return mPendingOnVibratorCompleteSteps.poll(); - } - return mNextSteps.poll(); - } - } - - private void markWaitToProcessVibratorCallbacks() { - synchronized (mLock) { - mWaitToProcessVibratorCompleteCallbacks = true; - } - } - - /** - * Notify the step in this queue that should be resumed by the vibrator completion - * callback and keep it separate to be consumed by {@link #runNextStep()}. - * - * <p>This is a lightweight method that do not trigger any operation from {@link - * VibratorController}, so it can be called directly from a native callback. - * - * <p>This assumes only one of the next steps is waiting on this given vibrator, so the - * first step found will be resumed by this method, in no particular order. - */ - @GuardedBy("mLock") - private void processVibratorCompleteCallbacks() { - mWaitToProcessVibratorCompleteCallbacks = false; - while (!mCompletionNotifiedVibrators.isEmpty()) { - int vibratorId = mCompletionNotifiedVibrators.poll(); - Iterator<Step> it = mNextSteps.iterator(); - while (it.hasNext()) { - Step step = it.next(); - if (step.acceptVibratorCompleteCallback(vibratorId)) { - it.remove(); - mPendingOnVibratorCompleteSteps.offer(step); - break; - } - } - } - } - } - - /** - * Represent a single step for playing a vibration. - * - * <p>Every step has a start time, which can be used to apply delays between steps while - * executing them in sequence. - */ - private abstract class Step implements Comparable<Step> { - public final long startTime; - - Step(long startTime) { - this.startTime = startTime; - } - - /** - * Returns true if this step is a clean up step and not part of a {@link VibrationEffect} or - * {@link CombinedVibration}. - */ - public boolean isCleanUp() { - return false; - } - - /** Play this step, returning a (possibly empty) list of next steps. */ - @NonNull - public abstract List<Step> play(); - - /** - * Cancel this pending step and return a (possibly empty) list of clean-up steps that should - * be played to gracefully cancel this step. - */ - @NonNull - public abstract List<Step> cancel(); - - /** Cancel this pending step immediately, skipping any clean-up. */ - public abstract void cancelImmediately(); - - /** - * Return the duration the vibrator was turned on when this step was played. - * - * @return A positive duration that the vibrator was turned on for by this step; - * Zero if the segment is not supported, the step was not played yet or vibrator was never - * turned on by this step; A negative value if the vibrator call has failed. - */ - public long getVibratorOnDuration() { - return 0; - } - - /** - * Return true to run this step right after a vibrator has notified vibration completed, - * used to resume steps waiting on vibrator callbacks with a timeout. - */ - public boolean acceptVibratorCompleteCallback(int vibratorId) { - return false; - } - - /** - * Returns the time in millis to wait before playing this step. This is performed - * while holding the queue lock, so should not rely on potentially slow operations. - */ - public long calculateWaitTime() { - if (startTime == Long.MAX_VALUE) { - // This step don't have a predefined start time, it's just marked to be executed - // after all other steps have finished. - return 0; - } - return Math.max(0, startTime - SystemClock.uptimeMillis()); - } - - @Override - public int compareTo(Step o) { - return Long.compare(startTime, o.startTime); - } - } - - /** - * Starts a sync vibration. - * - * <p>If this step has successfully started playing a vibration on any vibrator, it will always - * add a {@link FinishVibrateStep} to the queue, to be played after all vibrators have finished - * all their individual steps. - * - * <p>If this step does not start any vibrator, it will add a {@link StartVibrateStep} if the - * sequential effect isn't finished yet. - */ - private final class StartVibrateStep extends Step { - public final CombinedVibration.Sequential sequentialEffect; - public final int currentIndex; - - private long mVibratorsOnMaxDuration; - - StartVibrateStep(CombinedVibration.Sequential effect) { - this(SystemClock.uptimeMillis() + effect.getDelays().get(0), effect, /* index= */ 0); - } - - StartVibrateStep(long startTime, CombinedVibration.Sequential effect, int index) { - super(startTime); - sequentialEffect = effect; - currentIndex = index; - } - - @Override - public long getVibratorOnDuration() { - return mVibratorsOnMaxDuration; - } - - @Override - public List<Step> play() { - Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "StartVibrateStep"); - List<Step> nextSteps = new ArrayList<>(); - mVibratorsOnMaxDuration = -1; - try { - if (DEBUG) { - Slog.d(TAG, "StartVibrateStep for effect #" + currentIndex); - } - CombinedVibration effect = sequentialEffect.getEffects().get(currentIndex); - DeviceEffectMap effectMapping = createEffectToVibratorMapping(effect); - if (effectMapping == null) { - // Unable to map effects to vibrators, ignore this step. - return nextSteps; - } - - mVibratorsOnMaxDuration = startVibrating(effectMapping, nextSteps); - noteVibratorOn(mVibratorsOnMaxDuration); - } finally { - if (mVibratorsOnMaxDuration >= 0) { - // It least one vibrator was started then add a finish step to wait for all - // active vibrators to finish their individual steps before going to the next. - // Otherwise this step was ignored so just go to the next one. - Step nextStep = - mVibratorsOnMaxDuration > 0 ? new FinishVibrateStep(this) : nextStep(); - if (nextStep != null) { - nextSteps.add(nextStep); - } - } - Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR); - } - return nextSteps; - } - - @Override - public List<Step> cancel() { - return EMPTY_STEP_LIST; - } - - @Override - public void cancelImmediately() { - } - - /** - * Create the next {@link StartVibrateStep} to play this sequential effect, starting at the - * time this method is called, or null if sequence is complete. - */ - @Nullable - private Step nextStep() { - int nextIndex = currentIndex + 1; - if (nextIndex >= sequentialEffect.getEffects().size()) { - return null; - } - long nextEffectDelay = sequentialEffect.getDelays().get(nextIndex); - long nextStartTime = SystemClock.uptimeMillis() + nextEffectDelay; - return new StartVibrateStep(nextStartTime, sequentialEffect, nextIndex); - } - - /** Create a mapping of individual {@link VibrationEffect} to available vibrators. */ - @Nullable - private DeviceEffectMap createEffectToVibratorMapping( - CombinedVibration effect) { - if (effect instanceof CombinedVibration.Mono) { - return new DeviceEffectMap((CombinedVibration.Mono) effect); - } - if (effect instanceof CombinedVibration.Stereo) { - return new DeviceEffectMap((CombinedVibration.Stereo) effect); - } - return null; - } - - /** - * Starts playing effects on designated vibrators, in sync. - * - * @param effectMapping The {@link CombinedVibration} mapped to this device vibrators - * @param nextSteps An output list to accumulate the future {@link Step Steps} created - * by this method, typically one for each vibrator that has - * successfully started vibrating on this step. - * @return The duration, in millis, of the {@link CombinedVibration}. Repeating - * waveforms return {@link Long#MAX_VALUE}. Zero or negative values indicate the vibrators - * have ignored all effects. - */ - private long startVibrating(DeviceEffectMap effectMapping, List<Step> nextSteps) { - int vibratorCount = effectMapping.size(); - if (vibratorCount == 0) { - // No effect was mapped to any available vibrator. - return 0; - } - - SingleVibratorStep[] steps = new SingleVibratorStep[vibratorCount]; - long vibrationStartTime = SystemClock.uptimeMillis(); - for (int i = 0; i < vibratorCount; i++) { - steps[i] = nextVibrateStep(vibrationStartTime, - mVibrators.get(effectMapping.vibratorIdAt(i)), - effectMapping.effectAt(i), - /* segmentIndex= */ 0, /* vibratorOffTimeout= */ 0); - } - - if (steps.length == 1) { - // No need to prepare and trigger sync effects on a single vibrator. - return startVibrating(steps[0], nextSteps); - } - - // This synchronization of vibrators should be executed one at a time, even if we are - // vibrating different sets of vibrators in parallel. The manager can only prepareSynced - // one set of vibrators at a time. - synchronized (mLock) { - boolean hasPrepared = false; - boolean hasTriggered = false; - long maxDuration = 0; - try { - hasPrepared = mVibratorManagerHooks.prepareSyncedVibration( - effectMapping.getRequiredSyncCapabilities(), - effectMapping.getVibratorIds()); - - for (SingleVibratorStep step : steps) { - long duration = startVibrating(step, nextSteps); - if (duration < 0) { - // One vibrator has failed, fail this entire sync attempt. - return maxDuration = -1; - } - maxDuration = Math.max(maxDuration, duration); - } - - // Check if sync was prepared and if any step was accepted by a vibrator, - // otherwise there is nothing to trigger here. - if (hasPrepared && maxDuration > 0) { - hasTriggered = mVibratorManagerHooks.triggerSyncedVibration(mVibration.id); - } - return maxDuration; - } finally { - if (hasPrepared && !hasTriggered) { - // Trigger has failed or all steps were ignored by the vibrators. - mVibratorManagerHooks.cancelSyncedVibration(); - nextSteps.clear(); - } else if (maxDuration < 0) { - // Some vibrator failed without being prepared so other vibrators might be - // active. Cancel and remove every pending step from output list. - for (int i = nextSteps.size() - 1; i >= 0; i--) { - nextSteps.remove(i).cancelImmediately(); - } - } - } - } - } - - private long startVibrating(SingleVibratorStep step, List<Step> nextSteps) { - nextSteps.addAll(step.play()); - long stepDuration = step.getVibratorOnDuration(); - if (stepDuration < 0) { - // Step failed, so return negative duration to propagate failure. - return stepDuration; - } - // Return the longest estimation for the entire effect. - return Math.max(stepDuration, step.effect.getDuration()); - } - } - - /** - * Finish a sync vibration started by a {@link StartVibrateStep}. - * - * <p>This only plays after all active vibrators steps have finished, and adds a {@link - * StartVibrateStep} to the queue if the sequential effect isn't finished yet. - */ - private final class FinishVibrateStep extends Step { - public final StartVibrateStep startedStep; - - FinishVibrateStep(StartVibrateStep startedStep) { - super(Long.MAX_VALUE); // No predefined startTime, just wait for all steps in the queue. - this.startedStep = startedStep; - } - - @Override - public boolean isCleanUp() { - // This step only notes that all the vibrators has been turned off. - return true; - } - - @Override - public List<Step> play() { - Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "FinishVibrateStep"); - try { - if (DEBUG) { - Slog.d(TAG, "FinishVibrateStep for effect #" + startedStep.currentIndex); - } - noteVibratorOff(); - Step nextStep = startedStep.nextStep(); - return nextStep == null ? EMPTY_STEP_LIST : Arrays.asList(nextStep); - } finally { - Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR); - } - } - - @Override - public List<Step> cancel() { - cancelImmediately(); - return EMPTY_STEP_LIST; - } - - @Override - public void cancelImmediately() { - noteVibratorOff(); - } - } - - /** - * Represent a step on a single vibrator that plays one or more segments from a - * {@link VibrationEffect.Composed} effect. - */ - private abstract class SingleVibratorStep extends Step { - public final VibratorController controller; - public final VibrationEffect.Composed effect; - public final int segmentIndex; - public final long vibratorOffTimeout; - - long mVibratorOnResult; - boolean mVibratorCompleteCallbackReceived; - - /** - * @param startTime The time to schedule this step in the {@link StepQueue}. - * @param controller The vibrator that is playing the effect. - * @param effect The effect being played in this step. - * @param index The index of the next segment to be played by this step - * @param vibratorOffTimeout The time the vibrator is expected to complete any previous - * vibration and turn off. This is used to allow this step to be - * triggered when the completion callback is received, and can - * be used play effects back-to-back. - */ - SingleVibratorStep(long startTime, VibratorController controller, - VibrationEffect.Composed effect, int index, long vibratorOffTimeout) { - super(startTime); - this.controller = controller; - this.effect = effect; - this.segmentIndex = index; - this.vibratorOffTimeout = vibratorOffTimeout; - } - - @Override - public long getVibratorOnDuration() { - return mVibratorOnResult; - } - - @Override - public boolean acceptVibratorCompleteCallback(int vibratorId) { - boolean isSameVibrator = controller.getVibratorInfo().getId() == vibratorId; - mVibratorCompleteCallbackReceived |= isSameVibrator; - // Only activate this step if a timeout was set to wait for the vibration to complete, - // otherwise we are waiting for the correct time to play the next step. - return isSameVibrator && (vibratorOffTimeout > SystemClock.uptimeMillis()); - } - - @Override - public List<Step> cancel() { - return Arrays.asList(new EffectCompleteStep(SystemClock.uptimeMillis(), - /* cancelled= */ true, controller, vibratorOffTimeout)); - } - - @Override - public void cancelImmediately() { - if (vibratorOffTimeout > SystemClock.uptimeMillis()) { - // Vibrator might be running from previous steps, so turn it off while canceling. - stopVibrating(); - } - } - - void stopVibrating() { - if (DEBUG) { - Slog.d(TAG, "Turning off vibrator " + controller.getVibratorInfo().getId()); - } - controller.off(); - } - - void changeAmplitude(float amplitude) { - if (DEBUG) { - Slog.d(TAG, "Amplitude changed on vibrator " + controller.getVibratorInfo().getId() - + " to " + amplitude); - } - controller.setAmplitude(amplitude); - } - - /** Return the {@link #nextVibrateStep} with same timings, only jumping the segments. */ - public List<Step> skipToNextSteps(int segmentsSkipped) { - return nextSteps(startTime, vibratorOffTimeout, segmentsSkipped); - } - - /** - * Return the {@link #nextVibrateStep} with same start and off timings calculated from - * {@link #getVibratorOnDuration()}, jumping all played segments. - * - * <p>This method has same behavior as {@link #skipToNextSteps(int)} when the vibrator - * result is non-positive, meaning the vibrator has either ignored or failed to turn on. - */ - public List<Step> nextSteps(int segmentsPlayed) { - if (mVibratorOnResult <= 0) { - // Vibration was not started, so just skip the played segments and keep timings. - return skipToNextSteps(segmentsPlayed); - } - long nextStartTime = SystemClock.uptimeMillis() + mVibratorOnResult; - long nextVibratorOffTimeout = nextStartTime + CALLBACKS_EXTRA_TIMEOUT; - return nextSteps(nextStartTime, nextVibratorOffTimeout, segmentsPlayed); - } - - /** - * Return the {@link #nextVibrateStep} with given start and off timings, which might be - * calculated independently, jumping all played segments. - * - * <p>This should be used when the vibrator on/off state is not responsible for the steps - * execution timings, e.g. while playing the vibrator amplitudes. - */ - public List<Step> nextSteps(long nextStartTime, long vibratorOffTimeout, - int segmentsPlayed) { - Step nextStep = nextVibrateStep(nextStartTime, controller, effect, - segmentIndex + segmentsPlayed, vibratorOffTimeout); - return nextStep == null ? EMPTY_STEP_LIST : Arrays.asList(nextStep); - } - } - - /** - * Represent a step turn the vibrator on with a single prebaked effect. - * - * <p>This step automatically falls back by replacing the prebaked segment with - * {@link VibrationSettings#getFallbackEffect(int)}, if available. - */ - private final class PerformStep extends SingleVibratorStep { - - PerformStep(long startTime, VibratorController controller, - VibrationEffect.Composed effect, int index, long vibratorOffTimeout) { - // This step should wait for the last vibration to finish (with the timeout) and for the - // intended step start time (to respect the effect delays). - super(Math.max(startTime, vibratorOffTimeout), controller, effect, index, - vibratorOffTimeout); - } - - @Override - public List<Step> play() { - Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "PerformStep"); - try { - VibrationEffectSegment segment = effect.getSegments().get(segmentIndex); - if (!(segment instanceof PrebakedSegment)) { - Slog.w(TAG, "Ignoring wrong segment for a PerformStep: " + segment); - return skipToNextSteps(/* segmentsSkipped= */ 1); - } - - PrebakedSegment prebaked = (PrebakedSegment) segment; - if (DEBUG) { - Slog.d(TAG, "Perform " + VibrationEffect.effectIdToString( - prebaked.getEffectId()) + " on vibrator " - + controller.getVibratorInfo().getId()); - } - - VibrationEffect fallback = mVibration.getFallback(prebaked.getEffectId()); - mVibratorOnResult = controller.on(prebaked, mVibration.id); - - if (mVibratorOnResult == 0 && prebaked.shouldFallback() - && (fallback instanceof VibrationEffect.Composed)) { - if (DEBUG) { - Slog.d(TAG, "Playing fallback for effect " - + VibrationEffect.effectIdToString(prebaked.getEffectId())); - } - SingleVibratorStep fallbackStep = nextVibrateStep(startTime, controller, - replaceCurrentSegment((VibrationEffect.Composed) fallback), - segmentIndex, vibratorOffTimeout); - List<Step> fallbackResult = fallbackStep.play(); - // Update the result with the fallback result so this step is seamlessly - // replaced by the fallback to any outer application of this. - mVibratorOnResult = fallbackStep.getVibratorOnDuration(); - return fallbackResult; - } - - return nextSteps(/* segmentsPlayed= */ 1); - } finally { - Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR); - } - } - - /** - * Replace segment at {@link #segmentIndex} in {@link #effect} with given fallback segments. - * - * @return a copy of {@link #effect} with replaced segment. - */ - private VibrationEffect.Composed replaceCurrentSegment(VibrationEffect.Composed fallback) { - List<VibrationEffectSegment> newSegments = new ArrayList<>(effect.getSegments()); - int newRepeatIndex = effect.getRepeatIndex(); - newSegments.remove(segmentIndex); - newSegments.addAll(segmentIndex, fallback.getSegments()); - if (segmentIndex < effect.getRepeatIndex()) { - newRepeatIndex += fallback.getSegments().size() - 1; - } - return new VibrationEffect.Composed(newSegments, newRepeatIndex); - } - } - - /** - * Represent a step turn the vibrator on using a composition of primitives. - * - * <p>This step will use the maximum supported number of consecutive segments of type - * {@link PrimitiveSegment} starting at the current index. - */ - private final class ComposePrimitivesStep extends SingleVibratorStep { - - ComposePrimitivesStep(long startTime, VibratorController controller, - VibrationEffect.Composed effect, int index, long vibratorOffTimeout) { - // This step should wait for the last vibration to finish (with the timeout) and for the - // intended step start time (to respect the effect delays). - super(Math.max(startTime, vibratorOffTimeout), controller, effect, index, - vibratorOffTimeout); - } - - @Override - public List<Step> play() { - Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "ComposePrimitivesStep"); - try { - // Load the next PrimitiveSegments to create a single compose call to the vibrator, - // limited to the vibrator composition maximum size. - int limit = controller.getVibratorInfo().getCompositionSizeMax(); - int segmentCount = limit > 0 - ? Math.min(effect.getSegments().size(), segmentIndex + limit) - : effect.getSegments().size(); - List<PrimitiveSegment> primitives = new ArrayList<>(); - for (int i = segmentIndex; i < segmentCount; i++) { - VibrationEffectSegment segment = effect.getSegments().get(i); - if (segment instanceof PrimitiveSegment) { - primitives.add((PrimitiveSegment) segment); - } else { - break; - } - } - - if (primitives.isEmpty()) { - Slog.w(TAG, "Ignoring wrong segment for a ComposePrimitivesStep: " - + effect.getSegments().get(segmentIndex)); - return skipToNextSteps(/* segmentsSkipped= */ 1); - } - - if (DEBUG) { - Slog.d(TAG, "Compose " + primitives + " primitives on vibrator " - + controller.getVibratorInfo().getId()); - } - mVibratorOnResult = controller.on( - primitives.toArray(new PrimitiveSegment[primitives.size()]), - mVibration.id); - - return nextSteps(/* segmentsPlayed= */ primitives.size()); - } finally { - Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR); - } - } - } - - /** - * Represent a step turn the vibrator on using a composition of PWLE segments. - * - * <p>This step will use the maximum supported number of consecutive segments of type - * {@link StepSegment} or {@link RampSegment} starting at the current index. - */ - private final class ComposePwleStep extends SingleVibratorStep { - - ComposePwleStep(long startTime, VibratorController controller, - VibrationEffect.Composed effect, int index, long vibratorOffTimeout) { - // This step should wait for the last vibration to finish (with the timeout) and for the - // intended step start time (to respect the effect delays). - super(Math.max(startTime, vibratorOffTimeout), controller, effect, index, - vibratorOffTimeout); - } - - @Override - public List<Step> play() { - Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "ComposePwleStep"); - try { - // Load the next RampSegments to create a single composePwle call to the vibrator, - // limited to the vibrator PWLE maximum size. - int limit = controller.getVibratorInfo().getPwleSizeMax(); - int segmentCount = limit > 0 - ? Math.min(effect.getSegments().size(), segmentIndex + limit) - : effect.getSegments().size(); - List<RampSegment> pwles = new ArrayList<>(); - for (int i = segmentIndex; i < segmentCount; i++) { - VibrationEffectSegment segment = effect.getSegments().get(i); - if (segment instanceof RampSegment) { - pwles.add((RampSegment) segment); - } else { - break; - } - } - - if (pwles.isEmpty()) { - Slog.w(TAG, "Ignoring wrong segment for a ComposePwleStep: " - + effect.getSegments().get(segmentIndex)); - return skipToNextSteps(/* segmentsSkipped= */ 1); - } - - if (DEBUG) { - Slog.d(TAG, "Compose " + pwles + " PWLEs on vibrator " - + controller.getVibratorInfo().getId()); - } - mVibratorOnResult = controller.on(pwles.toArray(new RampSegment[pwles.size()]), - mVibration.id); - - return nextSteps(/* segmentsPlayed= */ pwles.size()); - } finally { - Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR); - } - } - } - - /** - * Represents a step to complete a {@link VibrationEffect}. - * - * <p>This runs right at the time the vibration is considered to end and will update the pending - * vibrators count. This can turn off the vibrator or slowly ramp it down to zero amplitude. - */ - private final class EffectCompleteStep extends SingleVibratorStep { - private final boolean mCancelled; - - EffectCompleteStep(long startTime, boolean cancelled, VibratorController controller, - long vibratorOffTimeout) { - super(startTime, controller, /* effect= */ null, /* index= */ -1, vibratorOffTimeout); - mCancelled = cancelled; - } - - @Override - public boolean isCleanUp() { - // If the vibration was cancelled then this is just a clean up to ramp off the vibrator. - // Otherwise this step is part of the vibration. - return mCancelled; - } - - @Override - public List<Step> cancel() { - if (mCancelled) { - // Double cancelling will just turn off the vibrator right away. - return Arrays.asList(new OffStep(SystemClock.uptimeMillis(), controller)); - } - return super.cancel(); - } - - @Override - public List<Step> play() { - Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "EffectCompleteStep"); - try { - if (DEBUG) { - Slog.d(TAG, "Running " + (mCancelled ? "cancel" : "complete") + " vibration" - + " step on vibrator " + controller.getVibratorInfo().getId()); - } - if (mVibratorCompleteCallbackReceived) { - // Vibration completion callback was received by this step, just turn if off - // and skip any clean-up. - stopVibrating(); - return EMPTY_STEP_LIST; - } - - float currentAmplitude = controller.getCurrentAmplitude(); - long remainingOnDuration = - vibratorOffTimeout - CALLBACKS_EXTRA_TIMEOUT - SystemClock.uptimeMillis(); - long rampDownDuration = - Math.min(remainingOnDuration, mVibrationSettings.getRampDownDuration()); - long stepDownDuration = mVibrationSettings.getRampStepDuration(); - if (currentAmplitude < RAMP_OFF_AMPLITUDE_MIN - || rampDownDuration <= stepDownDuration) { - // No need to ramp down the amplitude, just wait to turn it off. - if (mCancelled) { - // Vibration is completing because it was cancelled, turn off right away. - stopVibrating(); - return EMPTY_STEP_LIST; - } else { - return Arrays.asList(new OffStep(vibratorOffTimeout, controller)); - } - } - - if (DEBUG) { - Slog.d(TAG, "Ramping down vibrator " + controller.getVibratorInfo().getId() - + " from amplitude " + currentAmplitude - + " for " + rampDownDuration + "ms"); - } - float amplitudeDelta = currentAmplitude / (rampDownDuration / stepDownDuration); - float amplitudeTarget = currentAmplitude - amplitudeDelta; - long newVibratorOffTimeout = mCancelled ? rampDownDuration : vibratorOffTimeout; - return Arrays.asList(new RampOffStep(startTime, amplitudeTarget, amplitudeDelta, - controller, newVibratorOffTimeout)); - } finally { - Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR); - } - } - } - - /** Represents a step to ramp down the vibrator amplitude before turning it off. */ - private final class RampOffStep extends SingleVibratorStep { - private final float mAmplitudeTarget; - private final float mAmplitudeDelta; - - RampOffStep(long startTime, float amplitudeTarget, float amplitudeDelta, - VibratorController controller, long vibratorOffTimeout) { - super(startTime, controller, /* effect= */ null, /* index= */ -1, vibratorOffTimeout); - mAmplitudeTarget = amplitudeTarget; - mAmplitudeDelta = amplitudeDelta; - } - - @Override - public boolean isCleanUp() { - return true; - } - - @Override - public List<Step> cancel() { - return Arrays.asList(new OffStep(SystemClock.uptimeMillis(), controller)); - } - - @Override - public List<Step> play() { - Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "RampOffStep"); - try { - if (DEBUG) { - long latency = SystemClock.uptimeMillis() - startTime; - Slog.d(TAG, "Ramp down the vibrator amplitude, step with " - + latency + "ms latency."); - } - if (mVibratorCompleteCallbackReceived) { - // Vibration completion callback was received by this step, just turn if off - // and skip the rest of the steps to ramp down the vibrator amplitude. - stopVibrating(); - return EMPTY_STEP_LIST; - } - - changeAmplitude(mAmplitudeTarget); - - float newAmplitudeTarget = mAmplitudeTarget - mAmplitudeDelta; - if (newAmplitudeTarget < RAMP_OFF_AMPLITUDE_MIN) { - // Vibrator amplitude cannot go further down, just turn it off. - return Arrays.asList(new OffStep(vibratorOffTimeout, controller)); - } - return Arrays.asList(new RampOffStep( - startTime + mVibrationSettings.getRampStepDuration(), newAmplitudeTarget, - mAmplitudeDelta, controller, vibratorOffTimeout)); - } finally { - Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR); - } - } - } - - /** - * Represents a step to turn the vibrator off. - * - * <p>This runs after a timeout on the expected time the vibrator should have finished playing, - * and can be brought forward by vibrator complete callbacks. - */ - private final class OffStep extends SingleVibratorStep { - - OffStep(long startTime, VibratorController controller) { - super(startTime, controller, /* effect= */ null, /* index= */ -1, startTime); - } - - @Override - public boolean isCleanUp() { - return true; - } - - @Override - public List<Step> cancel() { - return Arrays.asList(new OffStep(SystemClock.uptimeMillis(), controller)); - } - - @Override - public void cancelImmediately() { - stopVibrating(); - } - - @Override - public List<Step> play() { - Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "OffStep"); - try { - stopVibrating(); - return EMPTY_STEP_LIST; - } finally { - Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR); - } - } - } - - /** - * Represents a step to turn the vibrator on and change its amplitude. - * - * <p>This step ignores vibration completion callbacks and control the vibrator on/off state - * and amplitude to simulate waveforms represented by a sequence of {@link StepSegment}. - */ - private final class AmplitudeStep extends SingleVibratorStep { - private long mNextOffTime; - - AmplitudeStep(long startTime, VibratorController controller, - VibrationEffect.Composed effect, int index, long vibratorOffTimeout) { - // This step has a fixed startTime coming from the timings of the waveform it's playing. - super(startTime, controller, effect, index, vibratorOffTimeout); - mNextOffTime = vibratorOffTimeout; - } - - @Override - public boolean acceptVibratorCompleteCallback(int vibratorId) { - if (controller.getVibratorInfo().getId() == vibratorId) { - mVibratorCompleteCallbackReceived = true; - mNextOffTime = SystemClock.uptimeMillis(); - } - // Timings are tightly controlled here, so only trigger this step if the vibrator was - // supposed to be ON but has completed prematurely, to turn it back on as soon as - // possible. - return mNextOffTime < startTime && controller.getCurrentAmplitude() > 0; - } - - @Override - public List<Step> play() { - Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "AmplitudeStep"); - try { - long now = SystemClock.uptimeMillis(); - long latency = now - startTime; - if (DEBUG) { - Slog.d(TAG, "Running amplitude step with " + latency + "ms latency."); - } - - if (mVibratorCompleteCallbackReceived && latency < 0) { - // This step was run early because the vibrator turned off prematurely. - // Turn it back on and return this same step to run at the exact right time. - mNextOffTime = turnVibratorBackOn(/* remainingDuration= */ -latency); - return Arrays.asList(new AmplitudeStep(startTime, controller, effect, - segmentIndex, mNextOffTime)); - } - - VibrationEffectSegment segment = effect.getSegments().get(segmentIndex); - if (!(segment instanceof StepSegment)) { - Slog.w(TAG, "Ignoring wrong segment for a AmplitudeStep: " + segment); - return skipToNextSteps(/* segmentsSkipped= */ 1); - } - - StepSegment stepSegment = (StepSegment) segment; - if (stepSegment.getDuration() == 0) { - // Skip waveform entries with zero timing. - return skipToNextSteps(/* segmentsSkipped= */ 1); - } - - float amplitude = stepSegment.getAmplitude(); - if (amplitude == 0) { - if (vibratorOffTimeout > now) { - // Amplitude cannot be set to zero, so stop the vibrator. - stopVibrating(); - mNextOffTime = now; - } - } else { - if (startTime >= mNextOffTime) { - // Vibrator is OFF. Turn vibrator back on for the duration of another - // cycle before setting the amplitude. - long onDuration = getVibratorOnDuration(effect, segmentIndex); - if (onDuration > 0) { - mVibratorOnResult = startVibrating(onDuration); - mNextOffTime = now + onDuration + CALLBACKS_EXTRA_TIMEOUT; - } - } - changeAmplitude(amplitude); - } - - // Use original startTime to avoid propagating latencies to the waveform. - long nextStartTime = startTime + segment.getDuration(); - return nextSteps(nextStartTime, mNextOffTime, /* segmentsPlayed= */ 1); - } finally { - Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR); - } - } - - private long turnVibratorBackOn(long remainingDuration) { - long onDuration = getVibratorOnDuration(effect, segmentIndex); - if (onDuration <= 0) { - // Vibrator is supposed to go back off when this step starts, so just leave it off. - return vibratorOffTimeout; - } - onDuration += remainingDuration; - float expectedAmplitude = controller.getCurrentAmplitude(); - mVibratorOnResult = startVibrating(onDuration); - if (mVibratorOnResult > 0) { - // Set the amplitude back to the value it was supposed to be playing at. - changeAmplitude(expectedAmplitude); - } - return SystemClock.uptimeMillis() + onDuration + CALLBACKS_EXTRA_TIMEOUT; - } - - private long startVibrating(long duration) { - if (DEBUG) { - Slog.d(TAG, "Turning on vibrator " + controller.getVibratorInfo().getId() + " for " - + duration + "ms"); - } - return controller.on(duration, mVibration.id); - } - - /** - * Get the duration the vibrator will be on for a waveform, starting at {@code startIndex} - * until the next time it's vibrating amplitude is zero or a different type of segment is - * found. - */ - private long getVibratorOnDuration(VibrationEffect.Composed effect, int startIndex) { - List<VibrationEffectSegment> segments = effect.getSegments(); - int segmentCount = segments.size(); - int repeatIndex = effect.getRepeatIndex(); - int i = startIndex; - long timing = 0; - while (i < segmentCount) { - VibrationEffectSegment segment = segments.get(i); - if (!(segment instanceof StepSegment) - || ((StepSegment) segment).getAmplitude() == 0) { - break; - } - timing += segment.getDuration(); - i++; - if (i == segmentCount && repeatIndex >= 0) { - i = repeatIndex; - // prevent infinite loop - repeatIndex = -1; - } - if (i == startIndex) { - // The repeating waveform keeps the vibrator ON all the time. Use a minimum - // of 1s duration to prevent short patterns from turning the vibrator ON too - // frequently. - return Math.max(timing, 1000); - } - } - if (i == segmentCount && effect.getRepeatIndex() < 0) { - // Vibration ending at non-zero amplitude, add extra timings to ramp down after - // vibration is complete. - timing += mVibrationSettings.getRampDownDuration(); - } - return timing; - } - } - - /** - * Map a {@link CombinedVibration} to the vibrators available on the device. - * - * <p>This contains the logic to find the capabilities required from {@link IVibratorManager} to - * play all of the effects in sync. - */ - private final class DeviceEffectMap { - private final SparseArray<VibrationEffect.Composed> mVibratorEffects; - private final int[] mVibratorIds; - private final long mRequiredSyncCapabilities; - - DeviceEffectMap(CombinedVibration.Mono mono) { - mVibratorEffects = new SparseArray<>(mVibrators.size()); - mVibratorIds = new int[mVibrators.size()]; - for (int i = 0; i < mVibrators.size(); i++) { - int vibratorId = mVibrators.keyAt(i); - VibratorInfo vibratorInfo = mVibrators.valueAt(i).getVibratorInfo(); - VibrationEffect effect = mDeviceEffectAdapter.apply(mono.getEffect(), vibratorInfo); - if (effect instanceof VibrationEffect.Composed) { - mVibratorEffects.put(vibratorId, (VibrationEffect.Composed) effect); - mVibratorIds[i] = vibratorId; - } - } - mRequiredSyncCapabilities = calculateRequiredSyncCapabilities(mVibratorEffects); - } - - DeviceEffectMap(CombinedVibration.Stereo stereo) { - SparseArray<VibrationEffect> stereoEffects = stereo.getEffects(); - mVibratorEffects = new SparseArray<>(); - for (int i = 0; i < stereoEffects.size(); i++) { - int vibratorId = stereoEffects.keyAt(i); - if (mVibrators.contains(vibratorId)) { - VibratorInfo vibratorInfo = mVibrators.valueAt(i).getVibratorInfo(); - VibrationEffect effect = mDeviceEffectAdapter.apply( - stereoEffects.valueAt(i), vibratorInfo); - if (effect instanceof VibrationEffect.Composed) { - mVibratorEffects.put(vibratorId, (VibrationEffect.Composed) effect); - } - } - } - mVibratorIds = new int[mVibratorEffects.size()]; - for (int i = 0; i < mVibratorEffects.size(); i++) { - mVibratorIds[i] = mVibratorEffects.keyAt(i); - } - mRequiredSyncCapabilities = calculateRequiredSyncCapabilities(mVibratorEffects); - } - - /** - * Return the number of vibrators mapped to play the {@link CombinedVibration} on this - * device. - */ - public int size() { - return mVibratorIds.length; - } - - /** - * Return all capabilities required to play the {@link CombinedVibration} in - * between calls to {@link IVibratorManager#prepareSynced} and - * {@link IVibratorManager#triggerSynced}. - */ - public long getRequiredSyncCapabilities() { - return mRequiredSyncCapabilities; - } - - /** Return all vibrator ids mapped to play the {@link CombinedVibration}. */ - public int[] getVibratorIds() { - return mVibratorIds; - } - - /** Return the id of the vibrator at given index. */ - public int vibratorIdAt(int index) { - return mVibratorEffects.keyAt(index); - } - - /** Return the {@link VibrationEffect} at given index. */ - public VibrationEffect.Composed effectAt(int index) { - return mVibratorEffects.valueAt(index); - } - - /** - * Return all capabilities required from the {@link IVibratorManager} to prepare and - * trigger all given effects in sync. - * - * @return {@link IVibratorManager#CAP_SYNC} together with all required - * IVibratorManager.CAP_PREPARE_* and IVibratorManager.CAP_MIXED_TRIGGER_* capabilities. - */ - private long calculateRequiredSyncCapabilities( - SparseArray<VibrationEffect.Composed> effects) { - long prepareCap = 0; - for (int i = 0; i < effects.size(); i++) { - VibrationEffectSegment firstSegment = effects.valueAt(i).getSegments().get(0); - if (firstSegment instanceof StepSegment) { - prepareCap |= IVibratorManager.CAP_PREPARE_ON; - } else if (firstSegment instanceof PrebakedSegment) { - prepareCap |= IVibratorManager.CAP_PREPARE_PERFORM; - } else if (firstSegment instanceof PrimitiveSegment) { - prepareCap |= IVibratorManager.CAP_PREPARE_COMPOSE; - } - } - int triggerCap = 0; - if (requireMixedTriggerCapability(prepareCap, IVibratorManager.CAP_PREPARE_ON)) { - triggerCap |= IVibratorManager.CAP_MIXED_TRIGGER_ON; - } - if (requireMixedTriggerCapability(prepareCap, IVibratorManager.CAP_PREPARE_PERFORM)) { - triggerCap |= IVibratorManager.CAP_MIXED_TRIGGER_PERFORM; - } - if (requireMixedTriggerCapability(prepareCap, IVibratorManager.CAP_PREPARE_COMPOSE)) { - triggerCap |= IVibratorManager.CAP_MIXED_TRIGGER_COMPOSE; - } - return IVibratorManager.CAP_SYNC | prepareCap | triggerCap; - } - - /** - * Return true if {@code prepareCapabilities} contains this {@code capability} mixed with - * different ones, requiring a mixed trigger capability from the vibrator manager for - * syncing all effects. - */ - private boolean requireMixedTriggerCapability(long prepareCapabilities, long capability) { - return (prepareCapabilities & capability) != 0 - && (prepareCapabilities & ~capability) != 0; - } - } } diff --git a/services/core/java/com/android/server/vibrator/VibratorManagerService.java b/services/core/java/com/android/server/vibrator/VibratorManagerService.java index 63f3af3f3095..01f9d0b94879 100644 --- a/services/core/java/com/android/server/vibrator/VibratorManagerService.java +++ b/services/core/java/com/android/server/vibrator/VibratorManagerService.java @@ -42,6 +42,7 @@ import android.os.IVibratorStateListener; import android.os.Looper; import android.os.PowerManager; import android.os.Process; +import android.os.RemoteException; import android.os.ResultReceiver; import android.os.ServiceManager; import android.os.ShellCallback; @@ -60,6 +61,7 @@ import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.app.IBatteryStats; import com.android.internal.util.DumpUtils; +import com.android.internal.util.FrameworkStatsLog; import com.android.server.LocalServices; import com.android.server.SystemService; @@ -88,6 +90,9 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { VibrationAttributes.FLAG_BYPASS_INTERRUPTION_POLICY | VibrationAttributes.FLAG_BYPASS_USER_VIBRATION_INTENSITY_OFF; + /** Fixed large duration used to note repeating vibrations to {@link IBatteryStats}. */ + private static final long BATTERY_STATS_REPEATING_VIBRATION_DURATION = 5_000; + /** Lifecycle responsible for initializing this class at the right system server phases. */ public static class Lifecycle extends SystemService { private VibratorManagerService mService; @@ -201,8 +206,7 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { mSystemUiPackage = LocalServices.getService(PackageManagerInternal.class) .getSystemUiServiceComponent().getPackageName(); - mBatteryStatsService = IBatteryStats.Stub.asInterface(ServiceManager.getService( - BatteryStats.SERVICE_NAME)); + mBatteryStatsService = injector.getBatteryStatsService(); mAppOps = mContext.getSystemService(AppOpsManager.class); @@ -634,7 +638,7 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { } VibrationThread vibThread = new VibrationThread(vib, mVibrationSettings, - mDeviceVibrationEffectAdapter, mVibrators, mWakeLock, mBatteryStatsService, + mDeviceVibrationEffectAdapter, mVibrators, mWakeLock, mVibrationThreadCallbacks); if (mCurrentVibration == null) { @@ -1105,6 +1109,11 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { return new Handler(looper); } + IBatteryStats getBatteryStatsService() { + return IBatteryStats.Stub.asInterface(ServiceManager.getService( + BatteryStats.SERVICE_NAME)); + } + VibratorController createVibratorController(int vibratorId, VibratorController.OnVibrationCompleteListener listener) { return new VibratorController(vibratorId, listener); @@ -1141,6 +1150,36 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { } @Override + public void noteVibratorOn(int uid, long duration) { + try { + if (duration <= 0) { + return; + } + if (duration == Long.MAX_VALUE) { + // Repeating duration has started. Report a fixed duration here, noteVibratorOff + // should be called when this is cancelled. + duration = BATTERY_STATS_REPEATING_VIBRATION_DURATION; + } + mBatteryStatsService.noteVibratorOn(uid, duration); + FrameworkStatsLog.write_non_chained(FrameworkStatsLog.VIBRATOR_STATE_CHANGED, + uid, null, FrameworkStatsLog.VIBRATOR_STATE_CHANGED__STATE__ON, + duration); + } catch (RemoteException e) { + } + } + + @Override + public void noteVibratorOff(int uid) { + try { + mBatteryStatsService.noteVibratorOff(uid); + FrameworkStatsLog.write_non_chained(FrameworkStatsLog.VIBRATOR_STATE_CHANGED, + uid, null, FrameworkStatsLog.VIBRATOR_STATE_CHANGED__STATE__OFF, + /* duration= */ 0); + } catch (RemoteException e) { + } + } + + @Override public void onVibrationCompleted(long vibrationId, Vibration.Status status) { if (DEBUG) { Slog.d(TAG, "Vibration " + vibrationId + " finished with status " + status); diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index ffa1a601d41b..98c74f8ad81d 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -5149,10 +5149,13 @@ class Task extends TaskFragment { // Ensure that we do not trigger entering PiP an activity on the root pinned task return false; } + final boolean isTransient = opts != null && opts.getTransientLaunch(); final Task targetRootTask = toFrontTask != null ? toFrontTask.getRootTask() : toFrontActivity.getRootTask(); - if (targetRootTask != null && targetRootTask.isActivityTypeAssistant()) { - // Ensure the task/activity being brought forward is not the assistant + if (targetRootTask != null && (targetRootTask.isActivityTypeAssistant() || isTransient)) { + // Ensure the task/activity being brought forward is not the assistant and is not + // transient. In the case of transient-launch, we want to wait until the end of the + // transition and only allow switch if the transient launch was committed. return false; } return true; diff --git a/services/core/java/com/android/server/wm/TaskSnapshotController.java b/services/core/java/com/android/server/wm/TaskSnapshotController.java index a9add59be1f6..6a23eb588102 100644 --- a/services/core/java/com/android/server/wm/TaskSnapshotController.java +++ b/services/core/java/com/android/server/wm/TaskSnapshotController.java @@ -310,7 +310,7 @@ class TaskSnapshotController { } final ActivityRecord activity = result.first; final WindowState mainWindow = result.second; - final Rect contentInsets = getSystemBarInsets(task.getBounds(), + final Rect contentInsets = getSystemBarInsets(mainWindow.getFrame(), mainWindow.getInsetsStateWithVisibilityOverride()); final Rect letterboxInsets = activity.getLetterboxInsets(); InsetUtils.addInsets(contentInsets, letterboxInsets); @@ -575,7 +575,7 @@ class TaskSnapshotController { final LayoutParams attrs = mainWindow.getAttrs(); final Rect taskBounds = task.getBounds(); final InsetsState insetsState = mainWindow.getInsetsStateWithVisibilityOverride(); - final Rect systemBarInsets = getSystemBarInsets(taskBounds, insetsState); + final Rect systemBarInsets = getSystemBarInsets(mainWindow.getFrame(), insetsState); final SystemBarBackgroundPainter decorPainter = new SystemBarBackgroundPainter(attrs.flags, attrs.privateFlags, attrs.insetsFlags.appearance, task.getTaskDescription(), mHighResTaskSnapshotScale, insetsState); diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java index 0b965c37a2ba..3a3103e752ad 100644 --- a/services/core/java/com/android/server/wm/Transition.java +++ b/services/core/java/com/android/server/wm/Transition.java @@ -459,9 +459,19 @@ class Transition extends Binder implements BLASTSyncEngine.TransactionReadyListe // activity in a bad state. if (!visibleAtTransitionEnd && !ar.isVisibleRequested()) { boolean commitVisibility = true; - if (ar.getDeferHidingClient() && ar.getTask() != null) { + if (ar.isVisible() && ar.getTask() != null) { if (ar.pictureInPictureArgs != null && ar.pictureInPictureArgs.isAutoEnterEnabled()) { + if (mTransientLaunches != null) { + for (int j = 0; j < mTransientLaunches.size(); ++j) { + if (mTransientLaunches.valueAt(j).isVisibleRequested()) { + // force enable pip-on-task-switch now that we've committed + // to actually launching to the transient activity. + ar.supportsEnterPipOnTaskSwitch = true; + break; + } + } + } mController.mAtm.enterPictureInPictureMode(ar, ar.pictureInPictureArgs); // Avoid commit visibility to false here, or else we will get a sudden // "flash" / surface going invisible for a split second. diff --git a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/parsing/parcelling/AndroidPackageTest.kt b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/parsing/parcelling/AndroidPackageTest.kt index cd2d0fce6691..83ccabf03935 100644 --- a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/parsing/parcelling/AndroidPackageTest.kt +++ b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/parsing/parcelling/AndroidPackageTest.kt @@ -238,6 +238,7 @@ class AndroidPackageTest : ParcelableComponentTest(AndroidPackage::class, Packag AndroidPackage::isVendor, AndroidPackage::isVisibleToInstantApps, AndroidPackage::isVmSafeMode, + AndroidPackage::isLeavingSharedUid, AndroidPackage::isResetEnabledSettingsOnAppDataCleared, AndroidPackage::getMaxAspectRatio, AndroidPackage::getMinAspectRatio, diff --git a/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationControllerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationControllerTest.java index a9b7cfb5e338..3ce2ed84d3e8 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationControllerTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationControllerTest.java @@ -52,7 +52,6 @@ import android.view.accessibility.MagnificationAnimationCallback; import androidx.test.InstrumentationRegistry; import androidx.test.runner.AndroidJUnit4; -import com.android.server.accessibility.AccessibilityManagerService; import com.android.server.accessibility.AccessibilityTraceManager; import com.android.server.accessibility.test.MessageCapturingHandler; import com.android.server.wm.WindowManagerInternal; @@ -94,7 +93,6 @@ public class FullScreenMagnificationControllerTest { final FullScreenMagnificationController.ControllerContext mMockControllerCtx = mock(FullScreenMagnificationController.ControllerContext.class); final Context mMockContext = mock(Context.class); - final AccessibilityManagerService mMockAms = mock(AccessibilityManagerService.class); final AccessibilityTraceManager mMockTraceManager = mock(AccessibilityTraceManager.class); final WindowManagerInternal mMockWindowManager = mock(WindowManagerInternal.class); private final MagnificationAnimationCallback mAnimationCallback = mock( @@ -121,12 +119,10 @@ public class FullScreenMagnificationControllerTest { // Pretending ID of the Thread associated with looper as main thread ID in controller when(mMockContext.getMainLooper()).thenReturn(looper); when(mMockControllerCtx.getContext()).thenReturn(mMockContext); - when(mMockControllerCtx.getAms()).thenReturn(mMockAms); when(mMockControllerCtx.getTraceManager()).thenReturn(mMockTraceManager); when(mMockControllerCtx.getWindowManager()).thenReturn(mMockWindowManager); when(mMockControllerCtx.getHandler()).thenReturn(mMessageCapturingHandler); when(mMockControllerCtx.getAnimationDuration()).thenReturn(1000L); - when(mMockAms.getTraceManager()).thenReturn(mMockTraceManager); initMockWindowManager(); mFullScreenMagnificationController = new FullScreenMagnificationController( @@ -357,8 +353,8 @@ public class FullScreenMagnificationControllerTest { assertEquals(newCenter.x, mFullScreenMagnificationController.getCenterX(displayId), 0.5); assertEquals(newCenter.y, mFullScreenMagnificationController.getCenterY(displayId), 0.5); assertThat(getCurrentMagnificationSpec(displayId), closeTo(endSpec)); - verify(mMockAms).notifyMagnificationChanged(eq(displayId), eq(INITIAL_MAGNIFICATION_REGION), - mConfigCaptor.capture()); + verify(mRequestObserver).onFullScreenMagnificationChanged(eq(displayId), + eq(INITIAL_MAGNIFICATION_REGION), mConfigCaptor.capture()); assertConfigEquals(config, mConfigCaptor.getValue()); verify(mMockValueAnimator).start(); verify(mRequestObserver).onRequestMagnificationSpec(displayId, SERVICE_ID_1); @@ -501,7 +497,7 @@ public class FullScreenMagnificationControllerTest { mMessageCapturingHandler.sendAllMessages(); MagnificationConfig config = buildConfig(1.0f, OTHER_MAGNIFICATION_BOUNDS.centerX(), OTHER_MAGNIFICATION_BOUNDS.centerY()); - verify(mMockAms).notifyMagnificationChanged(eq(displayId), eq(OTHER_REGION), + verify(mRequestObserver).onFullScreenMagnificationChanged(eq(displayId), eq(OTHER_REGION), mConfigCaptor.capture()); assertConfigEquals(config, mConfigCaptor.getValue()); } @@ -655,9 +651,9 @@ public class FullScreenMagnificationControllerTest { register(displayId); zoomIn2xToMiddle(displayId); mMessageCapturingHandler.sendAllMessages(); - reset(mMockAms); + reset(mRequestObserver); assertTrue(mFullScreenMagnificationController.resetIfNeeded(displayId, false)); - verify(mMockAms).notifyMagnificationChanged(eq(displayId), + verify(mRequestObserver).onFullScreenMagnificationChanged(eq(displayId), eq(INITIAL_MAGNIFICATION_REGION), any(MagnificationConfig.class)); assertFalse(mFullScreenMagnificationController.isMagnifying(displayId)); assertFalse(mFullScreenMagnificationController.resetIfNeeded(displayId, false)); @@ -676,8 +672,8 @@ public class FullScreenMagnificationControllerTest { assertFalse(mFullScreenMagnificationController.reset(displayId, mAnimationCallback)); mMessageCapturingHandler.sendAllMessages(); - verify(mMockAms, never()).notifyMagnificationChanged(eq(displayId), any(Region.class), - any(MagnificationConfig.class)); + verify(mRequestObserver, never()).onFullScreenMagnificationChanged(eq(displayId), + any(Region.class), any(MagnificationConfig.class)); verify(mAnimationCallback).onResult(true); } @@ -1072,8 +1068,8 @@ public class FullScreenMagnificationControllerTest { when(mMockValueAnimator.getAnimatedFraction()).thenReturn(0.0f); mTargetAnimationListener.onAnimationUpdate(mMockValueAnimator); verify(mMockWindowManager).setMagnificationSpec(eq(displayId), eq(startSpec)); - verify(mMockAms).notifyMagnificationChanged(eq(displayId), eq(INITIAL_MAGNIFICATION_REGION), - mConfigCaptor.capture()); + verify(mRequestObserver).onFullScreenMagnificationChanged(eq(displayId), + eq(INITIAL_MAGNIFICATION_REGION), mConfigCaptor.capture()); assertConfigEquals(config, mConfigCaptor.getValue()); Mockito.reset(mMockWindowManager); @@ -1097,7 +1093,7 @@ public class FullScreenMagnificationControllerTest { // Animation should have been restarted verify(mMockValueAnimator, times(2)).start(); - verify(mMockAms, times(2)).notifyMagnificationChanged(eq(displayId), + verify(mRequestObserver, times(2)).onFullScreenMagnificationChanged(eq(displayId), eq(INITIAL_MAGNIFICATION_REGION), mConfigCaptor.capture()); assertConfigEquals(newConfig, mConfigCaptor.getValue()); diff --git a/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandlerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandlerTest.java index 2060223f6f98..0fed89b831d6 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandlerTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandlerTest.java @@ -151,8 +151,6 @@ public class FullScreenMagnificationGestureHandlerTest { mock(FullScreenMagnificationController.ControllerContext.class); final WindowManagerInternal mockWindowManager = mock(WindowManagerInternal.class); when(mockController.getContext()).thenReturn(mContext); - when(mockController.getAms()).thenReturn(mMockAccessibilityManagerService); - when(mMockAccessibilityManagerService.getTraceManager()).thenReturn(mMockTraceManager); when(mockController.getTraceManager()).thenReturn(mMockTraceManager); when(mockController.getWindowManager()).thenReturn(mockWindowManager); when(mockController.getHandler()).thenReturn(new Handler(mContext.getMainLooper())); diff --git a/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationControllerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationControllerTest.java index 3fcce92fe141..ec59090240f3 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationControllerTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationControllerTest.java @@ -145,9 +145,10 @@ public class MagnificationControllerTest { mCallbackDelegate, mTraceManager, mScaleProvider)); mMockConnection = new MockWindowMagnificationConnection(true); mWindowMagnificationManager.setConnection(mMockConnection.getConnection()); - new FullScreenMagnificationControllerStubber(mScreenMagnificationController); mMagnificationController = new MagnificationController(mService, globalLock, mContext, mScreenMagnificationController, mWindowMagnificationManager, mScaleProvider); + new FullScreenMagnificationControllerStubber(mScreenMagnificationController, + mMagnificationController); mMagnificationController.setMagnificationCapabilities( Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_ALL); @@ -451,6 +452,29 @@ public class MagnificationControllerTest { } @Test + public void onFullScreenMagnificationChanged_fullScreenEnabled_notifyMagnificationChanged() + throws RemoteException { + setMagnificationEnabled(MODE_FULLSCREEN); + + final MagnificationConfig config = obtainMagnificationConfig(MODE_FULLSCREEN); + mScreenMagnificationController.setScaleAndCenter(TEST_DISPLAY, + config.getScale(), config.getCenterX(), config.getCenterY(), + true, TEST_SERVICE_ID); + + // The first time is triggered when setting magnification enabled. And the second time is + // triggered when calling setScaleAndCenter. + final ArgumentCaptor<MagnificationConfig> configCaptor = ArgumentCaptor.forClass( + MagnificationConfig.class); + verify(mService, times(2)).notifyMagnificationChanged(eq(TEST_DISPLAY), + eq(FullScreenMagnificationControllerStubber.MAGNIFICATION_REGION), + configCaptor.capture()); + final MagnificationConfig actualConfig = configCaptor.getValue(); + assertEquals(config.getCenterX(), actualConfig.getCenterX(), 0); + assertEquals(config.getCenterY(), actualConfig.getCenterY(), 0); + assertEquals(config.getScale(), actualConfig.getScale(), 0); + } + + @Test public void onAccessibilityActionPerformed_magnifierEnabled_showMagnificationButton() throws RemoteException { setMagnificationEnabled(MODE_WINDOW); @@ -679,7 +703,7 @@ public class MagnificationControllerTest { throws RemoteException { setMagnificationEnabled(MODE_FULLSCREEN); mScreenMagnificationController.setScaleAndCenter(TEST_DISPLAY, - /* scale= */1, MAGNIFIED_CENTER_X, MAGNIFIED_CENTER_Y, + /* scale= */ 1, MAGNIFIED_CENTER_X, MAGNIFIED_CENTER_Y, true, TEST_SERVICE_ID); mMagnificationController.onFullScreenMagnificationActivationState(TEST_DISPLAY, false); @@ -884,6 +908,8 @@ public class MagnificationControllerTest { private static class FullScreenMagnificationControllerStubber { private static final Region MAGNIFICATION_REGION = new Region(0, 0, 500, 600); private final FullScreenMagnificationController mScreenMagnificationController; + private final FullScreenMagnificationController.MagnificationInfoChangedCallback + mMagnificationChangedCallback; private boolean mIsMagnifying = false; private float mScale = 1.0f; private float mCenterX = MAGNIFICATION_REGION.getBounds().exactCenterX(); @@ -891,8 +917,10 @@ public class MagnificationControllerTest { private int mServiceId = -1; FullScreenMagnificationControllerStubber( - FullScreenMagnificationController screenMagnificationController) { + FullScreenMagnificationController screenMagnificationController, + FullScreenMagnificationController.MagnificationInfoChangedCallback callback) { mScreenMagnificationController = screenMagnificationController; + mMagnificationChangedCallback = callback; stubMethods(); } @@ -930,6 +958,14 @@ public class MagnificationControllerTest { } else { reset(); } + + + final MagnificationConfig config = new MagnificationConfig.Builder().setMode( + MODE_FULLSCREEN).setScale(mScale).setCenterX(mCenterX).setCenterY( + mCenterY).build(); + mMagnificationChangedCallback.onFullScreenMagnificationChanged(TEST_DISPLAY, + FullScreenMagnificationControllerStubber.MAGNIFICATION_REGION, + config); return true; }; doAnswer(setScaleAndCenterStubAnswer).when( diff --git a/services/tests/servicestests/src/com/android/server/backup/transport/BackupTransportClientTest.java b/services/tests/servicestests/src/com/android/server/backup/transport/BackupTransportClientTest.java new file mode 100644 index 000000000000..b154d6f6db0c --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/backup/transport/BackupTransportClientTest.java @@ -0,0 +1,265 @@ +/* + * 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.server.backup.transport; + +import static com.google.common.truth.Truth.assertThat; + +import static org.junit.Assert.fail; + +import android.app.backup.BackupTransport; +import android.app.backup.RestoreDescription; +import android.app.backup.RestoreSet; +import android.content.Intent; +import android.content.pm.PackageInfo; +import android.os.IBinder; +import android.os.ParcelFileDescriptor; +import android.os.RemoteException; +import android.platform.test.annotations.Presubmit; + +import androidx.test.runner.AndroidJUnit4; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.backup.IBackupTransport; +import com.android.internal.backup.ITransportStatusCallback; +import com.android.internal.infra.AndroidFuture; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.List; +import java.util.concurrent.CancellationException; + +@Presubmit +@RunWith(AndroidJUnit4.class) +public class BackupTransportClientTest { + + private static class TestFuturesFakeTransportBinder extends FakeTransportBinderBase { + public final Object mLock = new Object(); + + public String mNameCompletedImmediately; + + @GuardedBy("mLock") + public AndroidFuture<String> mNameCompletedInFuture; + + @Override public void name(AndroidFuture<String> name) throws RemoteException { + name.complete(mNameCompletedImmediately); + } + @Override public void transportDirName(AndroidFuture<String> name) throws RemoteException { + synchronized (mLock) { + mNameCompletedInFuture = name; + mLock.notifyAll(); + } + } + } + + @Test + public void testName_completesImmediately_returnsName() throws Exception { + TestFuturesFakeTransportBinder binder = new TestFuturesFakeTransportBinder(); + binder.mNameCompletedImmediately = "fake name"; + + BackupTransportClient client = new BackupTransportClient(binder); + String name = client.name(); + + assertThat(name).isEqualTo("fake name"); + } + + @Test + public void testTransportDirName_completesLater_returnsName() throws Exception { + TestFuturesFakeTransportBinder binder = new TestFuturesFakeTransportBinder(); + BackupTransportClient client = new BackupTransportClient(binder); + + Thread thread = new Thread(() -> { + try { + String name = client.transportDirName(); + assertThat(name).isEqualTo("fake name"); + } catch (Exception ex) { + fail("unexpected Exception: " + ex.getClass().getCanonicalName()); + } + }); + + thread.start(); + + synchronized (binder.mLock) { + while (binder.mNameCompletedInFuture == null) { + binder.mLock.wait(); + } + assertThat(binder.mNameCompletedInFuture.complete("fake name")).isTrue(); + } + + thread.join(); + } + + @Test + public void testTransportDirName_canceledBeforeCompletion_throwsException() throws Exception { + TestFuturesFakeTransportBinder binder = new TestFuturesFakeTransportBinder(); + BackupTransportClient client = new BackupTransportClient(binder); + + Thread thread = new Thread(() -> { + try { + /*String name =*/ client.transportDirName(); + fail("transportDirName should be cancelled"); + } catch (CancellationException ex) { + // This is expected. + } catch (Exception ex) { + fail("unexpected Exception: " + ex.getClass().getCanonicalName()); + } + }); + + thread.start(); + + synchronized (binder.mLock) { + while (binder.mNameCompletedInFuture == null) { + binder.mLock.wait(); + } + client.onBecomingUnusable(); + } + + thread.join(); + } + + private static class TestCallbacksFakeTransportBinder extends FakeTransportBinderBase { + public final Object mLock = new Object(); + + public int mStatusCompletedImmediately; + + @GuardedBy("mLock") + public ITransportStatusCallback mStatusCompletedInFuture; + + @Override public void initializeDevice(ITransportStatusCallback c) throws RemoteException { + c.onOperationCompleteWithStatus(mStatusCompletedImmediately); + } + @Override public void finishBackup(ITransportStatusCallback c) throws RemoteException { + synchronized (mLock) { + mStatusCompletedInFuture = c; + mLock.notifyAll(); + } + } + } + + @Test + public void testInitializeDevice_completesImmediately_returnsStatus() throws Exception { + TestCallbacksFakeTransportBinder binder = new TestCallbacksFakeTransportBinder(); + binder.mStatusCompletedImmediately = 123; + + BackupTransportClient client = new BackupTransportClient(binder); + int status = client.initializeDevice(); + + assertThat(status).isEqualTo(123); + } + + + @Test + public void testFinishBackup_completesLater_returnsStatus() throws Exception { + TestCallbacksFakeTransportBinder binder = new TestCallbacksFakeTransportBinder(); + BackupTransportClient client = new BackupTransportClient(binder); + + Thread thread = new Thread(() -> { + try { + int status = client.finishBackup(); + assertThat(status).isEqualTo(456); + } catch (Exception ex) { + fail("unexpected Exception: " + ex.getClass().getCanonicalName()); + } + }); + + thread.start(); + + synchronized (binder.mLock) { + while (binder.mStatusCompletedInFuture == null) { + binder.mLock.wait(); + } + binder.mStatusCompletedInFuture.onOperationCompleteWithStatus(456); + } + + thread.join(); + } + + @Test + public void testFinishBackup_canceledBeforeCompletion_throwsException() throws Exception { + TestCallbacksFakeTransportBinder binder = new TestCallbacksFakeTransportBinder(); + BackupTransportClient client = new BackupTransportClient(binder); + + Thread thread = new Thread(() -> { + try { + int status = client.finishBackup(); + assertThat(status).isEqualTo(BackupTransport.TRANSPORT_ERROR); + } catch (Exception ex) { + fail("unexpected Exception: " + ex.getClass().getCanonicalName()); + } + }); + + thread.start(); + + synchronized (binder.mLock) { + while (binder.mStatusCompletedInFuture == null) { + binder.mLock.wait(); + } + client.onBecomingUnusable(); + } + + thread.join(); + } + + // Convenience layer so we only need to fake specific methods useful for each test case. + private static class FakeTransportBinderBase implements IBackupTransport { + @Override public void name(AndroidFuture<String> f) throws RemoteException {} + @Override public void transportDirName(AndroidFuture<String> f) throws RemoteException {} + @Override public void configurationIntent(AndroidFuture<Intent> f) throws RemoteException {} + @Override public void currentDestinationString(AndroidFuture<String> f) + throws RemoteException {} + @Override public void dataManagementIntent(AndroidFuture<Intent> f) + throws RemoteException {} + @Override public void dataManagementIntentLabel(AndroidFuture<CharSequence> f) + throws RemoteException {} + @Override public void initializeDevice(ITransportStatusCallback c) throws RemoteException {} + @Override public void clearBackupData(PackageInfo i, ITransportStatusCallback c) + throws RemoteException {} + @Override public void finishBackup(ITransportStatusCallback c) throws RemoteException {} + @Override public void requestBackupTime(AndroidFuture<Long> f) throws RemoteException {} + @Override public void performBackup(PackageInfo i, ParcelFileDescriptor fd, int f, + ITransportStatusCallback c) throws RemoteException {} + @Override public void getAvailableRestoreSets(AndroidFuture<List<RestoreSet>> f) + throws RemoteException {} + @Override public void getCurrentRestoreSet(AndroidFuture<Long> f) throws RemoteException {} + @Override public void startRestore(long t, PackageInfo[] p, ITransportStatusCallback c) + throws RemoteException {} + @Override public void nextRestorePackage(AndroidFuture<RestoreDescription> f) + throws RemoteException {} + @Override public void getRestoreData(ParcelFileDescriptor fd, ITransportStatusCallback c) + throws RemoteException {} + @Override public void finishRestore(ITransportStatusCallback c) throws RemoteException {} + @Override public void requestFullBackupTime(AndroidFuture<Long> f) throws RemoteException {} + @Override public void performFullBackup(PackageInfo i, ParcelFileDescriptor fd, int f, + ITransportStatusCallback c) throws RemoteException {} + @Override public void checkFullBackupSize(long s, ITransportStatusCallback c) + throws RemoteException {} + @Override public void sendBackupData(int n, ITransportStatusCallback c) + throws RemoteException {} + @Override public void cancelFullBackup(ITransportStatusCallback c) throws RemoteException {} + @Override public void isAppEligibleForBackup(PackageInfo p, boolean b, + AndroidFuture<Boolean> f) throws RemoteException {} + @Override public void getBackupQuota(String s, boolean b, AndroidFuture<Long> f) + throws RemoteException {} + @Override public void getNextFullRestoreDataChunk(ParcelFileDescriptor fd, + ITransportStatusCallback c) throws RemoteException {} + @Override public void abortFullRestore(ITransportStatusCallback c) throws RemoteException {} + @Override public void getTransportFlags(AndroidFuture<Integer> f) throws RemoteException {} + @Override public IBinder asBinder() { + return null; + } + } +} diff --git a/services/tests/servicestests/src/com/android/server/vibrator/VibrationThreadTest.java b/services/tests/servicestests/src/com/android/server/vibrator/VibrationThreadTest.java index e1a4989e5a05..01e306e744fb 100644 --- a/services/tests/servicestests/src/com/android/server/vibrator/VibrationThreadTest.java +++ b/services/tests/servicestests/src/com/android/server/vibrator/VibrationThreadTest.java @@ -61,8 +61,6 @@ import android.util.SparseArray; import androidx.test.InstrumentationRegistry; -import com.android.internal.app.IBatteryStats; - import org.junit.After; import org.junit.Before; import org.junit.Rule; @@ -106,8 +104,6 @@ public class VibrationThreadTest { @Mock private IBinder mVibrationToken; @Mock - private IBatteryStats mIBatteryStatsMock; - @Mock private VibrationConfig mVibrationConfigMock; private final Map<Integer, FakeVibratorControllerProvider> mVibratorProviders = new HashMap<>(); @@ -178,8 +174,8 @@ public class VibrationThreadTest { VibrationThread thread = startThreadAndDispatcher(vibrationId, effect); waitForCompletion(thread); - verify(mIBatteryStatsMock).noteVibratorOn(eq(UID), eq(10L)); - verify(mIBatteryStatsMock).noteVibratorOff(eq(UID)); + verify(mManagerHooks).noteVibratorOn(eq(UID), eq(10L)); + verify(mManagerHooks).noteVibratorOff(eq(UID)); verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId)); verifyCallbacksTriggered(vibrationId, Vibration.Status.FINISHED); assertFalse(thread.getVibrators().get(VIBRATOR_ID).isVibrating()); @@ -197,8 +193,8 @@ public class VibrationThreadTest { VibrationThread thread = startThreadAndDispatcher(vibrationId, effect); waitForCompletion(thread); - verify(mIBatteryStatsMock).noteVibratorOn(eq(UID), eq(10L)); - verify(mIBatteryStatsMock).noteVibratorOff(eq(UID)); + verify(mManagerHooks).noteVibratorOn(eq(UID), eq(10L)); + verify(mManagerHooks).noteVibratorOff(eq(UID)); verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId)); verifyCallbacksTriggered(vibrationId, Vibration.Status.FINISHED); assertFalse(thread.getVibrators().get(VIBRATOR_ID).isVibrating()); @@ -219,8 +215,8 @@ public class VibrationThreadTest { VibrationThread thread = startThreadAndDispatcher(vibrationId, effect); waitForCompletion(thread); - verify(mIBatteryStatsMock).noteVibratorOn(eq(UID), eq(15L)); - verify(mIBatteryStatsMock).noteVibratorOff(eq(UID)); + verify(mManagerHooks).noteVibratorOn(eq(UID), eq(15L)); + verify(mManagerHooks).noteVibratorOff(eq(UID)); verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId)); verifyCallbacksTriggered(vibrationId, Vibration.Status.FINISHED); assertFalse(thread.getVibrators().get(VIBRATOR_ID).isVibrating()); @@ -252,8 +248,8 @@ public class VibrationThreadTest { thread.cancel(); waitForCompletion(thread); - verify(mIBatteryStatsMock).noteVibratorOn(eq(UID), anyLong()); - verify(mIBatteryStatsMock).noteVibratorOff(eq(UID)); + verify(mManagerHooks).noteVibratorOn(eq(UID), anyLong()); + verify(mManagerHooks).noteVibratorOff(eq(UID)); verifyCallbacksTriggered(vibrationId, Vibration.Status.CANCELLED); assertFalse(thread.getVibrators().get(VIBRATOR_ID).isVibrating()); @@ -404,8 +400,8 @@ public class VibrationThreadTest { VibrationThread thread = startThreadAndDispatcher(vibrationId, effect); waitForCompletion(thread); - verify(mIBatteryStatsMock).noteVibratorOn(eq(UID), eq(20L)); - verify(mIBatteryStatsMock).noteVibratorOff(eq(UID)); + verify(mManagerHooks).noteVibratorOn(eq(UID), eq(20L)); + verify(mManagerHooks).noteVibratorOff(eq(UID)); verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId)); verifyCallbacksTriggered(vibrationId, Vibration.Status.FINISHED); assertFalse(thread.getVibrators().get(VIBRATOR_ID).isVibrating()); @@ -427,8 +423,8 @@ public class VibrationThreadTest { VibrationThread thread = startThreadAndDispatcher(vibration); waitForCompletion(thread); - verify(mIBatteryStatsMock).noteVibratorOn(eq(UID), eq(10L)); - verify(mIBatteryStatsMock).noteVibratorOff(eq(UID)); + verify(mManagerHooks).noteVibratorOn(eq(UID), eq(10L)); + verify(mManagerHooks).noteVibratorOff(eq(UID)); verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId)); verifyCallbacksTriggered(vibrationId, Vibration.Status.FINISHED); assertFalse(thread.getVibrators().get(VIBRATOR_ID).isVibrating()); @@ -446,8 +442,8 @@ public class VibrationThreadTest { VibrationThread thread = startThreadAndDispatcher(vibrationId, effect); waitForCompletion(thread); - verify(mIBatteryStatsMock, never()).noteVibratorOn(eq(UID), anyLong()); - verify(mIBatteryStatsMock, never()).noteVibratorOff(eq(UID)); + verify(mManagerHooks, never()).noteVibratorOn(eq(UID), anyLong()); + verify(mManagerHooks, never()).noteVibratorOff(eq(UID)); verify(mControllerCallbacks, never()).onComplete(eq(VIBRATOR_ID), eq(vibrationId)); verifyCallbacksTriggered(vibrationId, Vibration.Status.IGNORED_UNSUPPORTED); assertTrue(mVibratorProviders.get(VIBRATOR_ID).getEffectSegments().isEmpty()); @@ -466,8 +462,8 @@ public class VibrationThreadTest { VibrationThread thread = startThreadAndDispatcher(vibrationId, effect); waitForCompletion(thread); - verify(mIBatteryStatsMock).noteVibratorOn(eq(UID), eq(40L)); - verify(mIBatteryStatsMock).noteVibratorOff(eq(UID)); + verify(mManagerHooks).noteVibratorOn(eq(UID), eq(40L)); + verify(mManagerHooks).noteVibratorOff(eq(UID)); verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId)); verifyCallbacksTriggered(vibrationId, Vibration.Status.FINISHED); assertFalse(thread.getVibrators().get(VIBRATOR_ID).isVibrating()); @@ -486,8 +482,8 @@ public class VibrationThreadTest { VibrationThread thread = startThreadAndDispatcher(vibrationId, effect); waitForCompletion(thread); - verify(mIBatteryStatsMock, never()).noteVibratorOn(eq(UID), anyLong()); - verify(mIBatteryStatsMock, never()).noteVibratorOff(eq(UID)); + verify(mManagerHooks, never()).noteVibratorOn(eq(UID), anyLong()); + verify(mManagerHooks, never()).noteVibratorOff(eq(UID)); verify(mControllerCallbacks, never()).onComplete(eq(VIBRATOR_ID), eq(vibrationId)); verifyCallbacksTriggered(vibrationId, Vibration.Status.IGNORED_UNSUPPORTED); assertTrue(mVibratorProviders.get(VIBRATOR_ID).getEffectSegments().isEmpty()); @@ -536,8 +532,8 @@ public class VibrationThreadTest { waitForCompletion(thread); // Use first duration the vibrator is turned on since we cannot estimate the clicks. - verify(mIBatteryStatsMock).noteVibratorOn(eq(UID), eq(10L)); - verify(mIBatteryStatsMock).noteVibratorOff(eq(UID)); + verify(mManagerHooks).noteVibratorOn(eq(UID), eq(10L)); + verify(mManagerHooks).noteVibratorOff(eq(UID)); verify(mControllerCallbacks, times(4)).onComplete(eq(VIBRATOR_ID), eq(vibrationId)); verifyCallbacksTriggered(vibrationId, Vibration.Status.FINISHED); assertFalse(thread.getVibrators().get(VIBRATOR_ID).isVibrating()); @@ -573,8 +569,8 @@ public class VibrationThreadTest { VibrationThread thread = startThreadAndDispatcher(vibrationId, effect); waitForCompletion(thread); - verify(mIBatteryStatsMock).noteVibratorOn(eq(UID), eq(100L)); - verify(mIBatteryStatsMock).noteVibratorOff(eq(UID)); + verify(mManagerHooks).noteVibratorOn(eq(UID), eq(100L)); + verify(mManagerHooks).noteVibratorOff(eq(UID)); verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId)); verifyCallbacksTriggered(vibrationId, Vibration.Status.FINISHED); assertFalse(thread.getVibrators().get(VIBRATOR_ID).isVibrating()); @@ -666,8 +662,8 @@ public class VibrationThreadTest { VibrationThread thread = startThreadAndDispatcher(vibrationId, effect); waitForCompletion(thread); - verify(mIBatteryStatsMock).noteVibratorOn(eq(UID), eq(20L)); - verify(mIBatteryStatsMock).noteVibratorOff(eq(UID)); + verify(mManagerHooks).noteVibratorOn(eq(UID), eq(20L)); + verify(mManagerHooks).noteVibratorOff(eq(UID)); verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId)); verify(mControllerCallbacks, never()).onComplete(eq(2), eq(vibrationId)); verifyCallbacksTriggered(vibrationId, Vibration.Status.FINISHED); @@ -690,8 +686,8 @@ public class VibrationThreadTest { VibrationThread thread = startThreadAndDispatcher(vibrationId, effect); waitForCompletion(thread); - verify(mIBatteryStatsMock).noteVibratorOn(eq(UID), eq(20L)); - verify(mIBatteryStatsMock).noteVibratorOff(eq(UID)); + verify(mManagerHooks).noteVibratorOn(eq(UID), eq(20L)); + verify(mManagerHooks).noteVibratorOff(eq(UID)); verify(mControllerCallbacks).onComplete(eq(1), eq(vibrationId)); verify(mControllerCallbacks).onComplete(eq(2), eq(vibrationId)); verify(mControllerCallbacks).onComplete(eq(3), eq(vibrationId)); @@ -728,8 +724,8 @@ public class VibrationThreadTest { VibrationThread thread = startThreadAndDispatcher(vibrationId, effect); waitForCompletion(thread); - verify(mIBatteryStatsMock).noteVibratorOn(eq(UID), eq(20L)); - verify(mIBatteryStatsMock).noteVibratorOff(eq(UID)); + verify(mManagerHooks).noteVibratorOn(eq(UID), eq(20L)); + verify(mManagerHooks).noteVibratorOff(eq(UID)); verify(mControllerCallbacks).onComplete(eq(1), eq(vibrationId)); verify(mControllerCallbacks).onComplete(eq(2), eq(vibrationId)); verify(mControllerCallbacks).onComplete(eq(3), eq(vibrationId)); @@ -777,13 +773,13 @@ public class VibrationThreadTest { controllerVerifier.verify(mControllerCallbacks).onComplete(eq(1), eq(vibrationId)); controllerVerifier.verify(mControllerCallbacks).onComplete(eq(2), eq(vibrationId)); - InOrder batterVerifier = inOrder(mIBatteryStatsMock); - batterVerifier.verify(mIBatteryStatsMock).noteVibratorOn(eq(UID), eq(20L)); - batterVerifier.verify(mIBatteryStatsMock).noteVibratorOff(eq(UID)); - batterVerifier.verify(mIBatteryStatsMock).noteVibratorOn(eq(UID), eq(10L)); - batterVerifier.verify(mIBatteryStatsMock).noteVibratorOff(eq(UID)); - batterVerifier.verify(mIBatteryStatsMock).noteVibratorOn(eq(UID), eq(20L)); - batterVerifier.verify(mIBatteryStatsMock).noteVibratorOff(eq(UID)); + InOrder batteryVerifier = inOrder(mManagerHooks); + batteryVerifier.verify(mManagerHooks).noteVibratorOn(eq(UID), eq(20L)); + batteryVerifier.verify(mManagerHooks).noteVibratorOff(eq(UID)); + batteryVerifier.verify(mManagerHooks).noteVibratorOn(eq(UID), eq(10L)); + batteryVerifier.verify(mManagerHooks).noteVibratorOff(eq(UID)); + batteryVerifier.verify(mManagerHooks).noteVibratorOn(eq(UID), eq(20L)); + batteryVerifier.verify(mManagerHooks).noteVibratorOff(eq(UID)); verifyCallbacksTriggered(vibrationId, Vibration.Status.FINISHED); assertFalse(thread.getVibrators().get(1).isVibrating()); @@ -952,8 +948,8 @@ public class VibrationThreadTest { waitForCompletion(thread); - verify(mIBatteryStatsMock).noteVibratorOn(eq(UID), eq(80L)); - verify(mIBatteryStatsMock).noteVibratorOff(eq(UID)); + verify(mManagerHooks).noteVibratorOn(eq(UID), eq(80L)); + verify(mManagerHooks).noteVibratorOff(eq(UID)); verify(mControllerCallbacks).onComplete(eq(1), eq(vibrationId)); verify(mControllerCallbacks).onComplete(eq(2), eq(vibrationId)); verify(mControllerCallbacks).onComplete(eq(3), eq(vibrationId)); @@ -1300,7 +1296,7 @@ public class VibrationThreadTest { private VibrationThread startThreadAndDispatcher(Vibration vib) { VibrationThread thread = new VibrationThread(vib, mVibrationSettings, mEffectAdapter, - createVibratorControllers(), mWakeLock, mIBatteryStatsMock, mManagerHooks); + createVibratorControllers(), mWakeLock, mManagerHooks); doAnswer(answer -> { thread.vibratorComplete(answer.getArgument(0)); return null; diff --git a/services/tests/servicestests/src/com/android/server/vibrator/VibratorManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/vibrator/VibratorManagerServiceTest.java index ccdb1050d32c..19111e5d16e9 100644 --- a/services/tests/servicestests/src/com/android/server/vibrator/VibratorManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/vibrator/VibratorManagerServiceTest.java @@ -71,6 +71,7 @@ import android.os.VibratorInfo; import android.os.test.TestLooper; import android.os.vibrator.PrebakedSegment; import android.os.vibrator.PrimitiveSegment; +import android.os.vibrator.VibrationConfig; import android.os.vibrator.VibrationEffectSegment; import android.platform.test.annotations.Presubmit; import android.provider.Settings; @@ -78,6 +79,7 @@ import android.view.InputDevice; import androidx.test.InstrumentationRegistry; +import com.android.internal.app.IBatteryStats; import com.android.internal.util.test.FakeSettingsProvider; import com.android.internal.util.test.FakeSettingsProviderRule; import com.android.server.LocalServices; @@ -144,6 +146,8 @@ public class VibratorManagerServiceTest { private AppOpsManager mAppOpsManagerMock; @Mock private IInputManager mIInputManagerMock; + @Mock + private IBatteryStats mBatteryStatsMock; private final Map<Integer, FakeVibratorControllerProvider> mVibratorProviders = new HashMap<>(); @@ -152,12 +156,14 @@ public class VibratorManagerServiceTest { private FakeVibrator mVibrator; private PowerManagerInternal.LowPowerModeListener mRegisteredPowerModeListener; private VibratorManagerService.ExternalVibratorService mExternalVibratorService; + private VibrationConfig mVibrationConfig; @Before public void setUp() throws Exception { mTestLooper = new TestLooper(); mContextSpy = spy(new ContextWrapper(InstrumentationRegistry.getContext())); InputManager inputManager = InputManager.resetInstance(mIInputManagerMock); + mVibrationConfig = new VibrationConfig(mContextSpy.getResources()); ContentResolver contentResolver = mSettingsProviderRule.mockContentResolver(mContextSpy); when(mContextSpy.getContentResolver()).thenReturn(contentResolver); @@ -222,6 +228,11 @@ public class VibratorManagerServiceTest { } @Override + IBatteryStats getBatteryStatsService() { + return mBatteryStatsMock; + } + + @Override VibratorController createVibratorController(int vibratorId, VibratorController.OnVibrationCompleteListener listener) { return mVibratorProviders.get(vibratorId) @@ -382,6 +393,11 @@ public class VibratorManagerServiceTest { inOrderVerifier.verify(listenerMock).onVibrating(eq(true)); inOrderVerifier.verify(listenerMock).onVibrating(eq(false)); inOrderVerifier.verifyNoMoreInteractions(); + + InOrder batteryVerifier = inOrder(mBatteryStatsMock); + batteryVerifier.verify(mBatteryStatsMock) + .noteVibratorOn(UID, 40 + mVibrationConfig.getRampDownDurationMs()); + batteryVerifier.verify(mBatteryStatsMock).noteVibratorOff(UID); } @Test @@ -731,6 +747,12 @@ public class VibratorManagerServiceTest { // Wait before checking it never played a second effect. assertFalse(waitUntil(s -> mVibratorProviders.get(1).getEffectSegments().size() > 1, service, /* timeout= */ 50)); + + // The time estimate is recorded when the vibration starts, repeating vibrations + // are capped at BATTERY_STATS_REPEATING_VIBRATION_DURATION (=5000). + verify(mBatteryStatsMock).noteVibratorOn(UID, 5000); + // The second vibration shouldn't have recorded that the vibrators were turned on. + verify(mBatteryStatsMock, times(1)).noteVibratorOn(anyInt(), anyLong()); } @Test diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java index f57c32c959d9..0394a546388d 100644 --- a/telephony/java/android/telephony/TelephonyManager.java +++ b/telephony/java/android/telephony/TelephonyManager.java @@ -4189,7 +4189,8 @@ public class TelephonyManager { * {@link UiccSlotMapping} which consist of both physical slot index and port index. * Logical slot is the slot that is seen by modem. Physical slot is the actual physical slot. * Port index is the index (enumerated value) for the associated port available on the SIM. - * Each physical slot can have multiple ports if multi-enabled profile(MEP) is supported. + * Each physical slot can have multiple ports if + * {@link PackageManager#FEATURE_TELEPHONY_EUICC_MEP} is supported. * * Example: no. of logical slots 1 and physical slots 2 do not support MEP, each physical slot * has one port: @@ -4285,11 +4286,11 @@ public class TelephonyManager { /** * Get the mapping from logical slots to physical sim slots and port indexes. Initially the * logical slot index was mapped to physical slot index, but with support for multi-enabled - * profile(MEP) logical slot is now mapped to port index. + * profile(MEP){@link PackageManager#FEATURE_TELEPHONY_EUICC_MEP},logical slot is now mapped to + * port index. * * @return a collection of {@link UiccSlotMapping} which indicates the mapping from logical * slots to ports and physical slots. - * * @hide */ @SystemApi diff --git a/telephony/java/android/telephony/UiccCardInfo.java b/telephony/java/android/telephony/UiccCardInfo.java index 30ca1627953f..3843a6240b43 100644 --- a/telephony/java/android/telephony/UiccCardInfo.java +++ b/telephony/java/android/telephony/UiccCardInfo.java @@ -17,6 +17,7 @@ package android.telephony; import android.annotation.NonNull; import android.annotation.Nullable; +import android.content.pm.PackageManager; import android.os.Parcel; import android.os.Parcelable; @@ -147,9 +148,10 @@ public final class UiccCardInfo implements Parcelable { * Note that this field may be omitted if the caller does not have the correct permissions * (see {@link TelephonyManager#getUiccCardsInfo()}). * - * @deprecated with support for MEP(multiple enabled profile), a SIM card can have more than one - * ICCID active at the same time.Instead use {@link UiccPortInfo#getIccId()} to retrieve ICCID. - * To find {@link UiccPortInfo} use {@link UiccCardInfo#getPorts()} + * @deprecated with support for MEP(multiple enabled profile) + * {@link PackageManager#FEATURE_TELEPHONY_EUICC_MEP}, a SIM card can have more than one + * ICCID active at the same time. Instead use {@link UiccPortInfo#getIccId()} to retrieve ICCID. + * To find {@link UiccPortInfo} use {@link UiccCardInfo#getPorts()}. * * @throws UnsupportedOperationException if the calling app's target SDK is T and beyond. */ @@ -192,11 +194,11 @@ public final class UiccCardInfo implements Parcelable { } /* - * Whether the UICC card supports multiple enable profile(MEP) + * Whether the UICC card supports multiple enabled profile(MEP) * UICCs are generally MEP disabled, there can be only one active profile on the physical * sim card. * - * @return {@code true} if the eUICC is supporting multiple enabled profile(MEP). + * @return {@code true} if the UICC is supporting multiple enabled profile(MEP). */ public boolean isMultipleEnabledProfilesSupported() { return mIsMultipleEnabledProfilesSupported; @@ -205,6 +207,9 @@ public final class UiccCardInfo implements Parcelable { /** * Get information regarding port, ICCID and its active status. * + * For device which support {@link PackageManager#FEATURE_TELEPHONY_EUICC_MEP}, it should return + * more than one {@link UiccPortInfo} object if the card is eUICC. + * * @return Collection of {@link UiccPortInfo} */ public @NonNull Collection<UiccPortInfo> getPorts() { diff --git a/telephony/java/android/telephony/UiccPortInfo.java b/telephony/java/android/telephony/UiccPortInfo.java index d1838c0b91f4..6fb0470d6225 100644 --- a/telephony/java/android/telephony/UiccPortInfo.java +++ b/telephony/java/android/telephony/UiccPortInfo.java @@ -29,7 +29,9 @@ import java.util.Objects; * Per GSMA SGP.22 V3.0, a port is a logical entity to which an active UICC profile can be bound on * a UICC card. If UICC supports 2 ports, then the port index is numbered 0,1. * Each port index is unique within an UICC, but not necessarily unique across UICC’s. - * For UICC's does not support MEP(Multi-enabled profile), just return the default port index 0. + * For UICC's does not support MEP(Multi-enabled profile) + * {@link android.content.pm.PackageManager#FEATURE_TELEPHONY_EUICC_MEP}, just return the default + * port index 0. */ public final class UiccPortInfo implements Parcelable{ private final String mIccId; diff --git a/telephony/java/android/telephony/UiccSlotInfo.java b/telephony/java/android/telephony/UiccSlotInfo.java index 17f34db4e44a..17ce45063d41 100644 --- a/telephony/java/android/telephony/UiccSlotInfo.java +++ b/telephony/java/android/telephony/UiccSlotInfo.java @@ -19,6 +19,7 @@ import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SystemApi; +import android.content.pm.PackageManager; import android.os.Parcel; import android.os.Parcelable; @@ -225,6 +226,9 @@ public class UiccSlotInfo implements Parcelable { /** * Get Information regarding port, iccid and its active status. * + * For device which support {@link PackageManager#FEATURE_TELEPHONY_EUICC_MEP}, it should return + * more than one {@link UiccPortInfo} object if the card is eUICC. + * * @return Collection of {@link UiccPortInfo} */ public @NonNull Collection<UiccPortInfo> getPorts() { diff --git a/telephony/java/android/telephony/euicc/EuiccManager.java b/telephony/java/android/telephony/euicc/EuiccManager.java index b6ae53017f64..4820d332de0f 100644 --- a/telephony/java/android/telephony/euicc/EuiccManager.java +++ b/telephony/java/android/telephony/euicc/EuiccManager.java @@ -37,6 +37,7 @@ import android.os.RemoteException; import android.telephony.SubscriptionInfo; import android.telephony.TelephonyFrameworkInitializer; import android.telephony.TelephonyManager; +import android.telephony.UiccCardInfo; import android.telephony.euicc.EuiccCardManager.ResetOption; import android.util.Log; @@ -931,6 +932,21 @@ public class EuiccManager { * intent to prompt the user to accept the download. The caller should also be authorized to * manage the subscription to be downloaded. * + * <p>If device support {@link PackageManager#FEATURE_TELEPHONY_EUICC_MEP} and + * switchAfterDownload is {@code true}, the subscription will be enabled on an esim port based + * on the following selection rules: + * <ul> + * <li>In SS(Single SIM) mode, if the embedded slot already has an active port, then download + * and enable the subscription on this port. + * <li>In SS mode, if the embedded slot is not active, then try to download and enable the + * subscription on the default port 0 of eUICC. + * <li>In DSDS mode, find first available port to download and enable the subscription. + * (see {@link #isSimPortAvailable(int)}) + *</ul> + * If there is no available port, an {@link #EMBEDDED_SUBSCRIPTION_RESULT_RESOLVABLE_ERROR} + * will be returned in the callback intent to prompt the user to disable an already-active + * subscription. + * * @param subscription the subscription to download. * @param switchAfterDownload if true, the profile will be activated upon successful download. * @param callbackIntent a PendingIntent to launch when the operation completes. @@ -1141,14 +1157,25 @@ public class EuiccManager { * intent to prompt the user to accept the download. The caller should also be authorized to * manage the subscription to be enabled. * - * <p> From Android T, devices might support MEP(Multiple Enabled Profiles), the subscription - * can be installed on different port from the eUICC. Calling apps with carrier privilege - * (see {@link TelephonyManager#hasCarrierPrivileges}) over the currently active subscriptions - * can use {@link #switchToSubscription(int, int, PendingIntent)} to specify which port to - * enable the subscription. Otherwise, use this API to enable the subscription on the eUICC - * and the platform will internally resolve a port. If there is no available port, - * an {@link #EMBEDDED_SUBSCRIPTION_RESULT_RESOLVABLE_ERROR} might be returned in the callback - * intent to prompt the user to disable an already-active subscription. + * <p> From Android T, devices might support {@link PackageManager#FEATURE_TELEPHONY_EUICC_MEP}, + * the subscription can be installed on different port from the eUICC. Calling apps with + * carrier privilege (see {@link TelephonyManager#hasCarrierPrivileges}) over the currently + * active subscriptions can use {@link #switchToSubscription(int, int, PendingIntent)} to + * specify which port to enable the subscription. Otherwise, use this API to enable the + * subscription on the eUICC and the platform will internally resolve a port based on following + * rules: + * <ul> + * <li>always use the default port 0 is eUICC does not support MEP. + * <li>In SS(Single SIM) mode, if the embedded slot already has an active port, then enable + * the subscription on this port. + * <li>In SS mode, if the embedded slot is not active, then try to enable the subscription on + * the default port 0 of eUICC. + * <li>In DSDS mode, find first available port to enable the subscription. + * (see {@link #isSimPortAvailable(int)}) + *</ul> + * If there is no available port, an {@link #EMBEDDED_SUBSCRIPTION_RESULT_RESOLVABLE_ERROR} + * will be returned in the callback intent to prompt the user to disable an already-active + * subscription. * * @param subscriptionId the ID of the subscription to enable. May be * {@link android.telephony.SubscriptionManager#INVALID_SUBSCRIPTION_ID} to deactivate the @@ -1197,7 +1224,15 @@ public class EuiccManager { * * <p> If the caller is passing invalid port index, * an {@link #EMBEDDED_SUBSCRIPTION_RESULT_ERROR} with detailed error code - * {@link #ERROR_INVALID_PORT} will be returned. + * {@link #ERROR_INVALID_PORT} will be returned. The port index is invalid if one of the + * following requirements is met: + * <ul> + * <li>index is beyond the range of {@link UiccCardInfo#getPorts()}. + * <li>In SS(Single SIM) mode, the embedded slot already has an active port with different + * port index. + * <li>In DSDS mode, if the psim slot is active and the embedded slot already has an active + * empty port with different port index. + * </ul> * * <p> Depending on the target port and permission check, * an {@link #EMBEDDED_SUBSCRIPTION_RESULT_RESOLVABLE_ERROR} might be returned to the callback @@ -1522,8 +1557,8 @@ public class EuiccManager { /** * Returns whether the passing portIndex is available. - * A port is available if it has no profiles enabled on it or calling app has carrier privilege - * over the profile installed on the selected port. + * A port is available if it is active without enabled profile on it or + * calling app has carrier privilege over the profile installed on the selected port. * Always returns false if the cardId is a physical card. * * @param portIndex is an enumeration of the ports available on the UICC. |