Merge "Fix navbar position assertion" into main
diff --git a/Android.bp b/Android.bp
index 93d6f53..be589b2 100644
--- a/Android.bp
+++ b/Android.bp
@@ -246,7 +246,6 @@
"android.system.suspend.control.internal-java",
"devicepolicyprotosnano",
- "com.android.sysprop.apex",
"com.android.sysprop.init",
"com.android.sysprop.localization",
"PlatformProperties",
diff --git a/apex/jobscheduler/service/java/com/android/server/usage/AppIdleHistory.java b/apex/jobscheduler/service/java/com/android/server/usage/AppIdleHistory.java
index fb342b9..913a76a 100644
--- a/apex/jobscheduler/service/java/com/android/server/usage/AppIdleHistory.java
+++ b/apex/jobscheduler/service/java/com/android/server/usage/AppIdleHistory.java
@@ -61,6 +61,7 @@
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
+import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
@@ -802,6 +803,9 @@
}
}
}
+ } catch (FileNotFoundException e) {
+ // Expected on first boot
+ Slog.d(TAG, "App idle file for user " + userId + " does not exist");
} catch (IOException | XmlPullParserException e) {
Slog.e(TAG, "Unable to read app idle file for user " + userId, e);
} finally {
diff --git a/core/java/Android.bp b/core/java/Android.bp
index 70cb597..fad4818 100644
--- a/core/java/Android.bp
+++ b/core/java/Android.bp
@@ -39,6 +39,15 @@
}
filegroup {
+ name: "feature_flags_aidl",
+ srcs: [
+ "android/flags/IFeatureFlags.aidl",
+ "android/flags/IFeatureFlagsCallback.aidl",
+ "android/flags/SyncableFlag.aidl",
+ ],
+}
+
+filegroup {
name: "ITracingServiceProxy.aidl",
srcs: ["android/tracing/ITracingServiceProxy.aidl"],
}
diff --git a/core/java/android/companion/CompanionDeviceManager.java b/core/java/android/companion/CompanionDeviceManager.java
index 4dea4a7..e4b2cf3 100644
--- a/core/java/android/companion/CompanionDeviceManager.java
+++ b/core/java/android/companion/CompanionDeviceManager.java
@@ -208,14 +208,6 @@
public static final String EXTRA_ASSOCIATION = "android.companion.extra.ASSOCIATION";
/**
- * The package name of the companion device discovery component.
- *
- * @hide
- */
- public static final String COMPANION_DEVICE_DISCOVERY_PACKAGE_NAME =
- "com.android.companiondevicemanager";
-
- /**
* Callback for applications to receive updates about and the outcome of
* {@link AssociationRequest} issued via {@code associate()} call.
*
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java
index 6d82922..2a6d84b 100644
--- a/core/java/android/content/Context.java
+++ b/core/java/android/content/Context.java
@@ -5298,6 +5298,13 @@
public static final String APP_PREDICTION_SERVICE = "app_prediction";
/**
+ * Used for reading system-wide, overridable flags.
+ *
+ * @hide
+ */
+ public static final String FEATURE_FLAGS_SERVICE = "feature_flags";
+
+ /**
* Official published name of the search ui service.
*
* <p><b>NOTE: </b> this service is optional; callers of
diff --git a/core/java/android/content/res/ApkAssets.java b/core/java/android/content/res/ApkAssets.java
index 4089cfe..653e243 100644
--- a/core/java/android/content/res/ApkAssets.java
+++ b/core/java/android/content/res/ApkAssets.java
@@ -15,8 +15,6 @@
*/
package android.content.res;
-import static android.content.res.Resources.ID_NULL;
-
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -388,7 +386,7 @@
synchronized (this) {
long nativeXmlPtr = nativeOpenXml(mNativePtr, fileName);
try (XmlBlock block = new XmlBlock(null, nativeXmlPtr)) {
- XmlResourceParser parser = block.newParser(ID_NULL, new Validator());
+ XmlResourceParser parser = block.newParser();
// If nativeOpenXml doesn't throw, it will always return a valid native pointer,
// which makes newParser always return non-null. But let's be careful.
if (parser == null) {
diff --git a/core/java/android/content/res/Element.java b/core/java/android/content/res/Element.java
index a6fea6a..62a46b6 100644
--- a/core/java/android/content/res/Element.java
+++ b/core/java/android/content/res/Element.java
@@ -17,7 +17,6 @@
package android.content.res;
import android.annotation.NonNull;
-import android.util.ArrayMap;
import android.util.Pools.SimplePool;
import androidx.annotation.StyleableRes;
@@ -27,9 +26,6 @@
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
-import java.util.Iterator;
-import java.util.Map;
-
/**
* Defines the string attribute length and child tag count restrictions for a xml element.
*
@@ -161,9 +157,11 @@
private static final String[] NAME_VALUE_ATTRS = {TAG_ATTR_NAME, TAG_ATTR_VALUE};
private String[] mStringAttrNames = new String[0];
- private final Map<String, TagCounter> mTagCounters = new ArrayMap<>();
+ // The length of mTagCounters corresponds to the number of tags defined in getCounterIdx. If new
+ // tags are added then the size here should be increased to match.
+ private final TagCounter[] mTagCounters = new TagCounter[35];
- private String mTag;
+ String mTag;
private static final ThreadLocal<SimplePool<Element>> sPool =
ThreadLocal.withInitial(() -> new SimplePool<>(MAX_POOL_SIZE));
@@ -180,17 +178,91 @@
void recycle() {
mStringAttrNames = new String[0];
- Iterator<Map.Entry<String, TagCounter>> it = mTagCounters.entrySet().iterator();
- while (it.hasNext()) {
- it.next().getValue().recycle();
- it.remove();
- }
mTag = null;
sPool.get().release(this);
}
+ private long mChildTagMask = 0;
+
+ private static int getCounterIdx(String tag) {
+ switch(tag) {
+ case TAG_LAYOUT:
+ return 0;
+ case TAG_META_DATA:
+ return 1;
+ case TAG_INTENT_FILTER:
+ return 2;
+ case TAG_PROFILEABLE:
+ return 3;
+ case TAG_USES_NATIVE_LIBRARY:
+ return 4;
+ case TAG_RECEIVER:
+ return 5;
+ case TAG_SERVICE:
+ return 6;
+ case TAG_ACTIVITY_ALIAS:
+ return 7;
+ case TAG_USES_LIBRARY:
+ return 8;
+ case TAG_PROVIDER:
+ return 9;
+ case TAG_ACTIVITY:
+ return 10;
+ case TAG_ACTION:
+ return 11;
+ case TAG_CATEGORY:
+ return 12;
+ case TAG_DATA:
+ return 13;
+ case TAG_APPLICATION:
+ return 14;
+ case TAG_OVERLAY:
+ return 15;
+ case TAG_INSTRUMENTATION:
+ return 16;
+ case TAG_PERMISSION_GROUP:
+ return 17;
+ case TAG_PERMISSION_TREE:
+ return 18;
+ case TAG_SUPPORTS_GL_TEXTURE:
+ return 19;
+ case TAG_SUPPORTS_SCREENS:
+ return 20;
+ case TAG_USES_CONFIGURATION:
+ return 21;
+ case TAG_USES_PERMISSION_SDK_23:
+ return 22;
+ case TAG_USES_SDK:
+ return 23;
+ case TAG_COMPATIBLE_SCREENS:
+ return 24;
+ case TAG_QUERIES:
+ return 25;
+ case TAG_ATTRIBUTION:
+ return 26;
+ case TAG_USES_FEATURE:
+ return 27;
+ case TAG_PERMISSION:
+ return 28;
+ case TAG_USES_PERMISSION:
+ return 29;
+ case TAG_GRANT_URI_PERMISSION:
+ return 30;
+ case TAG_PATH_PERMISSION:
+ return 31;
+ case TAG_PACKAGE:
+ return 32;
+ case TAG_INTENT:
+ return 33;
+ default:
+ // The size of the mTagCounters array should be equal to this value+1
+ return 34;
+ }
+ }
+
private void init(String tag) {
this.mTag = tag;
+ mChildTagMask = 0;
switch (tag) {
case TAG_ACTION:
case TAG_CATEGORY:
@@ -208,29 +280,29 @@
break;
case TAG_ACTIVITY:
setStringAttrNames(ACTIVITY_STR_ATTR_NAMES);
- addTagCounter(1000, TAG_LAYOUT);
- addTagCounter(8000, TAG_META_DATA);
- addTagCounter(20000, TAG_INTENT_FILTER);
+ initializeCounter(TAG_LAYOUT, 1000);
+ initializeCounter(TAG_META_DATA, 8000);
+ initializeCounter(TAG_INTENT_FILTER, 20000);
break;
case TAG_ACTIVITY_ALIAS:
setStringAttrNames(ACTIVITY_ALIAS_STR_ATTR_NAMES);
- addTagCounter(8000, TAG_META_DATA);
- addTagCounter(20000, TAG_INTENT_FILTER);
+ initializeCounter(TAG_META_DATA, 8000);
+ initializeCounter(TAG_INTENT_FILTER, 20000);
break;
case TAG_APPLICATION:
setStringAttrNames(APPLICATION_STR_ATTR_NAMES);
- addTagCounter(100, TAG_PROFILEABLE);
- addTagCounter(100, TAG_USES_NATIVE_LIBRARY);
- addTagCounter(1000, TAG_RECEIVER);
- addTagCounter(1000, TAG_SERVICE);
- addTagCounter(4000, TAG_ACTIVITY_ALIAS);
- addTagCounter(4000, TAG_USES_LIBRARY);
- addTagCounter(8000, TAG_PROVIDER);
- addTagCounter(8000, TAG_META_DATA);
- addTagCounter(40000, TAG_ACTIVITY);
+ initializeCounter(TAG_PROFILEABLE, 100);
+ initializeCounter(TAG_USES_NATIVE_LIBRARY, 100);
+ initializeCounter(TAG_RECEIVER, 1000);
+ initializeCounter(TAG_SERVICE, 1000);
+ initializeCounter(TAG_ACTIVITY_ALIAS, 4000);
+ initializeCounter(TAG_USES_LIBRARY, 4000);
+ initializeCounter(TAG_PROVIDER, 8000);
+ initializeCounter(TAG_META_DATA, 8000);
+ initializeCounter(TAG_ACTIVITY, 40000);
break;
case TAG_COMPATIBLE_SCREENS:
- addTagCounter(4000, TAG_SCREEN);
+ initializeCounter(TAG_SCREEN, 4000);
break;
case TAG_DATA:
setStringAttrNames(DATA_STR_ATTR_NAMES);
@@ -243,28 +315,28 @@
break;
case TAG_INTENT:
case TAG_INTENT_FILTER:
- addTagCounter(20000, TAG_ACTION);
- addTagCounter(40000, TAG_CATEGORY);
- addTagCounter(40000, TAG_DATA);
+ initializeCounter(TAG_ACTION, 20000);
+ initializeCounter(TAG_CATEGORY, 40000);
+ initializeCounter(TAG_DATA, 40000);
break;
case TAG_MANIFEST:
setStringAttrNames(MANIFEST_STR_ATTR_NAMES);
- addTagCounter(100, TAG_APPLICATION);
- addTagCounter(100, TAG_OVERLAY);
- addTagCounter(100, TAG_INSTRUMENTATION);
- addTagCounter(100, TAG_PERMISSION_GROUP);
- addTagCounter(100, TAG_PERMISSION_TREE);
- addTagCounter(100, TAG_SUPPORTS_GL_TEXTURE);
- addTagCounter(100, TAG_SUPPORTS_SCREENS);
- addTagCounter(100, TAG_USES_CONFIGURATION);
- addTagCounter(100, TAG_USES_PERMISSION_SDK_23);
- addTagCounter(100, TAG_USES_SDK);
- addTagCounter(200, TAG_COMPATIBLE_SCREENS);
- addTagCounter(200, TAG_QUERIES);
- addTagCounter(400, TAG_ATTRIBUTION);
- addTagCounter(400, TAG_USES_FEATURE);
- addTagCounter(2000, TAG_PERMISSION);
- addTagCounter(20000, TAG_USES_PERMISSION);
+ initializeCounter(TAG_APPLICATION, 100);
+ initializeCounter(TAG_OVERLAY, 100);
+ initializeCounter(TAG_INSTRUMENTATION, 100);
+ initializeCounter(TAG_PERMISSION_GROUP, 100);
+ initializeCounter(TAG_PERMISSION_TREE, 100);
+ initializeCounter(TAG_SUPPORTS_GL_TEXTURE, 100);
+ initializeCounter(TAG_SUPPORTS_SCREENS, 100);
+ initializeCounter(TAG_USES_CONFIGURATION, 100);
+ initializeCounter(TAG_USES_PERMISSION_SDK_23, 100);
+ initializeCounter(TAG_USES_SDK, 100);
+ initializeCounter(TAG_COMPATIBLE_SCREENS, 200);
+ initializeCounter(TAG_QUERIES, 200);
+ initializeCounter(TAG_ATTRIBUTION, 400);
+ initializeCounter(TAG_USES_FEATURE, 400);
+ initializeCounter(TAG_PERMISSION, 2000);
+ initializeCounter(TAG_USES_PERMISSION, 20000);
break;
case TAG_META_DATA:
case TAG_PROPERTY:
@@ -281,21 +353,21 @@
break;
case TAG_PROVIDER:
setStringAttrNames(PROVIDER_STR_ATTR_NAMES);
- addTagCounter(100, TAG_GRANT_URI_PERMISSION);
- addTagCounter(100, TAG_PATH_PERMISSION);
- addTagCounter(8000, TAG_META_DATA);
- addTagCounter(20000, TAG_INTENT_FILTER);
+ initializeCounter(TAG_GRANT_URI_PERMISSION, 100);
+ initializeCounter(TAG_PATH_PERMISSION, 100);
+ initializeCounter(TAG_META_DATA, 8000);
+ initializeCounter(TAG_INTENT_FILTER, 20000);
break;
case TAG_QUERIES:
- addTagCounter(1000, TAG_PACKAGE);
- addTagCounter(2000, TAG_INTENT);
- addTagCounter(8000, TAG_PROVIDER);
+ initializeCounter(TAG_PACKAGE, 1000);
+ initializeCounter(TAG_INTENT, 2000);
+ initializeCounter(TAG_PROVIDER, 8000);
break;
case TAG_RECEIVER:
case TAG_SERVICE:
setStringAttrNames(RECEIVER_SERVICE_STR_ATTR_NAMES);
- addTagCounter(8000, TAG_META_DATA);
- addTagCounter(20000, TAG_INTENT_FILTER);
+ initializeCounter(TAG_META_DATA, 8000);
+ initializeCounter(TAG_INTENT_FILTER, 20000);
break;
}
}
@@ -365,12 +437,17 @@
}
}
- private void addTagCounter(int max, String tag) {
- mTagCounters.put(tag, TagCounter.obtain(max));
+ private void initializeCounter(String tag, int max) {
+ int idx = getCounterIdx(tag);
+ if (mTagCounters[idx] == null) {
+ mTagCounters[idx] = new TagCounter();
+ }
+ mTagCounters[idx].reset(max);
+ mChildTagMask |= 1 << idx;
}
boolean hasChild(String tag) {
- return mTagCounters.containsKey(tag);
+ return (mChildTagMask & (1 << getCounterIdx(tag))) != 0;
}
void validateStringAttrs(@NonNull XmlPullParser attrs) throws XmlPullParserException {
@@ -393,8 +470,8 @@
}
void seen(@NonNull Element element) throws XmlPullParserException {
- if (mTagCounters.containsKey(element.mTag)) {
- TagCounter counter = mTagCounters.get(element.mTag);
+ TagCounter counter = mTagCounters[getCounterIdx(element.mTag)];
+ if (counter != null) {
counter.increment();
if (!counter.isValid()) {
throw new XmlPullParserException("The number of child " + element.mTag
diff --git a/core/java/android/content/res/TagCounter.java b/core/java/android/content/res/TagCounter.java
index 0e5510f..94deee7 100644
--- a/core/java/android/content/res/TagCounter.java
+++ b/core/java/android/content/res/TagCounter.java
@@ -16,47 +16,25 @@
package android.content.res;
-import android.annotation.NonNull;
-import android.util.Pools.SimplePool;
-
/**
* Counter used to track the number of tags seen during manifest validation.
*
* {@hide}
*/
public class TagCounter {
- private static final int MAX_POOL_SIZE = 512;
private static final int DEFAULT_MAX_COUNT = 512;
- private static final ThreadLocal<SimplePool<TagCounter>> sPool =
- ThreadLocal.withInitial(() -> new SimplePool<>(MAX_POOL_SIZE));
-
private int mMaxValue;
private int mCount;
- @NonNull
- static TagCounter obtain(int max) {
- TagCounter counter = sPool.get().acquire();
- if (counter == null) {
- counter = new TagCounter();
- }
- counter.setMaxValue(max);
- return counter;
- }
-
- void recycle() {
- mCount = 0;
- mMaxValue = DEFAULT_MAX_COUNT;
- sPool.get().release(this);
- }
-
public TagCounter() {
mMaxValue = DEFAULT_MAX_COUNT;
mCount = 0;
}
- private void setMaxValue(int maxValue) {
+ void reset(int maxValue) {
this.mMaxValue = maxValue;
+ this.mCount = 0;
}
void increment() {
@@ -66,8 +44,4 @@
public boolean isValid() {
return mCount <= mMaxValue;
}
-
- int value() {
- return mCount;
- }
}
diff --git a/core/java/android/content/res/Validator.java b/core/java/android/content/res/Validator.java
index daa7dfe..8b5e6c6 100644
--- a/core/java/android/content/res/Validator.java
+++ b/core/java/android/content/res/Validator.java
@@ -24,9 +24,6 @@
import org.xmlpull.v1.XmlPullParserException;
import java.util.ArrayDeque;
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.Map;
/**
* Validates manifest files by ensuring that tag counts and the length of string attributes are
@@ -36,30 +33,12 @@
*/
public class Validator {
- private static final int MAX_TAG_COUNT = 100000;
-
private final ArrayDeque<Element> mElements = new ArrayDeque<>();
- private final Map<String, TagCounter> mTagCounters = new HashMap<>();
private void cleanUp() {
while (!mElements.isEmpty()) {
mElements.pop().recycle();
}
- Iterator<Map.Entry<String, TagCounter>> it = mTagCounters.entrySet().iterator();
- while (it.hasNext()) {
- it.next().getValue().recycle();
- it.remove();
- }
- }
-
- private void seen(String tag) throws XmlPullParserException {
- mTagCounters.putIfAbsent(tag, TagCounter.obtain(MAX_TAG_COUNT));
- TagCounter counter = mTagCounters.get(tag);
- counter.increment();
- if (!counter.isValid()) {
- throw new XmlPullParserException("The number of " + tag
- + " tags exceeded " + MAX_TAG_COUNT);
- }
}
/**
@@ -84,7 +63,6 @@
}
Element parent = mElements.peek();
if (parent == null || parent.hasChild(tag)) {
- seen(tag);
Element element = Element.obtain(tag);
element.validateStringAttrs(parser);
if (parent != null) {
diff --git a/core/java/android/flags/BooleanFlag.java b/core/java/android/flags/BooleanFlag.java
new file mode 100644
index 0000000..d4a35b2
--- /dev/null
+++ b/core/java/android/flags/BooleanFlag.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2023 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.flags;
+
+import android.annotation.NonNull;
+
+/**
+ * A flag representing a true or false value.
+ *
+ * The value will always be the same during the lifetime of the process it is read in.
+ *
+ * @hide
+ */
+public class BooleanFlag extends BooleanFlagBase {
+ private final boolean mDefault;
+
+ /**
+ * @param namespace A namespace for this flag. See {@link android.provider.DeviceConfig}.
+ * @param name A name for this flag.
+ * @param defaultValue The value of this flag if no other override is present.
+ */
+ BooleanFlag(String namespace, String name, boolean defaultValue) {
+ super(namespace, name);
+ mDefault = defaultValue;
+ }
+
+ @Override
+ @NonNull
+ public Boolean getDefault() {
+ return mDefault;
+ }
+
+ @Override
+ public BooleanFlag defineMetaData(String label, String description, String categoryName) {
+ super.defineMetaData(label, description, categoryName);
+ return this;
+ }
+}
diff --git a/core/java/android/flags/BooleanFlagBase.java b/core/java/android/flags/BooleanFlagBase.java
new file mode 100644
index 0000000..985dbe3
--- /dev/null
+++ b/core/java/android/flags/BooleanFlagBase.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2023 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.flags;
+
+import android.annotation.NonNull;
+
+abstract class BooleanFlagBase implements Flag<Boolean> {
+
+ private final String mNamespace;
+ private final String mName;
+ private String mLabel;
+ private String mDescription;
+ private String mCategoryName;
+
+ /**
+ * @param namespace A namespace for this flag. See {@link android.provider.DeviceConfig}.
+ * @param name A name for this flag.
+ */
+ BooleanFlagBase(String namespace, String name) {
+ mNamespace = namespace;
+ mName = name;
+ mLabel = name;
+ }
+
+ public abstract Boolean getDefault();
+
+ @Override
+ @NonNull
+ public String getNamespace() {
+ return mNamespace;
+ }
+
+ @Override
+ @NonNull
+ public String getName() {
+ return mName;
+ }
+
+ @Override
+ public BooleanFlagBase defineMetaData(String label, String description, String categoryName) {
+ mLabel = label;
+ mDescription = description;
+ mCategoryName = categoryName;
+ return this;
+ }
+
+ @Override
+ @NonNull
+ public String getLabel() {
+ return mLabel;
+ }
+
+ @Override
+ public String getDescription() {
+ return mDescription;
+ }
+
+ @Override
+ public String getCategoryName() {
+ return mCategoryName;
+ }
+
+ @Override
+ @NonNull
+ public String toString() {
+ return getNamespace() + "." + getName() + "[" + getDefault() + "]";
+ }
+}
diff --git a/core/java/android/flags/DynamicBooleanFlag.java b/core/java/android/flags/DynamicBooleanFlag.java
new file mode 100644
index 0000000..271a8c5f4
--- /dev/null
+++ b/core/java/android/flags/DynamicBooleanFlag.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2023 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.flags;
+
+/**
+ * A flag representing a true or false value.
+ *
+ * The value may be different from one read to the next.
+ *
+ * @hide
+ */
+public class DynamicBooleanFlag extends BooleanFlagBase implements DynamicFlag<Boolean> {
+
+ private final boolean mDefault;
+
+ /**
+ * @param namespace A namespace for this flag. See {@link android.provider.DeviceConfig}.
+ * @param name A name for this flag.
+ * @param defaultValue The value of this flag if no other override is present.
+ */
+ DynamicBooleanFlag(String namespace, String name, boolean defaultValue) {
+ super(namespace, name);
+ mDefault = defaultValue;
+ }
+
+ @Override
+ public Boolean getDefault() {
+ return mDefault;
+ }
+
+ @Override
+ public DynamicBooleanFlag defineMetaData(String label, String description, String categoryName) {
+ super.defineMetaData(label, description, categoryName);
+ return this;
+ }
+}
diff --git a/core/java/android/flags/DynamicFlag.java b/core/java/android/flags/DynamicFlag.java
new file mode 100644
index 0000000..68819c5
--- /dev/null
+++ b/core/java/android/flags/DynamicFlag.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2023 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.flags;
+
+/**
+ * A flag for which the value may be different from one read to the next.
+ *
+ * @param <T> The type of value that this flag stores. E.g. Boolean or String.
+ *
+ * @hide
+ */
+public interface DynamicFlag<T> extends Flag<T> {
+ @Override
+ default boolean isDynamic() {
+ return true;
+ }
+}
diff --git a/core/java/android/flags/FeatureFlags.java b/core/java/android/flags/FeatureFlags.java
new file mode 100644
index 0000000..8d3112c
--- /dev/null
+++ b/core/java/android/flags/FeatureFlags.java
@@ -0,0 +1,379 @@
+/*
+ * Copyright (C) 2023 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.flags;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.util.ArraySet;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * A class for querying constants from the system - primarily booleans.
+ *
+ * Clients using this class can define their flags and their default values in one place,
+ * can override those values on running devices for debugging and testing purposes, and can control
+ * what flags are available to be used on release builds.
+ *
+ * TODO(b/279054964): A lot. This is skeleton code right now.
+ * @hide
+ */
+public class FeatureFlags {
+ private static final String TAG = "FeatureFlags";
+ private static FeatureFlags sInstance;
+ private static final Object sInstanceLock = new Object();
+
+ private final Set<Flag<?>> mKnownFlags = new ArraySet<>();
+ private final Set<Flag<?>> mDirtyFlags = new ArraySet<>();
+
+ private IFeatureFlags mIFeatureFlags;
+ private final Map<String, Map<String, Boolean>> mBooleanOverrides = new HashMap<>();
+ private final Set<ChangeListener> mListeners = new HashSet<>();
+
+ /**
+ * Obtain a per-process instance of FeatureFlags.
+ * @return A singleton instance of {@link FeatureFlags}.
+ */
+ @NonNull
+ public static FeatureFlags getInstance() {
+ synchronized (sInstanceLock) {
+ if (sInstance == null) {
+ sInstance = new FeatureFlags();
+ }
+ }
+
+ return sInstance;
+ }
+
+ /** See {@link FeatureFlagsFake}. */
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
+ public static void setInstance(FeatureFlags instance) {
+ synchronized (sInstanceLock) {
+ sInstance = instance;
+ }
+ }
+
+ private final IFeatureFlagsCallback mIFeatureFlagsCallback = new IFeatureFlagsCallback.Stub() {
+ @Override
+ public void onFlagChange(SyncableFlag flag) {
+ for (Flag<?> f : mKnownFlags) {
+ if (flagEqualsSyncableFlag(f, flag)) {
+ if (f instanceof DynamicFlag<?>) {
+ if (f instanceof DynamicBooleanFlag) {
+ String value = flag.getValue();
+ if (value == null) { // Null means any existing overrides were erased.
+ value = ((DynamicBooleanFlag) f).getDefault().toString();
+ }
+ addBooleanOverride(flag.getNamespace(), flag.getName(), value);
+ }
+ FeatureFlags.this.onFlagChange((DynamicFlag<?>) f);
+ }
+ break;
+ }
+ }
+ }
+ };
+
+ private FeatureFlags() {
+ this(null);
+ }
+
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
+ public FeatureFlags(IFeatureFlags iFeatureFlags) {
+ mIFeatureFlags = iFeatureFlags;
+
+ if (mIFeatureFlags != null) {
+ try {
+ mIFeatureFlags.registerCallback(mIFeatureFlagsCallback);
+ } catch (RemoteException e) {
+ // Shouldn't happen with things passed into tests.
+ Log.e(TAG, "Could not register callbacks!", e);
+ }
+ }
+ }
+
+ /**
+ * Construct a new {@link BooleanFlag}.
+ *
+ * Use this instead of constructing a {@link BooleanFlag} directly, as it registers the flag
+ * with the internals of the flagging system.
+ */
+ @NonNull
+ public static BooleanFlag booleanFlag(
+ @NonNull String namespace, @NonNull String name, boolean def) {
+ return getInstance().addFlag(new BooleanFlag(namespace, name, def));
+ }
+
+ /**
+ * Construct a new {@link FusedOffFlag}.
+ *
+ * Use this instead of constructing a {@link FusedOffFlag} directly, as it registers the
+ * flag with the internals of the flagging system.
+ */
+ @NonNull
+ public static FusedOffFlag fusedOffFlag(@NonNull String namespace, @NonNull String name) {
+ return getInstance().addFlag(new FusedOffFlag(namespace, name));
+ }
+
+ /**
+ * Construct a new {@link FusedOnFlag}.
+ *
+ * Use this instead of constructing a {@link FusedOnFlag} directly, as it registers the flag
+ * with the internals of the flagging system.
+ */
+ @NonNull
+ public static FusedOnFlag fusedOnFlag(@NonNull String namespace, @NonNull String name) {
+ return getInstance().addFlag(new FusedOnFlag(namespace, name));
+ }
+
+ /**
+ * Construct a new {@link DynamicBooleanFlag}.
+ *
+ * Use this instead of constructing a {@link DynamicBooleanFlag} directly, as it registers
+ * the flag with the internals of the flagging system.
+ */
+ @NonNull
+ public static DynamicBooleanFlag dynamicBooleanFlag(
+ @NonNull String namespace, @NonNull String name, boolean def) {
+ return getInstance().addFlag(new DynamicBooleanFlag(namespace, name, def));
+ }
+
+ /**
+ * Add a listener to be alerted when a {@link DynamicFlag} changes.
+ *
+ * See also {@link #removeChangeListener(ChangeListener)}.
+ *
+ * @param listener The listener to add.
+ */
+ public void addChangeListener(@NonNull ChangeListener listener) {
+ mListeners.add(listener);
+ }
+
+ /**
+ * Remove a listener that was added earlier.
+ *
+ * See also {@link #addChangeListener(ChangeListener)}.
+ *
+ * @param listener The listener to remove.
+ */
+ public void removeChangeListener(@NonNull ChangeListener listener) {
+ mListeners.remove(listener);
+ }
+
+ protected void onFlagChange(@NonNull DynamicFlag<?> flag) {
+ for (ChangeListener l : mListeners) {
+ l.onFlagChanged(flag);
+ }
+ }
+
+ /**
+ * Returns whether the supplied flag is true or not.
+ *
+ * {@link BooleanFlag} should only be used in debug builds. They do not get optimized out.
+ *
+ * The first time a flag is read, its value is cached for the lifetime of the process.
+ */
+ public boolean isEnabled(@NonNull BooleanFlag flag) {
+ return getBooleanInternal(flag);
+ }
+
+ /**
+ * Returns whether the supplied flag is true or not.
+ *
+ * Always returns false.
+ */
+ public boolean isEnabled(@NonNull FusedOffFlag flag) {
+ return false;
+ }
+
+ /**
+ * Returns whether the supplied flag is true or not.
+ *
+ * Always returns true;
+ */
+ public boolean isEnabled(@NonNull FusedOnFlag flag) {
+ return true;
+ }
+
+ /**
+ * Returns whether the supplied flag is true or not.
+ *
+ * Can return a different value for the flag each time it is called if an override comes in.
+ */
+ public boolean isCurrentlyEnabled(@NonNull DynamicBooleanFlag flag) {
+ return getBooleanInternal(flag);
+ }
+
+ private boolean getBooleanInternal(Flag<Boolean> flag) {
+ sync();
+ Map<String, Boolean> ns = mBooleanOverrides.get(flag.getNamespace());
+ Boolean value = null;
+ if (ns != null) {
+ value = ns.get(flag.getName());
+ }
+ if (value == null) {
+ throw new IllegalStateException("Boolean flag being read but was not synced: " + flag);
+ }
+
+ return value;
+ }
+
+ private <T extends Flag<?>> T addFlag(T flag) {
+ synchronized (FeatureFlags.class) {
+ mDirtyFlags.add(flag);
+ mKnownFlags.add(flag);
+ }
+ return flag;
+ }
+
+ /**
+ * Sync any known flags that have not yet been synced.
+ *
+ * This is called implicitly when any flag is read, and is not generally needed except in
+ * exceptional circumstances.
+ */
+ public void sync() {
+ synchronized (FeatureFlags.class) {
+ if (mDirtyFlags.isEmpty()) {
+ return;
+ }
+ syncInternal(mDirtyFlags);
+ mDirtyFlags.clear();
+ }
+ }
+
+ /**
+ * Called when new flags have been declared. Gives the implementation a chance to act on them.
+ *
+ * Guaranteed to be called from a synchronized, thread-safe context.
+ */
+ protected void syncInternal(Set<Flag<?>> dirtyFlags) {
+ IFeatureFlags iFeatureFlags = bind();
+ List<SyncableFlag> syncableFlags = new ArrayList<>();
+ for (Flag<?> f : dirtyFlags) {
+ syncableFlags.add(flagToSyncableFlag(f));
+ }
+
+ List<SyncableFlag> serverFlags = List.of(); // Need to initialize the list with something.
+ try {
+ // New values come back from the service.
+ serverFlags = iFeatureFlags.syncFlags(syncableFlags);
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+
+ for (Flag<?> f : dirtyFlags) {
+ boolean found = false;
+ for (SyncableFlag sf : serverFlags) {
+ if (flagEqualsSyncableFlag(f, sf)) {
+ if (f instanceof BooleanFlag || f instanceof DynamicBooleanFlag) {
+ addBooleanOverride(sf.getNamespace(), sf.getName(), sf.getValue());
+ }
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ if (f instanceof BooleanFlag) {
+ addBooleanOverride(
+ f.getNamespace(),
+ f.getName(),
+ ((BooleanFlag) f).getDefault() ? "true" : "false");
+ }
+ }
+ }
+ }
+
+ private void addBooleanOverride(String namespace, String name, String override) {
+ Map<String, Boolean> nsOverrides = mBooleanOverrides.get(namespace);
+ if (nsOverrides == null) {
+ nsOverrides = new HashMap<>();
+ mBooleanOverrides.put(namespace, nsOverrides);
+ }
+ nsOverrides.put(name, parseBoolean(override));
+ }
+
+ private SyncableFlag flagToSyncableFlag(Flag<?> f) {
+ return new SyncableFlag(
+ f.getNamespace(),
+ f.getName(),
+ f.getDefault().toString(),
+ f instanceof DynamicFlag<?>);
+ }
+
+ private IFeatureFlags bind() {
+ if (mIFeatureFlags == null) {
+ mIFeatureFlags = IFeatureFlags.Stub.asInterface(
+ ServiceManager.getService(Context.FEATURE_FLAGS_SERVICE));
+ try {
+ mIFeatureFlags.registerCallback(mIFeatureFlagsCallback);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to listen for flag changes!");
+ }
+ }
+
+ return mIFeatureFlags;
+ }
+
+ static boolean parseBoolean(String value) {
+ // Check for a truish string.
+ boolean result = value.equalsIgnoreCase("true")
+ || value.equals("1")
+ || value.equalsIgnoreCase("t")
+ || value.equalsIgnoreCase("on");
+ if (!result) { // Expect a falsish string, else log an error.
+ if (!(value.equalsIgnoreCase("false")
+ || value.equals("0")
+ || value.equalsIgnoreCase("f")
+ || value.equalsIgnoreCase("off"))) {
+ Log.e(TAG,
+ "Tried parsing " + value + " as boolean but it doesn't look like one. "
+ + "Value expected to be one of true|false, 1|0, t|f, on|off.");
+ }
+ }
+ return result;
+ }
+
+ private static boolean flagEqualsSyncableFlag(Flag<?> f, SyncableFlag sf) {
+ return f.getName().equals(sf.getName()) && f.getNamespace().equals(sf.getNamespace());
+ }
+
+
+ /**
+ * A simpler listener that is alerted when a {@link DynamicFlag} changes.
+ *
+ * See {@link #addChangeListener(ChangeListener)}
+ */
+ public interface ChangeListener {
+ /**
+ * Called when a {@link DynamicFlag} changes.
+ *
+ * @param flag The flag that has changed.
+ */
+ void onFlagChanged(DynamicFlag<?> flag);
+ }
+}
diff --git a/core/java/android/flags/FeatureFlagsFake.java b/core/java/android/flags/FeatureFlagsFake.java
new file mode 100644
index 0000000..daedcda
--- /dev/null
+++ b/core/java/android/flags/FeatureFlagsFake.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2023 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.flags;
+
+import android.annotation.NonNull;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * An implementation of {@link FeatureFlags} for testing.
+ *
+ * Before you read a flag from using this Fake, you must set that flag using
+ * {@link #setFlagValue(BooleanFlagBase, boolean)}. This ensures that your tests are deterministic.
+ *
+ * If you are relying on {@link FeatureFlags#getInstance()} to access FeatureFlags in your code
+ * under test, (instead of dependency injection), you can pass an instance of this fake to
+ * {@link FeatureFlags#setInstance(FeatureFlags)}. Be sure to call that method again, passing null,
+ * to ensure hermetic testing - you don't want static state persisting between your test methods.
+ *
+ * @hide
+ */
+public class FeatureFlagsFake extends FeatureFlags {
+ private final Map<BooleanFlagBase, Boolean> mFlagValues = new HashMap<>();
+ private final Set<BooleanFlagBase> mReadFlags = new HashSet<>();
+
+ public FeatureFlagsFake(IFeatureFlags iFeatureFlags) {
+ super(iFeatureFlags);
+ }
+
+ @Override
+ public boolean isEnabled(@NonNull BooleanFlag flag) {
+ return requireFlag(flag);
+ }
+
+ @Override
+ public boolean isEnabled(@NonNull FusedOffFlag flag) {
+ return requireFlag(flag);
+ }
+
+ @Override
+ public boolean isEnabled(@NonNull FusedOnFlag flag) {
+ return requireFlag(flag);
+ }
+
+ @Override
+ public boolean isCurrentlyEnabled(@NonNull DynamicBooleanFlag flag) {
+ return requireFlag(flag);
+ }
+
+ @Override
+ protected void syncInternal(Set<Flag<?>> dirtyFlags) {
+ }
+
+ /**
+ * Explicitly set a flag's value for reading in tests.
+ *
+ * You _must_ call this for every flag your code-under-test will read. Otherwise, an
+ * {@link IllegalStateException} will be thrown.
+ *
+ * You are able to set values for {@link FusedOffFlag} and {@link FusedOnFlag}, despite those
+ * flags having a fixed value at compile time, since unit tests should still test the state of
+ * those flags as both true and false. I.e. a flag that is off might be turned on in a future
+ * build or vice versa.
+ *
+ * You can not call this method _after_ a non-dynamic flag has been read. Non-dynamic flags
+ * are held stable in the system, so changing a value after reading would not match
+ * real-implementation behavior.
+ *
+ * Calling this method will trigger any {@link android.flags.FeatureFlags.ChangeListener}s that
+ * are registered for the supplied flag if the flag is a {@link DynamicFlag}.
+ *
+ * @param flag The BooleanFlag that you want to set a value for.
+ * @param value The value that the flag should return when accessed.
+ */
+ public void setFlagValue(@NonNull BooleanFlagBase flag, boolean value) {
+ if (!(flag instanceof DynamicBooleanFlag) && mReadFlags.contains(flag)) {
+ throw new RuntimeException(
+ "You can not set the value of a flag after it has been read. Tried to set "
+ + flag + " to " + value + " but it already " + mFlagValues.get(flag));
+ }
+ mFlagValues.put(flag, value);
+ if (flag instanceof DynamicBooleanFlag) {
+ onFlagChange((DynamicFlag<?>) flag);
+ }
+ }
+
+ private boolean requireFlag(BooleanFlagBase flag) {
+ if (!mFlagValues.containsKey(flag)) {
+ throw new IllegalStateException(
+ "Tried to access " + flag + " in test but no overrided specified. You must "
+ + "call #setFlagValue for each flag read in a test.");
+ }
+ mReadFlags.add(flag);
+
+ return mFlagValues.get(flag);
+ }
+
+}
diff --git a/core/java/android/flags/Flag.java b/core/java/android/flags/Flag.java
new file mode 100644
index 0000000..b97a4c8
--- /dev/null
+++ b/core/java/android/flags/Flag.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2023 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.flags;
+
+import android.annotation.NonNull;
+
+/**
+ * Base class for constants read via {@link android.flags.FeatureFlags}.
+ *
+ * @param <T> The type of value that this flag stores. E.g. Boolean or String.
+ *
+ * @hide
+ */
+public interface Flag<T> {
+ /** The namespace for a flag. Should combine uniquely with its name. */
+ @NonNull
+ String getNamespace();
+
+ /** The name of the flag. Should combine uniquely with its namespace. */
+ @NonNull
+ String getName();
+
+ /** The value of this flag if no override has been set. Null values are not supported. */
+ @NonNull
+ T getDefault();
+
+ /** Returns true if the value of this flag can change at runtime. */
+ default boolean isDynamic() {
+ return false;
+ }
+
+ /**
+ * Add human-readable details to the flag. Flag client's are not required to set this.
+ *
+ * See {@link #getLabel()}, {@link #getDescription()}, and {@link #getCategoryName()}.
+ *
+ * @return Returns `this`, to make a fluent api.
+ */
+ Flag<T> defineMetaData(String label, String description, String categoryName);
+
+ /**
+ * A human-readable name for the flag. Defaults to {@link #getName()}
+ *
+ * See {@link #defineMetaData(String, String, String)}
+ */
+ @NonNull
+ default String getLabel() {
+ return getName();
+ }
+
+ /**
+ * A human-readable description for the flag. Defaults to null if unset.
+ *
+ * See {@link #defineMetaData(String, String, String)}
+ */
+ default String getDescription() {
+ return null;
+ }
+
+ /**
+ * A human-readable category name for the flag. Defaults to null if unset.
+ *
+ * See {@link #defineMetaData(String, String, String)}
+ */
+ default String getCategoryName() {
+ return null;
+ }
+}
diff --git a/core/java/android/flags/FusedOffFlag.java b/core/java/android/flags/FusedOffFlag.java
new file mode 100644
index 0000000..6844b8f
--- /dev/null
+++ b/core/java/android/flags/FusedOffFlag.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2023 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.flags;
+
+import android.annotation.NonNull;
+import android.provider.DeviceConfig;
+
+/**
+ * A flag representing a false value.
+ *
+ * The flag can never be changed or overridden. It is false at compile time.
+ *
+ * @hide
+ */
+public final class FusedOffFlag extends BooleanFlagBase {
+ /**
+ * @param namespace A namespace for this flag. See {@link DeviceConfig}.
+ * @param name A name for this flag.
+ */
+ FusedOffFlag(String namespace, String name) {
+ super(namespace, name);
+ }
+
+ @Override
+ @NonNull
+ public Boolean getDefault() {
+ return false;
+ }
+
+ @Override
+ public FusedOffFlag defineMetaData(String label, String description, String categoryName) {
+ super.defineMetaData(label, description, categoryName);
+ return this;
+ }
+}
diff --git a/core/java/android/flags/FusedOnFlag.java b/core/java/android/flags/FusedOnFlag.java
new file mode 100644
index 0000000..e9adba7
--- /dev/null
+++ b/core/java/android/flags/FusedOnFlag.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2023 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.flags;
+
+import android.annotation.NonNull;
+import android.provider.DeviceConfig;
+
+/**
+ * A flag representing a true value.
+ *
+ * The flag can never be changed or overridden. It is true at compile time.
+ *
+ * @hide
+ */
+public final class FusedOnFlag extends BooleanFlagBase {
+ /**
+ * @param namespace A namespace for this flag. See {@link DeviceConfig}.
+ * @param name A name for this flag.
+ */
+ FusedOnFlag(String namespace, String name) {
+ super(namespace, name);
+ }
+
+ @Override
+ @NonNull
+ public Boolean getDefault() {
+ return true;
+ }
+
+ @Override
+ public FusedOnFlag defineMetaData(String label, String description, String categoryName) {
+ super.defineMetaData(label, description, categoryName);
+ return this;
+ }
+}
diff --git a/core/java/android/flags/IFeatureFlags.aidl b/core/java/android/flags/IFeatureFlags.aidl
new file mode 100644
index 0000000..3efcec9
--- /dev/null
+++ b/core/java/android/flags/IFeatureFlags.aidl
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2023 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.flags;
+
+import android.flags.IFeatureFlagsCallback;
+import android.flags.SyncableFlag;
+
+/**
+ * Binder interface for communicating with {@link com.android.server.flags.FeatureFlagsService}.
+ *
+ * This interface is used by {@link android.flags.FeatureFlags} and developers should use that to
+ * interface with the service. FeatureFlags is the "client" in this documentation.
+ *
+ * The methods allow client apps to communicate what flags they care about, and receive back
+ * current values for those flags. For stable flags, this is the finalized value until the device
+ * restarts. For {@link DynamicFlag}s, this is the last known value, though it may change in the
+ * future. Clients can listen for changes to flag values so that it can react accordingly.
+ * @hide
+ */
+interface IFeatureFlags {
+ /**
+ * Synchronize with the {@link com.android.server.flags.FeatureFlagsService} about flags of
+ * interest.
+ *
+ * The client should pass in a list of flags that it is using as {@link SyncableFlag}s, which
+ * includes what it thinks the default values of the flags are.
+ *
+ * The response will contain a list of matching SyncableFlags, whose values are set to what the
+ * value of the flags actually are. The client should update its internal state flag data to
+ * match.
+ *
+ * Generally speaking, if a flag that is passed in is new to the FeatureFlagsService, the
+ * service will cache the passed-in value, and return it back out. If, however, a different
+ * client has synced that flag with the service previously, FeatureFlagsService will return the
+ * existing cached value, which may or may not be what the current client passed in. This allows
+ * FeatureFlagsService to keep clients in agreement with one another.
+ */
+ List<SyncableFlag> syncFlags(in List<SyncableFlag> flagList);
+
+ /**
+ * Pass in an {@link IFeatureFlagsCallback} that will be called whenever a {@link DymamicFlag}
+ * changes.
+ */
+ void registerCallback(IFeatureFlagsCallback callback);
+
+ /**
+ * Remove a {@link IFeatureFlagsCallback} that was previously registered with
+ * {@link #registerCallback}.
+ */
+ void unregisterCallback(IFeatureFlagsCallback callback);
+
+ /**
+ * Query the {@link com.android.server.flags.FeatureFlagsService} for flags, but don't
+ * cache them. See {@link #syncFlags}.
+ *
+ * You almost certainly don't want this method. This is intended for the Flag Flipper
+ * application that needs to query the state of system but doesn't want to affect it by
+ * doing so. All other clients should use {@link syncFlags}.
+ */
+ List<SyncableFlag> queryFlags(in List<SyncableFlag> flagList);
+
+ /**
+ * Change a flags value in the system.
+ *
+ * This is intended for use by the Flag Flipper application.
+ */
+ void overrideFlag(in SyncableFlag flag);
+
+ /**
+ * Restore a flag to its default value.
+ *
+ * This is intended for use by the Flag Flipper application.
+ */
+ void resetFlag(in SyncableFlag flag);
+}
\ No newline at end of file
diff --git a/core/java/android/flags/IFeatureFlagsCallback.aidl b/core/java/android/flags/IFeatureFlagsCallback.aidl
new file mode 100644
index 0000000..f708667
--- /dev/null
+++ b/core/java/android/flags/IFeatureFlagsCallback.aidl
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2023 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.flags;
+
+import android.flags.SyncableFlag;
+
+/**
+ * Callback for {@link IFeatureFlags#registerCallback} to get alerts when a {@link DynamicFlag}
+ * changes.
+ *
+ * DynamicFlags can change at run time. Stable flags will never result in a call to this method.
+ *
+ * @hide
+ */
+oneway interface IFeatureFlagsCallback {
+ void onFlagChange(in SyncableFlag flag);
+}
\ No newline at end of file
diff --git a/core/java/android/flags/OWNERS b/core/java/android/flags/OWNERS
new file mode 100644
index 0000000..fa125c4
--- /dev/null
+++ b/core/java/android/flags/OWNERS
@@ -0,0 +1,7 @@
+# Bug component: 1306523
+
+mankoff@google.com
+pixel@google.com
+
+dsandler@android.com
+
diff --git a/core/java/android/flags/SyncableFlag.aidl b/core/java/android/flags/SyncableFlag.aidl
new file mode 100644
index 0000000..1526ec1
--- /dev/null
+++ b/core/java/android/flags/SyncableFlag.aidl
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2023 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.flags;
+
+/**
+ * A parcelable data class for serializing {@link Flag} across a Binder.
+ */
+parcelable SyncableFlag;
\ No newline at end of file
diff --git a/core/java/android/flags/SyncableFlag.java b/core/java/android/flags/SyncableFlag.java
new file mode 100644
index 0000000..449bcc3c
--- /dev/null
+++ b/core/java/android/flags/SyncableFlag.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2023 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.flags;
+
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * @hide
+ */
+public final class SyncableFlag implements Parcelable {
+ private final String mNamespace;
+ private final String mName;
+ private final String mValue;
+ private final boolean mDynamic;
+ private final boolean mOverridden;
+
+ public SyncableFlag(
+ @NonNull String namespace,
+ @NonNull String name,
+ @NonNull String value,
+ boolean dynamic) {
+ this(namespace, name, value, dynamic, false);
+ }
+
+ public SyncableFlag(
+ @NonNull String namespace,
+ @NonNull String name,
+ @NonNull String value,
+ boolean dynamic,
+ boolean overridden
+ ) {
+ mNamespace = namespace;
+ mName = name;
+ mValue = value;
+ mDynamic = dynamic;
+ mOverridden = overridden;
+ }
+
+ @NonNull
+ public String getNamespace() {
+ return mNamespace;
+ }
+
+ @NonNull
+ public String getName() {
+ return mName;
+ }
+
+ @NonNull
+ public String getValue() {
+ return mValue;
+ }
+
+ public boolean isDynamic() {
+ return mDynamic;
+ }
+
+ public boolean isOverridden() {
+ return mOverridden;
+ }
+
+ @NonNull
+ public static final Parcelable.Creator<SyncableFlag> CREATOR = new Parcelable.Creator<>() {
+ public SyncableFlag createFromParcel(Parcel in) {
+ return new SyncableFlag(
+ in.readString(),
+ in.readString(),
+ in.readString(),
+ in.readBoolean(),
+ in.readBoolean());
+ }
+
+ public SyncableFlag[] newArray(int size) {
+ return new SyncableFlag[size];
+ }
+ };
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeString(mNamespace);
+ dest.writeString(mName);
+ dest.writeString(mValue);
+ dest.writeBoolean(mDynamic);
+ dest.writeBoolean(mOverridden);
+ }
+
+ @Override
+ public String toString() {
+ return getNamespace() + "." + getName() + "[" + getValue() + "]";
+ }
+}
diff --git a/core/java/android/inputmethodservice/IInputMethodWrapper.java b/core/java/android/inputmethodservice/IInputMethodWrapper.java
index 70b72c8..b99996ff 100644
--- a/core/java/android/inputmethodservice/IInputMethodWrapper.java
+++ b/core/java/android/inputmethodservice/IInputMethodWrapper.java
@@ -433,7 +433,7 @@
@BinderThread
@Override
public void showSoftInput(IBinder showInputToken, @Nullable ImeTracker.Token statsToken,
- int flags, ResultReceiver resultReceiver) {
+ @InputMethod.ShowFlags int flags, ResultReceiver resultReceiver) {
ImeTracker.forLogging().onProgress(statsToken, ImeTracker.PHASE_IME_WRAPPER);
mCaller.executeOrSendMessage(mCaller.obtainMessageIOOO(DO_SHOW_SOFT_INPUT,
flags, showInputToken, resultReceiver, statsToken));
diff --git a/core/java/android/inputmethodservice/InputMethodService.java b/core/java/android/inputmethodservice/InputMethodService.java
index a9c4818..e1d2c32 100644
--- a/core/java/android/inputmethodservice/InputMethodService.java
+++ b/core/java/android/inputmethodservice/InputMethodService.java
@@ -607,6 +607,7 @@
InputConnection mStartedInputConnection;
EditorInfo mInputEditorInfo;
+ @InputMethod.ShowFlags
int mShowInputFlags;
boolean mShowInputRequested;
boolean mLastShowInputRequested;
@@ -931,8 +932,9 @@
*/
@MainThread
@Override
- public void showSoftInputWithToken(int flags, ResultReceiver resultReceiver,
- IBinder showInputToken, @Nullable ImeTracker.Token statsToken) {
+ public void showSoftInputWithToken(@InputMethod.ShowFlags int flags,
+ ResultReceiver resultReceiver, IBinder showInputToken,
+ @Nullable ImeTracker.Token statsToken) {
mSystemCallingShowSoftInput = true;
mCurShowInputToken = showInputToken;
mCurStatsToken = statsToken;
@@ -950,7 +952,7 @@
*/
@MainThread
@Override
- public void showSoftInput(int flags, ResultReceiver resultReceiver) {
+ public void showSoftInput(@InputMethod.ShowFlags int flags, ResultReceiver resultReceiver) {
ImeTracker.forLogging().onProgress(
mCurStatsToken, ImeTracker.PHASE_IME_SHOW_SOFT_INPUT);
if (DEBUG) Log.v(TAG, "showSoftInput()");
@@ -1321,7 +1323,8 @@
* InputMethodService#requestShowSelf} or {@link InputMethodService#requestHideSelf}
*/
@Deprecated
- public void toggleSoftInput(int showFlags, int hideFlags) {
+ public void toggleSoftInput(@InputMethodManager.ShowFlags int showFlags,
+ @InputMethodManager.HideFlags int hideFlags) {
InputMethodService.this.onToggleSoftInput(showFlags, hideFlags);
}
@@ -2793,18 +2796,16 @@
* {@link #onEvaluateInputViewShown()}, {@link #onEvaluateFullscreenMode()},
* and the current configuration to decide whether the input view should
* be shown at this point.
- *
- * @param flags Provides additional information about the show request,
- * as per {@link InputMethod#showSoftInput InputMethod.showSoftInput()}.
+ *
* @param configChange This is true if we are re-showing due to a
* configuration change.
* @return Returns true to indicate that the window should be shown.
*/
- public boolean onShowInputRequested(int flags, boolean configChange) {
+ public boolean onShowInputRequested(@InputMethod.ShowFlags int flags, boolean configChange) {
if (!onEvaluateInputViewShown()) {
return false;
}
- if ((flags&InputMethod.SHOW_EXPLICIT) == 0) {
+ if ((flags & InputMethod.SHOW_EXPLICIT) == 0) {
if (!configChange && onEvaluateFullscreenMode() && !isInputViewShown()) {
// Don't show if this is not explicitly requested by the user and
// the input method is fullscreen unless it is already shown. That
@@ -2830,14 +2831,14 @@
* exposed to IME authors as an overridable public method without {@code @CallSuper}, we have
* to have this method to ensure that those internal states are always updated no matter how
* {@link #onShowInputRequested(int, boolean)} is overridden by the IME author.
- * @param flags Provides additional information about the show request,
- * as per {@link InputMethod#showSoftInput InputMethod.showSoftInput()}.
+ *
* @param configChange This is true if we are re-showing due to a
* configuration change.
* @return Returns true to indicate that the window should be shown.
* @see #onShowInputRequested(int, boolean)
*/
- private boolean dispatchOnShowInputRequested(int flags, boolean configChange) {
+ private boolean dispatchOnShowInputRequested(@InputMethod.ShowFlags int flags,
+ boolean configChange) {
final boolean result = onShowInputRequested(flags, configChange);
mInlineSuggestionSessionController.notifyOnShowInputRequested(result);
if (result) {
@@ -3270,16 +3271,13 @@
*
* The input method will continue running, but the user can no longer use it to generate input
* by touching the screen.
- *
- * @see InputMethodManager#HIDE_IMPLICIT_ONLY
- * @see InputMethodManager#HIDE_NOT_ALWAYS
- * @param flags Provides additional operating flags.
*/
- public void requestHideSelf(int flags) {
+ public void requestHideSelf(@InputMethodManager.HideFlags int flags) {
requestHideSelf(flags, SoftInputShowHideReason.HIDE_SOFT_INPUT_FROM_IME);
}
- private void requestHideSelf(int flags, @SoftInputShowHideReason int reason) {
+ private void requestHideSelf(@InputMethodManager.HideFlags int flags,
+ @SoftInputShowHideReason int reason) {
ImeTracing.getInstance().triggerServiceDump("InputMethodService#requestHideSelf", mDumper,
null /* icProto */);
mPrivOps.hideMySoftInput(flags, reason);
@@ -3288,12 +3286,8 @@
/**
* Show the input method's soft input area, so the user sees the input method window and can
* interact with it.
- *
- * @see InputMethodManager#SHOW_IMPLICIT
- * @see InputMethodManager#SHOW_FORCED
- * @param flags Provides additional operating flags.
*/
- public final void requestShowSelf(int flags) {
+ public final void requestShowSelf(@InputMethodManager.ShowFlags int flags) {
ImeTracing.getInstance().triggerServiceDump("InputMethodService#requestShowSelf", mDumper,
null /* icProto */);
mPrivOps.showMySoftInput(flags);
@@ -3453,7 +3447,8 @@
/**
* Handle a request by the system to toggle the soft input area.
*/
- private void onToggleSoftInput(int showFlags, int hideFlags) {
+ private void onToggleSoftInput(@InputMethodManager.ShowFlags int showFlags,
+ @InputMethodManager.HideFlags int hideFlags) {
if (DEBUG) Log.v(TAG, "toggleSoftInput()");
if (isInputViewShown()) {
requestHideSelf(
diff --git a/core/java/android/os/GraphicsEnvironment.java b/core/java/android/os/GraphicsEnvironment.java
index ff8e3a0..7664bad 100644
--- a/core/java/android/os/GraphicsEnvironment.java
+++ b/core/java/android/os/GraphicsEnvironment.java
@@ -112,14 +112,16 @@
private static final int ANGLE_GL_DRIVER_ALL_ANGLE_OFF = 0;
// Values for ANGLE_GL_DRIVER_SELECTION_VALUES
- private static final String ANGLE_GL_DRIVER_CHOICE_DEFAULT = "default";
- private static final String ANGLE_GL_DRIVER_CHOICE_ANGLE = "angle";
- private static final String ANGLE_GL_DRIVER_CHOICE_NATIVE = "native";
-
private enum AngleDriverChoice {
- DEFAULT,
- ANGLE,
- NATIVE,
+ DEFAULT("default"),
+ ANGLE("angle"),
+ NATIVE("native");
+
+ public final String choice;
+
+ AngleDriverChoice(String choice) {
+ this.choice = choice;
+ }
}
private static final String PROPERTY_RO_ANGLE_SUPPORTED = "ro.gfx.angle.supported";
@@ -491,9 +493,9 @@
Log.v(TAG,
"ANGLE Developer option for '" + packageName + "' "
+ "set to: '" + optInValue + "'");
- if (optInValue.equals(ANGLE_GL_DRIVER_CHOICE_ANGLE)) {
+ if (optInValue.equals(AngleDriverChoice.ANGLE.choice)) {
return AngleDriverChoice.ANGLE;
- } else if (optInValue.equals(ANGLE_GL_DRIVER_CHOICE_NATIVE)) {
+ } else if (optInValue.equals(AngleDriverChoice.NATIVE.choice)) {
return AngleDriverChoice.NATIVE;
} else {
// The user either chose default or an invalid value; go with the default driver or what
diff --git a/core/java/android/os/VibrationEffect.java b/core/java/android/os/VibrationEffect.java
index aa67693..0461b2e 100644
--- a/core/java/android/os/VibrationEffect.java
+++ b/core/java/android/os/VibrationEffect.java
@@ -547,6 +547,30 @@
}
/**
+ * Resolve default values into integer amplitude numbers.
+ *
+ * @param defaultAmplitude the default amplitude to apply, must be between 0 and
+ * MAX_AMPLITUDE
+ * @return this if amplitude value is already set, or a copy of this effect with given default
+ * amplitude otherwise
+ *
+ * @hide
+ */
+ public abstract <T extends VibrationEffect> T resolve(int defaultAmplitude);
+
+ /**
+ * Scale the vibration effect intensity with the given constraints.
+ *
+ * @param scaleFactor scale factor to be applied to the intensity. Values within [0,1) will
+ * scale down the intensity, values larger than 1 will scale up
+ * @return this if there is no scaling to be done, or a copy of this effect with scaled
+ * vibration intensity otherwise
+ *
+ * @hide
+ */
+ public abstract <T extends VibrationEffect> T scale(float scaleFactor);
+
+ /**
* Ensures that the effect is repeating indefinitely or not. This is a lossy operation and
* should only be applied once to an original effect - it shouldn't be applied to the
* result of this method.
@@ -822,6 +846,40 @@
/** @hide */
@NonNull
@Override
+ public Composed resolve(int defaultAmplitude) {
+ int segmentCount = mSegments.size();
+ ArrayList<VibrationEffectSegment> resolvedSegments = new ArrayList<>(segmentCount);
+ for (int i = 0; i < segmentCount; i++) {
+ resolvedSegments.add(mSegments.get(i).resolve(defaultAmplitude));
+ }
+ if (resolvedSegments.equals(mSegments)) {
+ return this;
+ }
+ Composed resolved = new Composed(resolvedSegments, mRepeatIndex);
+ resolved.validate();
+ return resolved;
+ }
+
+ /** @hide */
+ @NonNull
+ @Override
+ public Composed scale(float scaleFactor) {
+ int segmentCount = mSegments.size();
+ ArrayList<VibrationEffectSegment> scaledSegments = new ArrayList<>(segmentCount);
+ for (int i = 0; i < segmentCount; i++) {
+ scaledSegments.add(mSegments.get(i).scale(scaleFactor));
+ }
+ if (scaledSegments.equals(mSegments)) {
+ return this;
+ }
+ Composed scaled = new Composed(scaledSegments, mRepeatIndex);
+ scaled.validate();
+ return scaled;
+ }
+
+ /** @hide */
+ @NonNull
+ @Override
public Composed applyRepeatingIndefinitely(boolean wantRepeating, int loopDelayMs) {
boolean isRepeating = mRepeatIndex >= 0;
if (isRepeating == wantRepeating) {
diff --git a/core/java/android/view/Choreographer.java b/core/java/android/view/Choreographer.java
index d80001d..654fa1d 100644
--- a/core/java/android/view/Choreographer.java
+++ b/core/java/android/view/Choreographer.java
@@ -749,6 +749,25 @@
return getExpectedPresentationTimeNanos() / TimeUtils.NANOS_PER_MS;
}
+ /**
+ * Same as {@link #getExpectedPresentationTimeNanos()},
+ * Should always use {@link #getExpectedPresentationTimeNanos()} if it's possilbe.
+ * This method involves a binder call to SF,
+ * calling this method can potentially influence the performance.
+ *
+ * @return The frame start time, in the {@link System#nanoTime()} time base.
+ *
+ * @hide
+ */
+ public long getLatestExpectedPresentTimeNanos() {
+ if (mDisplayEventReceiver == null) {
+ return System.nanoTime();
+ }
+
+ return mDisplayEventReceiver.getLatestVsyncEventData()
+ .preferredFrameTimeline().expectedPresentationTime;
+ }
+
private void scheduleFrameLocked(long now) {
if (!mFrameScheduled) {
mFrameScheduled = true;
diff --git a/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java b/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java
index ce2c180..467daa0 100644
--- a/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java
+++ b/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java
@@ -295,8 +295,8 @@
@AnyThread
static boolean showSoftInput(@NonNull IInputMethodClient client, @Nullable IBinder windowToken,
- @Nullable ImeTracker.Token statsToken, int flags, int lastClickToolType,
- @Nullable ResultReceiver resultReceiver,
+ @Nullable ImeTracker.Token statsToken, @InputMethodManager.ShowFlags int flags,
+ int lastClickToolType, @Nullable ResultReceiver resultReceiver,
@SoftInputShowHideReason int reason) {
final IInputMethodManager service = getService();
if (service == null) {
@@ -312,7 +312,7 @@
@AnyThread
static boolean hideSoftInput(@NonNull IInputMethodClient client, @Nullable IBinder windowToken,
- @Nullable ImeTracker.Token statsToken, int flags,
+ @Nullable ImeTracker.Token statsToken, @InputMethodManager.HideFlags int flags,
@Nullable ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) {
final IInputMethodManager service = getService();
if (service == null) {
diff --git a/core/java/android/view/inputmethod/InputMethod.java b/core/java/android/view/inputmethod/InputMethod.java
index 6340388..5b4efd8 100644
--- a/core/java/android/view/inputmethod/InputMethod.java
+++ b/core/java/android/view/inputmethod/InputMethod.java
@@ -17,6 +17,7 @@
package android.view.inputmethod;
import android.annotation.DurationMillisLong;
+import android.annotation.IntDef;
import android.annotation.MainThread;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -36,6 +37,8 @@
import com.android.internal.inputmethod.InlineSuggestionsRequestInfo;
import com.android.internal.inputmethod.InputMethodNavButtonFlags;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
import java.util.List;
/**
@@ -269,6 +272,14 @@
*/
@MainThread
public void revokeSession(InputMethodSession session);
+
+ /** @hide */
+ @IntDef(flag = true, prefix = { "SHOW_" }, value = {
+ SHOW_EXPLICIT,
+ SHOW_FORCED,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @interface ShowFlags {}
/**
* Flag for {@link #showSoftInput}: this show has been explicitly
@@ -292,8 +303,6 @@
/**
* Request that any soft input part of the input method be shown to the user.
*
- * @param flags Provides additional information about the show request.
- * Currently may be 0 or have the bit {@link #SHOW_EXPLICIT} set.
* @param resultReceiver The client requesting the show may wish to
* be told the impact of their request, which should be supplied here.
* The result code should be
@@ -308,7 +317,7 @@
* @hide
*/
@MainThread
- public default void showSoftInputWithToken(int flags, ResultReceiver resultReceiver,
+ public default void showSoftInputWithToken(@ShowFlags int flags, ResultReceiver resultReceiver,
IBinder showInputToken, @Nullable ImeTracker.Token statsToken) {
showSoftInput(flags, resultReceiver);
}
@@ -316,8 +325,6 @@
/**
* Request that any soft input part of the input method be shown to the user.
*
- * @param flags Provides additional information about the show request.
- * Currently may be 0 or have the bit {@link #SHOW_EXPLICIT} set.
* @param resultReceiver The client requesting the show may wish to
* be told the impact of their request, which should be supplied here.
* The result code should be
@@ -327,11 +334,12 @@
* {@link InputMethodManager#RESULT_HIDDEN InputMethodManager.RESULT_HIDDEN}.
*/
@MainThread
- public void showSoftInput(int flags, ResultReceiver resultReceiver);
+ public void showSoftInput(@ShowFlags int flags, ResultReceiver resultReceiver);
/**
* Request that any soft input part of the input method be hidden from the user.
- * @param flags Provides additional information about the show request.
+ *
+ * @param flags Provides additional information about the hide request.
* Currently always 0.
* @param resultReceiver The client requesting the show may wish to
* be told the impact of their request, which should be supplied here.
@@ -354,7 +362,8 @@
/**
* Request that any soft input part of the input method be hidden from the user.
- * @param flags Provides additional information about the show request.
+ *
+ * @param flags Provides additional information about the hide request.
* Currently always 0.
* @param resultReceiver The client requesting the show may wish to
* be told the impact of their request, which should be supplied here.
diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java
index fb94d49..0a4f8de 100644
--- a/core/java/android/view/inputmethod/InputMethodManager.java
+++ b/core/java/android/view/inputmethod/InputMethodManager.java
@@ -39,6 +39,7 @@
import android.annotation.DisplayContext;
import android.annotation.DrawableRes;
import android.annotation.DurationMillisLong;
+import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresFeature;
@@ -121,6 +122,8 @@
import java.io.FileDescriptor;
import java.io.PrintWriter;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.Proxy;
import java.util.Arrays;
import java.util.Collections;
@@ -2004,6 +2007,14 @@
}
}
+ /** @hide */
+ @IntDef(flag = true, prefix = { "SHOW_" }, value = {
+ SHOW_IMPLICIT,
+ SHOW_FORCED,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface ShowFlags {}
+
/**
* Flag for {@link #showSoftInput} to indicate that this is an implicit
* request to show the input window, not as the result of a direct request
@@ -2035,10 +2046,8 @@
* {@link View#isFocused view focus}, and its containing window has
* {@link View#hasWindowFocus window focus}. Otherwise the call fails and
* returns {@code false}.
- * @param flags Provides additional operating flags. Currently may be
- * 0 or have the {@link #SHOW_IMPLICIT} bit set.
*/
- public boolean showSoftInput(View view, int flags) {
+ public boolean showSoftInput(View view, @ShowFlags int flags) {
// Re-dispatch if there is a context mismatch.
final InputMethodManager fallbackImm = getFallbackInputMethodManagerIfNecessary(view);
if (fallbackImm != null) {
@@ -2101,21 +2110,20 @@
* {@link View#isFocused view focus}, and its containing window has
* {@link View#hasWindowFocus window focus}. Otherwise the call fails and
* returns {@code false}.
- * @param flags Provides additional operating flags. Currently may be
- * 0 or have the {@link #SHOW_IMPLICIT} bit set.
* @param resultReceiver If non-null, this will be called by the IME when
* it has processed your request to tell you what it has done. The result
* code you receive may be either {@link #RESULT_UNCHANGED_SHOWN},
* {@link #RESULT_UNCHANGED_HIDDEN}, {@link #RESULT_SHOWN}, or
* {@link #RESULT_HIDDEN}.
*/
- public boolean showSoftInput(View view, int flags, ResultReceiver resultReceiver) {
+ public boolean showSoftInput(View view, @ShowFlags int flags, ResultReceiver resultReceiver) {
return showSoftInput(view, null /* statsToken */, flags, resultReceiver,
SoftInputShowHideReason.SHOW_SOFT_INPUT);
}
- private boolean showSoftInput(View view, @Nullable ImeTracker.Token statsToken, int flags,
- ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) {
+ private boolean showSoftInput(View view, @Nullable ImeTracker.Token statsToken,
+ @ShowFlags int flags, ResultReceiver resultReceiver,
+ @SoftInputShowHideReason int reason) {
if (statsToken == null) {
statsToken = ImeTracker.forLogging().onRequestShow(null /* component */,
Process.myUid(), ImeTracker.ORIGIN_CLIENT_SHOW_SOFT_INPUT, reason);
@@ -2169,7 +2177,7 @@
*/
@Deprecated
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 123768499)
- public void showSoftInputUnchecked(int flags, ResultReceiver resultReceiver) {
+ public void showSoftInputUnchecked(@ShowFlags int flags, ResultReceiver resultReceiver) {
synchronized (mH) {
final ImeTracker.Token statsToken = ImeTracker.forLogging().onRequestShow(
null /* component */, Process.myUid(), ImeTracker.ORIGIN_CLIENT_SHOW_SOFT_INPUT,
@@ -2200,6 +2208,14 @@
}
}
+ /** @hide */
+ @IntDef(flag = true, prefix = { "HIDE_" }, value = {
+ HIDE_IMPLICIT_ONLY,
+ HIDE_NOT_ALWAYS,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface HideFlags {}
+
/**
* Flag for {@link #hideSoftInputFromWindow} and {@link InputMethodService#requestHideSelf(int)}
* to indicate that the soft input window should only be hidden if it was not explicitly shown
@@ -2221,10 +2237,8 @@
*
* @param windowToken The token of the window that is making the request,
* as returned by {@link View#getWindowToken() View.getWindowToken()}.
- * @param flags Provides additional operating flags. Currently may be
- * 0 or have the {@link #HIDE_IMPLICIT_ONLY} bit set.
*/
- public boolean hideSoftInputFromWindow(IBinder windowToken, int flags) {
+ public boolean hideSoftInputFromWindow(IBinder windowToken, @HideFlags int flags) {
return hideSoftInputFromWindow(windowToken, flags, null);
}
@@ -2246,21 +2260,19 @@
*
* @param windowToken The token of the window that is making the request,
* as returned by {@link View#getWindowToken() View.getWindowToken()}.
- * @param flags Provides additional operating flags. Currently may be
- * 0 or have the {@link #HIDE_IMPLICIT_ONLY} bit set.
* @param resultReceiver If non-null, this will be called by the IME when
* it has processed your request to tell you what it has done. The result
* code you receive may be either {@link #RESULT_UNCHANGED_SHOWN},
* {@link #RESULT_UNCHANGED_HIDDEN}, {@link #RESULT_SHOWN}, or
* {@link #RESULT_HIDDEN}.
*/
- public boolean hideSoftInputFromWindow(IBinder windowToken, int flags,
+ public boolean hideSoftInputFromWindow(IBinder windowToken, @HideFlags int flags,
ResultReceiver resultReceiver) {
return hideSoftInputFromWindow(windowToken, flags, resultReceiver,
SoftInputShowHideReason.HIDE_SOFT_INPUT);
}
- private boolean hideSoftInputFromWindow(IBinder windowToken, int flags,
+ private boolean hideSoftInputFromWindow(IBinder windowToken, @HideFlags int flags,
ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) {
final ImeTracker.Token statsToken = ImeTracker.forLogging().onRequestHide(
null /* component */, Process.myUid(),
@@ -2463,12 +2475,6 @@
* If not the input window will be displayed.
* @param windowToken The token of the window that is making the request,
* as returned by {@link View#getWindowToken() View.getWindowToken()}.
- * @param showFlags Provides additional operating flags. May be
- * 0 or have the {@link #SHOW_IMPLICIT},
- * {@link #SHOW_FORCED} bit set.
- * @param hideFlags Provides additional operating flags. May be
- * 0 or have the {@link #HIDE_IMPLICIT_ONLY},
- * {@link #HIDE_NOT_ALWAYS} bit set.
*
* @deprecated Use {@link #showSoftInput(View, int)} or
* {@link #hideSoftInputFromWindow(IBinder, int)} explicitly instead.
@@ -2477,7 +2483,8 @@
* has an effect if the calling app is the current IME focus.
*/
@Deprecated
- public void toggleSoftInputFromWindow(IBinder windowToken, int showFlags, int hideFlags) {
+ public void toggleSoftInputFromWindow(IBinder windowToken, @ShowFlags int showFlags,
+ @HideFlags int hideFlags) {
ImeTracing.getInstance().triggerClientDump(
"InputMethodManager#toggleSoftInputFromWindow", InputMethodManager.this,
null /* icProto */);
@@ -2495,12 +2502,6 @@
*
* If the input window is already displayed, it gets hidden.
* If not the input window will be displayed.
- * @param showFlags Provides additional operating flags. May be
- * 0 or have the {@link #SHOW_IMPLICIT},
- * {@link #SHOW_FORCED} bit set.
- * @param hideFlags Provides additional operating flags. May be
- * 0 or have the {@link #HIDE_IMPLICIT_ONLY},
- * {@link #HIDE_NOT_ALWAYS} bit set.
*
* @deprecated Use {@link #showSoftInput(View, int)} or
* {@link #hideSoftInputFromWindow(IBinder, int)} explicitly instead.
@@ -2509,7 +2510,7 @@
* has an effect if the calling app is the current IME focus.
*/
@Deprecated
- public void toggleSoftInput(int showFlags, int hideFlags) {
+ public void toggleSoftInput(@ShowFlags int showFlags, @HideFlags int hideFlags) {
ImeTracing.getInstance().triggerClientDump(
"InputMethodManager#toggleSoftInput", InputMethodManager.this,
null /* icProto */);
@@ -3522,15 +3523,12 @@
* @param token Supplies the identifying token given to an input method
* when it was started, which allows it to perform this operation on
* itself.
- * @param flags Provides additional operating flags. Currently may be
- * 0 or have the {@link #HIDE_IMPLICIT_ONLY},
- * {@link #HIDE_NOT_ALWAYS} bit set.
* @deprecated Use {@link InputMethodService#requestHideSelf(int)} instead. This method was
* intended for IME developers who should be accessing APIs through the service. APIs in this
* class are intended for app developers interacting with the IME.
*/
@Deprecated
- public void hideSoftInputFromInputMethod(IBinder token, int flags) {
+ public void hideSoftInputFromInputMethod(IBinder token, @HideFlags int flags) {
InputMethodPrivilegedOperationsRegistry.get(token).hideMySoftInput(
flags, SoftInputShowHideReason.HIDE_SOFT_INPUT_IMM_DEPRECATION);
}
@@ -3544,15 +3542,12 @@
* @param token Supplies the identifying token given to an input method
* when it was started, which allows it to perform this operation on
* itself.
- * @param flags Provides additional operating flags. Currently may be
- * 0 or have the {@link #SHOW_IMPLICIT} or
- * {@link #SHOW_FORCED} bit set.
* @deprecated Use {@link InputMethodService#requestShowSelf(int)} instead. This method was
* intended for IME developers who should be accessing APIs through the service. APIs in this
* class are intended for app developers interacting with the IME.
*/
@Deprecated
- public void showSoftInputFromInputMethod(IBinder token, int flags) {
+ public void showSoftInputFromInputMethod(IBinder token, @ShowFlags int flags) {
InputMethodPrivilegedOperationsRegistry.get(token).showMySoftInput(flags);
}
diff --git a/core/java/android/view/inputmethod/InputMethodSession.java b/core/java/android/view/inputmethod/InputMethodSession.java
index af6af14..4f48cb6 100644
--- a/core/java/android/view/inputmethod/InputMethodSession.java
+++ b/core/java/android/view/inputmethod/InputMethodSession.java
@@ -169,12 +169,6 @@
/**
* Toggle the soft input window.
* Applications can toggle the state of the soft input window.
- * @param showFlags Provides additional operating flags. May be
- * 0 or have the {@link InputMethodManager#SHOW_IMPLICIT},
- * {@link InputMethodManager#SHOW_FORCED} bit set.
- * @param hideFlags Provides additional operating flags. May be
- * 0 or have the {@link InputMethodManager#HIDE_IMPLICIT_ONLY},
- * {@link InputMethodManager#HIDE_NOT_ALWAYS} bit set.
*
* @deprecated Starting in {@link android.os.Build.VERSION_CODES#S} the system no longer invokes
* this method, instead it explicitly shows or hides the IME. An {@code InputMethodService}
@@ -182,7 +176,8 @@
* InputMethodService#requestShowSelf} or {@link InputMethodService#requestHideSelf}
*/
@Deprecated
- public void toggleSoftInput(int showFlags, int hideFlags);
+ public void toggleSoftInput(@InputMethodManager.ShowFlags int showFlags,
+ @InputMethodManager.HideFlags int hideFlags);
/**
* This method is called when the cursor and/or the character position relevant to text input
diff --git a/core/java/com/android/internal/flags/CoreFlags.java b/core/java/com/android/internal/flags/CoreFlags.java
new file mode 100644
index 0000000..f177ef8
--- /dev/null
+++ b/core/java/com/android/internal/flags/CoreFlags.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.flags;
+
+import android.flags.BooleanFlag;
+import android.flags.DynamicBooleanFlag;
+import android.flags.FeatureFlags;
+import android.flags.FusedOffFlag;
+import android.flags.FusedOnFlag;
+import android.flags.SyncableFlag;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Flags defined here are can be read by code in core.
+ *
+ * Flags not defined here will throw a security exception if third-party processes attempts to read
+ * them.
+ *
+ * DO NOT define a flag here unless you explicitly intend for that flag to be readable by code that
+ * runs inside a third party process.
+ */
+public abstract class CoreFlags {
+ private static final List<SyncableFlag> sKnownFlags = new ArrayList<>();
+
+ public static BooleanFlag BOOL_FLAG = booleanFlag("core", "bool_flag", false);
+ public static FusedOffFlag OFF_FLAG = fusedOffFlag("core", "off_flag");
+ public static FusedOnFlag ON_FLAG = fusedOnFlag("core", "on_flag");
+ public static DynamicBooleanFlag DYN_FLAG = dynamicBooleanFlag("core", "dyn_flag", true);
+
+ /** Returns true if the passed in flag matches a flag in this class. */
+ public static boolean isCoreFlag(SyncableFlag flag) {
+ for (SyncableFlag knownFlag : sKnownFlags) {
+ if (knownFlag.getName().equals(flag.getName())
+ && knownFlag.getNamespace().equals(flag.getNamespace())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public static List<SyncableFlag> getCoreFlags() {
+ return sKnownFlags;
+ }
+
+ private static BooleanFlag booleanFlag(String namespace, String name, boolean defaultValue) {
+ BooleanFlag f = FeatureFlags.booleanFlag(namespace, name, defaultValue);
+
+ sKnownFlags.add(new SyncableFlag(namespace, name, Boolean.toString(defaultValue), false));
+
+ return f;
+ }
+
+ private static FusedOffFlag fusedOffFlag(String namespace, String name) {
+ FusedOffFlag f = FeatureFlags.fusedOffFlag(namespace, name);
+
+ sKnownFlags.add(new SyncableFlag(namespace, name, "false", false));
+
+ return f;
+ }
+
+ private static FusedOnFlag fusedOnFlag(String namespace, String name) {
+ FusedOnFlag f = FeatureFlags.fusedOnFlag(namespace, name);
+
+ sKnownFlags.add(new SyncableFlag(namespace, name, "true", false));
+
+ return f;
+ }
+
+ private static DynamicBooleanFlag dynamicBooleanFlag(
+ String namespace, String name, boolean defaultValue) {
+ DynamicBooleanFlag f = FeatureFlags.dynamicBooleanFlag(namespace, name, defaultValue);
+
+ sKnownFlags.add(new SyncableFlag(namespace, name, Boolean.toString(defaultValue), true));
+
+ return f;
+ }
+}
diff --git a/core/java/com/android/internal/inputmethod/InputMethodPrivilegedOperations.java b/core/java/com/android/internal/inputmethod/InputMethodPrivilegedOperations.java
index 66e3333..8a5c7ef 100644
--- a/core/java/com/android/internal/inputmethod/InputMethodPrivilegedOperations.java
+++ b/core/java/com/android/internal/inputmethod/InputMethodPrivilegedOperations.java
@@ -26,6 +26,7 @@
import android.util.Log;
import android.view.View;
import android.view.inputmethod.ImeTracker;
+import android.view.inputmethod.InputMethodManager;
import android.view.inputmethod.InputMethodSubtype;
import com.android.internal.annotations.GuardedBy;
@@ -253,13 +254,11 @@
/**
* Calls {@link IInputMethodPrivilegedOperations#hideMySoftInput(int, int, AndroidFuture)}
*
- * @param flags additional operating flags
* @param reason the reason to hide soft input
- * @see android.view.inputmethod.InputMethodManager#HIDE_IMPLICIT_ONLY
- * @see android.view.inputmethod.InputMethodManager#HIDE_NOT_ALWAYS
*/
@AnyThread
- public void hideMySoftInput(int flags, @SoftInputShowHideReason int reason) {
+ public void hideMySoftInput(@InputMethodManager.HideFlags int flags,
+ @SoftInputShowHideReason int reason) {
final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull();
if (ops == null) {
return;
@@ -275,13 +274,9 @@
/**
* Calls {@link IInputMethodPrivilegedOperations#showMySoftInput(int, AndroidFuture)}
- *
- * @param flags additional operating flags
- * @see android.view.inputmethod.InputMethodManager#SHOW_IMPLICIT
- * @see android.view.inputmethod.InputMethodManager#SHOW_FORCED
*/
@AnyThread
- public void showMySoftInput(int flags) {
+ public void showMySoftInput(@InputMethodManager.ShowFlags int flags) {
final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull();
if (ops == null) {
return;
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 0c8707d..baa47da 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -7663,6 +7663,24 @@
<permission android:name="android.permission.GET_ANY_PROVIDER_TYPE"
android:protectionLevel="signature" />
+
+ <!-- @hide Allows internal applications to read and synchronize non-core flags.
+ Apps without this permission can only read a subset of flags specifically intended
+ for use in "core", (i.e. third party apps). Apps with this permission can define their
+ own flags, and federate those values with other system-level apps.
+ <p>Not for use by third-party applications.
+ <p>Protection level: signature
+ -->
+ <permission android:name="android.permission.SYNC_FLAGS"
+ android:protectionLevel="signature" />
+
+ <!-- @hide Allows internal applications to override flags in the FeatureFlags service.
+ <p>Not for use by third-party applications.
+ <p>Protection level: signature
+ -->
+ <permission android:name="android.permission.WRITE_FLAGS"
+ android:protectionLevel="signature" />
+
<!-- Attribution for Geofencing service. -->
<attribution android:tag="GeofencingService" android:label="@string/geofencing_service"/>
<!-- Attribution for Country Detector. -->
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 3b36c7a..6ffcf20 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -4384,12 +4384,12 @@
Example: "com.android.companiondevicemanager"
See android.companion.CompanionDeviceManager.
-->
- <string name="config_companionDeviceManagerPackage" translatable="false"></string>
+ <string name="config_companionDeviceManagerPackage" translatable="false">com.android.companiondevicemanager</string>
<!-- A list of packages managing companion device(s) by the same manufacturers as the main
device. It will fall back to showing a prompt if the association has been called multiple
times in a short period.
- Note that config_companionDeviceManagerPackage and config_companionDeviceCerts are
+ Note that config_companionDevicePackages and config_companionDeviceCerts are
parallel arrays.
-->
<string-array name="config_companionDevicePackages" translatable="false"></string-array>
@@ -4397,7 +4397,7 @@
<!-- A list of SHA256 Certificates managing companion device(s) by the same manufacturers as
the main device. It will fall back to showing a prompt if the association has been called
multiple times in a short period.
- Note that config_companionDeviceCerts and config_companionDeviceManagerPackage are parallel
+ Note that config_companionDeviceCerts and config_companionDevicePackages are parallel
arrays.
Example: "1A:2B:3C:4D"
-->
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 8d0ae19..96ba3d4 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -661,6 +661,7 @@
<java-symbol type="string" name="cfTemplateRegisteredTime" />
<java-symbol type="string" name="chooseActivity" />
<java-symbol type="string" name="checked" />
+ <java-symbol type="string" name="config_companionDeviceManagerPackage" />
<java-symbol type="array" name="config_companionDevicePackages" />
<java-symbol type="array" name="config_companionDeviceCerts" />
<java-symbol type="string" name="config_default_dns_server" />
diff --git a/core/tests/coretests/src/android/flags/FeatureFlagsTest.java b/core/tests/coretests/src/android/flags/FeatureFlagsTest.java
new file mode 100644
index 0000000..3fc9439
--- /dev/null
+++ b/core/tests/coretests/src/android/flags/FeatureFlagsTest.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2020 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.flags;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+
+@SmallTest
+@Presubmit
+public class FeatureFlagsTest {
+
+ IFeatureFlagsFake mIFeatureFlagsFake = new IFeatureFlagsFake();
+ FeatureFlags mFeatureFlags = new FeatureFlags(mIFeatureFlagsFake);
+
+ @Before
+ public void setup() {
+ FeatureFlags.setInstance(mFeatureFlags);
+ }
+
+ @Test
+ public void testFusedOff_Disabled() {
+ FusedOffFlag flag = FeatureFlags.fusedOffFlag("test", "a");
+ assertThat(mFeatureFlags.isEnabled(flag)).isFalse();
+ }
+
+ @Test
+ public void testFusedOn_Enabled() {
+ FusedOnFlag flag = FeatureFlags.fusedOnFlag("test", "a");
+ assertThat(mFeatureFlags.isEnabled(flag)).isTrue();
+ }
+
+ @Test
+ public void testBooleanFlag_DefaultDisabled() {
+ BooleanFlag flag = FeatureFlags.booleanFlag("test", "a", false);
+ assertThat(mFeatureFlags.isEnabled(flag)).isFalse();
+ }
+
+ @Test
+ public void testBooleanFlag_DefaultEnabled() {
+ BooleanFlag flag = FeatureFlags.booleanFlag("test", "a", true);
+ assertThat(mFeatureFlags.isEnabled(flag)).isTrue();
+ }
+
+ @Test
+ public void testDynamicBooleanFlag_DefaultDisabled() {
+ DynamicBooleanFlag flag = FeatureFlags.dynamicBooleanFlag("test", "a", false);
+ assertThat(mFeatureFlags.isCurrentlyEnabled(flag)).isFalse();
+ }
+
+ @Test
+ public void testDynamicBooleanFlag_DefaultEnabled() {
+ DynamicBooleanFlag flag = FeatureFlags.dynamicBooleanFlag("test", "a", true);
+ assertThat(mFeatureFlags.isCurrentlyEnabled(flag)).isTrue();
+ }
+
+ @Test
+ public void testBooleanFlag_OverrideBeforeRead() {
+ BooleanFlag flag = FeatureFlags.booleanFlag("test", "a", false);
+ SyncableFlag syncableFlag = new SyncableFlag(
+ flag.getNamespace(), flag.getName(), "true", false);
+
+ mIFeatureFlagsFake.setFlagOverrides(List.of(syncableFlag));
+
+ assertThat(mFeatureFlags.isEnabled(flag)).isTrue();
+ }
+
+ @Test
+ public void testFusedOffFlag_OverrideHasNoEffect() {
+ FusedOffFlag flag = FeatureFlags.fusedOffFlag("test", "a");
+ SyncableFlag syncableFlag = new SyncableFlag(
+ flag.getNamespace(), flag.getName(), "true", false);
+
+ mIFeatureFlagsFake.setFlagOverrides(List.of(syncableFlag));
+
+ assertThat(mFeatureFlags.isEnabled(flag)).isFalse();
+ }
+
+ @Test
+ public void testFusedOnFlag_OverrideHasNoEffect() {
+ FusedOnFlag flag = FeatureFlags.fusedOnFlag("test", "a");
+ SyncableFlag syncableFlag = new SyncableFlag(
+ flag.getNamespace(), flag.getName(), "false", false);
+
+ mIFeatureFlagsFake.setFlagOverrides(List.of(syncableFlag));
+
+ assertThat(mFeatureFlags.isEnabled(flag)).isTrue();
+ }
+
+ @Test
+ public void testDynamicFlag_OverrideBeforeRead() {
+ DynamicBooleanFlag flag = FeatureFlags.dynamicBooleanFlag("test", "a", false);
+ SyncableFlag syncableFlag = new SyncableFlag(
+ flag.getNamespace(), flag.getName(), "true", true);
+
+ mIFeatureFlagsFake.setFlagOverrides(List.of(syncableFlag));
+
+ // Changes to true
+ assertThat(mFeatureFlags.isCurrentlyEnabled(flag)).isTrue();
+ }
+
+ @Test
+ public void testDynamicFlag_OverrideAfterRead() {
+ DynamicBooleanFlag flag = FeatureFlags.dynamicBooleanFlag("test", "a", false);
+ SyncableFlag syncableFlag = new SyncableFlag(
+ flag.getNamespace(), flag.getName(), "true", true);
+
+ // Starts false
+ assertThat(mFeatureFlags.isCurrentlyEnabled(flag)).isFalse();
+
+ mIFeatureFlagsFake.setFlagOverrides(List.of(syncableFlag));
+
+ // Changes to true
+ assertThat(mFeatureFlags.isCurrentlyEnabled(flag)).isTrue();
+ }
+
+ @Test
+ public void testDynamicFlag_FiresListener() {
+ DynamicBooleanFlag flag = FeatureFlags.dynamicBooleanFlag("test", "a", false);
+ AtomicBoolean called = new AtomicBoolean(false);
+ FeatureFlags.ChangeListener listener = flag1 -> called.set(true);
+
+ mFeatureFlags.addChangeListener(listener);
+
+ SyncableFlag syncableFlag = new SyncableFlag(
+ flag.getNamespace(), flag.getName(), flag.getDefault().toString(), true);
+
+ mIFeatureFlagsFake.setFlagOverrides(List.of(syncableFlag));
+
+ // Fires listener.
+ assertThat(called.get()).isTrue();
+ }
+}
diff --git a/core/tests/coretests/src/android/flags/IFeatureFlagsFake.java b/core/tests/coretests/src/android/flags/IFeatureFlagsFake.java
new file mode 100644
index 0000000..bc5d8aa
--- /dev/null
+++ b/core/tests/coretests/src/android/flags/IFeatureFlagsFake.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2023 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.flags;
+
+import android.os.IBinder;
+import android.os.RemoteException;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+class IFeatureFlagsFake implements IFeatureFlags {
+
+ private final Set<IFeatureFlagsCallback> mCallbacks = new HashSet<>();
+
+ List<SyncableFlag> mOverrides;
+
+ @Override
+ public IBinder asBinder() {
+ return null;
+ }
+
+ @Override
+ public List<SyncableFlag> syncFlags(List<SyncableFlag> flagList) {
+ return mOverrides == null ? flagList : mOverrides;
+ }
+
+ @Override
+ public List<SyncableFlag> queryFlags(List<SyncableFlag> flagList) {
+ return mOverrides == null ? flagList : mOverrides; }
+
+ @Override
+ public void overrideFlag(SyncableFlag syncableFlag) {
+ SyncableFlag match = findFlag(syncableFlag);
+ if (match != null) {
+ mOverrides.remove(match);
+ }
+
+ mOverrides.add(syncableFlag);
+
+ for (IFeatureFlagsCallback cb : mCallbacks) {
+ try {
+ cb.onFlagChange(syncableFlag);
+ } catch (RemoteException e) {
+ // does not happen in fakes.
+ }
+ }
+ }
+
+ @Override
+ public void resetFlag(SyncableFlag syncableFlag) {
+ SyncableFlag match = findFlag(syncableFlag);
+ if (match != null) {
+ mOverrides.remove(match);
+ }
+
+ for (IFeatureFlagsCallback cb : mCallbacks) {
+ try {
+ cb.onFlagChange(syncableFlag);
+ } catch (RemoteException e) {
+ // does not happen in fakes.
+ }
+ }
+ }
+
+ private SyncableFlag findFlag(SyncableFlag syncableFlag) {
+ SyncableFlag match = null;
+ for (SyncableFlag sf : mOverrides) {
+ if (sf.getName().equals(syncableFlag.getName())
+ && sf.getNamespace().equals(syncableFlag.getNamespace())) {
+ match = sf;
+ break;
+ }
+ }
+
+ return match;
+ }
+ @Override
+ public void registerCallback(IFeatureFlagsCallback callback) {
+ mCallbacks.add(callback);
+ }
+
+ @Override
+ public void unregisterCallback(IFeatureFlagsCallback callback) {
+ mCallbacks.remove(callback);
+ }
+
+ public void setFlagOverrides(List<SyncableFlag> flagList) {
+ mOverrides = flagList;
+ for (SyncableFlag sf : flagList) {
+ for (IFeatureFlagsCallback cb : mCallbacks) {
+ try {
+ cb.onFlagChange(sf);
+ } catch (RemoteException e) {
+ // does not happen in fakes.
+ }
+ }
+ }
+ }
+}
diff --git a/core/tests/coretests/src/android/os/VibrationEffectTest.java b/core/tests/coretests/src/android/os/VibrationEffectTest.java
index 9107236..73954da 100644
--- a/core/tests/coretests/src/android/os/VibrationEffectTest.java
+++ b/core/tests/coretests/src/android/os/VibrationEffectTest.java
@@ -38,6 +38,8 @@
import android.hardware.vibrator.IVibrator;
import android.net.Uri;
import android.os.VibrationEffect.Composition.UnreachableAfterRepeatingIndefinitelyException;
+import android.os.vibrator.PrimitiveSegment;
+import android.os.vibrator.StepSegment;
import android.platform.test.annotations.Presubmit;
import androidx.test.InstrumentationRegistry;
@@ -634,6 +636,88 @@
.validate());
}
+ @Test
+ public void testResolveOneShot() {
+ VibrationEffect.Composed resolved = DEFAULT_ONE_SHOT.resolve(51);
+ assertEquals(0.2f, ((StepSegment) resolved.getSegments().get(0)).getAmplitude());
+
+ assertThrows(IllegalArgumentException.class, () -> DEFAULT_ONE_SHOT.resolve(1000));
+ }
+
+ @Test
+ public void testResolveWaveform() {
+ VibrationEffect.Composed resolved = TEST_WAVEFORM.resolve(102);
+ assertEquals(0.4f, ((StepSegment) resolved.getSegments().get(2)).getAmplitude());
+
+ assertThrows(IllegalArgumentException.class, () -> TEST_WAVEFORM.resolve(1000));
+ }
+
+ @Test
+ public void testResolvePrebaked() {
+ VibrationEffect effect = VibrationEffect.get(VibrationEffect.EFFECT_CLICK);
+ assertEquals(effect, effect.resolve(51));
+ }
+
+ @Test
+ public void testResolveComposed() {
+ VibrationEffect effect = VibrationEffect.startComposition()
+ .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1f, 1)
+ .compose();
+ assertEquals(effect, effect.resolve(51));
+
+ VibrationEffect.Composed resolved = VibrationEffect.startComposition()
+ .addEffect(DEFAULT_ONE_SHOT)
+ .compose()
+ .resolve(51);
+ assertEquals(0.2f, ((StepSegment) resolved.getSegments().get(0)).getAmplitude());
+ }
+
+ @Test
+ public void testScaleOneShot() {
+ VibrationEffect.Composed scaledUp = TEST_ONE_SHOT.scale(1.5f);
+ assertTrue(100 / 255f < ((StepSegment) scaledUp.getSegments().get(0)).getAmplitude());
+
+ VibrationEffect.Composed scaledDown = TEST_ONE_SHOT.scale(0.5f);
+ assertTrue(100 / 255f > ((StepSegment) scaledDown.getSegments().get(0)).getAmplitude());
+ }
+
+ @Test
+ public void testScaleWaveform() {
+ VibrationEffect.Composed scaledUp = TEST_WAVEFORM.scale(1.5f);
+ assertEquals(1f, ((StepSegment) scaledUp.getSegments().get(0)).getAmplitude(), 1e-5f);
+
+ VibrationEffect.Composed scaledDown = TEST_WAVEFORM.scale(0.5f);
+ assertTrue(1f > ((StepSegment) scaledDown.getSegments().get(0)).getAmplitude());
+ }
+
+ @Test
+ public void testScalePrebaked() {
+ VibrationEffect effect = VibrationEffect.get(VibrationEffect.EFFECT_CLICK);
+
+ VibrationEffect.Composed scaledUp = effect.scale(1.5f);
+ assertEquals(effect, scaledUp);
+
+ VibrationEffect.Composed scaledDown = effect.scale(0.5f);
+ assertEquals(effect, scaledDown);
+ }
+
+ @Test
+ public void testScaleComposed() {
+ VibrationEffect.Composed effect =
+ (VibrationEffect.Composed) VibrationEffect.startComposition()
+ .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 0.5f, 1)
+ .addEffect(TEST_ONE_SHOT)
+ .compose();
+
+ VibrationEffect.Composed scaledUp = effect.scale(1.5f);
+ assertTrue(0.5f < ((PrimitiveSegment) scaledUp.getSegments().get(0)).getScale());
+ assertTrue(100 / 255f < ((StepSegment) scaledUp.getSegments().get(1)).getAmplitude());
+
+ VibrationEffect.Composed scaledDown = effect.scale(0.5f);
+ assertTrue(0.5f > ((PrimitiveSegment) scaledDown.getSegments().get(0)).getScale());
+ assertTrue(100 / 255f > ((StepSegment) scaledDown.getSegments().get(1)).getAmplitude());
+ }
+
private void doTestApplyRepeatingWithNonRepeatingOriginal(@NotNull VibrationEffect original) {
assertTrue(original.getDuration() != Long.MAX_VALUE);
int loopDelayMs = 123;
diff --git a/libs/WindowManager/Shell/res/values-watch/dimen.xml b/libs/WindowManager/Shell/res/values-watch/dimen.xml
new file mode 100644
index 0000000..362e72c
--- /dev/null
+++ b/libs/WindowManager/Shell/res/values-watch/dimen.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2020 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.
+-->
+<resources>
+ <!-- The acceptable area ratio of fg icon area/bg icon area, i.e. (48 X 48) / (72 x 72) -->
+ <item type="dimen" format="float" name="splash_icon_enlarge_foreground_threshold">0.44</item>
+ <!-- Scaling factor applied to splash icons without provided background i.e. (60 / 48) -->
+ <item type="dimen" format="float" name="splash_icon_no_background_scale_factor">1.25</item>
+</resources>
diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml
index 64fed1c..ac73e1d 100644
--- a/libs/WindowManager/Shell/res/values/dimen.xml
+++ b/libs/WindowManager/Shell/res/values/dimen.xml
@@ -421,4 +421,9 @@
<!-- The height of the area at the top of the screen where a freeform task will transition to
fullscreen if dragged until the top bound of the task is within the area. -->
<dimen name="desktop_mode_transition_area_height">16dp</dimen>
+
+ <!-- The acceptable area ratio of fg icon area/bg icon area, i.e. (72 x 72) / (108 x 108) -->
+ <item type="dimen" format="float" name="splash_icon_enlarge_foreground_threshold">0.44</item>
+ <!-- Scaling factor applied to splash icons without provided background i.e. (192 / 160) -->
+ <item type="dimen" format="float" name="splash_icon_no_background_scale_factor">1.2</item>
</resources>
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
index 3eec513..6319dbe 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
@@ -1053,6 +1053,20 @@
mBubbleData.setExpanded(false /* expanded */);
}
+ /**
+ * Update expanded state when a single bubble is dragged in Launcher.
+ * Will be called only when bubble bar is expanded.
+ * @param bubbleKey key of the bubble to collapse/expand
+ * @param collapse whether to collapse or expand
+ */
+ public void collapseWhileDragging(String bubbleKey, boolean collapse) {
+ if (mBubbleData.getSelectedBubble() != null
+ && mBubbleData.getSelectedBubble().getKey().equals(bubbleKey)) {
+ // Should collapse/expand only if equals to selected bubble.
+ mBubbleBarViewCallback.expansionChanged(/* isExpanded = */ !collapse);
+ }
+ }
+
@VisibleForTesting
public boolean isBubbleNotificationSuppressedFromShade(String key, String groupKey) {
boolean isSuppressedBubble = (mBubbleData.hasAnyBubbleWithKey(key)
@@ -1079,9 +1093,9 @@
* <p>This is used by external callers (launcher).
*/
@VisibleForTesting
- public void expandStackAndSelectBubbleFromLauncher(String key, int bubbleBarXCoordinate,
- int bubbleBarYCoordinate) {
- mBubblePositioner.setBubbleBarPosition(bubbleBarXCoordinate, bubbleBarYCoordinate);
+ public void expandStackAndSelectBubbleFromLauncher(String key, int bubbleBarOffsetX,
+ int bubbleBarOffsetY) {
+ mBubblePositioner.setBubbleBarPosition(bubbleBarOffsetX, bubbleBarOffsetY);
if (BubbleOverflow.KEY.equals(key)) {
mBubbleData.setSelectedBubbleFromLauncher(mBubbleData.getOverflow());
@@ -1438,6 +1452,17 @@
}
}
+ /**
+ * Removes all the bubbles.
+ * <p>
+ * Must be called from the main thread.
+ */
+ @VisibleForTesting
+ @MainThread
+ public void removeAllBubbles(@Bubbles.DismissReason int reason) {
+ mBubbleData.dismissAll(reason);
+ }
+
private void onEntryAdded(BubbleEntry entry) {
if (canLaunchInTaskView(mContext, entry)) {
updateBubble(entry);
@@ -2092,21 +2117,32 @@
}
@Override
- public void showBubble(String key, int bubbleBarXCoordinate, int bubbleBarYCoordinate) {
+ public void showBubble(String key, int bubbleBarOffsetX, int bubbleBarOffsetY) {
mMainExecutor.execute(
() -> mController.expandStackAndSelectBubbleFromLauncher(
- key, bubbleBarXCoordinate, bubbleBarYCoordinate));
+ key, bubbleBarOffsetX, bubbleBarOffsetY));
}
@Override
- public void removeBubble(String key, int reason) {
- // TODO (b/271466616) allow removals from launcher
+ public void removeBubble(String key) {
+ mMainExecutor.execute(
+ () -> mController.removeBubble(key, Bubbles.DISMISS_USER_GESTURE));
+ }
+
+ @Override
+ public void removeAllBubbles() {
+ mMainExecutor.execute(() -> mController.removeAllBubbles(Bubbles.DISMISS_USER_GESTURE));
}
@Override
public void collapseBubbles() {
mMainExecutor.execute(() -> mController.collapseStack());
}
+
+ @Override
+ public void collapseWhileDragging(String bubbleKey, boolean collapse) {
+ mMainExecutor.execute(() -> mController.collapseWhileDragging(bubbleKey, collapse));
+ }
}
private class BubblesImpl implements Bubbles {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java
index 6a5e310..ee6996d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java
@@ -718,9 +718,16 @@
mShowingInBubbleBar = showingInBubbleBar;
}
- /** Sets the position of the bubble bar in screen coordinates. */
- public void setBubbleBarPosition(int x, int y) {
- mBubbleBarPosition.set(x, y);
+ /**
+ * Sets the position of the bubble bar in screen coordinates.
+ *
+ * @param offsetX the offset of the bubble bar from the edge of the screen on the X axis
+ * @param offsetY the offset of the bubble bar from the edge of the screen on the Y axis
+ */
+ public void setBubbleBarPosition(int offsetX, int offsetY) {
+ mBubbleBarPosition.set(
+ getAvailableRect().width() - offsetX,
+ getAvailableRect().height() + mInsets.top + mInsets.bottom - offsetY);
}
/**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl
index 59332f4..b4ed9d0 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl
@@ -29,11 +29,14 @@
oneway void unregisterBubbleListener(in IBubblesListener listener) = 2;
- oneway void showBubble(in String key, in int bubbleBarXCoordinate,
- in int bubbleBarYCoordinate) = 3;
+ oneway void showBubble(in String key, in int bubbleBarOffsetX, in int bubbleBarOffsetY) = 3;
- oneway void removeBubble(in String key, in int reason) = 4;
+ oneway void removeBubble(in String key) = 4;
- oneway void collapseBubbles() = 5;
+ oneway void removeAllBubbles() = 5;
+
+ oneway void collapseBubbles() = 6;
+
+ oneway void collapseWhileDragging(in String key, in boolean collapse) = 7;
}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java
index b3602b3..689323b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java
@@ -215,6 +215,14 @@
mExpandedViewAlphaAnimator.reverse();
}
+ /**
+ * Cancel current animations
+ */
+ public void cancelAnimations() {
+ PhysicsAnimator.getInstance(mExpandedViewContainerMatrix).cancel();
+ mExpandedViewAlphaAnimator.cancel();
+ }
+
private void updateExpandedView() {
if (mExpandedBubble == null || mExpandedBubble.getBubbleBarExpandedView() == null) {
Log.w(TAG, "Trying to update the expanded view without a bubble");
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java
index 8ead18b..bc04bfc 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java
@@ -150,6 +150,12 @@
mExpandedView = null;
}
if (mExpandedView == null) {
+ if (expandedView.getParent() != null) {
+ // Expanded view might be animating collapse and is still attached
+ // Cancel current animations and remove from parent
+ mAnimationHelper.cancelAnimations();
+ removeView(expandedView);
+ }
mExpandedBubble = b;
mExpandedView = expandedView;
boolean isOverflowExpanded = b.getKey().equals(BubbleOverflow.KEY);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubbleInfo.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubbleInfo.java
index 21355a3..24608d6 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubbleInfo.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubbleInfo.java
@@ -129,6 +129,11 @@
return (mFlags & Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION) != 0;
}
+ /** Sets the flags for this bubble. */
+ public void setFlags(int flags) {
+ mFlags = flags;
+ }
+
@Override
public boolean equals(Object o) {
if (this == o) return true;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/RelativeTouchListener.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/RelativeTouchListener.kt
index cc37bd3a4..c98d0ba 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/RelativeTouchListener.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/RelativeTouchListener.kt
@@ -78,6 +78,24 @@
velY: Float
)
+ /**
+ * Called when an ACTION_CANCEL event is received for the given view. This signals that the
+ * current gesture has been aborted.
+ *
+ * @param viewInitialX The view's translationX value when this touch gesture started.
+ * @param viewInitialY The view's translationY value when this touch gesture started.
+ * @param dx Horizontal distance covered since the initial ACTION_DOWN event, in pixels.
+ * @param dy Vertical distance covered since the initial ACTION_DOWN event, in pixels.
+ */
+ open fun onCancel(
+ v: View,
+ ev: MotionEvent,
+ viewInitialX: Float,
+ viewInitialY: Float,
+ dx: Float,
+ dy: Float
+ ) {}
+
/** The raw coordinates of the last ACTION_DOWN event. */
private val touchDown = PointF()
@@ -146,6 +164,7 @@
}
MotionEvent.ACTION_CANCEL -> {
+ onCancel(v, ev, viewPositionOnTouchDown.x, viewPositionOnTouchDown.y, dx, dy)
v.handler.removeCallbacksAndMessages(null)
velocityTracker.clear()
movedEnough = false
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
index b14c3c1..08da485 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
@@ -1927,6 +1927,7 @@
pw.println(innerPrefix + "mLeash=" + mLeash);
pw.println(innerPrefix + "mState=" + mPipTransitionState.getTransitionState());
pw.println(innerPrefix + "mPictureInPictureParams=" + mPictureInPictureParams);
+ mPipTransitionController.dump(pw, innerPrefix);
}
@Override
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
index 73eb62a..e3d53fc 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
@@ -72,6 +72,7 @@
import com.android.wm.shell.transition.Transitions;
import com.android.wm.shell.util.TransitionUtil;
+import java.io.PrintWriter;
import java.util.Optional;
/**
@@ -451,6 +452,9 @@
@Override
public void forceFinishTransition() {
+ // mFinishCallback might be null with an outdated mCurrentPipTaskToken
+ // for example, when app crashes while in PiP and exit transition has not started
+ mCurrentPipTaskToken = null;
if (mFinishCallback == null) return;
mFinishCallback.onTransitionFinished(null /* wct */, null /* callback */);
mFinishCallback = null;
@@ -1137,4 +1141,12 @@
PipMenuController.ALPHA_NO_CHANGE);
mPipMenuController.updateMenuBounds(destinationBounds);
}
+
+ @Override
+ public void dump(PrintWriter pw, String prefix) {
+ final String innerPrefix = prefix + " ";
+ pw.println(prefix + TAG);
+ pw.println(innerPrefix + "mCurrentPipTaskToken=" + mCurrentPipTaskToken);
+ pw.println(innerPrefix + "mFinishCallback=" + mFinishCallback);
+ }
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java
index e1bcd70c..6362793 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java
@@ -42,6 +42,7 @@
import com.android.wm.shell.sysui.ShellInit;
import com.android.wm.shell.transition.Transitions;
+import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
@@ -283,4 +284,9 @@
*/
void onPipTransitionCanceled(int direction);
}
+
+ /**
+ * Dumps internal states.
+ */
+ public void dump(PrintWriter pw, String prefix) {}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
index 283be9c..7d62f58 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
@@ -2564,6 +2564,9 @@
// so don't handle it.
Log.e(TAG, "Somehow removed the last task in a stage outside of a proper "
+ "transition.");
+ // This new transition would be merged to current one so we need to clear
+ // tile manually here.
+ clearSplitPairedInRecents(EXIT_REASON_APP_FINISHED);
final WindowContainerTransaction wct = new WindowContainerTransaction();
final int dismissTop = (dismissStages.size() == 1
&& getStageType(dismissStages.valueAt(0)) == STAGE_TYPE_MAIN)
@@ -2747,6 +2750,12 @@
Log.w(TAG, splitFailureMessage("startPendingEnterAnimation",
"launched 2 tasks in split, but didn't receive "
+ "2 tasks in transition. Possibly one of them failed to launch"));
+ if (mRecentTasks.isPresent() && mainChild != null) {
+ mRecentTasks.get().removeSplitPair(mainChild.getTaskInfo().taskId);
+ }
+ if (mRecentTasks.isPresent() && sideChild != null) {
+ mRecentTasks.get().removeSplitPair(sideChild.getTaskInfo().taskId);
+ }
mSplitUnsupportedToast.show();
return true;
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java
index dc91a11..84dcd4d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java
@@ -112,28 +112,15 @@
*/
static final long MAX_ANIMATION_DURATION = MINIMAL_ANIMATION_DURATION + TIME_WINDOW_DURATION;
- // The acceptable area ratio of foreground_icon_area/background_icon_area, if there is an
- // icon which it's non-transparent foreground area is similar to it's background area, then
- // do not enlarge the foreground drawable.
- // For example, an icon with the foreground 108*108 opaque pixels and it's background
- // also 108*108 pixels, then do not enlarge this icon if only need to show foreground icon.
- private static final float ENLARGE_FOREGROUND_ICON_THRESHOLD = (72f * 72f) / (108f * 108f);
-
- /**
- * If the developer doesn't specify a background for the icon, we slightly scale it up.
- *
- * The background is either manually specified in the theme or the Adaptive Icon
- * background is used if it's different from the window background.
- */
- private static final float NO_BACKGROUND_SCALE = 192f / 160;
private final Context mContext;
private final HighResIconProvider mHighResIconProvider;
-
private int mIconSize;
private int mDefaultIconSize;
private int mBrandingImageWidth;
private int mBrandingImageHeight;
private int mMainWindowShiftLength;
+ private float mEnlargeForegroundIconThreshold;
+ private float mNoBackgroundScale;
private int mLastPackageContextConfigHash;
private final TransactionPool mTransactionPool;
private final SplashScreenWindowAttrs mTmpAttrs = new SplashScreenWindowAttrs();
@@ -336,6 +323,10 @@
com.android.wm.shell.R.dimen.starting_surface_brand_image_height);
mMainWindowShiftLength = mContext.getResources().getDimensionPixelSize(
com.android.wm.shell.R.dimen.starting_surface_exit_animation_window_shift_length);
+ mEnlargeForegroundIconThreshold = mContext.getResources().getFloat(
+ com.android.wm.shell.R.dimen.splash_icon_enlarge_foreground_threshold);
+ mNoBackgroundScale = mContext.getResources().getFloat(
+ com.android.wm.shell.R.dimen.splash_icon_no_background_scale_factor);
}
/**
@@ -604,14 +595,14 @@
// There is no background below the icon, so scale the icon up
if (mTmpAttrs.mIconBgColor == Color.TRANSPARENT
|| mTmpAttrs.mIconBgColor == mThemeColor) {
- mFinalIconSize *= NO_BACKGROUND_SCALE;
+ mFinalIconSize *= mNoBackgroundScale;
}
createIconDrawable(iconDrawable, false /* legacy */, false /* loadInDetail */);
} else {
final float iconScale = (float) mIconSize / (float) mDefaultIconSize;
final int densityDpi = mContext.getResources().getConfiguration().densityDpi;
final int scaledIconDpi =
- (int) (0.5f + iconScale * densityDpi * NO_BACKGROUND_SCALE);
+ (int) (0.5f + iconScale * densityDpi * mNoBackgroundScale);
Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "getIcon");
iconDrawable = mHighResIconProvider.getIcon(
mActivityInfo, densityDpi, scaledIconDpi);
@@ -693,8 +684,8 @@
// Reference AdaptiveIcon description, outer is 108 and inner is 72, so we
// scale by 192/160 if we only draw adaptiveIcon's foreground.
final float noBgScale =
- iconColor.mFgNonTranslucentRatio < ENLARGE_FOREGROUND_ICON_THRESHOLD
- ? NO_BACKGROUND_SCALE : 1f;
+ iconColor.mFgNonTranslucentRatio < mEnlargeForegroundIconThreshold
+ ? mNoBackgroundScale : 1f;
// Using AdaptiveIconDrawable here can help keep the shape consistent with the
// current settings.
mFinalIconSize = (int) (0.5f + mIconSize * noBgScale);
diff --git a/libs/WindowManager/Shell/tests/flicker/AndroidTestTemplate.xml b/libs/WindowManager/Shell/tests/flicker/AndroidTestTemplate.xml
index c8a9637..87b20ed 100644
--- a/libs/WindowManager/Shell/tests/flicker/AndroidTestTemplate.xml
+++ b/libs/WindowManager/Shell/tests/flicker/AndroidTestTemplate.xml
@@ -90,9 +90,9 @@
<option name="directory-keys"
value="/data/user/0/com.android.wm.shell.flicker.bubbles/files"/>
<option name="directory-keys"
- value="/data/user/0/com.android.server.wm.flicker.pip/files"/>
+ value="/data/user/0/com.android.wm.shell.flicker.pip/files"/>
<option name="directory-keys"
- value="/data/user/0/com.android.server.wm.flicker.splitscreen/files"/>
+ value="/data/user/0/com.android.wm.shell.flicker.splitscreen/files"/>
<option name="directory-keys"
value="/data/user/0/com.android.server.wm.flicker.service/files"/>
<option name="collect-on-run-ended-only" value="true"/>
diff --git a/libs/dream/lowlight/src/com/android/dream/lowlight/LowLightDreamManager.kt b/libs/dream/lowlight/src/com/android/dream/lowlight/LowLightDreamManager.kt
index 96bfb78..d1dd8df 100644
--- a/libs/dream/lowlight/src/com/android/dream/lowlight/LowLightDreamManager.kt
+++ b/libs/dream/lowlight/src/com/android/dream/lowlight/LowLightDreamManager.kt
@@ -23,6 +23,7 @@
import android.util.Log
import com.android.dream.lowlight.dagger.LowLightDreamModule
import com.android.dream.lowlight.dagger.qualifiers.Application
+import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.TimeoutCancellationException
@@ -103,6 +104,11 @@
)
} catch (ex: TimeoutCancellationException) {
Log.e(TAG, "timed out while waiting for low light animation", ex)
+ } catch (ex: CancellationException) {
+ Log.w(TAG, "low light transition animation cancelled")
+ // Catch the cancellation so that we still set the system dream component if the
+ // animation is cancelled, such as by a user tapping to wake as the transition to
+ // low light happens.
}
dreamManager.setSystemDreamComponent(
if (shouldEnterLowLight) lowLightDreamComponent else null
diff --git a/libs/dream/lowlight/src/com/android/dream/lowlight/LowLightTransitionCoordinator.kt b/libs/dream/lowlight/src/com/android/dream/lowlight/LowLightTransitionCoordinator.kt
index 4736030..de1aee5 100644
--- a/libs/dream/lowlight/src/com/android/dream/lowlight/LowLightTransitionCoordinator.kt
+++ b/libs/dream/lowlight/src/com/android/dream/lowlight/LowLightTransitionCoordinator.kt
@@ -110,15 +110,5 @@
}
}
animator.addListener(listener)
- continuation.invokeOnCancellation {
- try {
- animator.removeListener(listener)
- animator.cancel()
- } catch (exception: IndexOutOfBoundsException) {
- // TODO(b/285666217): remove this try/catch once a proper fix is implemented.
- // Cancelling the animator can cause an exception since we may be removing a
- // listener during the cancellation. See b/285666217 for more details.
- }
- }
}
}
diff --git a/libs/dream/lowlight/src/com/android/dream/lowlight/util/TruncatedInterpolator.kt b/libs/dream/lowlight/src/com/android/dream/lowlight/util/TruncatedInterpolator.kt
new file mode 100644
index 0000000..f69c84d
--- /dev/null
+++ b/libs/dream/lowlight/src/com/android/dream/lowlight/util/TruncatedInterpolator.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2023 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.dream.lowlight.util
+
+import android.view.animation.Interpolator
+
+/**
+ * Interpolator wrapper that shortens another interpolator from its original duration to a portion
+ * of that duration.
+ *
+ * For example, an `originalDuration` of 1000 and a `newDuration` of 200 results in an animation
+ * that when played for 200ms is the exact same as the first 200ms of a 1000ms animation if using
+ * the original interpolator.
+ *
+ * This is useful for the transition between the user dream and the low light clock as some
+ * animations are defined in the spec to be longer than the total duration of the animation. For
+ * example, the low light clock exit translation animation is defined to last >1s while the actual
+ * fade out of the low light clock is only 250ms, meaning the clock isn't visible anymore after
+ * 250ms.
+ *
+ * Since the dream framework currently only allows one dream to be visible and running, we use this
+ * interpolator to play just the first 250ms of the translation animation. Simply reducing the
+ * duration of the animation would result in the text exiting much faster than intended, so a custom
+ * interpolator is needed.
+ */
+class TruncatedInterpolator(
+ private val baseInterpolator: Interpolator,
+ originalDuration: Float,
+ newDuration: Float
+) : Interpolator {
+ private val scaleFactor: Float
+
+ init {
+ scaleFactor = newDuration / originalDuration
+ }
+
+ override fun getInterpolation(input: Float): Float {
+ return baseInterpolator.getInterpolation(input * scaleFactor)
+ }
+}
diff --git a/libs/dream/lowlight/tests/Android.bp b/libs/dream/lowlight/tests/Android.bp
index 2d79090..64b53cb 100644
--- a/libs/dream/lowlight/tests/Android.bp
+++ b/libs/dream/lowlight/tests/Android.bp
@@ -27,6 +27,7 @@
"androidx.test.runner",
"androidx.test.rules",
"androidx.test.ext.junit",
+ "animationlib",
"frameworks-base-testutils",
"junit",
"kotlinx_coroutines_test",
diff --git a/libs/dream/lowlight/tests/src/com.android.dream.lowlight/LowLightDreamManagerTest.kt b/libs/dream/lowlight/tests/src/com.android.dream.lowlight/LowLightDreamManagerTest.kt
index 2a886bc..de84adb 100644
--- a/libs/dream/lowlight/tests/src/com.android.dream.lowlight/LowLightDreamManagerTest.kt
+++ b/libs/dream/lowlight/tests/src/com.android.dream.lowlight/LowLightDreamManagerTest.kt
@@ -152,6 +152,21 @@
verify(mDreamManager).setSystemDreamComponent(DREAM_COMPONENT)
}
+ @Test
+ fun setAmbientLightMode_animationCancelled_SetsSystemDream() = testScope.runTest {
+ mLowLightDreamManager.setAmbientLightMode(LowLightDreamManager.AMBIENT_LIGHT_MODE_LOW_LIGHT)
+ runCurrent()
+ cancelEnterAnimations()
+ runCurrent()
+ // Animation never finishes, but we should still set the system dream
+ verify(mDreamManager).setSystemDreamComponent(DREAM_COMPONENT)
+ }
+
+ private fun cancelEnterAnimations() {
+ val listener = withArgCaptor { verify(mEnterAnimator).addListener(capture()) }
+ listener.onAnimationCancel(mEnterAnimator)
+ }
+
private fun completeEnterAnimations() {
val listener = withArgCaptor { verify(mEnterAnimator).addListener(capture()) }
listener.onAnimationEnd(mEnterAnimator)
diff --git a/libs/dream/lowlight/tests/src/com.android.dream.lowlight/LowLightTransitionCoordinatorTest.kt b/libs/dream/lowlight/tests/src/com.android.dream.lowlight/LowLightTransitionCoordinatorTest.kt
index 4c526a6..9ae304f 100644
--- a/libs/dream/lowlight/tests/src/com.android.dream.lowlight/LowLightTransitionCoordinatorTest.kt
+++ b/libs/dream/lowlight/tests/src/com.android.dream.lowlight/LowLightTransitionCoordinatorTest.kt
@@ -158,26 +158,6 @@
assertThat(job.isCancelled).isTrue()
}
- @Test
- fun shouldCancelAnimatorWhenJobCancelled() = testScope.runTest {
- whenever(mEnterListener.onBeforeEnterLowLight()).thenReturn(mAnimator)
- val coordinator = LowLightTransitionCoordinator()
- coordinator.setLowLightEnterListener(mEnterListener)
- val job = launch {
- coordinator.waitForLowLightTransitionAnimation(timeout = TIMEOUT, entering = true)
- }
- runCurrent()
- // Animator listener is added and the runnable is not run yet.
- verify(mAnimator).addListener(mAnimatorListenerCaptor.capture())
- verify(mAnimator, never()).cancel()
- assertThat(job.isCompleted).isFalse()
-
- job.cancel()
- // We should have removed the listener and cancelled the animator
- verify(mAnimator).removeListener(mAnimatorListenerCaptor.value)
- verify(mAnimator).cancel()
- }
-
companion object {
private val TIMEOUT = 1.toDuration(DurationUnit.SECONDS)
}
diff --git a/libs/dream/lowlight/tests/src/com/android/dream/lowlight/util/TruncatedInterpolatorTest.kt b/libs/dream/lowlight/tests/src/com/android/dream/lowlight/util/TruncatedInterpolatorTest.kt
new file mode 100644
index 0000000..190f02e
--- /dev/null
+++ b/libs/dream/lowlight/tests/src/com/android/dream/lowlight/util/TruncatedInterpolatorTest.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2023 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.dream.lowlight.util
+
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.app.animation.Interpolators
+import com.google.common.truth.Truth
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class TruncatedInterpolatorTest {
+ @Test
+ fun truncatedInterpolator_matchesRegularInterpolator() {
+ val originalInterpolator = Interpolators.EMPHASIZED
+ val truncatedInterpolator =
+ TruncatedInterpolator(originalInterpolator, ORIGINAL_DURATION_MS, NEW_DURATION_MS)
+
+ // Both interpolators should start at the same value.
+ var animationPercent = 0f
+ Truth.assertThat(truncatedInterpolator.getInterpolation(animationPercent))
+ .isEqualTo(originalInterpolator.getInterpolation(animationPercent))
+
+ animationPercent = 1f
+ Truth.assertThat(truncatedInterpolator.getInterpolation(animationPercent))
+ .isEqualTo(originalInterpolator.getInterpolation(animationPercent * DURATION_RATIO))
+
+ animationPercent = 0.25f
+ Truth.assertThat(truncatedInterpolator.getInterpolation(animationPercent))
+ .isEqualTo(originalInterpolator.getInterpolation(animationPercent * DURATION_RATIO))
+ }
+
+ companion object {
+ private const val ORIGINAL_DURATION_MS: Float = 1000f
+ private const val NEW_DURATION_MS: Float = 200f
+ private const val DURATION_RATIO: Float = NEW_DURATION_MS / ORIGINAL_DURATION_MS
+ }
+}
diff --git a/libs/hwui/pipeline/skia/GLFunctorDrawable.cpp b/libs/hwui/pipeline/skia/GLFunctorDrawable.cpp
index d7ac501..dbd9ef3 100644
--- a/libs/hwui/pipeline/skia/GLFunctorDrawable.cpp
+++ b/libs/hwui/pipeline/skia/GLFunctorDrawable.cpp
@@ -79,7 +79,7 @@
}
// flush will create a GrRenderTarget if not already present.
- canvas->flush();
+ directContext->flushAndSubmit();
GLuint fboID = 0;
SkISize fboSize;
@@ -167,7 +167,7 @@
// GL ops get inserted here if previous flush is missing, which could dirty the stencil
bool stencilWritten = SkAndroidFrameworkUtils::clipWithStencil(tmpCanvas);
- tmpCanvas->flush(); // need this flush for the single op that draws into the stencil
+ directContext->flushAndSubmit(); // need this flush for the single op that draws into the stencil
// ensure that the framebuffer that the webview will render into is bound before after we
// draw into the stencil
diff --git a/packages/SystemUI/res/layout/activity_rear_display_education.xml b/packages/SystemUI/res/layout/activity_rear_display_education.xml
index c295cfe..1b6247f 100644
--- a/packages/SystemUI/res/layout/activity_rear_display_education.xml
+++ b/packages/SystemUI/res/layout/activity_rear_display_education.xml
@@ -28,7 +28,7 @@
app:cardCornerRadius="28dp"
app:cardBackgroundColor="@color/rear_display_overlay_animation_background_color">
- <com.airbnb.lottie.LottieAnimationView
+ <com.android.systemui.reardisplay.RearDisplayEducationLottieViewWrapper
android:id="@+id/rear_display_folded_animation"
android:importantForAccessibility="no"
android:layout_width="@dimen/rear_display_animation_width"
diff --git a/packages/SystemUI/res/layout/activity_rear_display_education_opened.xml b/packages/SystemUI/res/layout/activity_rear_display_education_opened.xml
index 0e6b281..bded012 100644
--- a/packages/SystemUI/res/layout/activity_rear_display_education_opened.xml
+++ b/packages/SystemUI/res/layout/activity_rear_display_education_opened.xml
@@ -29,7 +29,7 @@
app:cardCornerRadius="28dp"
app:cardBackgroundColor="@color/rear_display_overlay_animation_background_color">
- <com.airbnb.lottie.LottieAnimationView
+ <com.android.systemui.reardisplay.RearDisplayEducationLottieViewWrapper
android:id="@+id/rear_display_folded_animation"
android:importantForAccessibility="no"
android:layout_width="@dimen/rear_display_animation_width_opened"
diff --git a/packages/SystemUI/res/layout/auth_biometric_contents.xml b/packages/SystemUI/res/layout/auth_biometric_contents.xml
index efc661a..50b3bec 100644
--- a/packages/SystemUI/res/layout/auth_biometric_contents.xml
+++ b/packages/SystemUI/res/layout/auth_biometric_contents.xml
@@ -57,7 +57,7 @@
<include layout="@layout/auth_biometric_icon"/>
- <com.airbnb.lottie.LottieAnimationView
+ <com.android.systemui.biometrics.BiometricPromptLottieViewWrapper
android:id="@+id/biometric_icon_overlay"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
diff --git a/packages/SystemUI/res/layout/biometric_prompt_layout.xml b/packages/SystemUI/res/layout/biometric_prompt_layout.xml
index 8eb62ec..ecb0bfa 100644
--- a/packages/SystemUI/res/layout/biometric_prompt_layout.xml
+++ b/packages/SystemUI/res/layout/biometric_prompt_layout.xml
@@ -60,7 +60,7 @@
android:layout_height="wrap_content"
android:layout_gravity="center">
- <com.airbnb.lottie.LottieAnimationView
+ <com.android.systemui.biometrics.BiometricPromptLottieViewWrapper
android:id="@+id/biometric_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@@ -68,7 +68,7 @@
android:contentDescription="@null"
android:scaleType="fitXY" />
- <com.airbnb.lottie.LottieAnimationView
+ <com.android.systemui.biometrics.BiometricPromptLottieViewWrapper
android:id="@+id/biometric_icon_overlay"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
diff --git a/packages/SystemUI/res/layout/sidefps_view.xml b/packages/SystemUI/res/layout/sidefps_view.xml
index 73050c2..4d95220 100644
--- a/packages/SystemUI/res/layout/sidefps_view.xml
+++ b/packages/SystemUI/res/layout/sidefps_view.xml
@@ -14,7 +14,7 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
-<com.airbnb.lottie.LottieAnimationView
+<com.android.systemui.biometrics.SideFpsLottieViewWrapper
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/sidefps_animation"
diff --git a/packages/SystemUI/res/layout/udfps_keyguard_preview.xml b/packages/SystemUI/res/layout/udfps_keyguard_preview.xml
index c068b7b..0964a21 100644
--- a/packages/SystemUI/res/layout/udfps_keyguard_preview.xml
+++ b/packages/SystemUI/res/layout/udfps_keyguard_preview.xml
@@ -24,7 +24,7 @@
android:background="@drawable/fingerprint_bg">
<!-- LockScreen fingerprint icon from 0 stroke width to full width -->
- <com.airbnb.lottie.LottieAnimationView
+ <com.android.systemui.keyguard.ui.view.UdfpsLottieViewWrapper
android:layout_width="0dp"
android:layout_height="0dp"
android:scaleType="centerCrop"
diff --git a/packages/SystemUI/res/layout/udfps_keyguard_view_internal.xml b/packages/SystemUI/res/layout/udfps_keyguard_view_internal.xml
index 191158e..1d6147c 100644
--- a/packages/SystemUI/res/layout/udfps_keyguard_view_internal.xml
+++ b/packages/SystemUI/res/layout/udfps_keyguard_view_internal.xml
@@ -32,7 +32,7 @@
<!-- Fingerprint -->
<!-- AOD dashed fingerprint icon with moving dashes -->
- <com.airbnb.lottie.LottieAnimationView
+ <com.android.systemui.keyguard.ui.view.UdfpsLottieViewWrapper
android:id="@+id/udfps_aod_fp"
android:layout_width="match_parent"
android:layout_height="match_parent"
@@ -43,7 +43,7 @@
app:lottie_rawRes="@raw/udfps_aod_fp"/>
<!-- LockScreen fingerprint icon from 0 stroke width to full width -->
- <com.airbnb.lottie.LottieAnimationView
+ <com.android.systemui.keyguard.ui.view.UdfpsLottieViewWrapper
android:id="@+id/udfps_lockscreen_fp"
android:layout_width="match_parent"
android:layout_height="match_parent"
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 81012a75..f8c13b0 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -1114,6 +1114,16 @@
<!-- System sharing media projection permission button to continue. [CHAR LIMIT=60] -->
<string name="media_projection_entry_generic_permission_dialog_continue">Start</string>
+ <!-- Task switcher notification -->
+ <!-- Task switcher notification text. [CHAR LIMIT=100] -->
+ <string name="media_projection_task_switcher_text">Sharing pauses when you switch apps</string>
+ <!-- The action for switching to the foreground task. [CHAR LIMIT=40] -->
+ <string name="media_projection_task_switcher_action_switch">Share this app instead</string>
+ <!-- The action for switching back to the projected task. [CHAR LIMIT=40] -->
+ <string name="media_projection_task_switcher_action_back">Switch back</string>
+ <!-- Task switcher notification channel name. [CHAR LIMIT=40] -->
+ <string name="media_projection_task_switcher_notification_channel">App switch</string>
+
<!-- Title for the dialog that is shown when screen capturing is disabled by enterprise policy. [CHAR LIMIT=100] -->
<string name="screen_capturing_disabled_by_policy_dialog_title">Blocked by your IT admin</string>
diff --git a/packages/SystemUI/src/com/android/keyguard/dagger/KeyguardStatusBarViewModule.java b/packages/SystemUI/src/com/android/keyguard/dagger/KeyguardStatusBarViewModule.java
index a7d4455..8762769 100644
--- a/packages/SystemUI/src/com/android/keyguard/dagger/KeyguardStatusBarViewModule.java
+++ b/packages/SystemUI/src/com/android/keyguard/dagger/KeyguardStatusBarViewModule.java
@@ -20,6 +20,7 @@
import com.android.systemui.R;
import com.android.systemui.battery.BatteryMeterView;
import com.android.systemui.statusbar.phone.KeyguardStatusBarView;
+import com.android.systemui.statusbar.phone.StatusBarLocation;
import com.android.systemui.statusbar.phone.userswitcher.StatusBarUserSwitcherContainer;
import dagger.Module;
@@ -44,6 +45,13 @@
/** */
@Provides
@KeyguardStatusBarViewScope
+ static StatusBarLocation getStatusBarLocation() {
+ return StatusBarLocation.KEYGUARD;
+ }
+
+ /** */
+ @Provides
+ @KeyguardStatusBarViewScope
static StatusBarUserSwitcherContainer getUserSwitcherContainer(KeyguardStatusBarView view) {
return view.findViewById(R.id.user_switcher_container);
}
diff --git a/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt b/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt
index 43fb363..444491f 100644
--- a/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt
@@ -144,12 +144,7 @@
private val lockPatternUtils: LockPatternUtils,
) : AuthenticationRepository {
- override val isUnlocked: StateFlow<Boolean> =
- keyguardRepository.isKeyguardUnlocked.stateIn(
- scope = applicationScope,
- started = SharingStarted.WhileSubscribed(),
- initialValue = false,
- )
+ override val isUnlocked: StateFlow<Boolean> = keyguardRepository.isKeyguardUnlocked
override suspend fun isLockscreenEnabled(): Boolean {
return withContext(backgroundDispatcher) {
diff --git a/packages/SystemUI/src/com/android/systemui/battery/AccessorizedBatteryDrawable.kt b/packages/SystemUI/src/com/android/systemui/battery/AccessorizedBatteryDrawable.kt
index b52ddc1..b34f1b4 100644
--- a/packages/SystemUI/src/com/android/systemui/battery/AccessorizedBatteryDrawable.kt
+++ b/packages/SystemUI/src/com/android/systemui/battery/AccessorizedBatteryDrawable.kt
@@ -87,6 +87,10 @@
}
var displayShield: Boolean = false
+ set(value) {
+ field = value
+ postInvalidate()
+ }
private fun updateSizes() {
val b = bounds
@@ -204,4 +208,11 @@
val shieldPathString = context.resources.getString(R.string.config_batterymeterShieldPath)
shieldPath.set(PathParser.createPathFromPathData(shieldPathString))
}
+
+ private val invalidateRunnable: () -> Unit = { invalidateSelf() }
+
+ private fun postInvalidate() {
+ unscheduleSelf(invalidateRunnable)
+ scheduleSelf(invalidateRunnable, 0)
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterView.java b/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterView.java
index f5f47d0..234795f 100644
--- a/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterView.java
+++ b/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterView.java
@@ -466,9 +466,11 @@
public void dump(PrintWriter pw, String[] args) {
String powerSave = mDrawable == null ? null : mDrawable.getPowerSaveEnabled() + "";
+ String displayShield = mDrawable == null ? null : mDrawable.getDisplayShield() + "";
CharSequence percent = mBatteryPercentView == null ? null : mBatteryPercentView.getText();
pw.println(" BatteryMeterView:");
pw.println(" mDrawable.getPowerSave: " + powerSave);
+ pw.println(" mDrawable.getDisplayShield: " + displayShield);
pw.println(" mBatteryPercentView.getText(): " + percent);
pw.println(" mTextColor: #" + Integer.toHexString(mTextColor));
pw.println(" mBatteryStateUnknown: " + mBatteryStateUnknown);
diff --git a/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterViewController.java b/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterViewController.java
index f6a10bd..8b22024 100644
--- a/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterViewController.java
@@ -35,11 +35,14 @@
import com.android.systemui.flags.Flags;
import com.android.systemui.settings.UserTracker;
import com.android.systemui.statusbar.phone.StatusBarIconController;
+import com.android.systemui.statusbar.phone.StatusBarLocation;
import com.android.systemui.statusbar.policy.BatteryController;
import com.android.systemui.statusbar.policy.ConfigurationController;
import com.android.systemui.tuner.TunerService;
import com.android.systemui.util.ViewController;
+import java.io.PrintWriter;
+
import javax.inject.Inject;
/** Controller for {@link BatteryMeterView}. **/
@@ -53,6 +56,7 @@
private final String mSlotBattery;
private final SettingObserver mSettingObserver;
private final UserTracker mUserTracker;
+ private final StatusBarLocation mLocation;
private final ConfigurationController.ConfigurationListener mConfigurationListener =
new ConfigurationController.ConfigurationListener() {
@@ -94,6 +98,13 @@
public void onIsBatteryDefenderChanged(boolean isBatteryDefender) {
mView.onIsBatteryDefenderChanged(isBatteryDefender);
}
+
+ @Override
+ public void dump(@NonNull PrintWriter pw, @NonNull String[] args) {
+ pw.print(super.toString());
+ pw.println(" location=" + mLocation);
+ mView.dump(pw, args);
+ }
};
private final UserTracker.Callback mUserChangedCallback =
@@ -113,6 +124,7 @@
@Inject
public BatteryMeterViewController(
BatteryMeterView view,
+ StatusBarLocation location,
UserTracker userTracker,
ConfigurationController configurationController,
TunerService tunerService,
@@ -121,6 +133,7 @@
FeatureFlags featureFlags,
BatteryController batteryController) {
super(view);
+ mLocation = location;
mUserTracker = userTracker;
mConfigurationController = configurationController;
mTunerService = tunerService;
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/BiometricPromptLottieViewWrapper.kt b/packages/SystemUI/src/com/android/systemui/biometrics/BiometricPromptLottieViewWrapper.kt
new file mode 100644
index 0000000..e48e6e2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/BiometricPromptLottieViewWrapper.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2023 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.biometrics
+
+import android.content.Context
+import android.util.AttributeSet
+import com.android.systemui.util.wrapper.LottieViewWrapper
+
+class BiometricPromptLottieViewWrapper
+@JvmOverloads
+constructor(context: Context, attrs: AttributeSet? = null) : LottieViewWrapper(context, attrs)
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsController.kt
index 9d0cde1..37ce444 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsController.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsController.kt
@@ -117,6 +117,8 @@
private var overlayView: View? = null
set(value) {
field?.let { oldView ->
+ val lottie = oldView.findViewById(R.id.sidefps_animation) as LottieAnimationView
+ lottie.pauseAnimation()
windowManager.removeView(oldView)
orientationListener.disable()
}
@@ -193,7 +195,9 @@
requests.add(request)
mainExecutor.execute {
if (overlayView == null) {
- traceSection("SideFpsController#show(request=${request.name}, reason=$reason") {
+ traceSection(
+ "SideFpsController#show(request=${request.name}, reason=$reason)"
+ ) {
createOverlayForDisplay(reason)
}
} else {
@@ -208,7 +212,7 @@
requests.remove(request)
mainExecutor.execute {
if (requests.isEmpty()) {
- traceSection("SideFpsController#hide(${request.name}") { overlayView = null }
+ traceSection("SideFpsController#hide(${request.name})") { overlayView = null }
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsLottieViewWrapper.kt b/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsLottieViewWrapper.kt
new file mode 100644
index 0000000..e98f6db
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsLottieViewWrapper.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2023 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.biometrics
+
+import android.content.Context
+import android.util.AttributeSet
+import com.android.systemui.util.wrapper.LottieViewWrapper
+
+class SideFpsLottieViewWrapper
+@JvmOverloads
+constructor(context: Context, attrs: AttributeSet? = null) : LottieViewWrapper(context, attrs)
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt
index ee046c2..484bf3d 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt
@@ -26,6 +26,7 @@
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
import com.android.app.animation.Interpolators
+import com.android.dream.lowlight.util.TruncatedInterpolator
import com.android.systemui.R
import com.android.systemui.complication.ComplicationHostViewController
import com.android.systemui.complication.ComplicationLayoutParams
@@ -204,31 +205,28 @@
translationYAnimator(
from = 0f,
to = -mDreamInTranslationYDistance.toFloat(),
- durationMs = mDreamInTranslationYDurationMs,
+ durationMs = mDreamInComplicationsAnimDurationMs,
delayMs = 0,
- interpolator = Interpolators.EMPHASIZED
+ // Truncate the animation from the full duration to match the alpha
+ // animation so that the whole animation ends at the same time.
+ interpolator =
+ TruncatedInterpolator(
+ Interpolators.EMPHASIZED,
+ /*originalDuration=*/ mDreamInTranslationYDurationMs.toFloat(),
+ /*newDuration=*/ mDreamInComplicationsAnimDurationMs.toFloat()
+ )
),
alphaAnimator(
- from =
- mCurrentAlphaAtPosition.getOrDefault(
- key = POSITION_BOTTOM,
- defaultValue = 1f
- ),
- to = 0f,
- durationMs = mDreamInComplicationsAnimDurationMs,
- delayMs = 0,
- positions = POSITION_BOTTOM
- )
- .apply {
- doOnEnd {
- // The logical end of the animation is once the alpha and blur
- // animations finish, end the animation so that any listeners are
- // notified. The Y translation animation is much longer than all of
- // the other animations due to how the spec is defined, but is not
- // expected to run to completion.
- mAnimator?.end()
- }
- },
+ from =
+ mCurrentAlphaAtPosition.getOrDefault(
+ key = POSITION_BOTTOM,
+ defaultValue = 1f
+ ),
+ to = 0f,
+ durationMs = mDreamInComplicationsAnimDurationMs,
+ delayMs = 0,
+ positions = POSITION_BOTTOM
+ ),
alphaAnimator(
from =
mCurrentAlphaAtPosition.getOrDefault(
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
index a5a7995..68d1fbd 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
@@ -290,12 +290,17 @@
/** Migrate the lock icon view to the new keyguard root view. */
// TODO(b/286552209): Tracking bug.
@JvmField
- val MIGRATE_LOCK_ICON = unreleasedFlag(240, "migrate_lock_icon")
+ val MIGRATE_LOCK_ICON = unreleasedFlag(240, "migrate_lock_icon", teamfood = true)
// TODO(b/288276738): Tracking bug.
@JvmField
val WIDGET_ON_KEYGUARD = unreleasedFlag(241, "widget_on_keyguard")
+ /** Migrate the NSSL to the a sibling to both the panel and keyguard root view. */
+ // TODO(b/288074305): Tracking bug.
+ @JvmField
+ val MIGRATE_NSSL = unreleasedFlag(242, "migrate_nssl")
+
// 300 - power menu
// TODO(b/254512600): Tracking Bug
@JvmField val POWER_MENU_LITE = releasedFlag(300, "power_menu_lite")
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
index e7704d6..d119920 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
@@ -87,7 +87,7 @@
val isKeyguardShowing: Flow<Boolean>
/** Is the keyguard in a unlocked state? */
- val isKeyguardUnlocked: Flow<Boolean>
+ val isKeyguardUnlocked: StateFlow<Boolean>
/** Is an activity showing over the keyguard? */
val isKeyguardOccluded: Flow<Boolean>
@@ -299,7 +299,7 @@
}
.distinctUntilChanged()
- override val isKeyguardUnlocked: Flow<Boolean> =
+ override val isKeyguardUnlocked: StateFlow<Boolean> =
conflatedCallbackFlow {
val callback =
object : KeyguardStateController.Callback {
@@ -330,7 +330,11 @@
awaitClose { keyguardStateController.removeCallback(callback) }
}
- .distinctUntilChanged()
+ .stateIn(
+ scope = scope,
+ started = SharingStarted.WhileSubscribed(),
+ initialValue = keyguardStateController.isUnlocked,
+ )
override val isKeyguardGoingAway: Flow<Boolean> = conflatedCallbackFlow {
val callback =
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/UdfpsLottieViewWrapper.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/UdfpsLottieViewWrapper.kt
new file mode 100644
index 0000000..3a2c3c7
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/UdfpsLottieViewWrapper.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2023 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.keyguard.ui.view
+
+import android.content.Context
+import android.util.AttributeSet
+import com.android.systemui.util.wrapper.LottieViewWrapper
+
+class UdfpsLottieViewWrapper
+@JvmOverloads
+constructor(context: Context, attrs: AttributeSet? = null) : LottieViewWrapper(context, attrs)
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/ui/TaskSwitcherNotificationCoordinator.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/ui/TaskSwitcherNotificationCoordinator.kt
index a4f4076..a437139 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/ui/TaskSwitcherNotificationCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/ui/TaskSwitcherNotificationCoordinator.kt
@@ -16,15 +16,19 @@
package com.android.systemui.mediaprojection.taskswitcher.ui
+import android.app.Notification
+import android.app.NotificationChannel
+import android.app.NotificationManager
import android.content.Context
import android.util.Log
-import android.widget.Toast
+import com.android.systemui.R
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.mediaprojection.taskswitcher.ui.model.TaskSwitcherNotificationUiState.NotShowing
import com.android.systemui.mediaprojection.taskswitcher.ui.model.TaskSwitcherNotificationUiState.Showing
import com.android.systemui.mediaprojection.taskswitcher.ui.viewmodel.TaskSwitcherNotificationViewModel
+import com.android.systemui.util.NotificationChannels
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
@@ -37,38 +41,69 @@
@Inject
constructor(
private val context: Context,
+ private val notificationManager: NotificationManager,
@Application private val applicationScope: CoroutineScope,
@Main private val mainDispatcher: CoroutineDispatcher,
private val viewModel: TaskSwitcherNotificationViewModel,
) {
-
fun start() {
applicationScope.launch {
viewModel.uiState.flowOn(mainDispatcher).collect { uiState ->
Log.d(TAG, "uiState -> $uiState")
when (uiState) {
- is Showing -> showNotification(uiState)
+ is Showing -> showNotification()
is NotShowing -> hideNotification()
}
}
}
}
- private fun showNotification(uiState: Showing) {
- val text =
- """
- Sharing pauses when you switch apps.
- Share this app instead.
- Switch back.
- """
- .trimIndent()
- // TODO(b/286201515): Create actual notification.
- Toast.makeText(context, text, Toast.LENGTH_SHORT).show()
+ private fun showNotification() {
+ notificationManager.notify(TAG, NOTIFICATION_ID, createNotification())
}
- private fun hideNotification() {}
+ private fun createNotification(): Notification {
+ // TODO(b/286201261): implement actions
+ val actionSwitch =
+ Notification.Action.Builder(
+ /* icon = */ null,
+ context.getString(R.string.media_projection_task_switcher_action_switch),
+ /* intent = */ null
+ )
+ .build()
+
+ val actionBack =
+ Notification.Action.Builder(
+ /* icon = */ null,
+ context.getString(R.string.media_projection_task_switcher_action_back),
+ /* intent = */ null
+ )
+ .build()
+
+ val channel =
+ NotificationChannel(
+ NotificationChannels.HINTS,
+ context.getString(R.string.media_projection_task_switcher_notification_channel),
+ NotificationManager.IMPORTANCE_HIGH
+ )
+ notificationManager.createNotificationChannel(channel)
+ return Notification.Builder(context, channel.id)
+ .setSmallIcon(R.drawable.qs_screen_record_icon_on)
+ .setAutoCancel(true)
+ .setContentText(context.getString(R.string.media_projection_task_switcher_text))
+ .addAction(actionSwitch)
+ .addAction(actionBack)
+ .setPriority(Notification.PRIORITY_HIGH)
+ .setDefaults(Notification.DEFAULT_VIBRATE)
+ .build()
+ }
+
+ private fun hideNotification() {
+ notificationManager.cancel(NOTIFICATION_ID)
+ }
companion object {
private const val TAG = "TaskSwitchNotifCoord"
+ private const val NOTIFICATION_ID = 5566
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayEducationLottieViewWrapper.kt b/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayEducationLottieViewWrapper.kt
new file mode 100644
index 0000000..716a4d6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayEducationLottieViewWrapper.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2023 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.reardisplay
+
+import android.content.Context
+import android.util.AttributeSet
+import com.android.systemui.util.wrapper.LottieViewWrapper
+
+class RearDisplayEducationLottieViewWrapper
+@JvmOverloads
+constructor(context: Context, attrs: AttributeSet? = null) : LottieViewWrapper(context, attrs)
diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SystemUiDefaultSceneContainerStartable.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SystemUiDefaultSceneContainerStartable.kt
index 59f82f0..285ff74 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SystemUiDefaultSceneContainerStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SystemUiDefaultSceneContainerStartable.kt
@@ -22,6 +22,8 @@
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
+import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
+import com.android.systemui.keyguard.shared.model.WakefulnessState
import com.android.systemui.scene.domain.interactor.SceneInteractor
import com.android.systemui.scene.shared.model.SceneContainerNames
import com.android.systemui.scene.shared.model.SceneKey
@@ -45,6 +47,7 @@
@Application private val applicationScope: CoroutineScope,
private val sceneInteractor: SceneInteractor,
private val authenticationInteractor: AuthenticationInteractor,
+ private val keyguardInteractor: KeyguardInteractor,
private val featureFlags: FeatureFlags,
) : CoreStartable {
@@ -78,7 +81,7 @@
when {
isUnlocked ->
when (currentSceneKey) {
- // When the device becomes unlocked in Bouncer, go to the Gone.
+ // When the device becomes unlocked in Bouncer, go to Gone.
is SceneKey.Bouncer -> SceneKey.Gone
// When the device becomes unlocked in Lockscreen, go to Gone if
// bypass is enabled.
@@ -101,15 +104,30 @@
}
}
.filterNotNull()
- .collect { targetSceneKey ->
- sceneInteractor.setCurrentScene(
- containerName = CONTAINER_NAME,
- scene = SceneModel(targetSceneKey),
- )
+ .collect { targetSceneKey -> switchToScene(targetSceneKey) }
+ }
+
+ applicationScope.launch {
+ keyguardInteractor.wakefulnessModel
+ .map { it.state == WakefulnessState.ASLEEP }
+ .distinctUntilChanged()
+ .collect { isAsleep ->
+ if (isAsleep) {
+ // When the device goes to sleep, reset the current scene.
+ val isUnlocked = authenticationInteractor.isUnlocked.value
+ switchToScene(if (isUnlocked) SceneKey.Gone else SceneKey.Lockscreen)
+ }
}
}
}
+ private fun switchToScene(targetSceneKey: SceneKey) {
+ sceneInteractor.setCurrentScene(
+ containerName = CONTAINER_NAME,
+ scene = SceneModel(targetSceneKey),
+ )
+ }
+
companion object {
private const val CONTAINER_NAME = SceneContainerNames.SYSTEM_UI_DEFAULT
}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/CropView.java b/packages/SystemUI/src/com/android/systemui/screenshot/CropView.java
index a9cecaa..6f2256e 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/CropView.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/CropView.java
@@ -117,18 +117,22 @@
@Override
protected Parcelable onSaveInstanceState() {
+ Log.d(TAG, "onSaveInstanceState");
Parcelable superState = super.onSaveInstanceState();
SavedState ss = new SavedState(superState);
ss.mCrop = mCrop;
+ Log.d(TAG, "saving mCrop=" + mCrop);
+
return ss;
}
@Override
protected void onRestoreInstanceState(Parcelable state) {
+ Log.d(TAG, "onRestoreInstanceState");
SavedState ss = (SavedState) state;
super.onRestoreInstanceState(ss.getSuperState());
-
+ Log.d(TAG, "restoring mCrop=" + ss.mCrop + " (was " + mCrop + ")");
mCrop = ss.mCrop;
}
@@ -242,6 +246,7 @@
* Set the given boundary to the given value without animation.
*/
public void setBoundaryPosition(CropBoundary boundary, float position) {
+ Log.i(TAG, "setBoundaryPosition: " + boundary + ", position=" + position);
position = (float) getAllowedValues(boundary).clamp(position);
switch (boundary) {
case TOP:
@@ -260,6 +265,7 @@
Log.w(TAG, "No boundary selected");
break;
}
+ Log.i(TAG, "Updated mCrop: " + mCrop);
invalidate();
}
@@ -350,26 +356,31 @@
mCropInteractionListener = listener;
}
- private Range getAllowedValues(CropBoundary boundary) {
+ private Range<Float> getAllowedValues(CropBoundary boundary) {
+ float upper = 0f;
+ float lower = 1f;
switch (boundary) {
case TOP:
- return new Range<>(0f,
- mCrop.bottom - pixelDistanceToFraction(mCropTouchMargin,
- CropBoundary.BOTTOM));
+ lower = 0f;
+ upper = mCrop.bottom - pixelDistanceToFraction(mCropTouchMargin,
+ CropBoundary.BOTTOM);
+ break;
case BOTTOM:
- return new Range<>(
- mCrop.top + pixelDistanceToFraction(mCropTouchMargin,
- CropBoundary.TOP), 1f);
+ lower = mCrop.top + pixelDistanceToFraction(mCropTouchMargin, CropBoundary.TOP);
+ upper = 1;
+ break;
case LEFT:
- return new Range<>(0f,
- mCrop.right - pixelDistanceToFraction(mCropTouchMargin,
- CropBoundary.RIGHT));
+ lower = 0f;
+ upper = mCrop.right - pixelDistanceToFraction(mCropTouchMargin, CropBoundary.RIGHT);
+ break;
case RIGHT:
- return new Range<>(
- mCrop.left + pixelDistanceToFraction(mCropTouchMargin,
- CropBoundary.LEFT), 1f);
+ lower = mCrop.left + pixelDistanceToFraction(mCropTouchMargin, CropBoundary.LEFT);
+ upper = 1;
+ break;
}
- return null;
+ Log.i(TAG, "getAllowedValues: " + boundary + ", "
+ + "result=[lower=" + lower + ", upper=" + upper + "]");
+ return new Range<>(lower, upper);
}
/**
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/LongScreenshotActivity.java b/packages/SystemUI/src/com/android/systemui/screenshot/LongScreenshotActivity.java
index 4bc7ec8..e6e1fac 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/LongScreenshotActivity.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/LongScreenshotActivity.java
@@ -36,17 +36,18 @@
import android.util.Log;
import android.view.ScrollCaptureResponse;
import android.view.View;
-import android.view.ViewTreeObserver;
import android.widget.ImageView;
import androidx.constraintlayout.widget.ConstraintLayout;
import com.android.internal.app.ChooserActivity;
import com.android.internal.logging.UiEventLogger;
+import com.android.internal.view.OneShotPreDrawListener;
import com.android.systemui.R;
import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.screenshot.CropView.CropBoundary;
import com.android.systemui.screenshot.ScrollCaptureController.LongScreenshot;
import com.android.systemui.settings.UserTracker;
@@ -215,6 +216,7 @@
mPreview.setImageDrawable(drawable);
mMagnifierView.setDrawable(mLongScreenshot.getDrawable(),
mLongScreenshot.getWidth(), mLongScreenshot.getHeight());
+ Log.i(TAG, "Completed: " + longScreenshot);
// Original boundaries go from the image tile set's y=0 to y=pageSize, so
// we animate to that as a starting crop position.
float topFraction = Math.max(0,
@@ -223,31 +225,26 @@
1 - (mLongScreenshot.getBottom() - mLongScreenshot.getPageHeight())
/ (float) mLongScreenshot.getHeight());
+ Log.i(TAG, "topFraction: " + topFraction);
+ Log.i(TAG, "bottomFraction: " + bottomFraction);
+
mEnterTransitionView.setImageDrawable(drawable);
- mEnterTransitionView.getViewTreeObserver().addOnPreDrawListener(
- new ViewTreeObserver.OnPreDrawListener() {
- @Override
- public boolean onPreDraw() {
- mEnterTransitionView.getViewTreeObserver().removeOnPreDrawListener(this);
- updateImageDimensions();
- mEnterTransitionView.post(() -> {
- Rect dest = new Rect();
- mEnterTransitionView.getBoundsOnScreen(dest);
- mLongScreenshotHolder.takeTransitionDestinationCallback()
- .setTransitionDestination(dest, () -> {
- mPreview.animate().alpha(1f);
- mCropView.setBoundaryPosition(
- CropView.CropBoundary.TOP, topFraction);
- mCropView.setBoundaryPosition(
- CropView.CropBoundary.BOTTOM, bottomFraction);
- mCropView.animateEntrance();
- mCropView.setVisibility(View.VISIBLE);
- setButtonsEnabled(true);
- });
+ OneShotPreDrawListener.add(mEnterTransitionView, () -> {
+ updateImageDimensions();
+ mEnterTransitionView.post(() -> {
+ Rect dest = new Rect();
+ mEnterTransitionView.getBoundsOnScreen(dest);
+ mLongScreenshotHolder.takeTransitionDestinationCallback()
+ .setTransitionDestination(dest, () -> {
+ mPreview.animate().alpha(1f);
+ mCropView.setBoundaryPosition(CropBoundary.TOP, topFraction);
+ mCropView.setBoundaryPosition(CropBoundary.BOTTOM, bottomFraction);
+ mCropView.animateEntrance();
+ mCropView.setVisibility(View.VISIBLE);
+ setButtonsEnabled(true);
});
- return true;
- }
- });
+ });
+ });
// Immediately export to temp image file for saved state
mCacheSaveFuture = mImageExporter.exportToRawFile(mBackgroundExecutor,
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java
index 30a0b8f..bb34ede 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java
@@ -130,8 +130,14 @@
@Override
public String toString() {
- return "LongScreenshot{w=" + mImageTileSet.getWidth()
- + ", h=" + mImageTileSet.getHeight() + "}";
+ return "LongScreenshot{"
+ + "l=" + mImageTileSet.getLeft() + ", "
+ + "t=" + mImageTileSet.getTop() + ", "
+ + "r=" + mImageTileSet.getRight() + ", "
+ + "b=" + mImageTileSet.getBottom() + ", "
+ + "w=" + mImageTileSet.getWidth() + ", "
+ + "h=" + mImageTileSet.getHeight()
+ + "}";
}
public Drawable getDrawable() {
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index d97db3b..ea15035 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -1076,6 +1076,8 @@
mTapAgainViewController.init();
mShadeHeaderController.init();
+ mShadeHeaderController.setShadeCollapseAction(
+ () -> collapse(/* delayed= */ false , /* speedUpFactor= */ 1.0f));
mKeyguardUnfoldTransition.ifPresent(u -> u.setup(mView));
mNotificationPanelUnfoldAnimationController.ifPresent(controller ->
controller.setup(mNotificationContainerParent));
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeHeaderController.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeHeaderController.kt
index 411f91f..8b89ff4 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeHeaderController.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeHeaderController.kt
@@ -122,6 +122,8 @@
}
}
+ var shadeCollapseAction: Runnable? = null
+
private lateinit var iconManager: StatusBarIconController.TintedIconManager
private lateinit var carrierIconSlots: List<String>
private lateinit var mShadeCarrierGroupController: ShadeCarrierGroupController
@@ -469,9 +471,11 @@
if (largeScreenActive) {
logInstantEvent("Large screen constraints set")
header.setTransition(LARGE_SCREEN_HEADER_TRANSITION_ID)
+ systemIcons.setOnClickListener { shadeCollapseAction?.run() }
} else {
logInstantEvent("Small screen constraints set")
header.setTransition(HEADER_TRANSITION_ID)
+ systemIcons.setOnClickListener(null)
}
header.jumpToState(header.startState)
updatePosition()
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt
index e2e633a..65260979 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt
@@ -45,6 +45,7 @@
import com.android.systemui.statusbar.notification.shelf.ui.viewbinder.NotificationShelfViewBinderWrapperControllerImpl
import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout
import com.android.systemui.statusbar.phone.KeyguardBottomAreaView
+import com.android.systemui.statusbar.phone.StatusBarLocation
import com.android.systemui.statusbar.phone.StatusIconContainer
import com.android.systemui.statusbar.phone.TapAgainView
import com.android.systemui.statusbar.policy.BatteryController
@@ -268,6 +269,7 @@
): BatteryMeterViewController {
return BatteryMeterViewController(
batteryMeterView,
+ StatusBarLocation.QS,
userTracker,
configurationController,
tunerService,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java
index cff71d2..fbbee53 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java
@@ -160,7 +160,6 @@
private ObjectAnimator mIconAppearAnimator;
private ObjectAnimator mDotAnimator;
private float mDotAppearAmount;
- private OnVisibilityChangedListener mOnVisibilityChangedListener;
private int mDrawableColor;
private int mIconColor;
private int mDecorColor;
@@ -179,7 +178,6 @@
private int mCachedContrastBackgroundColor = NO_COLOR;
private float[] mMatrix;
private ColorMatrixColorFilter mMatrixColorFilter;
- private boolean mIsInShelf;
private Runnable mLayoutRunnable;
private boolean mDismissed;
private Runnable mOnDismissListener;
@@ -357,19 +355,6 @@
return mNotification != null;
}
- private static boolean streq(String a, String b) {
- if (a == b) {
- return true;
- }
- if (a == null && b != null) {
- return false;
- }
- if (a != null && b == null) {
- return false;
- }
- return a.equals(b);
- }
-
public boolean equalIcons(Icon a, Icon b) {
if (a == b) return true;
if (a.getType() != b.getType()) return false;
@@ -908,7 +893,7 @@
if (targetAmount != currentAmount) {
mDotAnimator = ObjectAnimator.ofFloat(this, DOT_APPEAR_AMOUNT,
currentAmount, targetAmount);
- mDotAnimator.setInterpolator(interpolator);;
+ mDotAnimator.setInterpolator(interpolator);
mDotAnimator.setDuration(duration == 0 ? ANIMATION_DURATION_FAST
: duration);
final boolean runRunnable = !runnableAdded;
@@ -965,22 +950,10 @@
}
}
- @Override
- public void setVisibility(int visibility) {
- super.setVisibility(visibility);
- if (mOnVisibilityChangedListener != null) {
- mOnVisibilityChangedListener.onVisibilityChanged(visibility);
- }
- }
-
public float getDotAppearAmount() {
return mDotAppearAmount;
}
- public void setOnVisibilityChangedListener(OnVisibilityChangedListener listener) {
- mOnVisibilityChangedListener = listener;
- }
-
public void setDozing(boolean dozing, boolean fade, long delay) {
mDozer.setDozing(f -> {
mDozeAmount = f;
@@ -1014,14 +987,6 @@
outRect.bottom += translationY;
}
- public void setIsInShelf(boolean isInShelf) {
- mIsInShelf = isInShelf;
- }
-
- public boolean isInShelf() {
- return mIsInShelf;
- }
-
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
@@ -1103,8 +1068,4 @@
public boolean showsConversation() {
return mShowsConversation;
}
-
- public interface OnVisibilityChangedListener {
- void onVisibilityChanged(int newVisibility);
- }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/Roundable.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/Roundable.kt
index 212f2c215..1cf9c1e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/Roundable.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/Roundable.kt
@@ -3,7 +3,10 @@
import android.util.FloatProperty
import android.view.View
import androidx.annotation.FloatRange
+import com.android.systemui.Dependency
import com.android.systemui.R
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
import com.android.systemui.statusbar.notification.stack.AnimationProperties
import com.android.systemui.statusbar.notification.stack.StackStateAnimator
import kotlin.math.abs
@@ -20,6 +23,8 @@
/** Properties required for a Roundable */
val roundableState: RoundableState
+ val clipHeight: Int
+
/** Current top roundness */
@get:FloatRange(from = 0.0, to = 1.0)
@JvmDefault
@@ -40,12 +45,16 @@
/** Current top corner in pixel, based on [topRoundness] and [maxRadius] */
@JvmDefault
val topCornerRadius: Float
- get() = topRoundness * maxRadius
+ get() =
+ if (roundableState.newHeadsUpAnimFlagEnabled) roundableState.topCornerRadius
+ else topRoundness * maxRadius
/** Current bottom corner in pixel, based on [bottomRoundness] and [maxRadius] */
@JvmDefault
val bottomCornerRadius: Float
- get() = bottomRoundness * maxRadius
+ get() =
+ if (roundableState.newHeadsUpAnimFlagEnabled) roundableState.bottomCornerRadius
+ else bottomRoundness * maxRadius
/** Get and update the current radii */
@JvmDefault
@@ -320,14 +329,20 @@
* @param roundable Target of the radius animation
* @param maxRadius Max corner radius in pixels
*/
-class RoundableState(
+class RoundableState
+@JvmOverloads
+constructor(
internal val targetView: View,
private val roundable: Roundable,
maxRadius: Float,
+ private val featureFlags: FeatureFlags = Dependency.get(FeatureFlags::class.java)
) {
internal var maxRadius = maxRadius
private set
+ internal val newHeadsUpAnimFlagEnabled
+ get() = featureFlags.isEnabled(Flags.IMPROVED_HUN_ANIMATIONS)
+
/** Animatable for top roundness */
private val topAnimatable = topAnimatable(roundable)
@@ -344,6 +359,41 @@
internal var bottomRoundness = 0f
private set
+ internal val topCornerRadius: Float
+ get() {
+ val height = roundable.clipHeight
+ val topRadius = topRoundness * maxRadius
+ val bottomRadius = bottomRoundness * maxRadius
+
+ if (height == 0) {
+ return 0f
+ } else if (topRadius + bottomRadius > height) {
+ // The sum of top and bottom corner radii should be at max the clipped height
+ val overShoot = topRadius + bottomRadius - height
+ return topRadius - (overShoot * topRoundness / (topRoundness + bottomRoundness))
+ }
+
+ return topRadius
+ }
+
+ internal val bottomCornerRadius: Float
+ get() {
+ val height = roundable.clipHeight
+ val topRadius = topRoundness * maxRadius
+ val bottomRadius = bottomRoundness * maxRadius
+
+ if (height == 0) {
+ return 0f
+ } else if (topRadius + bottomRadius > height) {
+ // The sum of top and bottom corner radii should be at max the clipped height
+ val overShoot = topRadius + bottomRadius - height
+ return bottomRadius -
+ (overShoot * bottomRoundness / (topRoundness + bottomRoundness))
+ }
+
+ return bottomRadius
+ }
+
/** Last requested top roundness associated by [SourceType] */
internal val topRoundnessMap = mutableMapOf<SourceType, Float>()
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProvider.kt
index 90014c2..78225df 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProvider.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProvider.kt
@@ -87,6 +87,7 @@
private val userTrackerCallback = object : UserTracker.Callback {
override fun onUserChanged(newUser: Int, userContext: Context) {
+ readShowSilentNotificationSetting()
if (isLockedOrLocking) {
// maybe public mode changed
notifyStateChanged("onUserSwitched")
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java
index 27510d4..908c11a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java
@@ -566,12 +566,20 @@
@Override
public float getTopCornerRadius() {
+ if (isNewHeadsUpAnimFlagEnabled()) {
+ return super.getTopCornerRadius();
+ }
+
float fraction = getInterpolatedAppearAnimationFraction();
return MathUtils.lerp(0, super.getTopCornerRadius(), fraction);
}
@Override
public float getBottomCornerRadius() {
+ if (isNewHeadsUpAnimFlagEnabled()) {
+ return super.getBottomCornerRadius();
+ }
+
float fraction = getInterpolatedAppearAnimationFraction();
return MathUtils.lerp(0, super.getBottomCornerRadius(), fraction);
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableOutlineView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableOutlineView.java
index 9aa50e9..7f23c1b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableOutlineView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableOutlineView.java
@@ -28,7 +28,10 @@
import android.view.View;
import android.view.ViewOutlineProvider;
+import com.android.systemui.Dependency;
import com.android.systemui.R;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.Flags;
import com.android.systemui.statusbar.notification.RoundableState;
import com.android.systemui.statusbar.notification.stack.NotificationChildrenContainer;
import com.android.systemui.util.DumpUtilsKt;
@@ -47,12 +50,14 @@
private float mOutlineAlpha = -1f;
private boolean mAlwaysRoundBothCorners;
private Path mTmpPath = new Path();
+ private final FeatureFlags mFeatureFlags;
/**
* {@code false} if the children views of the {@link ExpandableOutlineView} are translated when
* it is moved. Otherwise, the translation is set on the {@code ExpandableOutlineView} itself.
*/
protected boolean mDismissUsingRowTranslationX = true;
+
private float[] mTmpCornerRadii = new float[8];
private final ViewOutlineProvider mProvider = new ViewOutlineProvider() {
@@ -81,6 +86,15 @@
return mRoundableState;
}
+ @Override
+ public int getClipHeight() {
+ if (mCustomOutline) {
+ return mOutlineRect.height();
+ }
+
+ return super.getClipHeight();
+ }
+
protected Path getClipPath(boolean ignoreTranslation) {
int left;
int top;
@@ -112,7 +126,7 @@
return EMPTY_PATH;
}
float bottomRadius = mAlwaysRoundBothCorners ? getMaxRadius() : getBottomCornerRadius();
- if (topRadius + bottomRadius > height) {
+ if (!isNewHeadsUpAnimFlagEnabled() && (topRadius + bottomRadius > height)) {
float overShoot = topRadius + bottomRadius - height;
float currentTopRoundness = getTopRoundness();
float currentBottomRoundness = getBottomRoundness();
@@ -153,6 +167,7 @@
super(context, attrs);
setOutlineProvider(mProvider);
initDimens();
+ mFeatureFlags = Dependency.get(FeatureFlags.class);
}
@Override
@@ -360,4 +375,9 @@
}
});
}
+
+ // TODO(b/290365128) replace with ViewRefactorFlag
+ protected boolean isNewHeadsUpAnimFlagEnabled() {
+ return mFeatureFlags.isEnabled(Flags.IMPROVED_HUN_ANIMATIONS);
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java
index f986244..c4c116b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java
@@ -93,6 +93,12 @@
return mRoundableState;
}
+ @Override
+ public int getClipHeight() {
+ int clipHeight = Math.max(mActualHeight - mClipTopAmount - mClipBottomAmount, 0);
+ return Math.max(clipHeight, mMinimumHeightForClipping);
+ }
+
private void initDimens() {
mContentShift = getResources().getDimensionPixelSize(
R.dimen.shelf_transform_content_shift);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java
index ef5e86f06..87205e2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java
@@ -120,6 +120,11 @@
}
@Override
+ public int getClipHeight() {
+ return mView.getHeight();
+ }
+
+ @Override
public void applyRoundnessAndInvalidate() {
if (mRoundnessChangedListener != null) {
// We cannot apply the rounded corner to this View, so our parents (in drawChild()) will
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MediaContainerView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MediaContainerView.kt
index 04308b4..a8d8a8e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MediaContainerView.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MediaContainerView.kt
@@ -30,8 +30,8 @@
*/
class MediaContainerView(context: Context, attrs: AttributeSet?) : ExpandableView(context, attrs) {
+ override var clipHeight = 0
var cornerRadius = 0f
- var clipHeight = 0
var clipRect = RectF()
var clipPath = Path()
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java
index dad8064..626f851 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java
@@ -192,6 +192,11 @@
}
@Override
+ public int getClipHeight() {
+ return Math.max(mActualHeight - mClipBottomAmount, 0);
+ }
+
+ @Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childCount =
Math.min(mAttachedChildren.size(), NUMBER_OF_CHILDREN_WHEN_CHILDREN_EXPANDED);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.java
index 0fde3ab..e18c9d8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.java
@@ -33,14 +33,11 @@
import com.android.systemui.statusbar.NotificationShelfController;
import com.android.systemui.statusbar.StatusBarIconView;
import com.android.systemui.statusbar.StatusBarState;
-import com.android.systemui.statusbar.notification.AnimatableProperty;
import com.android.systemui.statusbar.notification.NotificationUtils;
import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator;
-import com.android.systemui.statusbar.notification.PropertyAnimator;
import com.android.systemui.statusbar.notification.collection.ListEntry;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.collection.provider.SectionStyleProvider;
-import com.android.systemui.statusbar.notification.stack.AnimationProperties;
import com.android.systemui.statusbar.window.StatusBarWindowController;
import com.android.wm.shell.bubbles.Bubbles;
@@ -91,8 +88,6 @@
private final ArrayList<Rect> mTintAreas = new ArrayList<>();
private final Context mContext;
- private final DemoModeController mDemoModeController;
-
private final FeatureFlags mFeatureFlags;
private int mAodIconAppearTranslation;
@@ -139,8 +134,7 @@
wakeUpCoordinator.addListener(this);
mBypassController = keyguardBypassController;
mBubblesOptional = bubblesOptional;
- mDemoModeController = demoModeController;
- mDemoModeController.addCallback(this);
+ demoModeController.addCallback(this);
mStatusBarWindowController = statusBarWindowController;
mScreenOffAnimationController = screenOffAnimationController;
notificationListener.addNotificationSettingsListener(mSettingsListener);
@@ -163,7 +157,6 @@
LayoutInflater layoutInflater = LayoutInflater.from(context);
mNotificationIconArea = inflateIconArea(layoutInflater);
mNotificationIcons = mNotificationIconArea.findViewById(R.id.notificationIcons);
-
}
/**
@@ -185,16 +178,6 @@
updateIconLayoutParams(mContext);
}
- /**
- * Update position of the view, with optional animation
- */
- public void updatePosition(int x, AnimationProperties props, boolean animate) {
- if (mAodIcons != null) {
- PropertyAnimator.setProperty(mAodIcons, AnimatableProperty.TRANSLATION_X, x, props,
- animate);
- }
- }
-
public void setupShelf(NotificationShelfController notificationShelfController) {
NotificationShelfController.assertRefactorFlagDisabled(mFeatureFlags);
mShelfIcons = notificationShelfController.getShelfIcons();
@@ -303,6 +286,7 @@
}
return true;
}
+
/**
* Updates the notifications with the given list of notifications to display.
*/
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java
index 418f920..3770c1d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java
@@ -54,19 +54,13 @@
* correctly on the screen.
*/
public class NotificationIconContainer extends ViewGroup {
- /**
- * A float value indicating how much before the overflow start the icons should transform into
- * a dot. A value of 0 means that they are exactly at the end and a value of 1 means it starts
- * 1 icon width early.
- */
- public static final float OVERFLOW_EARLY_AMOUNT = 0.2f;
private static final int NO_VALUE = Integer.MIN_VALUE;
private static final String TAG = "NotificationIconContainer";
private static final boolean DEBUG = false;
private static final boolean DEBUG_OVERFLOW = false;
private static final int CANNED_ANIMATION_DURATION = 100;
private static final AnimationProperties DOT_ANIMATION_PROPERTIES = new AnimationProperties() {
- private AnimationFilter mAnimationFilter = new AnimationFilter().animateX();
+ private final AnimationFilter mAnimationFilter = new AnimationFilter().animateX();
@Override
public AnimationFilter getAnimationFilter() {
@@ -75,7 +69,7 @@
}.setDuration(200);
private static final AnimationProperties ICON_ANIMATION_PROPERTIES = new AnimationProperties() {
- private AnimationFilter mAnimationFilter = new AnimationFilter()
+ private final AnimationFilter mAnimationFilter = new AnimationFilter()
.animateX()
.animateY()
.animateAlpha()
@@ -92,7 +86,7 @@
* Temporary AnimationProperties to avoid unnecessary allocations.
*/
private static final AnimationProperties sTempProperties = new AnimationProperties() {
- private AnimationFilter mAnimationFilter = new AnimationFilter();
+ private final AnimationFilter mAnimationFilter = new AnimationFilter();
@Override
public AnimationFilter getAnimationFilter() {
@@ -101,7 +95,7 @@
};
private static final AnimationProperties ADD_ICON_PROPERTIES = new AnimationProperties() {
- private AnimationFilter mAnimationFilter = new AnimationFilter().animateAlpha();
+ private final AnimationFilter mAnimationFilter = new AnimationFilter().animateAlpha();
@Override
public AnimationFilter getAnimationFilter() {
@@ -115,7 +109,7 @@
*/
private static final AnimationProperties UNISOLATION_PROPERTY_OTHERS
= new AnimationProperties() {
- private AnimationFilter mAnimationFilter = new AnimationFilter().animateAlpha();
+ private final AnimationFilter mAnimationFilter = new AnimationFilter().animateAlpha();
@Override
public AnimationFilter getAnimationFilter() {
@@ -128,7 +122,7 @@
* This animates the translation back to the right position.
*/
private static final AnimationProperties UNISOLATION_PROPERTY = new AnimationProperties() {
- private AnimationFilter mAnimationFilter = new AnimationFilter().animateX();
+ private final AnimationFilter mAnimationFilter = new AnimationFilter().animateX();
@Override
public AnimationFilter getAnimationFilter() {
@@ -147,7 +141,6 @@
private boolean mIsStaticLayout = true;
private final HashMap<View, IconState> mIconStates = new HashMap<>();
private int mDotPadding;
- private int mStaticDotRadius;
private int mStaticDotDiameter;
private int mActualLayoutWidth = NO_VALUE;
private float mActualPaddingEnd = NO_VALUE;
@@ -170,7 +163,7 @@
private boolean mIsShowingOverflowDot;
private StatusBarIconView mIsolatedIcon;
private Rect mIsolatedIconLocation;
- private int[] mAbsolutePosition = new int[2];
+ private final int[] mAbsolutePosition = new int[2];
private View mIsolatedIconForAnimation;
private int mThemedTextColorPrimary;
@@ -186,8 +179,8 @@
mMaxStaticIcons = getResources().getInteger(R.integer.max_notif_static_icons);
mDotPadding = getResources().getDimensionPixelSize(R.dimen.overflow_icon_dot_padding);
- mStaticDotRadius = getResources().getDimensionPixelSize(R.dimen.overflow_dot_radius);
- mStaticDotDiameter = 2 * mStaticDotRadius;
+ int staticDotRadius = getResources().getDimensionPixelSize(R.dimen.overflow_dot_radius);
+ mStaticDotDiameter = 2 * staticDotRadius;
final Context themedContext = new ContextThemeWrapper(getContext(),
com.android.internal.R.style.Theme_DeviceDefault_DayNight);
@@ -699,7 +692,6 @@
}
public class IconState extends ViewState {
- public static final int NO_VALUE = NotificationIconContainer.NO_VALUE;
public float iconAppearAmount = 1.0f;
public float clampedAppearAmount = 1.0f;
public int visibleState;
@@ -817,8 +809,6 @@
super.applyToView(view);
}
sTempProperties.setAnimationEndAction(null);
- boolean inShelf = iconAppearAmount == 1.0f;
- icon.setIsInShelf(inShelf);
}
justAdded = false;
justReplaced = false;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java
index f79a081..b5d3f08 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java
@@ -280,7 +280,7 @@
mShadeController.addPostCollapseAction(runnable);
mShadeController.collapseShade(true /* animate */);
} else if (mKeyguardStateController.isShowing()
- && mCentralSurfaces.isOccluded()) {
+ && mKeyguardStateController.isOccluded()) {
mStatusBarKeyguardViewManager.addAfterKeyguardGoneRunnable(runnable);
mShadeController.collapseShade();
} else {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/dagger/StatusBarFragmentModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/dagger/StatusBarFragmentModule.java
index eb4d963..cd6ccb5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/dagger/StatusBarFragmentModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/dagger/StatusBarFragmentModule.java
@@ -27,6 +27,7 @@
import com.android.systemui.statusbar.phone.PhoneStatusBarView;
import com.android.systemui.statusbar.phone.PhoneStatusBarViewController;
import com.android.systemui.statusbar.phone.StatusBarBoundsProvider;
+import com.android.systemui.statusbar.phone.StatusBarLocation;
import com.android.systemui.statusbar.phone.SystemBarAttributesListener;
import com.android.systemui.statusbar.phone.fragment.CollapsedStatusBarFragment;
import com.android.systemui.statusbar.phone.userswitcher.StatusBarUserSwitcherContainer;
@@ -73,6 +74,13 @@
/** */
@Provides
@StatusBarFragmentScope
+ static StatusBarLocation getStatusBarLocation() {
+ return StatusBarLocation.HOME;
+ }
+
+ /** */
+ @Provides
+ @StatusBarFragmentScope
@Named(START_SIDE_CONTENT)
static View startSideContent(@RootView PhoneStatusBarView view) {
return view.findViewById(R.id.status_bar_start_side_content);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryController.java
index 3d16591..7df083afc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryController.java
@@ -19,6 +19,9 @@
import android.annotation.Nullable;
import android.view.View;
+import androidx.annotation.NonNull;
+
+import com.android.systemui.Dumpable;
import com.android.systemui.demomode.DemoMode;
import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChangeCallback;
@@ -136,7 +139,7 @@
* A listener that will be notified whenever a change in battery level or power save mode has
* occurred.
*/
- interface BatteryStateChangeCallback {
+ interface BatteryStateChangeCallback extends Dumpable {
default void onBatteryLevelChanged(int level, boolean pluggedIn, boolean charging) {
}
@@ -158,6 +161,11 @@
default void onIsBatteryDefenderChanged(boolean isBatteryDefender) {
}
+
+ @Override
+ default void dump(@NonNull PrintWriter pw, @NonNull String[] args) {
+ pw.println(this);
+ }
}
/**
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java
index e69d86c..d5d8f4d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java
@@ -22,6 +22,7 @@
import static android.os.BatteryManager.EXTRA_PRESENT;
import static com.android.settingslib.fuelgauge.BatterySaverLogging.SAVER_ENABLED_QS;
+import static com.android.systemui.util.DumpUtilsKt.asIndenting;
import android.annotation.WorkerThread;
import android.content.BroadcastReceiver;
@@ -33,6 +34,7 @@
import android.os.Handler;
import android.os.PowerManager;
import android.os.PowerSaveState;
+import android.util.IndentingPrintWriter;
import android.util.Log;
import android.view.View;
@@ -157,15 +159,29 @@
}
@Override
- public void dump(PrintWriter pw, String[] args) {
- pw.println("BatteryController state:");
- pw.print(" mLevel="); pw.println(mLevel);
- pw.print(" mPluggedIn="); pw.println(mPluggedIn);
- pw.print(" mCharging="); pw.println(mCharging);
- pw.print(" mCharged="); pw.println(mCharged);
- pw.print(" mIsBatteryDefender="); pw.println(mIsBatteryDefender);
- pw.print(" mPowerSave="); pw.println(mPowerSave);
- pw.print(" mStateUnknown="); pw.println(mStateUnknown);
+ public void dump(@NonNull PrintWriter pw, @NonNull String[] args) {
+ IndentingPrintWriter ipw = asIndenting(pw);
+ ipw.println("BatteryController state:");
+ ipw.increaseIndent();
+ ipw.print("mHasReceivedBattery="); ipw.println(mHasReceivedBattery);
+ ipw.print("mLevel="); ipw.println(mLevel);
+ ipw.print("mPluggedIn="); ipw.println(mPluggedIn);
+ ipw.print("mCharging="); ipw.println(mCharging);
+ ipw.print("mCharged="); ipw.println(mCharged);
+ ipw.print("mIsBatteryDefender="); ipw.println(mIsBatteryDefender);
+ ipw.print("mPowerSave="); ipw.println(mPowerSave);
+ ipw.print("mStateUnknown="); ipw.println(mStateUnknown);
+ ipw.println("Callbacks:------------------");
+ // Since the above lines are already indented, we need to indent twice for the callbacks.
+ ipw.increaseIndent();
+ synchronized (mChangeCallbacks) {
+ final int n = mChangeCallbacks.size();
+ for (int i = 0; i < n; i++) {
+ mChangeCallbacks.get(i).dump(ipw, args);
+ }
+ }
+ ipw.decreaseIndent();
+ ipw.println("------------------");
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/stylus/StylusUsiPowerUI.kt b/packages/SystemUI/src/com/android/systemui/stylus/StylusUsiPowerUI.kt
index 3e1c13c..c1ac800 100644
--- a/packages/SystemUI/src/com/android/systemui/stylus/StylusUsiPowerUI.kt
+++ b/packages/SystemUI/src/com/android/systemui/stylus/StylusUsiPowerUI.kt
@@ -64,9 +64,9 @@
// These values must only be accessed on the handler.
private var batteryCapacity = 1.0f
private var suppressed = false
- private var inputDeviceId: Int? = null
private var instanceId: InstanceId? = null
-
+ @VisibleForTesting var inputDeviceId: Int? = null
+ private set
@VisibleForTesting var instanceIdSequence = InstanceIdSequence(1 shl 13)
fun init() {
@@ -110,10 +110,10 @@
fun updateBatteryState(deviceId: Int, batteryState: BatteryState) {
handler.post updateBattery@{
+ inputDeviceId = deviceId
if (batteryState.capacity == batteryCapacity || batteryState.capacity <= 0f)
return@updateBattery
- inputDeviceId = deviceId
batteryCapacity = batteryState.capacity
debugLog {
"Updating notification battery state to $batteryCapacity " +
diff --git a/packages/SystemUI/src/com/android/systemui/util/wrapper/LottieViewWrapper.kt b/packages/SystemUI/src/com/android/systemui/util/wrapper/LottieViewWrapper.kt
new file mode 100644
index 0000000..a804923
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/wrapper/LottieViewWrapper.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2023 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.util.wrapper
+
+import android.content.Context
+import android.util.AttributeSet
+import com.airbnb.lottie.LottieAnimationView
+import com.android.systemui.util.traceSection
+
+/** LottieAnimationView that traces each call to invalidate. */
+open class LottieViewWrapper : LottieAnimationView {
+ constructor(context: Context?) : super(context)
+ constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)
+ constructor(
+ context: Context?,
+ attrs: AttributeSet?,
+ defStyleAttr: Int
+ ) : super(context, attrs, defStyleAttr)
+
+ override fun invalidate() {
+ traceSection<Any?>("${this::class} invalidate") {
+ super.invalidate()
+ null
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
index 9362220..349f368 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
@@ -168,6 +168,13 @@
/** Volume dialog slider animation. */
private static final String TYPE_UPDATE = "update";
+ /**
+ * TODO(b/290612381): remove lingering animations or tolerate them
+ * When false, this will cause this class to not listen to animator events and not record jank
+ * events. This should never be false in production code, and only is false for unit tests for
+ * this class. This flag should be true in Scenario/Integration tests.
+ */
+ private final boolean mShouldListenForJank;
private final int mDialogShowAnimationDurationMs;
private final int mDialogHideAnimationDurationMs;
private int mDialogWidth;
@@ -304,6 +311,7 @@
VolumePanelFactory volumePanelFactory,
ActivityStarter activityStarter,
InteractionJankMonitor interactionJankMonitor,
+ boolean shouldListenForJank,
CsdWarningDialog.Factory csdWarningDialogFactory,
DevicePostureController devicePostureController,
Looper looper,
@@ -311,6 +319,8 @@
mContext =
new ContextThemeWrapper(context, R.style.volume_dialog_theme);
mHandler = new H(looper);
+
+ mShouldListenForJank = shouldListenForJank;
mController = volumeDialogController;
mKeyguard = (KeyguardManager) mContext.getSystemService(Context.KEYGUARD_SERVICE);
mActivityManager = (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE);
@@ -1368,7 +1378,10 @@
}
private Animator.AnimatorListener getJankListener(View v, String type, long timeout) {
- return new Animator.AnimatorListener() {
+ if (!mShouldListenForJank) {
+ // TODO(b/290612381): temporary fix to prevent null pointers on leftover JankMonitors
+ return null;
+ } else return new Animator.AnimatorListener() {
@Override
public void onAnimationStart(@NonNull Animator animation) {
if (!v.isAttachedToWindow()) {
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java b/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java
index aa4ee54..d0edc6e 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java
@@ -72,6 +72,7 @@
volumePanelFactory,
activityStarter,
interactionJankMonitor,
+ true, /* should listen for jank */
csdFactory,
devicePostureController,
Looper.getMainLooper(),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewControllerTest.java
index 1482f29..87811fb 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewControllerTest.java
@@ -37,6 +37,7 @@
import com.android.systemui.flags.FakeFeatureFlags;
import com.android.systemui.flags.Flags;
import com.android.systemui.settings.UserTracker;
+import com.android.systemui.statusbar.phone.StatusBarLocation;
import com.android.systemui.statusbar.policy.BatteryController;
import com.android.systemui.statusbar.policy.ConfigurationController;
import com.android.systemui.tuner.TunerService;
@@ -153,6 +154,7 @@
private void initController() {
mController = new BatteryMeterViewController(
mBatteryMeterView,
+ StatusBarLocation.HOME,
mUserTracker,
mConfigurationController,
mTunerService,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/ui/TaskSwitcherNotificationCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/ui/TaskSwitcherNotificationCoordinatorTest.kt
new file mode 100644
index 0000000..cfbbf76
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/ui/TaskSwitcherNotificationCoordinatorTest.kt
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2023 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.mediaprojection.taskswitcher.ui
+
+import android.app.Notification
+import android.app.NotificationManager
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.R
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.mediaprojection.taskswitcher.data.repository.ActivityTaskManagerTasksRepository
+import com.android.systemui.mediaprojection.taskswitcher.data.repository.FakeActivityTaskManager
+import com.android.systemui.mediaprojection.taskswitcher.data.repository.FakeMediaProjectionRepository
+import com.android.systemui.mediaprojection.taskswitcher.domain.interactor.TaskSwitchInteractor
+import com.android.systemui.mediaprojection.taskswitcher.ui.viewmodel.TaskSwitcherNotificationViewModel
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.argumentCaptor
+import com.android.systemui.util.mockito.mock
+import com.google.common.truth.Truth.assertThat
+import junit.framework.Assert.assertEquals
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.Mockito.verify
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@RunWith(AndroidTestingRunner::class)
+@SmallTest
+class TaskSwitcherNotificationCoordinatorTest : SysuiTestCase() {
+
+ private val notificationManager: NotificationManager = mock()
+
+ private val dispatcher = UnconfinedTestDispatcher()
+ private val testScope = TestScope(dispatcher)
+ private val fakeActivityTaskManager = FakeActivityTaskManager()
+ private val mediaRepo = FakeMediaProjectionRepository()
+ private val tasksRepo =
+ ActivityTaskManagerTasksRepository(
+ activityTaskManager = fakeActivityTaskManager.activityTaskManager,
+ applicationScope = testScope.backgroundScope,
+ backgroundDispatcher = dispatcher
+ )
+ private val interactor = TaskSwitchInteractor(mediaRepo, tasksRepo)
+ private val viewModel = TaskSwitcherNotificationViewModel(interactor)
+
+ private val coordinator =
+ TaskSwitcherNotificationCoordinator(
+ context,
+ notificationManager,
+ testScope.backgroundScope,
+ dispatcher,
+ viewModel
+ )
+
+ @Before
+ fun setup() {
+ coordinator.start()
+ }
+
+ @Test
+ fun showNotification() {
+ testScope.runTest {
+ switchTask()
+
+ val notification = ArgumentCaptor.forClass(Notification::class.java)
+ verify(notificationManager).notify(any(), any(), notification.capture())
+ assertNotification(notification)
+ }
+ }
+
+ @Test
+ fun hideNotification() {
+ testScope.runTest {
+ mediaRepo.stopProjecting()
+
+ verify(notificationManager).cancel(any())
+ }
+ }
+
+ @Test
+ fun notificationIdIsConsistent() {
+ testScope.runTest {
+ mediaRepo.stopProjecting()
+ val idCancel = argumentCaptor<Int>()
+ verify(notificationManager).cancel(idCancel.capture())
+
+ switchTask()
+ val idNotify = argumentCaptor<Int>()
+ verify(notificationManager).notify(any(), idNotify.capture(), any())
+
+ assertEquals(idCancel.value, idNotify.value)
+ }
+ }
+
+ private fun switchTask() {
+ val projectedTask = FakeActivityTaskManager.createTask(taskId = 1)
+ val foregroundTask = FakeActivityTaskManager.createTask(taskId = 2)
+ mediaRepo.switchProjectedTask(projectedTask)
+ fakeActivityTaskManager.moveTaskToForeground(foregroundTask)
+ }
+
+ private fun assertNotification(notification: ArgumentCaptor<Notification>) {
+ val text = notification.value.extras.getCharSequence(Notification.EXTRA_TEXT)
+ assertEquals(context.getString(R.string.media_projection_task_switcher_text), text)
+
+ val actions = notification.value.actions
+ assertThat(actions).hasLength(2)
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/scene/domain/startable/SystemUiDefaultSceneContainerStartableTest.kt b/packages/SystemUI/tests/src/com/android/systemui/scene/domain/startable/SystemUiDefaultSceneContainerStartableTest.kt
index df5e7bc..3e9ddcb 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/scene/domain/startable/SystemUiDefaultSceneContainerStartableTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/scene/domain/startable/SystemUiDefaultSceneContainerStartableTest.kt
@@ -20,6 +20,9 @@
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.flags.Flags
+import com.android.systemui.keyguard.shared.model.WakeSleepReason
+import com.android.systemui.keyguard.shared.model.WakefulnessModel
+import com.android.systemui.keyguard.shared.model.WakefulnessState
import com.android.systemui.scene.SceneTestUtils
import com.android.systemui.scene.shared.model.SceneContainerNames
import com.android.systemui.scene.shared.model.SceneKey
@@ -47,12 +50,18 @@
utils.authenticationInteractor(
repository = authenticationRepository,
)
+ private val keyguardRepository = utils.keyguardRepository()
+ private val keyguardInteractor =
+ utils.keyguardInteractor(
+ repository = keyguardRepository,
+ )
private val underTest =
SystemUiDefaultSceneContainerStartable(
applicationScope = testScope.backgroundScope,
sceneInteractor = sceneInteractor,
authenticationInteractor = authenticationInteractor,
+ keyguardInteractor = keyguardInteractor,
featureFlags = featureFlags,
)
@@ -280,6 +289,94 @@
assertThat(currentSceneKey).isEqualTo(SceneKey.Lockscreen)
}
+ @Test
+ fun switchToGoneWhenDeviceSleepsUnlocked_featureEnabled() =
+ testScope.runTest {
+ val currentSceneKey by
+ collectLastValue(
+ sceneInteractor.currentScene(SceneContainerNames.SYSTEM_UI_DEFAULT).map {
+ it.key
+ }
+ )
+ prepareState(
+ isFeatureEnabled = true,
+ isDeviceUnlocked = true,
+ initialSceneKey = SceneKey.Shade,
+ )
+ assertThat(currentSceneKey).isEqualTo(SceneKey.Shade)
+ underTest.start()
+
+ keyguardRepository.setWakefulnessModel(ASLEEP)
+
+ assertThat(currentSceneKey).isEqualTo(SceneKey.Gone)
+ }
+
+ @Test
+ fun switchToGoneWhenDeviceSleepsUnlocked_featureDisabled() =
+ testScope.runTest {
+ val currentSceneKey by
+ collectLastValue(
+ sceneInteractor.currentScene(SceneContainerNames.SYSTEM_UI_DEFAULT).map {
+ it.key
+ }
+ )
+ prepareState(
+ isFeatureEnabled = false,
+ isDeviceUnlocked = true,
+ initialSceneKey = SceneKey.Shade,
+ )
+ assertThat(currentSceneKey).isEqualTo(SceneKey.Shade)
+ underTest.start()
+
+ keyguardRepository.setWakefulnessModel(ASLEEP)
+
+ assertThat(currentSceneKey).isEqualTo(SceneKey.Shade)
+ }
+
+ @Test
+ fun switchToLockscreenWhenDeviceSleepsLocked_featureEnabled() =
+ testScope.runTest {
+ val currentSceneKey by
+ collectLastValue(
+ sceneInteractor.currentScene(SceneContainerNames.SYSTEM_UI_DEFAULT).map {
+ it.key
+ }
+ )
+ prepareState(
+ isFeatureEnabled = true,
+ isDeviceUnlocked = false,
+ initialSceneKey = SceneKey.Shade,
+ )
+ assertThat(currentSceneKey).isEqualTo(SceneKey.Shade)
+ underTest.start()
+
+ keyguardRepository.setWakefulnessModel(ASLEEP)
+
+ assertThat(currentSceneKey).isEqualTo(SceneKey.Lockscreen)
+ }
+
+ @Test
+ fun switchToLockscreenWhenDeviceSleepsLocked_featureDisabled() =
+ testScope.runTest {
+ val currentSceneKey by
+ collectLastValue(
+ sceneInteractor.currentScene(SceneContainerNames.SYSTEM_UI_DEFAULT).map {
+ it.key
+ }
+ )
+ prepareState(
+ isFeatureEnabled = false,
+ isDeviceUnlocked = false,
+ initialSceneKey = SceneKey.Shade,
+ )
+ assertThat(currentSceneKey).isEqualTo(SceneKey.Shade)
+ underTest.start()
+
+ keyguardRepository.setWakefulnessModel(ASLEEP)
+
+ assertThat(currentSceneKey).isEqualTo(SceneKey.Shade)
+ }
+
private fun prepareState(
isFeatureEnabled: Boolean = true,
isDeviceUnlocked: Boolean = false,
@@ -293,4 +390,13 @@
sceneInteractor.setCurrentScene(SceneContainerNames.SYSTEM_UI_DEFAULT, SceneModel(it))
}
}
+
+ companion object {
+ private val ASLEEP =
+ WakefulnessModel(
+ state = WakefulnessState.ASLEEP,
+ lastWakeReason = WakeSleepReason.POWER_BUTTON,
+ lastSleepReason = WakeSleepReason.POWER_BUTTON
+ )
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/ShadeHeaderControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/ShadeHeaderControllerTest.kt
index f542ab0..bf25f29 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/ShadeHeaderControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/ShadeHeaderControllerTest.kt
@@ -29,6 +29,7 @@
import android.view.View
import android.view.ViewPropertyAnimator
import android.view.WindowInsets
+import android.widget.LinearLayout
import android.widget.TextView
import androidx.constraintlayout.motion.widget.MotionLayout
import androidx.constraintlayout.widget.ConstraintSet
@@ -127,6 +128,7 @@
var viewVisibility = View.GONE
var viewAlpha = 1f
+ private val systemIcons = LinearLayout(context)
private lateinit var shadeHeaderController: ShadeHeaderController
private lateinit var carrierIconSlots: List<String>
private val configurationController = FakeConfigurationController()
@@ -146,6 +148,7 @@
.thenReturn(batteryMeterView)
whenever<StatusIconContainer>(view.findViewById(R.id.statusIcons)).thenReturn(statusIcons)
+ whenever<View>(view.findViewById(R.id.shade_header_system_icons)).thenReturn(systemIcons)
viewContext = Mockito.spy(context)
whenever(view.context).thenReturn(viewContext)
@@ -451,6 +454,17 @@
}
@Test
+ fun testLargeScreenActive_collapseActionRun_onSystemIconsClick() {
+ shadeHeaderController.largeScreenActive = true
+ var wasRun = false
+ shadeHeaderController.shadeCollapseAction = Runnable { wasRun = true }
+
+ systemIcons.performClick()
+
+ assertThat(wasRun).isTrue()
+ }
+
+ @Test
fun testShadeExpandedFraction() {
// View needs to be visible for this to actually take effect
shadeHeaderController.qsVisible = true
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/RoundableTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/RoundableTest.kt
index 89faa239..a56fb2c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/RoundableTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/RoundableTest.kt
@@ -3,7 +3,11 @@
import android.view.View
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
+import com.android.systemui.flags.FakeFeatureFlags
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
@@ -15,8 +19,9 @@
@SmallTest
@RunWith(JUnit4::class)
class RoundableTest : SysuiTestCase() {
- val targetView: View = mock()
- val roundable = FakeRoundable(targetView)
+ private val targetView: View = mock()
+ private val featureFlags = FakeFeatureFlags()
+ private val roundable = FakeRoundable(targetView = targetView, featureFlags = featureFlags)
@Test
fun defaultConfig_shouldNotHaveRoundedCorner() {
@@ -144,16 +149,62 @@
assertEquals(0.2f, roundable.roundableState.bottomRoundness)
}
+ @Test
+ fun getCornerRadii_radius_maxed_to_height() {
+ whenever(targetView.height).thenReturn(10)
+ featureFlags.set(Flags.IMPROVED_HUN_ANIMATIONS, true)
+ roundable.requestRoundness(1f, 1f, SOURCE1)
+
+ assertCornerRadiiEquals(5f, 5f)
+ }
+
+ @Test
+ fun getCornerRadii_topRadius_maxed_to_height() {
+ whenever(targetView.height).thenReturn(5)
+ featureFlags.set(Flags.IMPROVED_HUN_ANIMATIONS, true)
+ roundable.requestRoundness(1f, 0f, SOURCE1)
+
+ assertCornerRadiiEquals(5f, 0f)
+ }
+
+ @Test
+ fun getCornerRadii_bottomRadius_maxed_to_height() {
+ whenever(targetView.height).thenReturn(5)
+ featureFlags.set(Flags.IMPROVED_HUN_ANIMATIONS, true)
+ roundable.requestRoundness(0f, 1f, SOURCE1)
+
+ assertCornerRadiiEquals(0f, 5f)
+ }
+
+ @Test
+ fun getCornerRadii_radii_kept() {
+ whenever(targetView.height).thenReturn(100)
+ featureFlags.set(Flags.IMPROVED_HUN_ANIMATIONS, true)
+ roundable.requestRoundness(1f, 1f, SOURCE1)
+
+ assertCornerRadiiEquals(MAX_RADIUS, MAX_RADIUS)
+ }
+
+ private fun assertCornerRadiiEquals(top: Float, bottom: Float) {
+ assertEquals("topCornerRadius", top, roundable.topCornerRadius)
+ assertEquals("bottomCornerRadius", bottom, roundable.bottomCornerRadius)
+ }
+
class FakeRoundable(
targetView: View,
radius: Float = MAX_RADIUS,
+ featureFlags: FeatureFlags
) : Roundable {
override val roundableState =
RoundableState(
targetView = targetView,
roundable = this,
maxRadius = radius,
+ featureFlags = featureFlags
)
+
+ override val clipHeight: Int
+ get() = roundableState.targetView.height
}
companion object {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java
index d44af88..8f91950 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java
@@ -274,7 +274,7 @@
notification.flags |= Notification.FLAG_AUTO_CANCEL;
when(mKeyguardStateController.isShowing()).thenReturn(true);
- when(mCentralSurfaces.isOccluded()).thenReturn(true);
+ when(mKeyguardStateController.isOccluded()).thenReturn(true);
// When
mNotificationActivityStarter.onNotificationClicked(entry, mNotificationRow);
@@ -340,7 +340,7 @@
// Given
sbn.getNotification().contentIntent = null;
when(mKeyguardStateController.isShowing()).thenReturn(true);
- when(mCentralSurfaces.isOccluded()).thenReturn(true);
+ when(mKeyguardStateController.isOccluded()).thenReturn(true);
// When
mNotificationActivityStarter.onNotificationClicked(entry, mBubbleNotificationRow);
@@ -368,7 +368,7 @@
// Given
sbn.getNotification().contentIntent = mContentIntent;
when(mKeyguardStateController.isShowing()).thenReturn(true);
- when(mCentralSurfaces.isOccluded()).thenReturn(true);
+ when(mKeyguardStateController.isOccluded()).thenReturn(true);
// When
mNotificationActivityStarter.onNotificationClicked(entry, mBubbleNotificationRow);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusUsiPowerUiTest.kt b/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusUsiPowerUiTest.kt
index 90821bd..d212c02 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusUsiPowerUiTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusUsiPowerUiTest.kt
@@ -126,6 +126,16 @@
}
@Test
+ fun updateBatteryState_capacitySame_inputDeviceChanges_updatesInputDeviceId() {
+ stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.1f))
+ stylusUsiPowerUi.updateBatteryState(1, FixedCapacityBatteryState(0.1f))
+
+ assertThat(stylusUsiPowerUi.inputDeviceId).isEqualTo(1)
+ verify(notificationManager, times(1))
+ .notify(eq(R.string.stylus_battery_low_percentage), any())
+ }
+
+ @Test
fun updateBatteryState_existingNotification_capacityAboveThreshold_cancelsNotification() {
stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.1f))
stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.8f))
diff --git a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
index 8f725be..0c77529 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
@@ -142,6 +142,7 @@
mVolumePanelFactory,
mActivityStarter,
mInteractionJankMonitor,
+ false,
mCsdWarningDialogFactory,
mPostureController,
mTestableLooper.getLooper(),
@@ -378,6 +379,7 @@
mVolumePanelFactory,
mActivityStarter,
mInteractionJankMonitor,
+ false,
mCsdWarningDialogFactory,
devicePostureController,
mTestableLooper.getLooper(),
@@ -397,6 +399,8 @@
int gravity = dialog.getWindowGravity();
assertEquals(Gravity.TOP, gravity & Gravity.VERTICAL_GRAVITY_MASK);
+
+ cleanUp(dialog);
}
@Test
@@ -415,6 +419,7 @@
mVolumePanelFactory,
mActivityStarter,
mInteractionJankMonitor,
+ false,
mCsdWarningDialogFactory,
devicePostureController,
mTestableLooper.getLooper(),
@@ -433,6 +438,8 @@
int gravity = dialog.getWindowGravity();
assertEquals(Gravity.CENTER_VERTICAL, gravity & Gravity.VERTICAL_GRAVITY_MASK);
+
+ cleanUp(dialog);
}
@Test
@@ -451,6 +458,7 @@
mVolumePanelFactory,
mActivityStarter,
mInteractionJankMonitor,
+ false,
mCsdWarningDialogFactory,
devicePostureController,
mTestableLooper.getLooper(),
@@ -469,6 +477,8 @@
int gravity = dialog.getWindowGravity();
assertEquals(Gravity.CENTER_VERTICAL, gravity & Gravity.VERTICAL_GRAVITY_MASK);
+
+ cleanUp(dialog);
}
@Test
@@ -489,18 +499,19 @@
mVolumePanelFactory,
mActivityStarter,
mInteractionJankMonitor,
+ false,
mCsdWarningDialogFactory,
mPostureController,
mTestableLooper.getLooper(),
- mDumpManager
- );
+ mDumpManager);
dialog.init(0, null);
verify(mPostureController, never()).removeCallback(any());
-
dialog.destroy();
verify(mPostureController).removeCallback(any());
+
+ cleanUp(dialog);
}
private void setOrientation(int orientation) {
@@ -513,14 +524,18 @@
@After
public void teardown() {
- if (mDialog != null) {
- mDialog.clearInternalHandlerAfterTest();
- }
+ cleanUp(mDialog);
setOrientation(mOriginalOrientation);
mTestableLooper.processAllMessages();
reset(mPostureController);
}
+ private void cleanUp(VolumeDialogImpl dialog) {
+ if (dialog != null) {
+ dialog.clearInternalHandlerAfterTest();
+ }
+ }
+
/*
@Test
public void testContentDescriptions() {
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt
index 60a4951..6309740 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt
@@ -48,7 +48,7 @@
override val isKeyguardShowing: Flow<Boolean> = _isKeyguardShowing
private val _isKeyguardUnlocked = MutableStateFlow(false)
- override val isKeyguardUnlocked: Flow<Boolean> = _isKeyguardUnlocked
+ override val isKeyguardUnlocked: StateFlow<Boolean> = _isKeyguardUnlocked.asStateFlow()
private val _isKeyguardOccluded = MutableStateFlow(false)
override val isKeyguardOccluded: Flow<Boolean> = _isKeyguardOccluded
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt
index d797962..47e1daf4 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt
@@ -22,11 +22,20 @@
import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository
import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
import com.android.systemui.bouncer.data.repository.BouncerRepository
+import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository
import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel
+import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository
import com.android.systemui.flags.FakeFeatureFlags
import com.android.systemui.flags.Flags
+import com.android.systemui.keyguard.data.repository.FakeCommandQueue
+import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
+import com.android.systemui.keyguard.data.repository.KeyguardRepository
+import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.keyguard.domain.interactor.LockscreenSceneInteractor
+import com.android.systemui.keyguard.shared.model.WakeSleepReason
+import com.android.systemui.keyguard.shared.model.WakefulnessModel
+import com.android.systemui.keyguard.shared.model.WakefulnessState
import com.android.systemui.scene.data.repository.SceneContainerRepository
import com.android.systemui.scene.domain.interactor.SceneInteractor
import com.android.systemui.scene.shared.model.SceneContainerConfig
@@ -53,7 +62,11 @@
) {
val testDispatcher = StandardTestDispatcher()
val testScope = TestScope(testDispatcher)
- val featureFlags = FakeFeatureFlags().apply { set(Flags.SCENE_CONTAINER, true) }
+ val featureFlags =
+ FakeFeatureFlags().apply {
+ set(Flags.SCENE_CONTAINER, true)
+ set(Flags.FACE_AUTH_REFACTOR, false)
+ }
private val userRepository: UserRepository by lazy {
FakeUserRepository().apply {
val users = listOf(UserInfo(/* id= */ 0, "name", /* flags= */ 0))
@@ -67,6 +80,17 @@
currentTime = { testScope.currentTime },
)
}
+ val keyguardRepository: FakeKeyguardRepository by lazy {
+ FakeKeyguardRepository().apply {
+ setWakefulnessModel(
+ WakefulnessModel(
+ WakefulnessState.AWAKE,
+ WakeSleepReason.OTHER,
+ WakeSleepReason.OTHER,
+ )
+ )
+ }
+ }
private val context = test.context
fun fakeSceneContainerRepository(
@@ -122,6 +146,20 @@
)
}
+ fun keyguardRepository(): FakeKeyguardRepository {
+ return keyguardRepository
+ }
+
+ fun keyguardInteractor(repository: KeyguardRepository): KeyguardInteractor {
+ return KeyguardInteractor(
+ repository = repository,
+ commandQueue = FakeCommandQueue(),
+ featureFlags = featureFlags,
+ bouncerRepository = FakeKeyguardBouncerRepository(),
+ configurationRepository = FakeConfigurationRepository()
+ )
+ }
+
fun bouncerInteractor(
authenticationInteractor: AuthenticationInteractor,
sceneInteractor: SceneInteractor,
diff --git a/services/Android.bp b/services/Android.bp
index 7b64b47..53dc068 100644
--- a/services/Android.bp
+++ b/services/Android.bp
@@ -164,6 +164,7 @@
"services.coverage",
"services.credentials",
"services.devicepolicy",
+ "services.flags",
"services.midi",
"services.musicsearch",
"services.net",
diff --git a/services/companion/java/com/android/server/companion/AssociationRequestsProcessor.java b/services/companion/java/com/android/server/companion/AssociationRequestsProcessor.java
index 77275e0..3e7d759 100644
--- a/services/companion/java/com/android/server/companion/AssociationRequestsProcessor.java
+++ b/services/companion/java/com/android/server/companion/AssociationRequestsProcessor.java
@@ -19,7 +19,6 @@
import static android.app.PendingIntent.FLAG_CANCEL_CURRENT;
import static android.app.PendingIntent.FLAG_IMMUTABLE;
import static android.app.PendingIntent.FLAG_ONE_SHOT;
-import static android.companion.CompanionDeviceManager.COMPANION_DEVICE_DISCOVERY_PACKAGE_NAME;
import static android.companion.CompanionDeviceManager.REASON_INTERNAL_ERROR;
import static android.companion.CompanionDeviceManager.RESULT_INTERNAL_ERROR;
import static android.content.ComponentName.createRelative;
@@ -60,6 +59,7 @@
import android.util.PackageUtils;
import android.util.Slog;
+import com.android.internal.R;
import com.android.internal.util.ArrayUtils;
import java.util.Arrays;
@@ -114,9 +114,6 @@
class AssociationRequestsProcessor {
private static final String TAG = "CDM_AssociationRequestsProcessor";
- private static final ComponentName ASSOCIATION_REQUEST_APPROVAL_ACTIVITY =
- createRelative(COMPANION_DEVICE_DISCOVERY_PACKAGE_NAME, ".CompanionDeviceActivity");
-
// AssociationRequestsProcessor <-> UI
private static final String EXTRA_APPLICATION_CALLBACK = "application_callback";
private static final String EXTRA_ASSOCIATION_REQUEST = "association_request";
@@ -138,6 +135,8 @@
private final @NonNull CompanionDeviceManagerService mService;
private final @NonNull PackageManagerInternal mPackageManager;
private final @NonNull AssociationStoreImpl mAssociationStore;
+ @NonNull
+ private final ComponentName mCompanionDeviceActivity;
AssociationRequestsProcessor(@NonNull CompanionDeviceManagerService service,
@NonNull AssociationStoreImpl associationStore) {
@@ -145,6 +144,9 @@
mService = service;
mPackageManager = service.mPackageManagerInternal;
mAssociationStore = associationStore;
+ mCompanionDeviceActivity = createRelative(
+ mContext.getString(R.string.config_companionDeviceManagerPackage),
+ ".CompanionDeviceActivity");
}
/**
@@ -197,7 +199,7 @@
extras.putParcelable(EXTRA_RESULT_RECEIVER, prepareForIpc(mOnRequestConfirmationReceiver));
final Intent intent = new Intent();
- intent.setComponent(ASSOCIATION_REQUEST_APPROVAL_ACTIVITY);
+ intent.setComponent(mCompanionDeviceActivity);
intent.putExtras(extras);
// 2b.3. Create a PendingIntent.
@@ -224,7 +226,7 @@
extras.putBoolean(EXTRA_FORCE_CANCEL_CONFIRMATION, true);
final Intent intent = new Intent();
- intent.setComponent(ASSOCIATION_REQUEST_APPROVAL_ACTIVITY);
+ intent.setComponent(mCompanionDeviceActivity);
intent.putExtras(extras);
return createPendingIntent(packageUid, intent);
diff --git a/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferProcessor.java b/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferProcessor.java
index dd7d38f..82387a1 100644
--- a/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferProcessor.java
+++ b/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferProcessor.java
@@ -19,7 +19,6 @@
import static android.app.PendingIntent.FLAG_CANCEL_CURRENT;
import static android.app.PendingIntent.FLAG_IMMUTABLE;
import static android.app.PendingIntent.FLAG_ONE_SHOT;
-import static android.companion.CompanionDeviceManager.COMPANION_DEVICE_DISCOVERY_PACKAGE_NAME;
import static android.content.ComponentName.createRelative;
import static com.android.server.companion.Utils.prepareForIpc;
@@ -48,6 +47,7 @@
import android.permission.PermissionControllerManager;
import android.util.Slog;
+import com.android.internal.R;
import com.android.server.companion.AssociationStore;
import com.android.server.companion.CompanionDeviceManagerService;
import com.android.server.companion.PermissionsUtils;
@@ -75,9 +75,6 @@
private static final String EXTRA_COMPANION_DEVICE_NAME = "companion_device_name";
private static final String EXTRA_SYSTEM_DATA_TRANSFER_RESULT_RECEIVER =
"system_data_transfer_result_receiver";
- private static final ComponentName SYSTEM_DATA_TRANSFER_REQUEST_APPROVAL_ACTIVITY =
- createRelative(COMPANION_DEVICE_DISCOVERY_PACKAGE_NAME,
- ".CompanionDeviceDataTransferActivity");
private final Context mContext;
private final AssociationStore mAssociationStore;
@@ -85,6 +82,7 @@
private final CompanionTransportManager mTransportManager;
private final PermissionControllerManager mPermissionControllerManager;
private final ExecutorService mExecutor;
+ private final ComponentName mCompanionDeviceDataTransferActivity;
public SystemDataTransferProcessor(CompanionDeviceManagerService service,
AssociationStore associationStore,
@@ -108,6 +106,9 @@
mTransportManager.addListener(MESSAGE_REQUEST_PERMISSION_RESTORE, messageListener);
mPermissionControllerManager = mContext.getSystemService(PermissionControllerManager.class);
mExecutor = Executors.newSingleThreadExecutor();
+ mCompanionDeviceDataTransferActivity = createRelative(
+ mContext.getString(R.string.config_companionDeviceManagerPackage),
+ ".CompanionDeviceDataTransferActivity");
}
/**
@@ -146,7 +147,7 @@
prepareForIpc(mOnSystemDataTransferRequestConfirmationReceiver));
final Intent intent = new Intent();
- intent.setComponent(SYSTEM_DATA_TRANSFER_REQUEST_APPROVAL_ACTIVITY);
+ intent.setComponent(mCompanionDeviceDataTransferActivity);
intent.putExtras(extras);
// Create a PendingIntent
diff --git a/services/core/java/com/android/server/SystemConfig.java b/services/core/java/com/android/server/SystemConfig.java
index bca2d60..caf1684 100644
--- a/services/core/java/com/android/server/SystemConfig.java
+++ b/services/core/java/com/android/server/SystemConfig.java
@@ -35,7 +35,6 @@
import android.os.incremental.IncrementalManager;
import android.os.storage.StorageManager;
import android.permission.PermissionManager.SplitPermissionInfo;
-import android.sysprop.ApexProperties;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.ArraySet;
@@ -1208,8 +1207,7 @@
boolean systemExt = permFile.toPath().startsWith(
Environment.getSystemExtDirectory().toPath() + "/");
boolean apex = permFile.toPath().startsWith(
- Environment.getApexDirectory().toPath() + "/")
- && ApexProperties.updatable().orElse(false);
+ Environment.getApexDirectory().toPath() + "/");
if (vendor) {
readPrivAppPermissions(parser,
mPermissionAllowlist.getVendorPrivilegedAppAllowlist());
diff --git a/services/core/java/com/android/server/accounts/AccountManagerService.java b/services/core/java/com/android/server/accounts/AccountManagerService.java
index f4c8a57..146215b 100644
--- a/services/core/java/com/android/server/accounts/AccountManagerService.java
+++ b/services/core/java/com/android/server/accounts/AccountManagerService.java
@@ -357,15 +357,25 @@
@Override
public void onPackageAdded(String packageName, int uid) {
// Called on a handler, and running as the system
- UserAccounts accounts = getUserAccounts(UserHandle.getUserId(uid));
- cancelAccountAccessRequestNotificationIfNeeded(uid, true, accounts);
+ try {
+ UserAccounts accounts = getUserAccounts(UserHandle.getUserId(uid));
+ cancelAccountAccessRequestNotificationIfNeeded(uid, true, accounts);
+ } catch (SQLiteCantOpenDatabaseException e) {
+ Log.w(TAG, "Can't read accounts database", e);
+ return;
+ }
}
@Override
public void onPackageUpdateFinished(String packageName, int uid) {
// Called on a handler, and running as the system
- UserAccounts accounts = getUserAccounts(UserHandle.getUserId(uid));
- cancelAccountAccessRequestNotificationIfNeeded(uid, true, accounts);
+ try {
+ UserAccounts accounts = getUserAccounts(UserHandle.getUserId(uid));
+ cancelAccountAccessRequestNotificationIfNeeded(uid, true, accounts);
+ } catch (SQLiteCantOpenDatabaseException e) {
+ Log.w(TAG, "Can't read accounts database", e);
+ return;
+ }
}
}.register(mContext, mHandler.getLooper(), UserHandle.ALL, true);
@@ -391,6 +401,9 @@
}
} catch (NameNotFoundException e) {
/* ignore */
+ } catch (SQLiteCantOpenDatabaseException e) {
+ Log.w(TAG, "Can't read accounts database", e);
+ return;
}
}
});
diff --git a/services/core/java/com/android/server/am/OomAdjuster.java b/services/core/java/com/android/server/am/OomAdjuster.java
index e51fc0a..a682c85 100644
--- a/services/core/java/com/android/server/am/OomAdjuster.java
+++ b/services/core/java/com/android/server/am/OomAdjuster.java
@@ -1713,6 +1713,11 @@
}
}
+ private boolean isScreenOnOrAnimatingLocked(ProcessStateRecord state) {
+ return mService.mWakefulness.get() == PowerManagerInternal.WAKEFULNESS_AWAKE
+ || state.isRunningRemoteAnimation();
+ }
+
@GuardedBy({"mService", "mProcLock"})
private boolean computeOomAdjLSP(ProcessRecord app, int cachedAdj,
ProcessRecord topApp, boolean doingAll, long now, boolean cycleReEval,
@@ -1794,8 +1799,7 @@
state.setSystemNoUi(false);
}
if (!state.isSystemNoUi()) {
- if (mService.mWakefulness.get() == PowerManagerInternal.WAKEFULNESS_AWAKE
- || state.isRunningRemoteAnimation()) {
+ if (isScreenOnOrAnimatingLocked(state)) {
// screen on or animating, promote UI
state.setCurProcState(ActivityManager.PROCESS_STATE_PERSISTENT_UI);
state.setCurrentSchedulingGroup(SCHED_GROUP_TOP_APP);
@@ -3281,8 +3285,10 @@
} else {
setThreadPriority(app.getPid(), THREAD_PRIORITY_TOP_APP_BOOST);
}
- initialSchedGroup = SCHED_GROUP_TOP_APP;
- initialProcState = PROCESS_STATE_TOP;
+ if (isScreenOnOrAnimatingLocked(state)) {
+ initialSchedGroup = SCHED_GROUP_TOP_APP;
+ initialProcState = PROCESS_STATE_TOP;
+ }
initialCapability = PROCESS_CAPABILITY_ALL;
initialCached = false;
} catch (Exception e) {
diff --git a/services/core/java/com/android/server/appop/AppOpsService.java b/services/core/java/com/android/server/appop/AppOpsService.java
index 1dff62e..b6e58ad 100644
--- a/services/core/java/com/android/server/appop/AppOpsService.java
+++ b/services/core/java/com/android/server/appop/AppOpsService.java
@@ -2124,11 +2124,7 @@
try {
pvr = verifyAndGetBypass(uid, packageName, null);
} catch (SecurityException e) {
- if (Process.isIsolated(uid)) {
- Slog.e(TAG, "Cannot setMode: isolated process");
- } else {
- Slog.e(TAG, "Cannot setMode", e);
- }
+ logVerifyAndGetBypassFailure(uid, e, "setMode");
return;
}
@@ -2580,11 +2576,7 @@
try {
pvr = verifyAndGetBypass(uid, packageName, null);
} catch (SecurityException e) {
- if (Process.isIsolated(uid)) {
- Slog.e(TAG, "Cannot checkOperation: isolated process");
- } else {
- Slog.e(TAG, "Cannot checkOperation", e);
- }
+ logVerifyAndGetBypassFailure(uid, e, "checkOperation");
return AppOpsManager.opToDefaultMode(code);
}
@@ -2796,11 +2788,7 @@
attributionTag = null;
}
} catch (SecurityException e) {
- if (Process.isIsolated(uid)) {
- Slog.e(TAG, "Cannot noteOperation: isolated process");
- } else {
- Slog.e(TAG, "Cannot noteOperation", e);
- }
+ logVerifyAndGetBypassFailure(uid, e, "noteOperation");
return new SyncNotedAppOp(AppOpsManager.MODE_ERRORED, code, attributionTag,
packageName);
}
@@ -3333,11 +3321,7 @@
attributionTag = null;
}
} catch (SecurityException e) {
- if (Process.isIsolated(uid)) {
- Slog.e(TAG, "Cannot startOperation: isolated process");
- } else {
- Slog.e(TAG, "Cannot startOperation", e);
- }
+ logVerifyAndGetBypassFailure(uid, e, "startOperation");
return new SyncNotedAppOp(AppOpsManager.MODE_ERRORED, code, attributionTag,
packageName);
}
@@ -3513,11 +3497,7 @@
attributionTag = null;
}
} catch (SecurityException e) {
- if (Process.isIsolated(uid)) {
- Slog.e(TAG, "Cannot finishOperation: isolated process");
- } else {
- Slog.e(TAG, "Cannot finishOperation", e);
- }
+ logVerifyAndGetBypassFailure(uid, e, "finishOperation");
return;
}
@@ -4073,6 +4053,17 @@
return false;
}
+ private void logVerifyAndGetBypassFailure(int uid, @NonNull SecurityException e,
+ @NonNull String methodName) {
+ if (Process.isIsolated(uid)) {
+ Slog.e(TAG, "Cannot " + methodName + ": isolated UID");
+ } else if (UserHandle.getAppId(uid) < Process.FIRST_APPLICATION_UID) {
+ Slog.e(TAG, "Cannot " + methodName + ": non-application UID " + uid);
+ } else {
+ Slog.e(TAG, "Cannot " + methodName, e);
+ }
+ }
+
/**
* Get (and potentially create) ops.
*
diff --git a/services/core/java/com/android/server/biometrics/Utils.java b/services/core/java/com/android/server/biometrics/Utils.java
index 06417d7..f51b62d 100644
--- a/services/core/java/com/android/server/biometrics/Utils.java
+++ b/services/core/java/com/android/server/biometrics/Utils.java
@@ -66,7 +66,6 @@
import com.android.internal.widget.LockPatternUtils;
import com.android.server.biometrics.sensors.BaseClientMonitor;
-import java.util.ArrayList;
import java.util.List;
public class Utils {
@@ -98,33 +97,6 @@
}
/**
- * Get the enabled HAL instances. If virtual is enabled and available it will be returned as
- * the only instance, otherwise all other instances will be returned.
- *
- * @param context system context
- * @param declaredInstances known instances
- * @return filtered list of enabled instances
- */
- @NonNull
- public static List<String> filterAvailableHalInstances(@NonNull Context context,
- @NonNull List<String> declaredInstances) {
- if (declaredInstances.size() <= 1) {
- return declaredInstances;
- }
-
- final int virtualAt = declaredInstances.indexOf("virtual");
- if (isVirtualEnabled(context) && virtualAt != -1) {
- return List.of(declaredInstances.get(virtualAt));
- }
-
- declaredInstances = new ArrayList<>(declaredInstances);
- if (virtualAt != -1) {
- declaredInstances.remove(virtualAt);
- }
- return declaredInstances;
- }
-
- /**
* Combines {@link PromptInfo#setDeviceCredentialAllowed(boolean)} with
* {@link PromptInfo#setAuthenticators(int)}, as the former is not flexible enough.
*/
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintService.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintService.java
index 28cb7d9..7cc6940 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintService.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintService.java
@@ -871,19 +871,22 @@
super.registerAuthenticators_enforcePermission();
mRegistry.registerAll(() -> {
- final List<ServiceProvider> providers = new ArrayList<>();
- providers.addAll(getHidlProviders(hidlSensors));
List<String> aidlSensors = new ArrayList<>();
final String[] instances = mAidlInstanceNameSupplier.get();
if (instances != null) {
aidlSensors.addAll(Lists.newArrayList(instances));
}
- providers.addAll(getAidlProviders(
- Utils.filterAvailableHalInstances(getContext(), aidlSensors)));
+
+ final Pair<List<FingerprintSensorPropertiesInternal>, List<String>>
+ filteredInstances = filterAvailableHalInstances(hidlSensors, aidlSensors);
+
+ final List<ServiceProvider> providers = new ArrayList<>();
+ providers.addAll(getHidlProviders(filteredInstances.first));
+ providers.addAll(getAidlProviders(filteredInstances.second));
+
return providers;
});
}
-
@android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL)
@Override
public void addAuthenticatorsRegisteredCallback(
@@ -1038,6 +1041,33 @@
});
}
+ private Pair<List<FingerprintSensorPropertiesInternal>, List<String>>
+ filterAvailableHalInstances(
+ @NonNull List<FingerprintSensorPropertiesInternal> hidlInstances,
+ @NonNull List<String> aidlInstances) {
+ if ((hidlInstances.size() + aidlInstances.size()) <= 1) {
+ return new Pair(hidlInstances, aidlInstances);
+ }
+
+ final int virtualAt = aidlInstances.indexOf("virtual");
+ if (Utils.isVirtualEnabled(getContext())) {
+ if (virtualAt != -1) {
+ //only virtual instance should be returned
+ return new Pair(new ArrayList<>(), List.of(aidlInstances.get(virtualAt)));
+ } else {
+ Slog.e(TAG, "Could not find virtual interface while it is enabled");
+ return new Pair(hidlInstances, aidlInstances);
+ }
+ } else {
+ //remove virtual instance
+ aidlInstances = new ArrayList<>(aidlInstances);
+ if (virtualAt != -1) {
+ aidlInstances.remove(virtualAt);
+ }
+ return new Pair(hidlInstances, aidlInstances);
+ }
+ }
+
@NonNull
private List<ServiceProvider> getHidlProviders(
@NonNull List<FingerprintSensorPropertiesInternal> hidlSensors) {
diff --git a/services/core/java/com/android/server/inputmethod/DefaultImeVisibilityApplier.java b/services/core/java/com/android/server/inputmethod/DefaultImeVisibilityApplier.java
index a1b67e1..f1698dd 100644
--- a/services/core/java/com/android/server/inputmethod/DefaultImeVisibilityApplier.java
+++ b/services/core/java/com/android/server/inputmethod/DefaultImeVisibilityApplier.java
@@ -37,6 +37,7 @@
import android.util.EventLog;
import android.util.Slog;
import android.view.inputmethod.ImeTracker;
+import android.view.inputmethod.InputMethod;
import android.view.inputmethod.InputMethodManager;
import com.android.internal.annotations.GuardedBy;
@@ -75,7 +76,8 @@
@GuardedBy("ImfLock.class")
@Override
public void performShowIme(IBinder showInputToken, @Nullable ImeTracker.Token statsToken,
- int showFlags, ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) {
+ @InputMethod.ShowFlags int showFlags, ResultReceiver resultReceiver,
+ @SoftInputShowHideReason int reason) {
final IInputMethodInvoker curMethod = mService.getCurMethodLocked();
if (curMethod != null) {
if (DEBUG) {
diff --git a/services/core/java/com/android/server/inputmethod/IInputMethodInvoker.java b/services/core/java/com/android/server/inputmethod/IInputMethodInvoker.java
index c53f1a5..b12a816 100644
--- a/services/core/java/com/android/server/inputmethod/IInputMethodInvoker.java
+++ b/services/core/java/com/android/server/inputmethod/IInputMethodInvoker.java
@@ -30,6 +30,7 @@
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.ImeTracker;
import android.view.inputmethod.InputBinding;
+import android.view.inputmethod.InputMethod;
import android.view.inputmethod.InputMethodSubtype;
import android.window.ImeOnBackInvokedDispatcher;
@@ -198,8 +199,8 @@
// TODO(b/192412909): Convert this back to void method
@AnyThread
- boolean showSoftInput(IBinder showInputToken, @Nullable ImeTracker.Token statsToken, int flags,
- ResultReceiver resultReceiver) {
+ boolean showSoftInput(IBinder showInputToken, @Nullable ImeTracker.Token statsToken,
+ @InputMethod.ShowFlags int flags, ResultReceiver resultReceiver) {
try {
mTarget.showSoftInput(showInputToken, statsToken, flags, resultReceiver);
} catch (RemoteException e) {
diff --git a/services/core/java/com/android/server/inputmethod/ImeVisibilityApplier.java b/services/core/java/com/android/server/inputmethod/ImeVisibilityApplier.java
index 27f6a89..29fa369 100644
--- a/services/core/java/com/android/server/inputmethod/ImeVisibilityApplier.java
+++ b/services/core/java/com/android/server/inputmethod/ImeVisibilityApplier.java
@@ -21,6 +21,7 @@
import android.os.IBinder;
import android.os.ResultReceiver;
import android.view.inputmethod.ImeTracker;
+import android.view.inputmethod.InputMethod;
import com.android.internal.inputmethod.SoftInputShowHideReason;
@@ -34,13 +35,13 @@
*
* @param showInputToken A token that represents the requester to show IME.
* @param statsToken A token that tracks the progress of an IME request.
- * @param showFlags Provides additional operating flags to show IME.
* @param resultReceiver If non-null, this will be called back to the caller when
* it has processed request to tell what it has done.
* @param reason The reason for requesting to show IME.
*/
default void performShowIme(IBinder showInputToken, @Nullable ImeTracker.Token statsToken,
- int showFlags, ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) {}
+ @InputMethod.ShowFlags int showFlags, ResultReceiver resultReceiver,
+ @SoftInputShowHideReason int reason) {}
/**
* Performs hiding IME to the given window
diff --git a/services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java b/services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java
index f012d91..9ad4628 100644
--- a/services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java
+++ b/services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java
@@ -221,17 +221,21 @@
/**
* Called when {@link InputMethodManagerService} is processing the show IME request.
- * @param statsToken The token for tracking this show request
- * @param showFlags The additional operation flags to indicate whether this show request mode is
- * implicit or explicit.
- * @return {@code true} when the computer has proceed this show request operation.
+ *
+ * @param statsToken The token for tracking this show request.
+ * @return {@code true} when the show request can proceed.
*/
- boolean onImeShowFlags(@NonNull ImeTracker.Token statsToken, int showFlags) {
+ boolean onImeShowFlags(@NonNull ImeTracker.Token statsToken,
+ @InputMethodManager.ShowFlags int showFlags) {
if (mPolicy.mA11yRequestingNoSoftKeyboard || mPolicy.mImeHiddenByDisplayPolicy) {
ImeTracker.forLogging().onFailed(statsToken, ImeTracker.PHASE_SERVER_ACCESSIBILITY);
return false;
}
ImeTracker.forLogging().onProgress(statsToken, ImeTracker.PHASE_SERVER_ACCESSIBILITY);
+ // We only "set" the state corresponding to the flags, as this will be reset
+ // in clearImeShowFlags during a hide request.
+ // Thus, we keep the strongest values set (e.g. an implicit show right after
+ // an explicit show will still be considered explicit, likewise for forced).
if ((showFlags & InputMethodManager.SHOW_FORCED) != 0) {
mRequestedShowExplicitly = true;
mShowForced = true;
@@ -243,12 +247,12 @@
/**
* Called when {@link InputMethodManagerService} is processing the hide IME request.
- * @param statsToken The token for tracking this hide request
- * @param hideFlags The additional operation flags to indicate whether this hide request mode is
- * implicit or explicit.
- * @return {@code true} when the computer has proceed this hide request operations.
+ *
+ * @param statsToken The token for tracking this hide request.
+ * @return {@code true} when the hide request can proceed.
*/
- boolean canHideIme(@NonNull ImeTracker.Token statsToken, int hideFlags) {
+ boolean canHideIme(@NonNull ImeTracker.Token statsToken,
+ @InputMethodManager.HideFlags int hideFlags) {
if ((hideFlags & InputMethodManager.HIDE_IMPLICIT_ONLY) != 0
&& (mRequestedShowExplicitly || mShowForced)) {
if (DEBUG) Slog.v(TAG, "Not hiding: explicit show not cancelled by non-explicit hide");
@@ -264,13 +268,31 @@
return true;
}
- int getImeShowFlags() {
+ /**
+ * Returns the show flags for IME. This translates from {@link InputMethodManager.ShowFlags}
+ * to {@link InputMethod.ShowFlags}.
+ */
+ @InputMethod.ShowFlags
+ int getShowFlagsForInputMethodServiceOnly() {
int flags = 0;
if (mShowForced) {
flags |= InputMethod.SHOW_FORCED | InputMethod.SHOW_EXPLICIT;
} else if (mRequestedShowExplicitly) {
flags |= InputMethod.SHOW_EXPLICIT;
- } else {
+ }
+ return flags;
+ }
+
+ /**
+ * Returns the show flags for IMM. This translates from {@link InputMethod.ShowFlags}
+ * to {@link InputMethodManager.ShowFlags}.
+ */
+ @InputMethodManager.ShowFlags
+ int getShowFlags() {
+ int flags = 0;
+ if (mShowForced) {
+ flags |= InputMethodManager.SHOW_FORCED;
+ } else if (!mRequestedShowExplicitly) {
flags |= InputMethodManager.SHOW_IMPLICIT;
}
return flags;
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index 02ee96a..0bf2820 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -2462,7 +2462,7 @@
final ImeTracker.Token statsToken = mCurStatsToken;
mCurStatsToken = null;
showCurrentInputLocked(mCurFocusedWindow, statsToken,
- mVisibilityStateComputer.getImeShowFlags(),
+ mVisibilityStateComputer.getShowFlags(),
null /* resultReceiver */, SoftInputShowHideReason.ATTACH_NEW_INPUT);
}
@@ -3398,8 +3398,9 @@
@Override
public boolean showSoftInput(IInputMethodClient client, IBinder windowToken,
- @Nullable ImeTracker.Token statsToken, int flags, int lastClickTooType,
- ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) {
+ @Nullable ImeTracker.Token statsToken, @InputMethodManager.ShowFlags int flags,
+ int lastClickTooType, ResultReceiver resultReceiver,
+ @SoftInputShowHideReason int reason) {
Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMMS.showSoftInput");
int uid = Binder.getCallingUid();
ImeTracing.getInstance().triggerManagerServiceDump(
@@ -3572,15 +3573,17 @@
@GuardedBy("ImfLock.class")
boolean showCurrentInputLocked(IBinder windowToken, @Nullable ImeTracker.Token statsToken,
- int flags, ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) {
+ @InputMethodManager.ShowFlags int flags, ResultReceiver resultReceiver,
+ @SoftInputShowHideReason int reason) {
return showCurrentInputLocked(windowToken, statsToken, flags,
MotionEvent.TOOL_TYPE_UNKNOWN, resultReceiver, reason);
}
@GuardedBy("ImfLock.class")
private boolean showCurrentInputLocked(IBinder windowToken,
- @Nullable ImeTracker.Token statsToken, int flags, int lastClickToolType,
- ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) {
+ @Nullable ImeTracker.Token statsToken, @InputMethodManager.ShowFlags int flags,
+ int lastClickToolType, ResultReceiver resultReceiver,
+ @SoftInputShowHideReason int reason) {
// Create statsToken is none exists.
if (statsToken == null) {
statsToken = createStatsTokenForFocusedClient(true /* show */,
@@ -3611,7 +3614,8 @@
curMethod.updateEditorToolType(lastClickToolType);
}
mVisibilityApplier.performShowIme(windowToken, statsToken,
- mVisibilityStateComputer.getImeShowFlags(), resultReceiver, reason);
+ mVisibilityStateComputer.getShowFlagsForInputMethodServiceOnly(),
+ resultReceiver, reason);
mVisibilityStateComputer.setInputShown(true);
return true;
} else {
@@ -3623,8 +3627,8 @@
@Override
public boolean hideSoftInput(IInputMethodClient client, IBinder windowToken,
- @Nullable ImeTracker.Token statsToken, int flags, ResultReceiver resultReceiver,
- @SoftInputShowHideReason int reason) {
+ @Nullable ImeTracker.Token statsToken, @InputMethodManager.HideFlags int flags,
+ ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) {
int uid = Binder.getCallingUid();
ImeTracing.getInstance().triggerManagerServiceDump(
"InputMethodManagerService#hideSoftInput");
@@ -3654,7 +3658,8 @@
@GuardedBy("ImfLock.class")
boolean hideCurrentInputLocked(IBinder windowToken, @Nullable ImeTracker.Token statsToken,
- int flags, ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) {
+ @InputMethodManager.HideFlags int flags, ResultReceiver resultReceiver,
+ @SoftInputShowHideReason int reason) {
// Create statsToken is none exists.
if (statsToken == null) {
statsToken = createStatsTokenForFocusedClient(false /* show */,
@@ -4841,7 +4846,7 @@
}
@BinderThread
- private void hideMySoftInput(@NonNull IBinder token, int flags,
+ private void hideMySoftInput(@NonNull IBinder token, @InputMethodManager.HideFlags int flags,
@SoftInputShowHideReason int reason) {
try {
Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMMS.hideMySoftInput");
@@ -4863,7 +4868,7 @@
}
@BinderThread
- private void showMySoftInput(@NonNull IBinder token, int flags) {
+ private void showMySoftInput(@NonNull IBinder token, @InputMethodManager.ShowFlags int flags) {
try {
Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMMS.showMySoftInput");
synchronized (ImfLock.class) {
@@ -6822,8 +6827,8 @@
@BinderThread
@Override
- public void hideMySoftInput(int flags, @SoftInputShowHideReason int reason,
- AndroidFuture future /* T=Void */) {
+ public void hideMySoftInput(@InputMethodManager.HideFlags int flags,
+ @SoftInputShowHideReason int reason, AndroidFuture future /* T=Void */) {
@SuppressWarnings("unchecked")
final AndroidFuture<Void> typedFuture = future;
try {
@@ -6836,7 +6841,8 @@
@BinderThread
@Override
- public void showMySoftInput(int flags, AndroidFuture future /* T=Void */) {
+ public void showMySoftInput(@InputMethodManager.ShowFlags int flags,
+ AndroidFuture future /* T=Void */) {
@SuppressWarnings("unchecked")
final AndroidFuture<Void> typedFuture = future;
try {
diff --git a/services/core/java/com/android/server/pm/ApexManager.java b/services/core/java/com/android/server/pm/ApexManager.java
index 5e62b56..2206eac 100644
--- a/services/core/java/com/android/server/pm/ApexManager.java
+++ b/services/core/java/com/android/server/pm/ApexManager.java
@@ -35,7 +35,6 @@
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.Trace;
-import android.sysprop.ApexProperties;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.ArraySet;
@@ -82,18 +81,12 @@
new Singleton<ApexManager>() {
@Override
protected ApexManager create() {
- if (ApexProperties.updatable().orElse(false)) {
- return new ApexManagerImpl();
- } else {
- return new ApexManagerFlattenedApex();
- }
+ return new ApexManagerImpl();
}
};
/**
- * Returns an instance of either {@link ApexManagerImpl} or {@link ApexManagerFlattenedApex}
- * depending on whether this device supports APEX, i.e. {@link ApexProperties#updatable()}
- * evaluates to {@code true}.
+ * Returns an instance of {@link ApexManagerImpl}
* @hide
*/
public static ApexManager getInstance() {
@@ -991,203 +984,4 @@
}
}
}
-
- /**
- * An implementation of {@link ApexManager} that should be used in case device does not support
- * updating APEX packages.
- */
- @VisibleForTesting
- static final class ApexManagerFlattenedApex extends ApexManager {
- @Override
- ApexInfo[] getAllApexInfos() {
- return null;
- }
-
- @Override
- void notifyScanResult(List<ScanResult> scanResults) {
- // No-op
- }
-
- @Override
- public List<ActiveApexInfo> getActiveApexInfos() {
- // There is no apexd running in case of flattened apex
- // We look up the /apex directory and identify the active APEX modules from there.
- // As "preinstalled" path, we just report /system since in the case of flattened APEX
- // the /apex directory is just a symlink to /system/apex.
- List<ActiveApexInfo> result = new ArrayList<>();
- File apexDir = Environment.getApexDirectory();
- if (apexDir.isDirectory()) {
- File[] files = apexDir.listFiles();
- // listFiles might be null if system server doesn't have permission to read
- // a directory.
- if (files != null) {
- for (File file : files) {
- if (file.isDirectory() && !file.getName().contains("@")
- // In flattened configuration, init special-cases the art directory
- // and bind-mounts com.android.art.debug to com.android.art.
- && !file.getName().equals("com.android.art.debug")) {
- result.add(
- new ActiveApexInfo(file, Environment.getRootDirectory(), file));
- }
- }
- }
- }
- return result;
- }
-
- @Override
- @Nullable
- public String getActiveApexPackageNameContainingPackage(
- @NonNull String containedPackageName) {
- Objects.requireNonNull(containedPackageName);
-
- return null;
- }
-
- @Override
- ApexSessionInfo getStagedSessionInfo(int sessionId) {
- throw new UnsupportedOperationException();
- }
-
- @Override
- SparseArray<ApexSessionInfo> getSessions() {
- return new SparseArray<>(0);
- }
-
- @Override
- ApexInfoList submitStagedSession(ApexSessionParams params)
- throws PackageManagerException {
- throw new PackageManagerException(PackageManager.INSTALL_FAILED_INTERNAL_ERROR,
- "Device doesn't support updating APEX");
- }
-
- @Override
- ApexInfo[] getStagedApexInfos(ApexSessionParams params) {
- throw new UnsupportedOperationException();
- }
-
- @Override
- void markStagedSessionReady(int sessionId) {
- throw new UnsupportedOperationException();
- }
-
- @Override
- void markStagedSessionSuccessful(int sessionId) {
- throw new UnsupportedOperationException();
- }
-
- @Override
- boolean isApexSupported() {
- return false;
- }
-
- @Override
- boolean revertActiveSessions() {
- throw new UnsupportedOperationException();
- }
-
- @Override
- boolean abortStagedSession(int sessionId) {
- throw new UnsupportedOperationException();
- }
-
- @Override
- boolean uninstallApex(String apexPackagePath) {
- throw new UnsupportedOperationException();
- }
-
- @Override
- void registerApkInApex(AndroidPackage pkg) {
- // No-op
- }
-
- @Override
- void reportErrorWithApkInApex(String scanDirPath, String errorMsg) {
- // No-op
- }
-
- @Override
- @Nullable
- String getApkInApexInstallError(String apexPackageName) {
- return null;
- }
-
- @Override
- List<String> getApksInApex(String apexPackageName) {
- return Collections.emptyList();
- }
-
- @Override
- @Nullable
- public String getApexModuleNameForPackageName(String apexPackageName) {
- return null;
- }
-
- @Override
- @Nullable
- public String getActivePackageNameForApexModuleName(String apexModuleName) {
- return null;
- }
-
- @Override
- public boolean snapshotCeData(int userId, int rollbackId, String apexPackageName) {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public boolean restoreCeData(int userId, int rollbackId, String apexPackageName) {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public boolean destroyDeSnapshots(int rollbackId) {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public boolean destroyCeSnapshots(int userId, int rollbackId) {
- return true;
- }
-
- @Override
- public boolean destroyCeSnapshotsNotSpecified(int userId, int[] retainRollbackIds) {
- return true;
- }
-
- @Override
- public void markBootCompleted() {
- // No-op
- }
-
- @Override
- public long calculateSizeForCompressedApex(CompressedApexInfoList infoList) {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public void reserveSpaceForCompressedApex(CompressedApexInfoList infoList) {
- throw new UnsupportedOperationException();
- }
-
- @Override
- ApexInfo installPackage(File apexFile) {
- throw new UnsupportedOperationException("APEX updates are not supported");
- }
-
- @Override
- public List<ApexSystemServiceInfo> getApexSystemServices() {
- // TODO(satayev): we can't really support flattened apex use case, and need to migrate
- // the manifest entries into system's manifest asap.
- return Collections.emptyList();
- }
-
- @Override
- void dump(PrintWriter pw) {
- }
-
- @Override
- public File getBackingApexFile(File file) {
- return null;
- }
- }
}
diff --git a/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java b/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java
index 58c31b8..c44b8852 100644
--- a/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java
+++ b/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java
@@ -26,7 +26,6 @@
import android.app.DownloadManager;
import android.app.SearchManager;
import android.app.admin.DevicePolicyManager;
-import android.companion.CompanionDeviceManager;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
@@ -902,7 +901,7 @@
// Companion devices
grantSystemFixedPermissionsToSystemPackage(pm,
- CompanionDeviceManager.COMPANION_DEVICE_DISCOVERY_PACKAGE_NAME, userId,
+ getDefaultCompanionDeviceManagerPackage(), userId,
ALWAYS_LOCATION_PERMISSIONS, NEARBY_DEVICES_PERMISSIONS);
// Ringtone Picker
@@ -952,6 +951,10 @@
return mContext.getString(R.string.config_defaultDockManagerPackageName);
}
+ private String getDefaultCompanionDeviceManagerPackage() {
+ return mContext.getString(R.string.config_companionDeviceManagerPackage);
+ }
+
@SafeVarargs
private final void grantPermissionToEachSystemPackage(PackageManagerWrapper pm,
ArrayList<String> packages, int userId, Set<String>... permissions) {
diff --git a/services/core/java/com/android/server/policy/role/RoleServicePlatformHelperImpl.java b/services/core/java/com/android/server/policy/role/RoleServicePlatformHelperImpl.java
index 6cc4258..5e8b4de 100644
--- a/services/core/java/com/android/server/policy/role/RoleServicePlatformHelperImpl.java
+++ b/services/core/java/com/android/server/policy/role/RoleServicePlatformHelperImpl.java
@@ -19,6 +19,7 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
+import android.app.admin.DevicePolicyManagerInternal;
import android.app.role.RoleManager;
import android.content.ComponentName;
import android.content.ContentResolver;
@@ -303,6 +304,8 @@
public String computePackageStateHash(@UserIdInt int userId) {
PackageManagerInternal packageManagerInternal = LocalServices.getService(
PackageManagerInternal.class);
+ DevicePolicyManagerInternal devicePolicyManagerInternal = LocalServices.getService(
+ DevicePolicyManagerInternal.class);
final MessageDigestOutputStream mdos = new MessageDigestOutputStream();
DataOutputStream dataOutputStream = new DataOutputStream(new BufferedOutputStream(mdos));
@@ -342,6 +345,34 @@
throw new AssertionError(e);
}
}, userId);
+ try {
+ String deviceOwner= "";
+ if (devicePolicyManagerInternal != null) {
+ if (devicePolicyManagerInternal.getDeviceOwnerUserId() == userId) {
+ ComponentName deviceOwnerComponent =
+ devicePolicyManagerInternal.getDeviceOwnerComponent(false);
+ if (deviceOwnerComponent != null) {
+ deviceOwner = deviceOwnerComponent.getPackageName();
+ }
+ }
+ }
+ dataOutputStream.writeUTF(deviceOwner);
+ String profileOwner = "";
+ if (devicePolicyManagerInternal != null) {
+ ComponentName profileOwnerComponent =
+ devicePolicyManagerInternal.getProfileOwnerAsUser(userId);
+ if (profileOwnerComponent != null) {
+ profileOwner = profileOwnerComponent.getPackageName();
+ }
+ }
+ dataOutputStream.writeUTF(profileOwner);
+ dataOutputStream.writeInt(Settings.Global.getInt(mContext.getContentResolver(),
+ Settings.Global.DEVICE_DEMO_MODE, 0));
+ dataOutputStream.flush();
+ } catch (IOException e) {
+ // Never happens for MessageDigestOutputStream and DataOutputStream.
+ throw new AssertionError(e);
+ }
return mdos.getDigestAsString();
}
diff --git a/services/core/java/com/android/server/recoverysystem/RecoverySystemService.java b/services/core/java/com/android/server/recoverysystem/RecoverySystemService.java
index 86c4985..375ef61 100644
--- a/services/core/java/com/android/server/recoverysystem/RecoverySystemService.java
+++ b/services/core/java/com/android/server/recoverysystem/RecoverySystemService.java
@@ -52,7 +52,6 @@
import android.os.ShellCallback;
import android.os.SystemProperties;
import android.provider.DeviceConfig;
-import android.sysprop.ApexProperties;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.FastImmutableArraySet;
@@ -921,10 +920,6 @@
return rebootWithLskfImpl(packageName, reason, slotSwitch);
}
- public static boolean isUpdatableApexSupported() {
- return ApexProperties.updatable().orElse(false);
- }
-
// Metadata should be no more than few MB, if it's larger than 100MB something is wrong.
private static final long APEX_INFO_SIZE_LIMIT = 24 * 1024 * 100;
@@ -975,11 +970,6 @@
@Override
public boolean allocateSpaceForUpdate(String packageFile) {
allocateSpaceForUpdate_enforcePermission();
- if (!isUpdatableApexSupported()) {
- Log.i(TAG, "Updatable Apex not supported, "
- + "allocateSpaceForUpdate does nothing.");
- return true;
- }
final long token = Binder.clearCallingIdentity();
try {
CompressedApexInfoList apexInfoList = getCompressedApexInfoList(packageFile);
diff --git a/services/core/java/com/android/server/vibrator/SetAmplitudeVibratorStep.java b/services/core/java/com/android/server/vibrator/SetAmplitudeVibratorStep.java
index cce1ef4..6d01123 100644
--- a/services/core/java/com/android/server/vibrator/SetAmplitudeVibratorStep.java
+++ b/services/core/java/com/android/server/vibrator/SetAmplitudeVibratorStep.java
@@ -179,7 +179,9 @@
while (i < segmentCount) {
VibrationEffectSegment segment = segments.get(i);
if (!(segment instanceof StepSegment)
- || ((StepSegment) segment).getAmplitude() == 0) {
+ // play() will ignore segments with zero duration, so it's important that
+ // zero-duration segments don't affect this method.
+ || (segment.getDuration() > 0 && ((StepSegment) segment).getAmplitude() == 0)) {
break;
}
timing += segment.getDuration();
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 5165e86..35ff3ee 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -2129,8 +2129,7 @@
hasBeenLaunched = false;
mTaskSupervisor = supervisor;
- info.taskAffinity = computeTaskAffinity(info.taskAffinity, info.applicationInfo.uid,
- info.launchMode, mActivityComponent);
+ info.taskAffinity = computeTaskAffinity(info.taskAffinity, info.applicationInfo.uid);
taskAffinity = info.taskAffinity;
final String uid = Integer.toString(info.applicationInfo.uid);
if (info.windowLayout != null && info.windowLayout.windowLayoutAffinity != null
@@ -2230,19 +2229,12 @@
*
* @param affinity The affinity of the activity.
* @param uid The user-ID that has been assigned to this application.
- * @param launchMode The activity launch mode
- * @param componentName The activity component name. This is only useful when the given
- * launchMode is {@link ActivityInfo#LAUNCH_SINGLE_INSTANCE}
* @return The task affinity
*/
- static String computeTaskAffinity(String affinity, int uid, int launchMode,
- ComponentName componentName) {
+ static String computeTaskAffinity(String affinity, int uid) {
final String uidStr = Integer.toString(uid);
if (affinity != null && !affinity.startsWith(uidStr)) {
affinity = uidStr + ":" + affinity;
- if (launchMode == LAUNCH_SINGLE_INSTANCE && componentName != null) {
- affinity += ":" + componentName.hashCode();
- }
}
return affinity;
}
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index bfd2a10..1dad48f 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -522,7 +522,6 @@
boolean mWaitingForConfig;
// TODO(multi-display): remove some of the usages.
- @VisibleForTesting
boolean isDefaultDisplay;
/** Detect user tapping outside of current focused task bounds .*/
@@ -3358,6 +3357,7 @@
mRootWindowContainer.mTaskSupervisor
.getKeyguardController().onDisplayRemoved(mDisplayId);
mWallpaperController.resetLargestDisplay(mDisplay);
+ mWmService.mDisplayWindowSettings.onDisplayRemoved(this);
} finally {
mDisplayReady = false;
}
diff --git a/services/core/java/com/android/server/wm/DisplayWindowSettings.java b/services/core/java/com/android/server/wm/DisplayWindowSettings.java
index 4c0435e..e1753d7 100644
--- a/services/core/java/com/android/server/wm/DisplayWindowSettings.java
+++ b/services/core/java/com/android/server/wm/DisplayWindowSettings.java
@@ -33,6 +33,7 @@
import android.view.DisplayInfo;
import android.view.IWindowManager;
import android.view.Surface;
+import android.view.WindowManager;
import android.view.WindowManager.DisplayImePolicy;
import com.android.server.policy.WindowManagerPolicy;
@@ -45,15 +46,20 @@
* delegates the persistence and lookup of settings values to the supplied {@link SettingsProvider}.
*/
class DisplayWindowSettings {
+ @NonNull
private final WindowManagerService mService;
+ @NonNull
private final SettingsProvider mSettingsProvider;
- DisplayWindowSettings(WindowManagerService service, SettingsProvider settingsProvider) {
+ DisplayWindowSettings(@NonNull WindowManagerService service,
+ @NonNull SettingsProvider settingsProvider) {
mService = service;
mSettingsProvider = settingsProvider;
}
- void setUserRotation(DisplayContent displayContent, int rotationMode, int rotation) {
+ void setUserRotation(@NonNull DisplayContent displayContent,
+ @WindowManagerPolicy.UserRotationMode int rotationMode,
+ @Surface.Rotation int rotation) {
final DisplayInfo displayInfo = displayContent.getDisplayInfo();
final SettingsProvider.SettingsEntry overrideSettings =
mSettingsProvider.getOverrideSettings(displayInfo);
@@ -62,7 +68,7 @@
mSettingsProvider.updateOverrideSettings(displayInfo, overrideSettings);
}
- void setForcedSize(DisplayContent displayContent, int width, int height) {
+ void setForcedSize(@NonNull DisplayContent displayContent, int width, int height) {
if (displayContent.isDefaultDisplay) {
final String sizeString = (width == 0 || height == 0) ? "" : (width + "," + height);
Settings.Global.putString(mService.mContext.getContentResolver(),
@@ -77,21 +83,20 @@
mSettingsProvider.updateOverrideSettings(displayInfo, overrideSettings);
}
- void setForcedDensity(DisplayInfo info, int density, int userId) {
+ void setForcedDensity(@NonNull DisplayInfo info, int density, int userId) {
if (info.displayId == Display.DEFAULT_DISPLAY) {
final String densityString = density == 0 ? "" : Integer.toString(density);
Settings.Secure.putStringForUser(mService.mContext.getContentResolver(),
Settings.Secure.DISPLAY_DENSITY_FORCED, densityString, userId);
}
- final DisplayInfo displayInfo = info;
final SettingsProvider.SettingsEntry overrideSettings =
- mSettingsProvider.getOverrideSettings(displayInfo);
+ mSettingsProvider.getOverrideSettings(info);
overrideSettings.mForcedDensity = density;
- mSettingsProvider.updateOverrideSettings(displayInfo, overrideSettings);
+ mSettingsProvider.updateOverrideSettings(info, overrideSettings);
}
- void setForcedScalingMode(DisplayContent displayContent, @ForceScalingMode int mode) {
+ void setForcedScalingMode(@NonNull DisplayContent displayContent, @ForceScalingMode int mode) {
if (displayContent.isDefaultDisplay) {
Settings.Global.putInt(mService.mContext.getContentResolver(),
Settings.Global.DISPLAY_SCALING_FORCE, mode);
@@ -104,7 +109,7 @@
mSettingsProvider.updateOverrideSettings(displayInfo, overrideSettings);
}
- void setFixedToUserRotation(DisplayContent displayContent, int fixedToUserRotation) {
+ void setFixedToUserRotation(@NonNull DisplayContent displayContent, int fixedToUserRotation) {
final DisplayInfo displayInfo = displayContent.getDisplayInfo();
final SettingsProvider.SettingsEntry overrideSettings =
mSettingsProvider.getOverrideSettings(displayInfo);
@@ -112,8 +117,8 @@
mSettingsProvider.updateOverrideSettings(displayInfo, overrideSettings);
}
- void setIgnoreOrientationRequest(
- DisplayContent displayContent, boolean ignoreOrientationRequest) {
+ void setIgnoreOrientationRequest(@NonNull DisplayContent displayContent,
+ boolean ignoreOrientationRequest) {
final DisplayInfo displayInfo = displayContent.getDisplayInfo();
final SettingsProvider.SettingsEntry overrideSettings =
mSettingsProvider.getOverrideSettings(displayInfo);
@@ -121,7 +126,9 @@
mSettingsProvider.updateOverrideSettings(displayInfo, overrideSettings);
}
- private int getWindowingModeLocked(SettingsProvider.SettingsEntry settings, DisplayContent dc) {
+ @WindowConfiguration.WindowingMode
+ private int getWindowingModeLocked(@NonNull SettingsProvider.SettingsEntry settings,
+ @NonNull DisplayContent dc) {
int windowingMode = settings.mWindowingMode;
// This display used to be in freeform, but we don't support freeform anymore, so fall
// back to fullscreen.
@@ -139,13 +146,15 @@
return windowingMode;
}
- int getWindowingModeLocked(DisplayContent dc) {
+ @WindowConfiguration.WindowingMode
+ int getWindowingModeLocked(@NonNull DisplayContent dc) {
final DisplayInfo displayInfo = dc.getDisplayInfo();
final SettingsProvider.SettingsEntry settings = mSettingsProvider.getSettings(displayInfo);
return getWindowingModeLocked(settings, dc);
}
- void setWindowingModeLocked(DisplayContent dc, int mode) {
+ void setWindowingModeLocked(@NonNull DisplayContent dc,
+ @WindowConfiguration.WindowingMode int mode) {
final DisplayInfo displayInfo = dc.getDisplayInfo();
final SettingsProvider.SettingsEntry overrideSettings =
mSettingsProvider.getOverrideSettings(displayInfo);
@@ -157,7 +166,8 @@
mSettingsProvider.updateOverrideSettings(displayInfo, overrideSettings);
}
- int getRemoveContentModeLocked(DisplayContent dc) {
+ @WindowManager.RemoveContentMode
+ int getRemoveContentModeLocked(@NonNull DisplayContent dc) {
final DisplayInfo displayInfo = dc.getDisplayInfo();
final SettingsProvider.SettingsEntry settings = mSettingsProvider.getSettings(displayInfo);
if (settings.mRemoveContentMode == REMOVE_CONTENT_MODE_UNDEFINED) {
@@ -171,7 +181,8 @@
return settings.mRemoveContentMode;
}
- void setRemoveContentModeLocked(DisplayContent dc, int mode) {
+ void setRemoveContentModeLocked(@NonNull DisplayContent dc,
+ @WindowManager.RemoveContentMode int mode) {
final DisplayInfo displayInfo = dc.getDisplayInfo();
final SettingsProvider.SettingsEntry overrideSettings =
mSettingsProvider.getOverrideSettings(displayInfo);
@@ -179,14 +190,14 @@
mSettingsProvider.updateOverrideSettings(displayInfo, overrideSettings);
}
- boolean shouldShowWithInsecureKeyguardLocked(DisplayContent dc) {
+ boolean shouldShowWithInsecureKeyguardLocked(@NonNull DisplayContent dc) {
final DisplayInfo displayInfo = dc.getDisplayInfo();
final SettingsProvider.SettingsEntry settings = mSettingsProvider.getSettings(displayInfo);
return settings.mShouldShowWithInsecureKeyguard != null
? settings.mShouldShowWithInsecureKeyguard : false;
}
- void setShouldShowWithInsecureKeyguardLocked(DisplayContent dc, boolean shouldShow) {
+ void setShouldShowWithInsecureKeyguardLocked(@NonNull DisplayContent dc, boolean shouldShow) {
if (!dc.isPrivate() && shouldShow) {
throw new IllegalArgumentException("Public display can't be allowed to show content"
+ " when locked");
@@ -199,7 +210,7 @@
mSettingsProvider.updateOverrideSettings(displayInfo, overrideSettings);
}
- void setDontMoveToTop(DisplayContent dc, boolean dontMoveToTop) {
+ void setDontMoveToTop(@NonNull DisplayContent dc, boolean dontMoveToTop) {
DisplayInfo displayInfo = dc.getDisplayInfo();
SettingsProvider.SettingsEntry overrideSettings =
mSettingsProvider.getSettings(displayInfo);
@@ -207,7 +218,7 @@
mSettingsProvider.updateOverrideSettings(displayInfo, overrideSettings);
}
- boolean shouldShowSystemDecorsLocked(DisplayContent dc) {
+ boolean shouldShowSystemDecorsLocked(@NonNull DisplayContent dc) {
if (dc.getDisplayId() == Display.DEFAULT_DISPLAY) {
// Default display should show system decors.
return true;
@@ -218,7 +229,7 @@
return settings.mShouldShowSystemDecors != null ? settings.mShouldShowSystemDecors : false;
}
- void setShouldShowSystemDecorsLocked(DisplayContent dc, boolean shouldShow) {
+ void setShouldShowSystemDecorsLocked(@NonNull DisplayContent dc, boolean shouldShow) {
final DisplayInfo displayInfo = dc.getDisplayInfo();
final SettingsProvider.SettingsEntry overrideSettings =
mSettingsProvider.getOverrideSettings(displayInfo);
@@ -226,7 +237,8 @@
mSettingsProvider.updateOverrideSettings(displayInfo, overrideSettings);
}
- @DisplayImePolicy int getImePolicyLocked(DisplayContent dc) {
+ @DisplayImePolicy
+ int getImePolicyLocked(@NonNull DisplayContent dc) {
if (dc.getDisplayId() == Display.DEFAULT_DISPLAY) {
// Default display should show IME.
return DISPLAY_IME_POLICY_LOCAL;
@@ -238,7 +250,7 @@
: DISPLAY_IME_POLICY_FALLBACK_DISPLAY;
}
- void setDisplayImePolicy(DisplayContent dc, @DisplayImePolicy int imePolicy) {
+ void setDisplayImePolicy(@NonNull DisplayContent dc, @DisplayImePolicy int imePolicy) {
final DisplayInfo displayInfo = dc.getDisplayInfo();
final SettingsProvider.SettingsEntry overrideSettings =
mSettingsProvider.getOverrideSettings(displayInfo);
@@ -246,11 +258,11 @@
mSettingsProvider.updateOverrideSettings(displayInfo, overrideSettings);
}
- void applySettingsToDisplayLocked(DisplayContent dc) {
+ void applySettingsToDisplayLocked(@NonNull DisplayContent dc) {
applySettingsToDisplayLocked(dc, /* includeRotationSettings */ true);
}
- void applySettingsToDisplayLocked(DisplayContent dc, boolean includeRotationSettings) {
+ void applySettingsToDisplayLocked(@NonNull DisplayContent dc, boolean includeRotationSettings) {
final DisplayInfo displayInfo = dc.getDisplayInfo();
final SettingsProvider.SettingsEntry settings = mSettingsProvider.getSettings(displayInfo);
@@ -274,9 +286,8 @@
dc.mIsDensityForced = hasDensityOverride;
dc.mIsSizeForced = hasSizeOverride;
- final boolean ignoreDisplayCutout = settings.mIgnoreDisplayCutout != null
+ dc.mIgnoreDisplayCutout = settings.mIgnoreDisplayCutout != null
? settings.mIgnoreDisplayCutout : false;
- dc.mIgnoreDisplayCutout = ignoreDisplayCutout;
final int width = hasSizeOverride ? settings.mForcedWidth : dc.mInitialDisplayWidth;
final int height = hasSizeOverride ? settings.mForcedHeight : dc.mInitialDisplayHeight;
@@ -296,7 +307,7 @@
if (includeRotationSettings) applyRotationSettingsToDisplayLocked(dc);
}
- void applyRotationSettingsToDisplayLocked(DisplayContent dc) {
+ void applyRotationSettingsToDisplayLocked(@NonNull DisplayContent dc) {
final DisplayInfo displayInfo = dc.getDisplayInfo();
final SettingsProvider.SettingsEntry settings = mSettingsProvider.getSettings(displayInfo);
@@ -315,7 +326,7 @@
* @return {@code true} if any settings for this display has changed; {@code false} if nothing
* changed.
*/
- boolean updateSettingsForDisplay(DisplayContent dc) {
+ boolean updateSettingsForDisplay(@NonNull DisplayContent dc) {
final TaskDisplayArea defaultTda = dc.getDefaultTaskDisplayArea();
if (defaultTda != null && defaultTda.getWindowingMode() != getWindowingModeLocked(dc)) {
// For the time being the only thing that may change is windowing mode, so just update
@@ -327,6 +338,13 @@
}
/**
+ * Called when the given {@link DisplayContent} is removed to cleanup.
+ */
+ void onDisplayRemoved(@NonNull DisplayContent dc) {
+ mSettingsProvider.onDisplayRemoved(dc.getDisplayInfo());
+ }
+
+ /**
* Provides the functionality to lookup the {@link SettingsEntry settings} for a given
* {@link DisplayInfo}.
* <p>
@@ -364,19 +382,29 @@
void updateOverrideSettings(@NonNull DisplayInfo info, @NonNull SettingsEntry overrides);
/**
+ * Called when a display is removed to cleanup.
+ */
+ void onDisplayRemoved(@NonNull DisplayInfo info);
+
+ /**
* Settings for a display.
*/
class SettingsEntry {
+ @WindowConfiguration.WindowingMode
int mWindowingMode = WindowConfiguration.WINDOWING_MODE_UNDEFINED;
@Nullable
+ @WindowManagerPolicy.UserRotationMode
Integer mUserRotationMode;
@Nullable
+ @Surface.Rotation
Integer mUserRotation;
int mForcedWidth;
int mForcedHeight;
int mForcedDensity;
@Nullable
+ @ForceScalingMode
Integer mForcedScalingMode;
+ @WindowManager.RemoveContentMode
int mRemoveContentMode = REMOVE_CONTENT_MODE_UNDEFINED;
@Nullable
Boolean mShouldShowWithInsecureKeyguard;
@@ -395,7 +423,7 @@
SettingsEntry() {}
- SettingsEntry(SettingsEntry copyFrom) {
+ SettingsEntry(@NonNull SettingsEntry copyFrom) {
setTo(copyFrom);
}
diff --git a/services/core/java/com/android/server/wm/DisplayWindowSettingsProvider.java b/services/core/java/com/android/server/wm/DisplayWindowSettingsProvider.java
index 1abb0a1..ea668fa 100644
--- a/services/core/java/com/android/server/wm/DisplayWindowSettingsProvider.java
+++ b/services/core/java/com/android/server/wm/DisplayWindowSettingsProvider.java
@@ -16,6 +16,7 @@
package com.android.server.wm;
+import static android.view.Display.TYPE_VIRTUAL;
import static android.view.WindowManager.DISPLAY_IME_POLICY_FALLBACK_DISPLAY;
import static android.view.WindowManager.DISPLAY_IME_POLICY_LOCAL;
import static android.view.WindowManager.REMOVE_CONTENT_MODE_UNDEFINED;
@@ -28,6 +29,8 @@
import android.annotation.Nullable;
import android.app.WindowConfiguration;
import android.os.Environment;
+import android.util.ArrayMap;
+import android.util.ArraySet;
import android.util.AtomicFile;
import android.util.Slog;
import android.util.Xml;
@@ -49,7 +52,6 @@
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
-import java.util.HashMap;
import java.util.Map;
/**
@@ -86,7 +88,9 @@
void finishWrite(OutputStream os, boolean success);
}
+ @NonNull
private ReadableSettings mBaseSettings;
+ @NonNull
private final WritableSettings mOverrideSettings;
DisplayWindowSettingsProvider() {
@@ -155,6 +159,16 @@
mOverrideSettings.updateSettingsEntry(info, overrides);
}
+ @Override
+ public void onDisplayRemoved(@NonNull DisplayInfo info) {
+ mOverrideSettings.onDisplayRemoved(info);
+ }
+
+ @VisibleForTesting
+ int getOverrideSettingsSize() {
+ return mOverrideSettings.mSettings.size();
+ }
+
/**
* Class that allows reading {@link SettingsEntry entries} from a
* {@link ReadableSettingsStorage}.
@@ -168,14 +182,15 @@
*/
@DisplayIdentifierType
protected int mIdentifierType;
- protected final Map<String, SettingsEntry> mSettings = new HashMap<>();
+ @NonNull
+ protected final ArrayMap<String, SettingsEntry> mSettings = new ArrayMap<>();
- ReadableSettings(ReadableSettingsStorage settingsStorage) {
+ ReadableSettings(@NonNull ReadableSettingsStorage settingsStorage) {
loadSettings(settingsStorage);
}
@Nullable
- final SettingsEntry getSettingsEntry(DisplayInfo info) {
+ final SettingsEntry getSettingsEntry(@NonNull DisplayInfo info) {
final String identifier = getIdentifier(info);
SettingsEntry settings;
// Try to get corresponding settings using preferred identifier for the current config.
@@ -193,7 +208,8 @@
}
/** Gets the identifier of choice for the current config. */
- protected final String getIdentifier(DisplayInfo displayInfo) {
+ @NonNull
+ protected final String getIdentifier(@NonNull DisplayInfo displayInfo) {
if (mIdentifierType == IDENTIFIER_PORT && displayInfo.address != null) {
// Config suggests using port as identifier for physical displays.
if (displayInfo.address instanceof DisplayAddress.Physical) {
@@ -203,7 +219,7 @@
return displayInfo.uniqueId;
}
- private void loadSettings(ReadableSettingsStorage settingsStorage) {
+ private void loadSettings(@NonNull ReadableSettingsStorage settingsStorage) {
FileData fileData = readSettings(settingsStorage);
if (fileData != null) {
mIdentifierType = fileData.mIdentifierType;
@@ -217,15 +233,18 @@
* {@link WritableSettingsStorage}.
*/
private static final class WritableSettings extends ReadableSettings {
+ @NonNull
private final WritableSettingsStorage mSettingsStorage;
+ @NonNull
+ private final ArraySet<String> mVirtualDisplayIdentifiers = new ArraySet<>();
- WritableSettings(WritableSettingsStorage settingsStorage) {
+ WritableSettings(@NonNull WritableSettingsStorage settingsStorage) {
super(settingsStorage);
mSettingsStorage = settingsStorage;
}
@NonNull
- SettingsEntry getOrCreateSettingsEntry(DisplayInfo info) {
+ SettingsEntry getOrCreateSettingsEntry(@NonNull DisplayInfo info) {
final String identifier = getIdentifier(info);
SettingsEntry settings;
// Try to get corresponding settings using preferred identifier for the current config.
@@ -243,21 +262,47 @@
settings = new SettingsEntry();
mSettings.put(identifier, settings);
+ if (info.type == TYPE_VIRTUAL) {
+ // Keep track of virtual display. We don't want to write virtual display settings to
+ // file.
+ mVirtualDisplayIdentifiers.add(identifier);
+ }
return settings;
}
- void updateSettingsEntry(DisplayInfo info, SettingsEntry settings) {
+ void updateSettingsEntry(@NonNull DisplayInfo info, @NonNull SettingsEntry settings) {
final SettingsEntry overrideSettings = getOrCreateSettingsEntry(info);
final boolean changed = overrideSettings.setTo(settings);
- if (changed) {
+ if (changed && info.type != TYPE_VIRTUAL) {
writeSettings();
}
}
+ void onDisplayRemoved(@NonNull DisplayInfo info) {
+ final String identifier = getIdentifier(info);
+ if (!mSettings.containsKey(identifier)) {
+ return;
+ }
+ if (mVirtualDisplayIdentifiers.remove(identifier)
+ || mSettings.get(identifier).isEmpty()) {
+ // Don't keep track of virtual display or empty settings to avoid growing the cached
+ // map.
+ mSettings.remove(identifier);
+ }
+ }
+
private void writeSettings() {
- FileData fileData = new FileData();
+ final FileData fileData = new FileData();
fileData.mIdentifierType = mIdentifierType;
- fileData.mSettings.putAll(mSettings);
+ final int size = mSettings.size();
+ for (int i = 0; i < size; i++) {
+ final String identifier = mSettings.keyAt(i);
+ if (mVirtualDisplayIdentifiers.contains(identifier)) {
+ // Do not write virtual display settings to file.
+ continue;
+ }
+ fileData.mSettings.put(identifier, mSettings.get(identifier));
+ }
DisplayWindowSettingsProvider.writeSettings(mSettingsStorage, fileData);
}
}
@@ -283,7 +328,7 @@
}
@Nullable
- private static FileData readSettings(ReadableSettingsStorage storage) {
+ private static FileData readSettings(@NonNull ReadableSettingsStorage storage) {
InputStream stream;
try {
stream = storage.openRead();
@@ -348,13 +393,14 @@
return fileData;
}
- private static int getIntAttribute(TypedXmlPullParser parser, String name, int defaultValue) {
+ private static int getIntAttribute(@NonNull TypedXmlPullParser parser, @NonNull String name,
+ int defaultValue) {
return parser.getAttributeInt(null, name, defaultValue);
}
@Nullable
- private static Integer getIntegerAttribute(TypedXmlPullParser parser, String name,
- @Nullable Integer defaultValue) {
+ private static Integer getIntegerAttribute(@NonNull TypedXmlPullParser parser,
+ @NonNull String name, @Nullable Integer defaultValue) {
try {
return parser.getAttributeInt(null, name);
} catch (Exception ignored) {
@@ -363,8 +409,8 @@
}
@Nullable
- private static Boolean getBooleanAttribute(TypedXmlPullParser parser, String name,
- @Nullable Boolean defaultValue) {
+ private static Boolean getBooleanAttribute(@NonNull TypedXmlPullParser parser,
+ @NonNull String name, @Nullable Boolean defaultValue) {
try {
return parser.getAttributeBoolean(null, name);
} catch (Exception ignored) {
@@ -372,7 +418,7 @@
}
}
- private static void readDisplay(TypedXmlPullParser parser, FileData fileData)
+ private static void readDisplay(@NonNull TypedXmlPullParser parser, @NonNull FileData fileData)
throws NumberFormatException, XmlPullParserException, IOException {
String name = parser.getAttributeValue(null, "name");
if (name != null) {
@@ -420,7 +466,7 @@
XmlUtils.skipCurrentTag(parser);
}
- private static void readConfig(TypedXmlPullParser parser, FileData fileData)
+ private static void readConfig(@NonNull TypedXmlPullParser parser, @NonNull FileData fileData)
throws NumberFormatException,
XmlPullParserException, IOException {
fileData.mIdentifierType = getIntAttribute(parser, "identifier",
@@ -428,7 +474,8 @@
XmlUtils.skipCurrentTag(parser);
}
- private static void writeSettings(WritableSettingsStorage storage, FileData data) {
+ private static void writeSettings(@NonNull WritableSettingsStorage storage,
+ @NonNull FileData data) {
OutputStream stream;
try {
stream = storage.startWrite();
@@ -525,7 +572,8 @@
private static final class FileData {
int mIdentifierType;
- final Map<String, SettingsEntry> mSettings = new HashMap<>();
+ @NonNull
+ final Map<String, SettingsEntry> mSettings = new ArrayMap<>();
@Override
public String toString() {
@@ -537,6 +585,7 @@
}
private static final class AtomicFileStorage implements WritableSettingsStorage {
+ @NonNull
private final AtomicFile mAtomicFile;
AtomicFileStorage(@NonNull AtomicFile atomicFile) {
diff --git a/services/core/java/com/android/server/wm/RecentTasks.java b/services/core/java/com/android/server/wm/RecentTasks.java
index 9ef5ed0..b71d918b 100644
--- a/services/core/java/com/android/server/wm/RecentTasks.java
+++ b/services/core/java/com/android/server/wm/RecentTasks.java
@@ -183,6 +183,8 @@
/** The non-empty tasks that are removed from recent tasks (see {@link #removeForAddTask}). */
private final ArrayList<Task> mHiddenTasks = new ArrayList<>();
+ /** The maximum size that the hidden tasks are cached. */
+ private static final int MAX_HIDDEN_TASK_SIZE = 10;
/** Whether to trim inactive tasks when activities are idle. */
private boolean mCheckTrimmableTasksOnIdle;
@@ -1477,9 +1479,13 @@
return task.compareTo(rootHomeTask) < 0;
}
- /** Remove the tasks that user may not be able to return. */
+ /** Remove the tasks that user may not be able to return when exceeds the cache limit. */
private void removeUnreachableHiddenTasks(int windowingMode) {
- for (int i = mHiddenTasks.size() - 1; i >= 0; i--) {
+ final int size = mHiddenTasks.size();
+ if (size <= MAX_HIDDEN_TASK_SIZE) {
+ return;
+ }
+ for (int i = size - 1; i >= MAX_HIDDEN_TASK_SIZE; i--) {
final Task hiddenTask = mHiddenTasks.get(i);
if (!hiddenTask.hasChild() || hiddenTask.inRecents) {
// The task was removed by other path or it became reachable (added to recents).
@@ -1523,7 +1529,7 @@
// A non-empty task is replaced by a new task. Because the removed task is no longer
// managed by the recent tasks list, add it to the hidden list to prevent the task
// from becoming dangling.
- mHiddenTasks.add(removedTask);
+ mHiddenTasks.add(0, removedTask);
}
notifyTaskRemoved(removedTask, false /* wasTrimmed */, false /* killProcess */);
if (DEBUG_RECENTS_TRIM_TASKS) {
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index 9c23beb..92e90ae 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -5406,8 +5406,7 @@
// Basic case: for simple app-centric recents, we need to recreate
// the task if the affinity has changed.
- final String affinity = ActivityRecord.computeTaskAffinity(destAffinity, srec.getUid(),
- srec.launchMode, srec.mActivityComponent);
+ final String affinity = ActivityRecord.computeTaskAffinity(destAffinity, srec.getUid());
if (srec == null || srec.getTask().affinity == null
|| !srec.getTask().affinity.equals(affinity)) {
return true;
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index 0248acc..358ee87 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -723,6 +723,14 @@
mFlags |= WindowManager.TRANSIT_FLAG_INVISIBLE;
return;
}
+ // Activity doesn't need to capture snapshot if the starting window has associated to task.
+ if (wc.asActivityRecord() != null) {
+ final ActivityRecord activityRecord = wc.asActivityRecord();
+ if (activityRecord.mStartingData != null
+ && activityRecord.mStartingData.mAssociatedTask != null) {
+ return;
+ }
+ }
if (mContainerFreezer == null) {
mContainerFreezer = new ScreenshotFreezer();
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 8dc2840..c992ed9 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -552,7 +552,9 @@
final PackageManagerInternal mPmInternal;
private final TestUtilityService mTestUtilityService;
+ @NonNull
final DisplayWindowSettingsProvider mDisplayWindowSettingsProvider;
+ @NonNull
final DisplayWindowSettings mDisplayWindowSettings;
/** If the system should display notifications for apps displaying an alert window. */
diff --git a/services/flags/Android.bp b/services/flags/Android.bp
new file mode 100644
index 0000000..29d2b9c
--- /dev/null
+++ b/services/flags/Android.bp
@@ -0,0 +1,18 @@
+package {
+ // See: http://go/android-license-faq
+ // A large-scale-change added 'default_applicable_licenses' to import
+ // all of the 'license_kinds' from "frameworks_base_license"
+ // to get the below license kinds:
+ // SPDX-license-identifier-Apache-2.0
+ default_applicable_licenses: ["frameworks_base_license"],
+}
+
+java_library_static {
+ name: "services.flags",
+ defaults: ["platform_service_defaults"],
+ srcs: [
+ "java/**/*.java",
+ ":feature_flags_aidl",
+ ],
+ libs: ["services.core"],
+}
diff --git a/services/flags/OWNERS b/services/flags/OWNERS
new file mode 100644
index 0000000..3925b5c
--- /dev/null
+++ b/services/flags/OWNERS
@@ -0,0 +1,6 @@
+# Bug component: 1306523
+
+mankoff@google.com
+
+pixel@google.com
+dsandler@android.com
diff --git a/services/flags/java/com/android/server/flags/DynamicFlagBinderDelegate.java b/services/flags/java/com/android/server/flags/DynamicFlagBinderDelegate.java
new file mode 100644
index 0000000..0db3287
--- /dev/null
+++ b/services/flags/java/com/android/server/flags/DynamicFlagBinderDelegate.java
@@ -0,0 +1,280 @@
+/*
+ * Copyright (C) 2023 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.flags;
+
+import android.annotation.NonNull;
+import android.flags.IFeatureFlagsCallback;
+import android.flags.SyncableFlag;
+import android.os.Build;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.provider.DeviceConfig;
+import android.util.Slog;
+
+import com.android.internal.os.BackgroundThread;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Function;
+
+/**
+ * Handles DynamicFlags for {@link FeatureFlagsBinder}.
+ *
+ * Dynamic flags are simultaneously simpler and more complicated than process stable flags. We can
+ * return whatever value is last known for a flag is, without too much worry about the flags
+ * changing (they are dynamic after all). However, we have to alert all the relevant clients
+ * about those flag changes, and need to be able to restore to a default value if the flag gets
+ * reset/erased during runtime.
+ */
+class DynamicFlagBinderDelegate {
+
+ private final FlagOverrideStore mFlagStore;
+ private final FlagCache<DynamicFlagData> mDynamicFlags = new FlagCache<>();
+ private final Map<Integer, Set<IFeatureFlagsCallback>> mCallbacks = new HashMap<>();
+ private static final Function<Integer, Set<IFeatureFlagsCallback>> NEW_CALLBACK_SET =
+ k -> new HashSet<>();
+
+ private final DeviceConfig.OnPropertiesChangedListener mDeviceConfigListener =
+ new DeviceConfig.OnPropertiesChangedListener() {
+ @Override
+ public void onPropertiesChanged(@NonNull DeviceConfig.Properties properties) {
+ String ns = properties.getNamespace();
+ for (String name : properties.getKeyset()) {
+ // Don't alert for flags we don't care about.
+ // Don't alert for flags that have been overridden locally.
+ if (!mDynamicFlags.contains(ns, name) || mFlagStore.contains(ns, name)) {
+ continue;
+ }
+ mFlagChangeCallback.onFlagChanged(
+ ns, name, properties.getString(name, null));
+ }
+ }
+ };
+
+ private final FlagOverrideStore.FlagChangeCallback mFlagChangeCallback =
+ (namespace, name, value) -> {
+ // Don't bother with callbacks for non-dynamic flags.
+ if (!mDynamicFlags.contains(namespace, name)) {
+ return;
+ }
+
+ // Don't bother with callbacks if nothing changed.
+ // Handling erasure (null) is special, as we may be restoring back to a value
+ // we were already at.
+ DynamicFlagData data = mDynamicFlags.getOrNull(namespace, name);
+ if (data == null) {
+ return; // shouldn't happen, but better safe than sorry.
+ }
+ if (value == null) {
+ if (data.getValue().equals(data.getDefaultValue())) {
+ return;
+ }
+ value = data.getDefaultValue();
+ } else if (data.getValue().equals(value)) {
+ return;
+ }
+ data.setValue(value);
+
+ final Set<IFeatureFlagsCallback> cbCopy;
+ synchronized (mCallbacks) {
+ cbCopy = new HashSet<>();
+
+ for (Integer pid : mCallbacks.keySet()) {
+ if (data.containsPid(pid)) {
+ cbCopy.addAll(mCallbacks.get(pid));
+ }
+ }
+ }
+ SyncableFlag sFlag = new SyncableFlag(namespace, name, value, true);
+ cbCopy.forEach(cb -> {
+ try {
+ cb.onFlagChange(sFlag);
+ } catch (RemoteException e) {
+ Slog.w(
+ FeatureFlagsService.TAG,
+ "Failed to communicate flag change to client.");
+ }
+ });
+ };
+
+ DynamicFlagBinderDelegate(FlagOverrideStore flagStore) {
+ mFlagStore = flagStore;
+ mFlagStore.setChangeCallback(mFlagChangeCallback);
+ }
+
+ SyncableFlag syncDynamicFlag(int pid, SyncableFlag sf) {
+ if (!sf.isDynamic()) {
+ return sf;
+ }
+
+ String ns = sf.getNamespace();
+ String name = sf.getName();
+
+ // Dynamic flags don't need any special threading or synchronization considerations.
+ // We simply give them whatever the current value is.
+ // However, we do need to keep track of dynamic flags, so that we can alert
+ // about changes coming in from adb, DeviceConfig, or other sources.
+ // And also so that we can keep flags relatively consistent across processes.
+
+ DynamicFlagData data = mDynamicFlags.getOrNull(ns, name);
+ String value = getFlagValue(ns, name, sf.getValue());
+ // DeviceConfig listeners are per-namespace.
+ if (!mDynamicFlags.containsNamespace(ns)) {
+ DeviceConfig.addOnPropertiesChangedListener(
+ ns, BackgroundThread.getExecutor(), mDeviceConfigListener);
+ }
+ data.addClientPid(pid);
+ data.setValue(value);
+ // Store the default value so that if an override gets erased, we can restore
+ // to something.
+ data.setDefaultValue(sf.getValue());
+
+ return new SyncableFlag(sf.getNamespace(), sf.getName(), value, true);
+ }
+
+
+ void registerCallback(int pid, IFeatureFlagsCallback callback) {
+ // Always add callback so that we don't end up with a possible race/leak.
+ // We remove the callback directly if we fail to call #linkToDeath.
+ // If we tried to add the callback after we linked, then we could end up in a
+ // scenario where we link, then the binder dies, firing our BinderGriever which tries
+ // to remove the callback (which has not yet been added), then finally we add the
+ // callback, creating a leak.
+ Set<IFeatureFlagsCallback> callbacks;
+ synchronized (mCallbacks) {
+ callbacks = mCallbacks.computeIfAbsent(pid, NEW_CALLBACK_SET);
+ callbacks.add(callback);
+ }
+ try {
+ callback.asBinder().linkToDeath(new BinderGriever(pid), 0);
+ } catch (RemoteException e) {
+ Slog.e(
+ FeatureFlagsService.TAG,
+ "Failed to link to binder death. Callback not registered.");
+ synchronized (mCallbacks) {
+ callbacks.remove(callback);
+ }
+ }
+ }
+
+ void unregisterCallback(int pid, IFeatureFlagsCallback callback) {
+ // No need to unlink, since the BinderGriever will essentially be a no-op.
+ // We would have to track our BinderGriever's in a map otherwise.
+ synchronized (mCallbacks) {
+ Set<IFeatureFlagsCallback> callbacks =
+ mCallbacks.computeIfAbsent(pid, NEW_CALLBACK_SET);
+ callbacks.remove(callback);
+ }
+ }
+
+ String getFlagValue(String namespace, String name, String defaultValue) {
+ // If we already have a value cached, just use that.
+ String value = null;
+ DynamicFlagData data = mDynamicFlags.getOrNull(namespace, name);
+ if (data != null) {
+ value = data.getValue();
+ } else {
+ // Put the value in the cache for future reference.
+ data = new DynamicFlagData(namespace, name);
+ mDynamicFlags.setIfChanged(namespace, name, data);
+ }
+ // If we're not in a release build, flags can be overridden locally on device.
+ if (!Build.IS_USER && value == null) {
+ value = mFlagStore.get(namespace, name);
+ }
+ // If we still don't have a value, maybe DeviceConfig does?
+ // Fallback to sf.getValue() here as well.
+ if (value == null) {
+ value = DeviceConfig.getString(namespace, name, defaultValue);
+ }
+
+ return value;
+ }
+
+ private static class DynamicFlagData {
+ private final String mNamespace;
+ private final String mName;
+ private final Set<Integer> mPids = new HashSet<>();
+ private String mValue;
+ private String mDefaultValue;
+
+ private DynamicFlagData(String namespace, String name) {
+ mNamespace = namespace;
+ mName = name;
+ }
+
+ String getValue() {
+ return mValue;
+ }
+
+ void setValue(String value) {
+ mValue = value;
+ }
+
+ String getDefaultValue() {
+ return mDefaultValue;
+ }
+
+ void setDefaultValue(String value) {
+ mDefaultValue = value;
+ }
+
+ void addClientPid(int pid) {
+ mPids.add(pid);
+ }
+
+ boolean containsPid(int pid) {
+ return mPids.contains(pid);
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == null || !(other instanceof DynamicFlagData)) {
+ return false;
+ }
+
+ DynamicFlagData o = (DynamicFlagData) other;
+
+ return mName.equals(o.mName) && mNamespace.equals(o.mNamespace)
+ && mValue.equals(o.mValue) && mDefaultValue.equals(o.mDefaultValue);
+ }
+
+ @Override
+ public int hashCode() {
+ return mName.hashCode() + mNamespace.hashCode()
+ + mValue.hashCode() + mDefaultValue.hashCode();
+ }
+ }
+
+
+ private class BinderGriever implements IBinder.DeathRecipient {
+ private final int mPid;
+
+ private BinderGriever(int pid) {
+ mPid = pid;
+ }
+
+ @Override
+ public void binderDied() {
+ synchronized (mCallbacks) {
+ mCallbacks.remove(mPid);
+ }
+ }
+ }
+}
diff --git a/services/flags/java/com/android/server/flags/FeatureFlagsBinder.java b/services/flags/java/com/android/server/flags/FeatureFlagsBinder.java
new file mode 100644
index 0000000..1fa8532
--- /dev/null
+++ b/services/flags/java/com/android/server/flags/FeatureFlagsBinder.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright (C) 2023 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.flags;
+
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.flags.IFeatureFlags;
+import android.flags.IFeatureFlagsCallback;
+import android.flags.SyncableFlag;
+import android.os.Build;
+import android.os.ParcelFileDescriptor;
+
+import com.android.internal.flags.CoreFlags;
+import com.android.server.flags.FeatureFlagsService.PermissionsChecker;
+
+import java.io.FileOutputStream;
+import java.util.ArrayList;
+import java.util.List;
+
+class FeatureFlagsBinder extends IFeatureFlags.Stub {
+ private final FlagOverrideStore mFlagStore;
+ private final FlagsShellCommand mShellCommand;
+ private final FlagCache<String> mFlagCache = new FlagCache<>();
+ private final DynamicFlagBinderDelegate mDynamicFlagDelegate;
+ private final PermissionsChecker mPermissionsChecker;
+
+ FeatureFlagsBinder(
+ FlagOverrideStore flagStore,
+ FlagsShellCommand shellCommand,
+ PermissionsChecker permissionsChecker) {
+ mFlagStore = flagStore;
+ mShellCommand = shellCommand;
+ mDynamicFlagDelegate = new DynamicFlagBinderDelegate(flagStore);
+ mPermissionsChecker = permissionsChecker;
+ }
+
+ @Override
+ public void registerCallback(IFeatureFlagsCallback callback) {
+ mDynamicFlagDelegate.registerCallback(getCallingPid(), callback);
+ }
+
+ @Override
+ public void unregisterCallback(IFeatureFlagsCallback callback) {
+ mDynamicFlagDelegate.unregisterCallback(getCallingPid(), callback);
+ }
+
+ // Note: The internals of this method should be kept in sync with queryFlags
+ // as they both should return identical results. The difference is that this method
+ // caches any values it receives and/or reads, whereas queryFlags does not.
+
+ @Override
+ public List<SyncableFlag> syncFlags(List<SyncableFlag> incomingFlags) {
+ int pid = getCallingPid();
+ List<SyncableFlag> outputFlags = new ArrayList<>();
+
+ boolean hasFullSyncPrivileges = false;
+ SecurityException permissionFailureException = null;
+ try {
+ assertSyncPermission();
+ hasFullSyncPrivileges = true;
+ } catch (SecurityException e) {
+ permissionFailureException = e;
+ }
+
+ for (SyncableFlag sf : incomingFlags) {
+ if (!hasFullSyncPrivileges && !CoreFlags.isCoreFlag(sf)) {
+ throw permissionFailureException;
+ }
+
+ String ns = sf.getNamespace();
+ String name = sf.getName();
+ SyncableFlag outFlag;
+ if (sf.isDynamic()) {
+ outFlag = mDynamicFlagDelegate.syncDynamicFlag(pid, sf);
+ } else {
+ synchronized (mFlagCache) {
+ String value = mFlagCache.getOrNull(ns, name);
+ if (value == null) {
+ String overrideValue = Build.IS_USER ? null : mFlagStore.get(ns, name);
+ value = overrideValue != null ? overrideValue : sf.getValue();
+ mFlagCache.setIfChanged(ns, name, value);
+ }
+ outFlag = new SyncableFlag(sf.getNamespace(), sf.getName(), value, false);
+ }
+ }
+ outputFlags.add(outFlag);
+ }
+ return outputFlags;
+ }
+
+ @Override
+ public void overrideFlag(SyncableFlag flag) {
+ assertWritePermission();
+ mFlagStore.set(flag.getNamespace(), flag.getName(), flag.getValue());
+ }
+
+ @Override
+ public void resetFlag(SyncableFlag flag) {
+ assertWritePermission();
+ mFlagStore.erase(flag.getNamespace(), flag.getName());
+ }
+
+ @Override
+ public List<SyncableFlag> queryFlags(List<SyncableFlag> incomingFlags) {
+ assertSyncPermission();
+ List<SyncableFlag> outputFlags = new ArrayList<>();
+ for (SyncableFlag sf : incomingFlags) {
+ String ns = sf.getNamespace();
+ String name = sf.getName();
+ String value;
+ String storeValue = mFlagStore.get(ns, name);
+ boolean overridden = storeValue != null;
+
+ if (sf.isDynamic()) {
+ value = mDynamicFlagDelegate.getFlagValue(ns, name, sf.getValue());
+ } else {
+ value = mFlagCache.getOrNull(ns, name);
+ if (value == null) {
+ value = Build.IS_USER ? null : storeValue;
+ if (value == null) {
+ value = sf.getValue();
+ }
+ }
+ }
+ outputFlags.add(new SyncableFlag(
+ sf.getNamespace(), sf.getName(), value, sf.isDynamic(), overridden));
+ }
+
+ return outputFlags;
+ }
+
+ private void assertSyncPermission() {
+ mPermissionsChecker.assertSyncPermission();
+ clearCallingIdentity();
+ }
+
+ private void assertWritePermission() {
+ mPermissionsChecker.assertWritePermission();
+ clearCallingIdentity();
+ }
+
+
+ @SystemApi
+ public int handleShellCommand(
+ @NonNull ParcelFileDescriptor in,
+ @NonNull ParcelFileDescriptor out,
+ @NonNull ParcelFileDescriptor err,
+ @NonNull String[] args) {
+ FileOutputStream fout = new FileOutputStream(out.getFileDescriptor());
+ FileOutputStream ferr = new FileOutputStream(err.getFileDescriptor());
+
+ return mShellCommand.process(args, fout, ferr);
+ }
+}
diff --git a/services/flags/java/com/android/server/flags/FeatureFlagsService.java b/services/flags/java/com/android/server/flags/FeatureFlagsService.java
new file mode 100644
index 0000000..93b9e9e
--- /dev/null
+++ b/services/flags/java/com/android/server/flags/FeatureFlagsService.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2023 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.flags;
+
+import static android.Manifest.permission.SYNC_FLAGS;
+import static android.Manifest.permission.WRITE_FLAGS;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.flags.FeatureFlags;
+import android.util.Slog;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.SystemService;
+
+/**
+ * A service that manages syncing {@link android.flags.FeatureFlags} across processes.
+ *
+ * This service holds flags stable for at least the lifetime of a process, meaning that if
+ * a process comes online with a flag set to true, any other process that connects here and
+ * tries to read the same flag will also receive the flag as true. The flag will remain stable
+ * until either all of the interested processes have died, or the device restarts.
+ *
+ * TODO(279054964): Add to dumpsys
+ * @hide
+ */
+public class FeatureFlagsService extends SystemService {
+
+ static final String TAG = "FeatureFlagsService";
+ private final FlagOverrideStore mFlagStore;
+ private final FlagsShellCommand mShellCommand;
+
+ /**
+ * Initializes the system service.
+ *
+ * @param context The system server context.
+ */
+ public FeatureFlagsService(Context context) {
+ super(context);
+ mFlagStore = new FlagOverrideStore(
+ new GlobalSettingsProxy(context.getContentResolver()));
+ mShellCommand = new FlagsShellCommand(mFlagStore);
+ }
+
+ @Override
+ public void onStart() {
+ Slog.d(TAG, "Started Feature Flag Service");
+ FeatureFlagsBinder service = new FeatureFlagsBinder(
+ mFlagStore, mShellCommand, new PermissionsChecker(getContext()));
+ publishBinderService(
+ Context.FEATURE_FLAGS_SERVICE, service);
+ publishLocalService(FeatureFlags.class, new FeatureFlags(service));
+ }
+
+ @Override
+ public void onBootPhase(int phase) {
+ super.onBootPhase(phase);
+
+ if (phase == PHASE_SYSTEM_SERVICES_READY) {
+ // Immediately sync our core flags so that they get locked in. We don't want third-party
+ // apps to override them, and syncing immediately is the easiest way to prevent that.
+ FeatureFlags.getInstance().sync();
+ }
+ }
+
+ /**
+ * Delegate for checking flag permissions.
+ */
+ @VisibleForTesting
+ public static class PermissionsChecker {
+ private final Context mContext;
+
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+ public PermissionsChecker(Context context) {
+ mContext = context;
+ }
+
+ /**
+ * Ensures that the caller has {@link SYNC_FLAGS} permission.
+ */
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+ public void assertSyncPermission() {
+ if (mContext.checkCallingOrSelfPermission(SYNC_FLAGS)
+ != PackageManager.PERMISSION_GRANTED) {
+ throw new SecurityException(
+ "Non-core flag queried. Requires SYNC_FLAGS permission!");
+ }
+ }
+
+ /**
+ * Ensures that the caller has {@link WRITE_FLAGS} permission.
+ */
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+ public void assertWritePermission() {
+ if (mContext.checkCallingPermission(WRITE_FLAGS) != PackageManager.PERMISSION_GRANTED) {
+ throw new SecurityException("Requires WRITE_FLAGS permission!");
+ }
+ }
+ }
+}
diff --git a/services/flags/java/com/android/server/flags/FlagCache.java b/services/flags/java/com/android/server/flags/FlagCache.java
new file mode 100644
index 0000000..cee1578
--- /dev/null
+++ b/services/flags/java/com/android/server/flags/FlagCache.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2023 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.flags;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.Function;
+
+/**
+ * Threadsafe cache of values that stores the supplied default on cache miss.
+ *
+ * @param <V> The type of value to store.
+ */
+public class FlagCache<V> {
+ private final Function<String, HashMap<String, V>> mNewHashMap = k -> new HashMap<>();
+
+ // Cache is organized first by namespace, then by name. All values are stored as strings.
+ final Map<String, Map<String, V>> mCache = new HashMap<>();
+
+ FlagCache() {
+ }
+
+ /**
+ * Returns true if the namespace exists in the cache already.
+ */
+ boolean containsNamespace(String namespace) {
+ synchronized (mCache) {
+ return mCache.containsKey(namespace);
+ }
+ }
+
+ /**
+ * Returns true if the value is stored in the cache.
+ */
+ boolean contains(String namespace, String name) {
+ synchronized (mCache) {
+ Map<String, V> nsCache = mCache.get(namespace);
+ return nsCache != null && nsCache.containsKey(name);
+ }
+ }
+
+ /**
+ * Sets the value if it is different from what is currently stored.
+ *
+ * If the value is not set, or the current value is null, it will store the value and
+ * return true.
+ *
+ * @return True if the value was set. False if the value is the same.
+ */
+ boolean setIfChanged(String namespace, String name, V value) {
+ synchronized (mCache) {
+ Map<String, V> nsCache = mCache.computeIfAbsent(namespace, mNewHashMap);
+ V curValue = nsCache.get(name);
+ if (curValue == null || !curValue.equals(value)) {
+ nsCache.put(name, value);
+ return true;
+ }
+ return false;
+ }
+ }
+
+ /**
+ * Gets the current value from the cache, setting it if it is currently absent.
+ *
+ * @return The value that is now in the cache after the call to the method.
+ */
+ V getOrSet(String namespace, String name, V defaultValue) {
+ synchronized (mCache) {
+ Map<String, V> nsCache = mCache.computeIfAbsent(namespace, mNewHashMap);
+ V value = nsCache.putIfAbsent(name, defaultValue);
+ return value == null ? defaultValue : value;
+ }
+ }
+
+ /**
+ * Gets the current value from the cache, returning null if not present.
+ *
+ * @return The value that is now in the cache if there is one.
+ */
+ V getOrNull(String namespace, String name) {
+ synchronized (mCache) {
+ Map<String, V> nsCache = mCache.get(namespace);
+ if (nsCache == null) {
+ return null;
+ }
+ return nsCache.get(name);
+ }
+ }
+}
diff --git a/services/flags/java/com/android/server/flags/FlagOverrideStore.java b/services/flags/java/com/android/server/flags/FlagOverrideStore.java
new file mode 100644
index 0000000..b1ddc7e6
--- /dev/null
+++ b/services/flags/java/com/android/server/flags/FlagOverrideStore.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2023 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.flags;
+
+import android.database.Cursor;
+import android.provider.Settings;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Persistent storage for the {@link FeatureFlagsService}.
+ *
+ * The implementation stores data in Settings.<store> (generally {@link Settings.Global}
+ * is expected).
+ */
+@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+public class FlagOverrideStore {
+ private static final String KEYNAME_PREFIX = "flag|";
+ private static final String NAMESPACE_NAME_SEPARATOR = ".";
+
+ private final SettingsProxy mSettingsProxy;
+
+ private FlagChangeCallback mCallback;
+
+ FlagOverrideStore(SettingsProxy settingsProxy) {
+ mSettingsProxy = settingsProxy;
+ }
+
+ void setChangeCallback(FlagChangeCallback callback) {
+ mCallback = callback;
+ }
+
+ /** Returns true if a non-null value is in the store. */
+ boolean contains(String namespace, String name) {
+ return get(namespace, name) != null;
+ }
+
+ /** Put a value in the store. */
+ @VisibleForTesting
+ public void set(String namespace, String name, String value) {
+ mSettingsProxy.putString(getPropName(namespace, name), value);
+ mCallback.onFlagChanged(namespace, name, value);
+ }
+
+ /** Read a value out of the store. */
+ @VisibleForTesting
+ public String get(String namespace, String name) {
+ return mSettingsProxy.getString(getPropName(namespace, name));
+ }
+
+ /** Erase a value from the store. */
+ @VisibleForTesting
+ public void erase(String namespace, String name) {
+ set(namespace, name, null);
+ }
+
+ Map<String, Map<String, String>> getFlags() {
+ return getFlagsForNamespace(null);
+ }
+
+ Map<String, Map<String, String>> getFlagsForNamespace(String namespace) {
+ Cursor c = mSettingsProxy.getContentResolver().query(
+ Settings.Global.CONTENT_URI,
+ new String[]{Settings.NameValueTable.NAME, Settings.NameValueTable.VALUE},
+ null, // Doesn't support a "LIKE" query
+ null,
+ null
+ );
+
+ if (c == null) {
+ return Map.of();
+ }
+ int keynamePrefixLength = KEYNAME_PREFIX.length();
+ Map<String, Map<String, String>> results = new HashMap<>();
+ while (c.moveToNext()) {
+ String key = c.getString(0);
+ if (!key.startsWith(KEYNAME_PREFIX)
+ || key.indexOf(NAMESPACE_NAME_SEPARATOR, keynamePrefixLength) < 0) {
+ continue;
+ }
+ String value = c.getString(1);
+ if (value == null || value.isEmpty()) {
+ continue;
+ }
+ String ns = key.substring(keynamePrefixLength, key.indexOf(NAMESPACE_NAME_SEPARATOR));
+ if (namespace != null && !namespace.equals(ns)) {
+ continue;
+ }
+ String name = key.substring(key.indexOf(NAMESPACE_NAME_SEPARATOR) + 1);
+ results.putIfAbsent(ns, new HashMap<>());
+ results.get(ns).put(name, value);
+ }
+ c.close();
+ return results;
+ }
+
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
+ static String getPropName(String namespace, String name) {
+ return KEYNAME_PREFIX + namespace + NAMESPACE_NAME_SEPARATOR + name;
+ }
+
+ interface FlagChangeCallback {
+ void onFlagChanged(String namespace, String name, String value);
+ }
+}
diff --git a/services/flags/java/com/android/server/flags/FlagsShellCommand.java b/services/flags/java/com/android/server/flags/FlagsShellCommand.java
new file mode 100644
index 0000000..b7896ee
--- /dev/null
+++ b/services/flags/java/com/android/server/flags/FlagsShellCommand.java
@@ -0,0 +1,214 @@
+/*
+ * Copyright (C) 2023 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.flags;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.FastPrintWriter;
+
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.util.Locale;
+import java.util.Map;
+
+/**
+ * Process command line input for the flags service.
+ */
+@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+public class FlagsShellCommand {
+ private final FlagOverrideStore mFlagStore;
+
+ FlagsShellCommand(FlagOverrideStore flagStore) {
+ mFlagStore = flagStore;
+ }
+
+ /**
+ * Interpret the command supplied in the constructor.
+ *
+ * @return Zero on success or non-zero on error.
+ */
+ public int process(
+ String[] args,
+ OutputStream out,
+ OutputStream err) {
+ PrintWriter outPw = new FastPrintWriter(out);
+ PrintWriter errPw = new FastPrintWriter(err);
+
+ if (args.length == 0) {
+ return printHelp(outPw);
+ }
+ switch (args[0].toLowerCase(Locale.ROOT)) {
+ case "help":
+ return printHelp(outPw);
+ case "list":
+ return listCmd(args, outPw, errPw);
+ case "set":
+ return setCmd(args, outPw, errPw);
+ case "get":
+ return getCmd(args, outPw, errPw);
+ case "erase":
+ return eraseCmd(args, outPw, errPw);
+ default:
+ return unknownCmd(outPw);
+ }
+ }
+
+ private int printHelp(PrintWriter outPw) {
+ outPw.println("Feature Flags command, allowing listing, setting, getting, and erasing of");
+ outPw.println("local flag overrides on a device.");
+ outPw.println();
+ outPw.println("Commands:");
+ outPw.println(" list [namespace]");
+ outPw.println(" List all flag overrides. Namespace is optional.");
+ outPw.println();
+ outPw.println(" get <namespace> <name>");
+ outPw.println(" Return the string value of a specific flag, or <unset>");
+ outPw.println();
+ outPw.println(" set <namespace> <name> <value>");
+ outPw.println(" Set a specific flag");
+ outPw.println();
+ outPw.println(" erase <namespace> <name>");
+ outPw.println(" Unset a specific flag");
+ outPw.flush();
+ return 0;
+ }
+
+ private int listCmd(String[] args, PrintWriter outPw, PrintWriter errPw) {
+ if (!validateNumArguments(args, 0, 1, args[0], errPw)) {
+ errPw.println("Expected `" + args[0] + " [namespace]`");
+ errPw.flush();
+ return -1;
+ }
+ Map<String, Map<String, String>> overrides;
+ if (args.length == 2) {
+ overrides = mFlagStore.getFlagsForNamespace(args[1]);
+ } else {
+ overrides = mFlagStore.getFlags();
+ }
+ if (overrides.isEmpty()) {
+ outPw.println("No overrides set");
+ } else {
+ int longestNamespaceLen = "namespace".length();
+ int longestFlagLen = "flag".length();
+ int longestValLen = "value".length();
+ for (Map.Entry<String, Map<String, String>> namespace : overrides.entrySet()) {
+ longestNamespaceLen = Math.max(longestNamespaceLen, namespace.getKey().length());
+ for (Map.Entry<String, String> flag : namespace.getValue().entrySet()) {
+ longestFlagLen = Math.max(longestFlagLen, flag.getKey().length());
+ longestValLen = Math.max(longestValLen, flag.getValue().length());
+ }
+ }
+ outPw.print(String.format("%-" + longestNamespaceLen + "s", "namespace"));
+ outPw.print(' ');
+ outPw.print(String.format("%-" + longestFlagLen + "s", "flag"));
+ outPw.print(' ');
+ outPw.println("value");
+ for (int i = 0; i < longestNamespaceLen; i++) {
+ outPw.print('=');
+ }
+ outPw.print(' ');
+ for (int i = 0; i < longestFlagLen; i++) {
+ outPw.print('=');
+ }
+ outPw.print(' ');
+ for (int i = 0; i < longestValLen; i++) {
+ outPw.print('=');
+ }
+ outPw.println();
+ for (Map.Entry<String, Map<String, String>> namespace : overrides.entrySet()) {
+ for (Map.Entry<String, String> flag : namespace.getValue().entrySet()) {
+ outPw.print(
+ String.format("%-" + longestNamespaceLen + "s", namespace.getKey()));
+ outPw.print(' ');
+ outPw.print(String.format("%-" + longestFlagLen + "s", flag.getKey()));
+ outPw.print(' ');
+ outPw.println(flag.getValue());
+ }
+ }
+ }
+ outPw.flush();
+ return 0;
+ }
+
+ private int setCmd(String[] args, PrintWriter outPw, PrintWriter errPw) {
+ if (!validateNumArguments(args, 3, args[0], errPw)) {
+ errPw.println("Expected `" + args[0] + " <namespace> <name> <value>`");
+ errPw.flush();
+ return -1;
+ }
+ mFlagStore.set(args[1], args[2], args[3]);
+ outPw.println("Flag " + args[1] + "." + args[2] + " is now " + args[3]);
+ outPw.flush();
+ return 0;
+ }
+
+ private int getCmd(String[] args, PrintWriter outPw, PrintWriter errPw) {
+ if (!validateNumArguments(args, 2, args[0], errPw)) {
+ errPw.println("Expected `" + args[0] + " <namespace> <name>`");
+ errPw.flush();
+ return -1;
+ }
+
+ String value = mFlagStore.get(args[1], args[2]);
+ outPw.print(args[1] + "." + args[2] + " is ");
+ if (value == null || value.isEmpty()) {
+ outPw.println("<unset>");
+ } else {
+ outPw.println("\"" + value.translateEscapes() + "\"");
+ }
+ outPw.flush();
+ return 0;
+ }
+
+ private int eraseCmd(String[] args, PrintWriter outPw, PrintWriter errPw) {
+ if (!validateNumArguments(args, 2, args[0], errPw)) {
+ errPw.println("Expected `" + args[0] + " <namespace> <name>`");
+ errPw.flush();
+ return -1;
+ }
+ mFlagStore.erase(args[1], args[2]);
+ outPw.println("Erased " + args[1] + "." + args[2]);
+ return 0;
+ }
+
+ private int unknownCmd(PrintWriter outPw) {
+ outPw.println("This command is unknown.");
+ printHelp(outPw);
+ outPw.flush();
+ return -1;
+ }
+
+ private boolean validateNumArguments(
+ String[] args, int exactly, String cmdName, PrintWriter errPw) {
+ return validateNumArguments(args, exactly, exactly, cmdName, errPw);
+ }
+
+ private boolean validateNumArguments(
+ String[] args, int min, int max, String cmdName, PrintWriter errPw) {
+ int len = args.length - 1; // Discount the command itself.
+ if (len < min) {
+ errPw.println(
+ "Less than " + min + " arguments provided for \"" + cmdName + "\" command.");
+ return false;
+ } else if (len > max) {
+ errPw.println(
+ "More than " + max + " arguments provided for \"" + cmdName + "\" command.");
+ return false;
+ }
+
+ return true;
+ }
+}
diff --git a/services/flags/java/com/android/server/flags/GlobalSettingsProxy.java b/services/flags/java/com/android/server/flags/GlobalSettingsProxy.java
new file mode 100644
index 0000000..acb7bb5
--- /dev/null
+++ b/services/flags/java/com/android/server/flags/GlobalSettingsProxy.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2023 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.flags;
+
+import android.content.ContentResolver;
+import android.net.Uri;
+import android.provider.Settings;
+
+class GlobalSettingsProxy implements SettingsProxy {
+ private final ContentResolver mContentResolver;
+
+ GlobalSettingsProxy(ContentResolver contentResolver) {
+ mContentResolver = contentResolver;
+ }
+
+ @Override
+ public ContentResolver getContentResolver() {
+ return mContentResolver;
+ }
+
+ @Override
+ public Uri getUriFor(String name) {
+ return Settings.Global.getUriFor(name);
+ }
+
+ @Override
+ public String getStringForUser(String name, int userHandle) {
+ return Settings.Global.getStringForUser(mContentResolver, name, userHandle);
+ }
+
+ @Override
+ public boolean putString(String name, String value, boolean overrideableByRestore) {
+ throw new UnsupportedOperationException(
+ "This method only exists publicly for Settings.System and Settings.Secure");
+ }
+
+ @Override
+ public boolean putStringForUser(String name, String value, int userHandle) {
+ return Settings.Global.putStringForUser(mContentResolver, name, value, userHandle);
+ }
+
+ @Override
+ public boolean putStringForUser(String name, String value, String tag, boolean makeDefault,
+ int userHandle, boolean overrideableByRestore) {
+ return Settings.Global.putStringForUser(
+ mContentResolver, name, value, tag, makeDefault, userHandle,
+ overrideableByRestore);
+ }
+
+ @Override
+ public boolean putString(String name, String value, String tag, boolean makeDefault) {
+ return Settings.Global.putString(mContentResolver, name, value, tag, makeDefault);
+ }
+}
diff --git a/services/flags/java/com/android/server/flags/SettingsProxy.java b/services/flags/java/com/android/server/flags/SettingsProxy.java
new file mode 100644
index 0000000..c6e85d5
--- /dev/null
+++ b/services/flags/java/com/android/server/flags/SettingsProxy.java
@@ -0,0 +1,381 @@
+/*
+ * Copyright (C) 2023 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.flags;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.UserIdInt;
+import android.content.ContentResolver;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.provider.Settings;
+
+/**
+ * Wrapper class meant to enable hermetic testing of {@link Settings}.
+ *
+ * Implementations of this class are expected to be constructed with a {@link ContentResolver} or,
+ * otherwise have access to an implicit one. All the proxy methods in this class exclude
+ * {@link ContentResolver} from their signature and rely on an internally defined one instead.
+ *
+ * Most methods in the {@link Settings} classes have default implementations defined.
+ * Implementations of this interfac need only concern themselves with getting and putting Strings.
+ * They should also override any methods for a class they are proxying that _are not_ defined, and
+ * throw an appropriate {@link UnsupportedOperationException}. For instance, {@link Settings.Global}
+ * does not define {@link #putString(String, String, boolean)}, so an implementation of this
+ * interface that proxies through to it should throw an exception when that method is called.
+ *
+ * This class adds in the following helpers as well:
+ * - {@link #getBool(String)}
+ * - {@link #putBool(String, boolean)}
+ * - {@link #registerContentObserver(Uri, ContentObserver)}
+ *
+ * ... and similar variations for all of those.
+ */
+public interface SettingsProxy {
+
+ /**
+ * Returns the {@link ContentResolver} this instance uses.
+ */
+ ContentResolver getContentResolver();
+
+ /**
+ * Construct the content URI for a particular name/value pair,
+ * useful for monitoring changes with a ContentObserver.
+ * @param name to look up in the table
+ * @return the corresponding content URI, or null if not present
+ */
+ Uri getUriFor(String name);
+
+ /**See {@link Settings.Secure#getString(ContentResolver, String)} */
+ String getStringForUser(String name, int userHandle);
+
+ /**See {@link Settings.Secure#putString(ContentResolver, String, String, boolean)} */
+ boolean putString(String name, String value, boolean overrideableByRestore);
+
+ /** See {@link Settings.Secure#putStringForUser(ContentResolver, String, String, int)} */
+ boolean putStringForUser(String name, String value, int userHandle);
+
+ /**
+ * See {@link Settings.Secure#putStringForUser(ContentResolver, String, String, String, boolean,
+ * int, boolean)}
+ */
+ boolean putStringForUser(@NonNull String name, @Nullable String value, @Nullable String tag,
+ boolean makeDefault, @UserIdInt int userHandle, boolean overrideableByRestore);
+
+ /** See {@link Settings.Secure#putString(ContentResolver, String, String, String, boolean)} */
+ boolean putString(@NonNull String name, @Nullable String value, @Nullable String tag,
+ boolean makeDefault);
+
+ /**
+ * Returns the user id for the associated {@link ContentResolver}.
+ */
+ default int getUserId() {
+ return getContentResolver().getUserId();
+ }
+
+ /** See {@link Settings.Secure#getString(ContentResolver, String)} */
+ default String getString(String name) {
+ return getStringForUser(name, getUserId());
+ }
+
+ /** See {@link Settings.Secure#putString(ContentResolver, String, String)} */
+ default boolean putString(String name, String value) {
+ return putStringForUser(name, value, getUserId());
+ }
+ /** See {@link Settings.Secure#getIntForUser(ContentResolver, String, int, int)} */
+ default int getIntForUser(String name, int def, int userHandle) {
+ String v = getStringForUser(name, userHandle);
+ try {
+ return v != null ? Integer.parseInt(v) : def;
+ } catch (NumberFormatException e) {
+ return def;
+ }
+ }
+
+ /** See {@link Settings.Secure#getInt(ContentResolver, String)} */
+ default int getInt(String name) throws Settings.SettingNotFoundException {
+ return getIntForUser(name, getUserId());
+ }
+
+ /** See {@link Settings.Secure#getIntForUser(ContentResolver, String, int)} */
+ default int getIntForUser(String name, int userHandle)
+ throws Settings.SettingNotFoundException {
+ String v = getStringForUser(name, userHandle);
+ try {
+ return Integer.parseInt(v);
+ } catch (NumberFormatException e) {
+ throw new Settings.SettingNotFoundException(name);
+ }
+ }
+
+ /** See {@link Settings.Secure#putInt(ContentResolver, String, int)} */
+ default boolean putInt(String name, int value) {
+ return putIntForUser(name, value, getUserId());
+ }
+
+ /** See {@link Settings.Secure#putIntForUser(ContentResolver, String, int, int)} */
+ default boolean putIntForUser(String name, int value, int userHandle) {
+ return putStringForUser(name, Integer.toString(value), userHandle);
+ }
+
+ /**
+ * Convenience function for retrieving a single settings value
+ * as a boolean. Note that internally setting values are always
+ * stored as strings; this function converts the string to a boolean
+ * for you. The default value will be returned if the setting is
+ * not defined or not a boolean.
+ *
+ * @param name The name of the setting to retrieve.
+ * @param def Value to return if the setting is not defined.
+ *
+ * @return The setting's current value, or 'def' if it is not defined
+ * or not a valid boolean.
+ */
+ default boolean getBool(String name, boolean def) {
+ return getBoolForUser(name, def, getUserId());
+ }
+
+ /** See {@link #getBool(String, boolean)}. */
+ default boolean getBoolForUser(String name, boolean def, int userHandle) {
+ return getIntForUser(name, def ? 1 : 0, userHandle) != 0;
+ }
+
+ /**
+ * Convenience function for retrieving a single settings value
+ * as a boolean. Note that internally setting values are always
+ * stored as strings; this function converts the string to a boolean
+ * for you.
+ * <p>
+ * This version does not take a default value. If the setting has not
+ * been set, or the string value is not a number,
+ * it throws {@link Settings.SettingNotFoundException}.
+ *
+ * @param name The name of the setting to retrieve.
+ *
+ * @throws Settings.SettingNotFoundException Thrown if a setting by the given
+ * name can't be found or the setting value is not a boolean.
+ *
+ * @return The setting's current value.
+ */
+ default boolean getBool(String name) throws Settings.SettingNotFoundException {
+ return getBoolForUser(name, getUserId());
+ }
+
+ /** See {@link #getBool(String)}. */
+ default boolean getBoolForUser(String name, int userHandle)
+ throws Settings.SettingNotFoundException {
+ return getIntForUser(name, userHandle) != 0;
+ }
+
+ /**
+ * Convenience function for updating a single settings value as a
+ * boolean. This will either create a new entry in the table if the
+ * given name does not exist, or modify the value of the existing row
+ * with that name. Note that internally setting values are always
+ * stored as strings, so this function converts the given value to a
+ * string before storing it.
+ *
+ * @param name The name of the setting to modify.
+ * @param value The new value for the setting.
+ * @return true if the value was set, false on database errors
+ */
+ default boolean putBool(String name, boolean value) {
+ return putBoolForUser(name, value, getUserId());
+ }
+
+ /** See {@link #putBool(String, boolean)}. */
+ default boolean putBoolForUser(String name, boolean value, int userHandle) {
+ return putIntForUser(name, value ? 1 : 0, userHandle);
+ }
+
+ /** See {@link Settings.Secure#getLong(ContentResolver, String, long)} */
+ default long getLong(String name, long def) {
+ return getLongForUser(name, def, getUserId());
+ }
+
+ /** See {@link Settings.Secure#getLongForUser(ContentResolver, String, long, int)} */
+ default long getLongForUser(String name, long def, int userHandle) {
+ String valString = getStringForUser(name, userHandle);
+ long value;
+ try {
+ value = valString != null ? Long.parseLong(valString) : def;
+ } catch (NumberFormatException e) {
+ value = def;
+ }
+ return value;
+ }
+
+ /** See {@link Settings.Secure#getLong(ContentResolver, String)} */
+ default long getLong(String name) throws Settings.SettingNotFoundException {
+ return getLongForUser(name, getUserId());
+ }
+
+ /** See {@link Settings.Secure#getLongForUser(ContentResolver, String, int)} */
+ default long getLongForUser(String name, int userHandle)
+ throws Settings.SettingNotFoundException {
+ String valString = getStringForUser(name, userHandle);
+ try {
+ return Long.parseLong(valString);
+ } catch (NumberFormatException e) {
+ throw new Settings.SettingNotFoundException(name);
+ }
+ }
+
+ /** See {@link Settings.Secure#putLong(ContentResolver, String, long)} */
+ default boolean putLong(String name, long value) {
+ return putLongForUser(name, value, getUserId());
+ }
+
+ /** See {@link Settings.Secure#putLongForUser(ContentResolver, String, long, int)} */
+ default boolean putLongForUser(String name, long value, int userHandle) {
+ return putStringForUser(name, Long.toString(value), userHandle);
+ }
+
+ /** See {@link Settings.Secure#getFloat(ContentResolver, String, float)} */
+ default float getFloat(String name, float def) {
+ return getFloatForUser(name, def, getUserId());
+ }
+
+ /** See {@link Settings.Secure#getFloatForUser(ContentResolver, String, int)} */
+ default float getFloatForUser(String name, float def, int userHandle) {
+ String v = getStringForUser(name, userHandle);
+ try {
+ return v != null ? Float.parseFloat(v) : def;
+ } catch (NumberFormatException e) {
+ return def;
+ }
+ }
+
+
+ /** See {@link Settings.Secure#getFloat(ContentResolver, String)} */
+ default float getFloat(String name) throws Settings.SettingNotFoundException {
+ return getFloatForUser(name, getUserId());
+ }
+
+ /** See {@link Settings.Secure#getFloatForUser(ContentResolver, String, int)} */
+ default float getFloatForUser(String name, int userHandle)
+ throws Settings.SettingNotFoundException {
+ String v = getStringForUser(name, userHandle);
+ if (v == null) {
+ throw new Settings.SettingNotFoundException(name);
+ }
+ try {
+ return Float.parseFloat(v);
+ } catch (NumberFormatException e) {
+ throw new Settings.SettingNotFoundException(name);
+ }
+ }
+
+ /** See {@link Settings.Secure#putFloat(ContentResolver, String, float)} */
+ default boolean putFloat(String name, float value) {
+ return putFloatForUser(name, value, getUserId());
+ }
+
+ /** See {@link Settings.Secure#putFloatForUser(ContentResolver, String, float, int)} */
+ default boolean putFloatForUser(String name, float value, int userHandle) {
+ return putStringForUser(name, Float.toString(value), userHandle);
+ }
+
+ /**
+ * Convenience wrapper around
+ * {@link ContentResolver#registerContentObserver(Uri, boolean, ContentObserver)}.'
+ *
+ * Implicitly calls {@link #getUriFor(String)} on the passed in name.
+ */
+ default void registerContentObserver(String name, ContentObserver settingsObserver) {
+ registerContentObserver(getUriFor(name), settingsObserver);
+ }
+
+ /**
+ * Convenience wrapper around
+ * {@link ContentResolver#registerContentObserver(Uri, boolean, ContentObserver)}.'
+ */
+ default void registerContentObserver(Uri uri, ContentObserver settingsObserver) {
+ registerContentObserverForUser(uri, settingsObserver, getUserId());
+ }
+
+ /**
+ * Convenience wrapper around
+ * {@link ContentResolver#registerContentObserver(Uri, boolean, ContentObserver)}.
+ *
+ * Implicitly calls {@link #getUriFor(String)} on the passed in name.
+ */
+ default void registerContentObserver(String name, boolean notifyForDescendants,
+ ContentObserver settingsObserver) {
+ registerContentObserver(getUriFor(name), notifyForDescendants, settingsObserver);
+ }
+
+ /**
+ * Convenience wrapper around
+ * {@link ContentResolver#registerContentObserver(Uri, boolean, ContentObserver)}.'
+ */
+ default void registerContentObserver(Uri uri, boolean notifyForDescendants,
+ ContentObserver settingsObserver) {
+ registerContentObserverForUser(uri, notifyForDescendants, settingsObserver, getUserId());
+ }
+
+ /**
+ * Convenience wrapper around
+ * {@link ContentResolver#registerContentObserver(Uri, boolean, ContentObserver, int)}
+ *
+ * Implicitly calls {@link #getUriFor(String)} on the passed in name.
+ */
+ default void registerContentObserverForUser(
+ String name, ContentObserver settingsObserver, int userHandle) {
+ registerContentObserverForUser(
+ getUriFor(name), settingsObserver, userHandle);
+ }
+
+ /**
+ * Convenience wrapper around
+ * {@link ContentResolver#registerContentObserver(Uri, boolean, ContentObserver, int)}
+ */
+ default void registerContentObserverForUser(
+ Uri uri, ContentObserver settingsObserver, int userHandle) {
+ registerContentObserverForUser(
+ uri, false, settingsObserver, userHandle);
+ }
+
+ /**
+ * Convenience wrapper around
+ * {@link ContentResolver#registerContentObserver(Uri, boolean, ContentObserver, int)}
+ *
+ * Implicitly calls {@link #getUriFor(String)} on the passed in name.
+ */
+ default void registerContentObserverForUser(
+ String name, boolean notifyForDescendants, ContentObserver settingsObserver,
+ int userHandle) {
+ registerContentObserverForUser(
+ getUriFor(name), notifyForDescendants, settingsObserver, userHandle);
+ }
+
+ /**
+ * Convenience wrapper around
+ * {@link ContentResolver#registerContentObserver(Uri, boolean, ContentObserver, int)}
+ */
+ default void registerContentObserverForUser(
+ Uri uri, boolean notifyForDescendants, ContentObserver settingsObserver,
+ int userHandle) {
+ getContentResolver().registerContentObserver(
+ uri, notifyForDescendants, settingsObserver, userHandle);
+ }
+
+ /** See {@link ContentResolver#unregisterContentObserver(ContentObserver)}. */
+ default void unregisterContentObserver(ContentObserver settingsObserver) {
+ getContentResolver().unregisterContentObserver(settingsObserver);
+ }
+}
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index 2f2e1cb..9d8f820 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -133,6 +133,7 @@
import com.android.server.display.color.ColorDisplayService;
import com.android.server.dreams.DreamManagerService;
import com.android.server.emergency.EmergencyAffordanceService;
+import com.android.server.flags.FeatureFlagsService;
import com.android.server.gpu.GpuService;
import com.android.server.grammaticalinflection.GrammaticalInflectionService;
import com.android.server.graphics.fonts.FontManagerService;
@@ -1113,6 +1114,12 @@
mSystemServiceManager.startService(DeviceIdentifiersPolicyService.class);
t.traceEnd();
+ // Starts a service for reading runtime flag overrides, and keeping processes
+ // in sync with one another.
+ t.traceBegin("StartFeatureFlagsService");
+ mSystemServiceManager.startService(FeatureFlagsService.class);
+ t.traceEnd();
+
// Uri Grants Manager.
t.traceBegin("UriGrantsManagerService");
mSystemServiceManager.startService(UriGrantsManagerService.Lifecycle.class);
diff --git a/services/midi/java/com/android/server/midi/MidiService.java b/services/midi/java/com/android/server/midi/MidiService.java
index f660b42..5e7dd59 100644
--- a/services/midi/java/com/android/server/midi/MidiService.java
+++ b/services/midi/java/com/android/server/midi/MidiService.java
@@ -85,7 +85,7 @@
//TODO Introduce a single lock object to lock the whole state and avoid the requirement above.
// All users should be able to connect to USB and Bluetooth MIDI devices.
-// All users can create can install an app that provides, a Virtual MIDI Device Service.
+// All users can create can install an app that provides a Virtual MIDI Device Service.
// Users can not open virtual MIDI devices created by other users.
// getDevices() surfaces devices that can be opened by that user.
// openDevice() rejects devices that are cannot be opened by that user.
diff --git a/services/tests/InputMethodSystemServerTests/AndroidManifest.xml b/services/tests/InputMethodSystemServerTests/AndroidManifest.xml
index 212ec14..bef56ce 100644
--- a/services/tests/InputMethodSystemServerTests/AndroidManifest.xml
+++ b/services/tests/InputMethodSystemServerTests/AndroidManifest.xml
@@ -17,7 +17,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.frameworks.inputmethodtests">
- <uses-sdk android:targetSdkVersion="31" />
<queries>
<intent>
<action android:name="android.view.InputMethod" />
diff --git a/services/tests/InputMethodSystemServerTests/TEST_MAPPING b/services/tests/InputMethodSystemServerTests/TEST_MAPPING
index 77e32a7..cedbfd2b 100644
--- a/services/tests/InputMethodSystemServerTests/TEST_MAPPING
+++ b/services/tests/InputMethodSystemServerTests/TEST_MAPPING
@@ -9,5 +9,16 @@
{"exclude-annotation": "org.junit.Ignore"}
]
}
+ ],
+ "postsubmit": [
+ {
+ "name": "FrameworksImeTests",
+ "options": [
+ {"include-filter": "com.android.inputmethodservice"},
+ {"exclude-annotation": "android.platform.test.annotations.FlakyTest"},
+ {"exclude-annotation": "androidx.test.filters.FlakyTest"},
+ {"exclude-annotation": "org.junit.Ignore"}
+ ]
+ }
]
}
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/AndroidManifest.xml b/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/AndroidManifest.xml
index 0104f71..b7de749 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/AndroidManifest.xml
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/AndroidManifest.xml
@@ -18,8 +18,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.inputmethod.imetests">
- <uses-sdk android:targetSdkVersion="31" />
-
<!-- Permissions required for granting and logging -->
<uses-permission android:name="android.permission.LOG_COMPAT_CHANGE"/>
<uses-permission android:name="android.permission.READ_COMPAT_CHANGE_CONFIG"/>
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java
index 898658e..e8acb06 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java
@@ -20,6 +20,8 @@
import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+
import android.app.Instrumentation;
import android.content.Context;
import android.content.res.Configuration;
@@ -45,6 +47,7 @@
import org.junit.Test;
import org.junit.runner.RunWith;
+import java.io.IOException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
@@ -56,9 +59,9 @@
private static final String EDIT_TEXT_DESC = "Input box";
private static final long TIMEOUT_IN_SECONDS = 3;
private static final String ENABLE_SHOW_IME_WITH_HARD_KEYBOARD_CMD =
- "settings put secure show_ime_with_hard_keyboard 1";
+ "settings put secure " + Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD + " 1";
private static final String DISABLE_SHOW_IME_WITH_HARD_KEYBOARD_CMD =
- "settings put secure show_ime_with_hard_keyboard 0";
+ "settings put secure " + Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD + " 0";
private Instrumentation mInstrumentation;
private UiDevice mUiDevice;
@@ -82,29 +85,19 @@
mUiDevice.freezeRotation();
mUiDevice.setOrientationNatural();
// Waits for input binding ready.
- eventually(
- () -> {
- mInputMethodService =
- InputMethodServiceWrapper.getInputMethodServiceWrapperForTesting();
- assertThat(mInputMethodService).isNotNull();
+ eventually(() -> {
+ mInputMethodService =
+ InputMethodServiceWrapper.getInputMethodServiceWrapperForTesting();
+ assertThat(mInputMethodService).isNotNull();
- // The editor won't bring up keyboard by default.
- assertThat(mInputMethodService.getCurrentInputStarted()).isTrue();
- assertThat(mInputMethodService.getCurrentInputViewStarted()).isFalse();
- });
- // Save the original value of show_ime_with_hard_keyboard in Settings.
+ // The editor won't bring up keyboard by default.
+ assertThat(mInputMethodService.getCurrentInputStarted()).isTrue();
+ assertThat(mInputMethodService.getCurrentInputViewStarted()).isFalse();
+ });
+ // Save the original value of show_ime_with_hard_keyboard from Settings.
mShowImeWithHardKeyboardEnabled = Settings.Secure.getInt(
mInputMethodService.getContentResolver(),
Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD, 0) != 0;
- // Disable showing Ime with hard keyboard because it is the precondition the for most test
- // cases
- if (mShowImeWithHardKeyboardEnabled) {
- executeShellCommand(DISABLE_SHOW_IME_WITH_HARD_KEYBOARD_CMD);
- }
- mInputMethodService.getResources().getConfiguration().keyboard =
- Configuration.KEYBOARD_NOKEYS;
- mInputMethodService.getResources().getConfiguration().hardKeyboardHidden =
- Configuration.HARDKEYBOARDHIDDEN_YES;
}
@After
@@ -112,82 +105,141 @@
mUiDevice.unfreezeRotation();
executeShellCommand("ime disable " + mInputMethodId);
// Change back the original value of show_ime_with_hard_keyboard in Settings.
- executeShellCommand(mShowImeWithHardKeyboardEnabled ? ENABLE_SHOW_IME_WITH_HARD_KEYBOARD_CMD
+ executeShellCommand(mShowImeWithHardKeyboardEnabled
+ ? ENABLE_SHOW_IME_WITH_HARD_KEYBOARD_CMD
: DISABLE_SHOW_IME_WITH_HARD_KEYBOARD_CMD);
}
+ /**
+ * This checks that the IME can be shown and hidden by user actions
+ * (i.e. tapping on an EditText, tapping the Home button).
+ */
@Test
- public void testShowHideKeyboard_byUserAction() throws InterruptedException {
+ public void testShowHideKeyboard_byUserAction() throws Exception {
+ setShowImeWithHardKeyboard(true /* enabled */);
+
// Performs click on editor box to bring up the soft keyboard.
Log.i(TAG, "Click on EditText.");
- verifyInputViewStatus(() -> clickOnEditorText(), true /* inputViewStarted */);
-
- // Press back key to hide soft keyboard.
- Log.i(TAG, "Press back");
verifyInputViewStatus(
- () -> assertThat(mUiDevice.pressHome()).isTrue(), false /* inputViewStarted */);
+ () -> clickOnEditorText(),
+ true /* expected */,
+ true /* inputViewStarted */);
+ assertThat(mInputMethodService.isInputViewShown()).isTrue();
+
+ // Press home key to hide soft keyboard.
+ Log.i(TAG, "Press home");
+ verifyInputViewStatus(
+ () -> assertThat(mUiDevice.pressHome()).isTrue(),
+ true /* expected */,
+ false /* inputViewStarted */);
+ assertThat(mInputMethodService.isInputViewShown()).isFalse();
}
+ /**
+ * This checks that the IME can be shown and hidden using the WindowInsetsController APIs.
+ */
@Test
- public void testShowHideKeyboard_byApi() throws InterruptedException {
+ public void testShowHideKeyboard_byApi() throws Exception {
+ setShowImeWithHardKeyboard(true /* enabled */);
+
// Triggers to show IME via public API.
verifyInputViewStatus(
() -> assertThat(mActivity.showImeWithWindowInsetsController()).isTrue(),
+ true /* expected */,
true /* inputViewStarted */);
+ assertThat(mInputMethodService.isInputViewShown()).isTrue();
// Triggers to hide IME via public API.
verifyInputViewStatusOnMainSync(
- () -> assertThat(mActivity.hideImeWithInputMethodManager(0 /* flags */)).isTrue(),
+ () -> assertThat(mActivity.hideImeWithWindowInsetsController()).isTrue(),
+ true /* expected */,
false /* inputViewStarted */);
+ assertThat(mInputMethodService.isInputViewShown()).isFalse();
}
+ /**
+ * This checks the result of calling IMS#requestShowSelf and IMS#requestHideSelf.
+ */
@Test
- public void testShowHideSelf() throws InterruptedException {
- // IME requests to show itself without any flags: expect shown.
+ public void testShowHideSelf() throws Exception {
+ setShowImeWithHardKeyboard(true /* enabled */);
+
+ // IME request to show itself without any flags, expect shown.
Log.i(TAG, "Call IMS#requestShowSelf(0)");
verifyInputViewStatusOnMainSync(
- () -> mInputMethodService.requestShowSelf(0), true /* inputViewStarted */);
+ () -> mInputMethodService.requestShowSelf(0 /* flags */),
+ true /* expected */,
+ true /* inputViewStarted */);
+ assertThat(mInputMethodService.isInputViewShown()).isTrue();
- // IME requests to hide itself with flag: HIDE_IMPLICIT_ONLY, expect not hide (shown).
+ // IME request to hide itself with flag HIDE_IMPLICIT_ONLY, expect not hide (shown).
Log.i(TAG, "Call IMS#requestHideSelf(InputMethodManager.HIDE_IMPLICIT_ONLY)");
verifyInputViewStatusOnMainSync(
() -> mInputMethodService.requestHideSelf(InputMethodManager.HIDE_IMPLICIT_ONLY),
+ false /* expected */,
true /* inputViewStarted */);
+ assertThat(mInputMethodService.isInputViewShown()).isTrue();
- // IME request to hide itself without any flags: expect hidden.
+ // IME request to hide itself without any flags, expect hidden.
Log.i(TAG, "Call IMS#requestHideSelf(0)");
verifyInputViewStatusOnMainSync(
- () -> mInputMethodService.requestHideSelf(0), false /* inputViewStarted */);
+ () -> mInputMethodService.requestHideSelf(0 /* flags */),
+ true /* expected */,
+ false /* inputViewStarted */);
+ assertThat(mInputMethodService.isInputViewShown()).isFalse();
- // IME request to show itself with flag SHOW_IMPLICIT: expect shown.
+ // IME request to show itself with flag SHOW_IMPLICIT, expect shown.
Log.i(TAG, "Call IMS#requestShowSelf(InputMethodManager.SHOW_IMPLICIT)");
verifyInputViewStatusOnMainSync(
() -> mInputMethodService.requestShowSelf(InputMethodManager.SHOW_IMPLICIT),
+ true /* expected */,
true /* inputViewStarted */);
+ assertThat(mInputMethodService.isInputViewShown()).isTrue();
- // IME request to hide itself with flag: HIDE_IMPLICIT_ONLY, expect hidden.
+ // IME request to hide itself with flag HIDE_IMPLICIT_ONLY, expect hidden.
Log.i(TAG, "Call IMS#requestHideSelf(InputMethodManager.HIDE_IMPLICIT_ONLY)");
verifyInputViewStatusOnMainSync(
() -> mInputMethodService.requestHideSelf(InputMethodManager.HIDE_IMPLICIT_ONLY),
+ true /* expected */,
false /* inputViewStarted */);
+ assertThat(mInputMethodService.isInputViewShown()).isFalse();
}
+ /**
+ * This checks the return value of IMS#onEvaluateInputViewShown,
+ * when show_ime_with_hard_keyboard is enabled.
+ */
@Test
public void testOnEvaluateInputViewShown_showImeWithHardKeyboard() throws Exception {
- executeShellCommand(ENABLE_SHOW_IME_WITH_HARD_KEYBOARD_CMD);
- mInstrumentation.waitForIdleSync();
+ setShowImeWithHardKeyboard(true /* enabled */);
- // Simulate connecting a hard keyboard
mInputMethodService.getResources().getConfiguration().keyboard =
Configuration.KEYBOARD_QWERTY;
mInputMethodService.getResources().getConfiguration().hardKeyboardHidden =
Configuration.HARDKEYBOARDHIDDEN_NO;
+ eventually(() -> assertThat(mInputMethodService.onEvaluateInputViewShown()).isTrue());
+ mInputMethodService.getResources().getConfiguration().keyboard =
+ Configuration.KEYBOARD_NOKEYS;
+ mInputMethodService.getResources().getConfiguration().hardKeyboardHidden =
+ Configuration.HARDKEYBOARDHIDDEN_NO;
+ eventually(() -> assertThat(mInputMethodService.onEvaluateInputViewShown()).isTrue());
+
+ mInputMethodService.getResources().getConfiguration().keyboard =
+ Configuration.KEYBOARD_QWERTY;
+ mInputMethodService.getResources().getConfiguration().hardKeyboardHidden =
+ Configuration.HARDKEYBOARDHIDDEN_YES;
eventually(() -> assertThat(mInputMethodService.onEvaluateInputViewShown()).isTrue());
}
+ /**
+ * This checks the return value of IMSonEvaluateInputViewShown,
+ * when show_ime_with_hard_keyboard is disabled.
+ */
@Test
- public void testOnEvaluateInputViewShown_disableShowImeWithHardKeyboard() {
+ public void testOnEvaluateInputViewShown_disableShowImeWithHardKeyboard() throws Exception {
+ setShowImeWithHardKeyboard(false /* enabled */);
+
mInputMethodService.getResources().getConfiguration().keyboard =
Configuration.KEYBOARD_QWERTY;
mInputMethodService.getResources().getConfiguration().hardKeyboardHidden =
@@ -196,6 +248,8 @@
mInputMethodService.getResources().getConfiguration().keyboard =
Configuration.KEYBOARD_NOKEYS;
+ mInputMethodService.getResources().getConfiguration().hardKeyboardHidden =
+ Configuration.HARDKEYBOARDHIDDEN_NO;
eventually(() -> assertThat(mInputMethodService.onEvaluateInputViewShown()).isTrue());
mInputMethodService.getResources().getConfiguration().keyboard =
@@ -205,149 +259,386 @@
eventually(() -> assertThat(mInputMethodService.onEvaluateInputViewShown()).isTrue());
}
+ /**
+ * This checks that any (implicit or explicit) show request,
+ * when IMS#onEvaluateInputViewShown returns false, results in the IME not being shown.
+ */
@Test
public void testShowSoftInput_disableShowImeWithHardKeyboard() throws Exception {
- // Simulate connecting a hard keyboard
+ setShowImeWithHardKeyboard(false /* enabled */);
+
+ // Simulate connecting a hard keyboard.
mInputMethodService.getResources().getConfiguration().keyboard =
Configuration.KEYBOARD_QWERTY;
mInputMethodService.getResources().getConfiguration().hardKeyboardHidden =
Configuration.HARDKEYBOARDHIDDEN_NO;
+
// When InputMethodService#onEvaluateInputViewShown() returns false, the Ime should not be
// shown no matter what the show flag is.
verifyInputViewStatusOnMainSync(() -> assertThat(
mActivity.showImeWithInputMethodManager(InputMethodManager.SHOW_IMPLICIT)).isTrue(),
+ false /* expected */,
false /* inputViewStarted */);
+ assertThat(mInputMethodService.isInputViewShown()).isFalse();
+
verifyInputViewStatusOnMainSync(
() -> assertThat(mActivity.showImeWithInputMethodManager(0 /* flags */)).isTrue(),
+ false /* expected */,
false /* inputViewStarted */);
+ assertThat(mInputMethodService.isInputViewShown()).isFalse();
}
+ /**
+ * This checks that an explicit show request results in the IME being shown.
+ */
@Test
public void testShowSoftInputExplicitly() throws Exception {
+ setShowImeWithHardKeyboard(true /* enabled */);
+
// When InputMethodService#onEvaluateInputViewShown() returns true and flag is EXPLICIT, the
// Ime should be shown.
verifyInputViewStatusOnMainSync(
() -> assertThat(mActivity.showImeWithInputMethodManager(0 /* flags */)).isTrue(),
+ true /* expected */,
true /* inputViewStarted */);
+ assertThat(mInputMethodService.isInputViewShown()).isTrue();
}
+ /**
+ * This checks that an implicit show request results in the IME being shown.
+ */
@Test
public void testShowSoftInputImplicitly() throws Exception {
- // When InputMethodService#onEvaluateInputViewShown() returns true and flag is IMPLICIT, the
- // Ime should be shown.
+ setShowImeWithHardKeyboard(true /* enabled */);
+
+ // When InputMethodService#onEvaluateInputViewShown() returns true and flag is IMPLICIT,
+ // the IME should be shown.
verifyInputViewStatusOnMainSync(() -> assertThat(
mActivity.showImeWithInputMethodManager(InputMethodManager.SHOW_IMPLICIT)).isTrue(),
+ true /* expected */,
true /* inputViewStarted */);
+ assertThat(mInputMethodService.isInputViewShown()).isTrue();
}
+ /**
+ * This checks that an explicit show request when the IME is not previously shown,
+ * and it should be shown in fullscreen mode, results in the IME being shown.
+ */
@Test
- public void testShowSoftInputImplicitly_fullScreenMode() throws Exception {
- // When keyboard is off, InputMethodService#onEvaluateInputViewShown returns true, flag is
- // IMPLICIT and InputMethodService#onEvaluateFullScreenMode returns true, the Ime should not
- // be shown.
+ public void testShowSoftInputExplicitly_fullScreenMode() throws Exception {
+ setShowImeWithHardKeyboard(true /* enabled */);
+
+ // Set orientation landscape to enable fullscreen mode.
setOrientation(2);
eventually(() -> assertThat(mUiDevice.isNaturalOrientation()).isFalse());
- // Wait for the TestActivity to be recreated
+ // Wait for the TestActivity to be recreated.
eventually(() ->
assertThat(TestActivity.getLastCreatedInstance()).isNotEqualTo(mActivity));
- // Get the new TestActivity
+ // Get the new TestActivity.
mActivity = TestActivity.getLastCreatedInstance();
assertThat(mActivity).isNotNull();
InputMethodManager imm = mContext.getSystemService(InputMethodManager.class);
- // Wait for the new EditText to be served by InputMethodManager
- eventually(() ->
- assertThat(imm.hasActiveInputConnection(mActivity.getEditText())).isTrue());
+ // Wait for the new EditText to be served by InputMethodManager.
+ eventually(() -> assertThat(
+ imm.hasActiveInputConnection(mActivity.getEditText())).isTrue());
+
verifyInputViewStatusOnMainSync(() -> assertThat(
- mActivity.showImeWithInputMethodManager(InputMethodManager.SHOW_IMPLICIT)).isTrue(),
- false /* inputViewStarted */);
+ mActivity.showImeWithInputMethodManager(0 /* flags */)).isTrue(),
+ true /* expected */,
+ true /* inputViewStarted */);
+ assertThat(mInputMethodService.isInputViewShown()).isTrue();
}
+ /**
+ * This checks that an implicit show request when the IME is not previously shown,
+ * and it should be shown in fullscreen mode, results in the IME not being shown.
+ */
+ @Test
+ public void testShowSoftInputImplicitly_fullScreenMode() throws Exception {
+ setShowImeWithHardKeyboard(true /* enabled */);
+
+ // Set orientation landscape to enable fullscreen mode.
+ setOrientation(2);
+ eventually(() -> assertThat(mUiDevice.isNaturalOrientation()).isFalse());
+ // Wait for the TestActivity to be recreated.
+ eventually(() ->
+ assertThat(TestActivity.getLastCreatedInstance()).isNotEqualTo(mActivity));
+ // Get the new TestActivity.
+ mActivity = TestActivity.getLastCreatedInstance();
+ assertThat(mActivity).isNotNull();
+ InputMethodManager imm = mContext.getSystemService(InputMethodManager.class);
+ // Wait for the new EditText to be served by InputMethodManager.
+ eventually(() -> assertThat(
+ imm.hasActiveInputConnection(mActivity.getEditText())).isTrue());
+
+ verifyInputViewStatusOnMainSync(() -> assertThat(
+ mActivity.showImeWithInputMethodManager(InputMethodManager.SHOW_IMPLICIT)).isTrue(),
+ false /* expected */,
+ false /* inputViewStarted */);
+ assertThat(mInputMethodService.isInputViewShown()).isFalse();
+ }
+
+ /**
+ * This checks that an explicit show request when a hard keyboard is connected,
+ * results in the IME being shown.
+ */
+ @Test
+ public void testShowSoftInputExplicitly_withHardKeyboard() throws Exception {
+ setShowImeWithHardKeyboard(false /* enabled */);
+
+ // Simulate connecting a hard keyboard.
+ mInputMethodService.getResources().getConfiguration().keyboard =
+ Configuration.KEYBOARD_QWERTY;
+ mInputMethodService.getResources().getConfiguration().hardKeyboardHidden =
+ Configuration.HARDKEYBOARDHIDDEN_YES;
+
+ verifyInputViewStatusOnMainSync(() -> assertThat(
+ mActivity.showImeWithInputMethodManager(0 /* flags */)).isTrue(),
+ true /* expected */,
+ true /* inputViewStarted */);
+ assertThat(mInputMethodService.isInputViewShown()).isTrue();
+ }
+
+ /**
+ * This checks that an implicit show request when a hard keyboard is connected,
+ * results in the IME not being shown.
+ */
@Test
public void testShowSoftInputImplicitly_withHardKeyboard() throws Exception {
+ setShowImeWithHardKeyboard(false /* enabled */);
+
+ // Simulate connecting a hard keyboard.
mInputMethodService.getResources().getConfiguration().keyboard =
Configuration.KEYBOARD_QWERTY;
- // When connecting to a hard keyboard and the flag is IMPLICIT, the Ime should not be shown.
+ mInputMethodService.getResources().getConfiguration().hardKeyboardHidden =
+ Configuration.HARDKEYBOARDHIDDEN_YES;
+
verifyInputViewStatusOnMainSync(() -> assertThat(
mActivity.showImeWithInputMethodManager(InputMethodManager.SHOW_IMPLICIT)).isTrue(),
+ false /* expected */,
false /* inputViewStarted */);
+ assertThat(mInputMethodService.isInputViewShown()).isFalse();
}
+ /**
+ * This checks that an explicit show request followed by connecting a hard keyboard
+ * and a configuration change, still results in the IME being shown.
+ */
@Test
- public void testConfigurationChanged_withKeyboardShownExplicitly() throws InterruptedException {
+ public void testShowSoftInputExplicitly_thenConfigurationChanged() throws Exception {
+ setShowImeWithHardKeyboard(false /* enabled */);
+
+ // Start with no hard keyboard.
+ mInputMethodService.getResources().getConfiguration().keyboard =
+ Configuration.KEYBOARD_NOKEYS;
+ mInputMethodService.getResources().getConfiguration().hardKeyboardHidden =
+ Configuration.HARDKEYBOARDHIDDEN_YES;
+
verifyInputViewStatusOnMainSync(
() -> assertThat(mActivity.showImeWithInputMethodManager(0 /* flags */)).isTrue(),
+ true /* expected */,
true /* inputViewStarted */);
- // Simulate a fake configuration change to avoid triggering the recreation of TestActivity.
- mInputMethodService.getResources().getConfiguration().orientation =
- Configuration.ORIENTATION_LANDSCAPE;
- verifyInputViewStatusOnMainSync(() -> mInputMethodService.onConfigurationChanged(
- mInputMethodService.getResources().getConfiguration()),
- true /* inputViewStarted */);
- }
+ assertThat(mInputMethodService.isInputViewShown()).isTrue();
- @Test
- public void testConfigurationChanged_withKeyboardShownImplicitly() throws InterruptedException {
- verifyInputViewStatusOnMainSync(() -> assertThat(
- mActivity.showImeWithInputMethodManager(InputMethodManager.SHOW_IMPLICIT)).isTrue(),
- true /* inputViewStarted */);
- // Simulate a fake configuration change to avoid triggering the recreation of TestActivity.
- mInputMethodService.getResources().getConfiguration().orientation =
- Configuration.ORIENTATION_LANDSCAPE;
+ // Simulate connecting a hard keyboard.
mInputMethodService.getResources().getConfiguration().keyboard =
Configuration.KEYBOARD_QWERTY;
+ mInputMethodService.getResources().getConfiguration().hardKeyboardHidden =
+ Configuration.HARDKEYBOARDHIDDEN_YES;
+
+ // Simulate a fake configuration change to avoid triggering the recreation of TestActivity.
+ mInputMethodService.getResources().getConfiguration().orientation =
+ Configuration.ORIENTATION_LANDSCAPE;
+
+ verifyInputViewStatusOnMainSync(() -> mInputMethodService.onConfigurationChanged(
+ mInputMethodService.getResources().getConfiguration()),
+ true /* expected */,
+ true /* inputViewStarted */);
+ assertThat(mInputMethodService.isInputViewShown()).isTrue();
+ }
+
+ /**
+ * This checks that an implicit show request followed by connecting a hard keyboard
+ * and a configuration change, does not trigger IMS#onFinishInputView,
+ * but results in the IME being hidden.
+ */
+ @Test
+ public void testShowSoftInputImplicitly_thenConfigurationChanged() throws Exception {
+ setShowImeWithHardKeyboard(false /* enabled */);
+
+ // Start with no hard keyboard.
+ mInputMethodService.getResources().getConfiguration().keyboard =
+ Configuration.KEYBOARD_NOKEYS;
+ mInputMethodService.getResources().getConfiguration().hardKeyboardHidden =
+ Configuration.HARDKEYBOARDHIDDEN_YES;
+
+ verifyInputViewStatusOnMainSync(() -> assertThat(
+ mActivity.showImeWithInputMethodManager(InputMethodManager.SHOW_IMPLICIT)).isTrue(),
+ true /* expected */,
+ true /* inputViewStarted */);
+ assertThat(mInputMethodService.isInputViewShown()).isTrue();
+
+ // Simulate connecting a hard keyboard.
+ mInputMethodService.getResources().getConfiguration().keyboard =
+ Configuration.KEYBOARD_QWERTY;
+ mInputMethodService.getResources().getConfiguration().keyboard =
+ Configuration.HARDKEYBOARDHIDDEN_YES;
+
+ // Simulate a fake configuration change to avoid triggering the recreation of TestActivity.
+ mInputMethodService.getResources().getConfiguration().orientation =
+ Configuration.ORIENTATION_LANDSCAPE;
// Normally, IMS#onFinishInputView will be called when finishing the input view by the user.
// But if IMS#hideWindow is called when receiving a new configuration change, we don't
// expect that it's user-driven to finish the lifecycle of input view with
// IMS#onFinishInputView, because the input view will be re-initialized according to the
- // last mShowSoftRequested state. So in this case we treat the input view is still alive.
+ // last #mShowInputRequested state. So in this case we treat the input view as still alive.
verifyInputViewStatusOnMainSync(() -> mInputMethodService.onConfigurationChanged(
- mInputMethodService.getResources().getConfiguration()),
+ mInputMethodService.getResources().getConfiguration()),
+ true /* expected */,
true /* inputViewStarted */);
assertThat(mInputMethodService.isInputViewShown()).isFalse();
}
- private void verifyInputViewStatus(Runnable runnable, boolean inputViewStarted)
- throws InterruptedException {
- verifyInputViewStatusInternal(runnable, inputViewStarted, false /*runOnMainSync*/);
+ /**
+ * This checks that an explicit show request directly followed by an implicit show request,
+ * while a hardware keyboard is connected, still results in the IME being shown
+ * (i.e. the implicit show request is treated as explicit).
+ */
+ @Test
+ public void testShowSoftInputExplicitly_thenShowSoftInputImplicitly_withHardKeyboard()
+ throws Exception {
+ setShowImeWithHardKeyboard(false /* enabled */);
+
+ // Simulate connecting a hard keyboard.
+ mInputMethodService.getResources().getConfiguration().keyboard =
+ Configuration.KEYBOARD_QWERTY;
+ mInputMethodService.getResources().getConfiguration().hardKeyboardHidden =
+ Configuration.HARDKEYBOARDHIDDEN_YES;
+
+ // Explicit show request.
+ verifyInputViewStatusOnMainSync(() -> assertThat(
+ mActivity.showImeWithInputMethodManager(0 /* flags */)).isTrue(),
+ true /* expected */,
+ true /* inputViewStarted */);
+ assertThat(mInputMethodService.isInputViewShown()).isTrue();
+
+ // Implicit show request.
+ verifyInputViewStatusOnMainSync(() -> assertThat(
+ mActivity.showImeWithInputMethodManager(InputMethodManager.SHOW_IMPLICIT)).isTrue(),
+ false /* expected */,
+ true /* inputViewStarted */);
+ assertThat(mInputMethodService.isInputViewShown()).isTrue();
+
+ // Simulate a fake configuration change to avoid triggering the recreation of TestActivity.
+ // This should now consider the implicit show request, but keep the state from the
+ // explicit show request, and thus not hide the keyboard.
+ verifyInputViewStatusOnMainSync(() -> mInputMethodService.onConfigurationChanged(
+ mInputMethodService.getResources().getConfiguration()),
+ true /* expected */,
+ true /* inputViewStarted */);
+ assertThat(mInputMethodService.isInputViewShown()).isTrue();
}
- private void verifyInputViewStatusOnMainSync(Runnable runnable, boolean inputViewStarted)
- throws InterruptedException {
- verifyInputViewStatusInternal(runnable, inputViewStarted, true /*runOnMainSync*/);
+ /**
+ * This checks that a forced show request directly followed by an explicit show request,
+ * and then a hide not always request, still results in the IME being shown
+ * (i.e. the explicit show request retains the forced state).
+ */
+ @Test
+ public void testShowSoftInputForced_testShowSoftInputExplicitly_thenHideSoftInputNotAlways()
+ throws Exception {
+ setShowImeWithHardKeyboard(true /* enabled */);
+
+ verifyInputViewStatusOnMainSync(() -> assertThat(
+ mActivity.showImeWithInputMethodManager(InputMethodManager.SHOW_FORCED)).isTrue(),
+ true /* expected */,
+ true /* inputViewStarted */);
+ assertThat(mInputMethodService.isInputViewShown()).isTrue();
+
+ verifyInputViewStatusOnMainSync(() -> assertThat(
+ mActivity.showImeWithInputMethodManager(0 /* flags */)).isTrue(),
+ false /* expected */,
+ true /* inputViewStarted */);
+ assertThat(mInputMethodService.isInputViewShown()).isTrue();
+
+ verifyInputViewStatusOnMainSync(() ->
+ mActivity.hideImeWithInputMethodManager(InputMethodManager.HIDE_NOT_ALWAYS),
+ false /* expected */,
+ true /* inputViewStarted */);
+ assertThat(mInputMethodService.isInputViewShown()).isTrue();
}
+ /**
+ * This checks that the IME fullscreen mode state is updated after changing orientation.
+ */
+ @Test
+ public void testFullScreenMode() throws Exception {
+ setShowImeWithHardKeyboard(true /* enabled */);
+
+ Log.i(TAG, "Set orientation natural");
+ verifyFullscreenMode(() -> setOrientation(0),
+ false /* expected */,
+ true /* orientationPortrait */);
+
+ Log.i(TAG, "Set orientation left");
+ verifyFullscreenMode(() -> setOrientation(1),
+ true /* expected */,
+ false /* orientationPortrait */);
+
+ Log.i(TAG, "Set orientation right");
+ verifyFullscreenMode(() -> setOrientation(2),
+ false /* expected */,
+ false /* orientationPortrait */);
+ }
+
+ private void verifyInputViewStatus(
+ Runnable runnable, boolean expected, boolean inputViewStarted)
+ throws InterruptedException {
+ verifyInputViewStatusInternal(runnable, expected, inputViewStarted,
+ false /* runOnMainSync */);
+ }
+
+ private void verifyInputViewStatusOnMainSync(
+ Runnable runnable, boolean expected, boolean inputViewStarted)
+ throws InterruptedException {
+ verifyInputViewStatusInternal(runnable, expected, inputViewStarted,
+ true /* runOnMainSync */);
+ }
+
+ /**
+ * Verifies the status of the Input View after executing the given runnable.
+ *
+ * @param runnable the runnable to execute for showing or hiding the IME.
+ * @param expected whether the runnable is expected to trigger the signal.
+ * @param inputViewStarted the expected state of the Input View after executing the runnable.
+ * @param runOnMainSync whether to execute the runnable on the main thread.
+ */
private void verifyInputViewStatusInternal(
- Runnable runnable, boolean inputViewStarted, boolean runOnMainSync)
+ Runnable runnable, boolean expected, boolean inputViewStarted, boolean runOnMainSync)
throws InterruptedException {
CountDownLatch signal = new CountDownLatch(1);
mInputMethodService.setCountDownLatchForTesting(signal);
- // Runnable to trigger onStartInputView()/ onFinishInputView()
+ // Runnable to trigger onStartInputView() / onFinishInputView() / onConfigurationChanged()
if (runOnMainSync) {
mInstrumentation.runOnMainSync(runnable);
} else {
runnable.run();
}
- // Waits for onStartInputView() to finish.
mInstrumentation.waitForIdleSync();
- signal.await(TIMEOUT_IN_SECONDS, TimeUnit.SECONDS);
+ boolean completed = signal.await(TIMEOUT_IN_SECONDS, TimeUnit.SECONDS);
+ if (expected && !completed) {
+ fail("Timed out waiting for"
+ + " onStartInputView() / onFinishInputView() / onConfigurationChanged()");
+ } else if (!expected && completed) {
+ fail("Unexpected call"
+ + " onStartInputView() / onFinishInputView() / onConfigurationChanged()");
+ }
// Input is not finished.
assertThat(mInputMethodService.getCurrentInputStarted()).isTrue();
assertThat(mInputMethodService.getCurrentInputViewStarted()).isEqualTo(inputViewStarted);
}
- @Test
- public void testFullScreenMode() throws Exception {
- Log.i(TAG, "Set orientation natural");
- verifyFullscreenMode(() -> setOrientation(0), true /* orientationPortrait */);
-
- Log.i(TAG, "Set orientation left");
- verifyFullscreenMode(() -> setOrientation(1), false /* orientationPortrait */);
-
- Log.i(TAG, "Set orientation right");
- verifyFullscreenMode(() -> setOrientation(2), false /* orientationPortrait */);
- }
-
private void setOrientation(int orientation) {
// Simple wrapper for catching RemoteException.
try {
@@ -366,7 +657,15 @@
}
}
- private void verifyFullscreenMode(Runnable runnable, boolean orientationPortrait)
+ /**
+ * Verifies the IME fullscreen mode state after executing the given runnable.
+ *
+ * @param runnable the runnable to execute for setting the orientation.
+ * @param expected whether the runnable is expected to trigger the signal.
+ * @param orientationPortrait whether the orientation is expected to be portrait.
+ */
+ private void verifyFullscreenMode(
+ Runnable runnable, boolean expected, boolean orientationPortrait)
throws InterruptedException {
CountDownLatch signal = new CountDownLatch(1);
mInputMethodService.setCountDownLatchForTesting(signal);
@@ -379,7 +678,12 @@
}
// Waits for onConfigurationChanged() to finish.
mInstrumentation.waitForIdleSync();
- signal.await(TIMEOUT_IN_SECONDS, TimeUnit.SECONDS);
+ boolean completed = signal.await(TIMEOUT_IN_SECONDS, TimeUnit.SECONDS);
+ if (expected && !completed) {
+ fail("Timed out waiting for onConfigurationChanged()");
+ } else if (!expected && completed) {
+ fail("Unexpected call onConfigurationChanged()");
+ }
clickOnEditorText();
eventually(() -> assertThat(mInputMethodService.isInputViewShown()).isTrue());
@@ -416,7 +720,21 @@
return mTargetPackageName + "/" + INPUT_METHOD_SERVICE_NAME;
}
- private String executeShellCommand(String cmd) throws Exception {
+ /**
+ * Sets the value of show_ime_with_hard_keyboard, only if it is different to the default value.
+ *
+ * @param enabled the value to be set.
+ */
+ private void setShowImeWithHardKeyboard(boolean enabled) throws IOException {
+ if (mShowImeWithHardKeyboardEnabled != enabled) {
+ executeShellCommand(enabled
+ ? ENABLE_SHOW_IME_WITH_HARD_KEYBOARD_CMD
+ : DISABLE_SHOW_IME_WITH_HARD_KEYBOARD_CMD);
+ mInstrumentation.waitForIdleSync();
+ }
+ }
+
+ private String executeShellCommand(String cmd) throws IOException {
Log.i(TAG, "Run command: " + cmd);
return UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
.executeShellCommand(cmd);
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/DefaultImeVisibilityApplierTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/DefaultImeVisibilityApplierTest.java
index 869497c..3199e06 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/DefaultImeVisibilityApplierTest.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/DefaultImeVisibilityApplierTest.java
@@ -40,7 +40,6 @@
import android.os.IBinder;
import android.os.RemoteException;
import android.view.Display;
-import android.view.inputmethod.InputMethodManager;
import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -77,9 +76,9 @@
public void testPerformShowIme() throws Exception {
synchronized (ImfLock.class) {
mVisibilityApplier.performShowIme(new Binder() /* showInputToken */,
- null /* statsToken */, InputMethodManager.SHOW_IMPLICIT, null, SHOW_SOFT_INPUT);
+ null /* statsToken */, 0 /* showFlags */, null, SHOW_SOFT_INPUT);
}
- verifyShowSoftInput(false, true, InputMethodManager.SHOW_IMPLICIT);
+ verifyShowSoftInput(false, true, 0 /* showFlags */);
}
@Test
@@ -126,7 +125,7 @@
@Test
public void testApplyImeVisibility_showImeImplicit() throws Exception {
mVisibilityApplier.applyImeVisibility(mWindowToken, null, STATE_SHOW_IME_IMPLICIT);
- verifyShowSoftInput(true, true, InputMethodManager.SHOW_IMPLICIT);
+ verifyShowSoftInput(true, true, 0 /* showFlags */);
}
@Test
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/ImeVisibilityStateComputerTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/ImeVisibilityStateComputerTest.java
index a38c162..fae5f86 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/ImeVisibilityStateComputerTest.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/ImeVisibilityStateComputerTest.java
@@ -106,7 +106,7 @@
@Test
public void testRequestImeVisibility_showExplicit() {
initImeTargetWindowState(mWindowToken);
- boolean res = mComputer.onImeShowFlags(null, 0 /* show explicit */);
+ boolean res = mComputer.onImeShowFlags(null, 0 /* showFlags */);
mComputer.requestImeVisibility(mWindowToken, res);
final ImeTargetWindowState state = mComputer.getWindowStateOrNull(mWindowToken);
@@ -118,6 +118,34 @@
assertThat(mComputer.mRequestedShowExplicitly).isTrue();
}
+ /**
+ * This checks that the state after an explicit show request does not get reset during
+ * a subsequent implicit show request, without an intermediary hide request.
+ */
+ @Test
+ public void testRequestImeVisibility_showExplicit_thenShowImplicit() {
+ initImeTargetWindowState(mWindowToken);
+ mComputer.onImeShowFlags(null, 0 /* showFlags */);
+ assertThat(mComputer.mRequestedShowExplicitly).isTrue();
+
+ mComputer.onImeShowFlags(null, InputMethodManager.SHOW_IMPLICIT);
+ assertThat(mComputer.mRequestedShowExplicitly).isTrue();
+ }
+
+ /**
+ * This checks that the state after a forced show request does not get reset during
+ * a subsequent explicit show request, without an intermediary hide request.
+ */
+ @Test
+ public void testRequestImeVisibility_showForced_thenShowExplicit() {
+ initImeTargetWindowState(mWindowToken);
+ mComputer.onImeShowFlags(null, InputMethodManager.SHOW_FORCED);
+ assertThat(mComputer.mShowForced).isTrue();
+
+ mComputer.onImeShowFlags(null, 0 /* showFlags */);
+ assertThat(mComputer.mShowForced).isTrue();
+ }
+
@Test
public void testRequestImeVisibility_showImplicit_a11yNoImePolicy() {
// Precondition: set AccessibilityService#SHOW_MODE_HIDDEN policy
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodBindingControllerTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodBindingControllerTest.java
index 42d373b..e87a34e 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodBindingControllerTest.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodBindingControllerTest.java
@@ -19,6 +19,7 @@
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.doReturn;
@@ -162,7 +163,10 @@
assertThat(mBindingController.getCurToken()).isNotNull();
}
// Wait for onServiceConnected()
- mCountDownLatch.await(TIMEOUT_IN_SECONDS, TimeUnit.SECONDS);
+ boolean completed = mCountDownLatch.await(TIMEOUT_IN_SECONDS, TimeUnit.SECONDS);
+ if (!completed) {
+ fail("Timed out waiting for onServiceConnected()");
+ }
// Verify onServiceConnected() is called and bound successfully.
synchronized (ImfLock.class) {
diff --git a/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/Android.bp b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/Android.bp
index 8d0e0c4..e1fd2b3 100644
--- a/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/Android.bp
+++ b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/Android.bp
@@ -43,6 +43,8 @@
},
export_package_resources: true,
sdk_version: "current",
+
+ certificate: "platform",
}
android_library {
diff --git a/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/AndroidManifest.xml b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/AndroidManifest.xml
index 996322d..cf7d660 100644
--- a/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/AndroidManifest.xml
+++ b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/AndroidManifest.xml
@@ -18,8 +18,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.apps.inputmethod.simpleime">
- <uses-sdk android:targetSdkVersion="31" />
-
<uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
<application android:debuggable="true"
diff --git a/services/tests/displayservicetests/src/com/android/server/display/OWNERS b/services/tests/displayservicetests/OWNERS
similarity index 100%
rename from services/tests/displayservicetests/src/com/android/server/display/OWNERS
rename to services/tests/displayservicetests/OWNERS
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/ApexManagerTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/ApexManagerTest.java
index 5b0e2f3..1ae9124 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/ApexManagerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/ApexManagerTest.java
@@ -474,15 +474,6 @@
}
@Test
- public void testGetBackingApexFiles_flattenedApex() {
- ApexManager flattenedApexManager = new ApexManager.ApexManagerFlattenedApex();
- final File backingApexFile = flattenedApexManager.getBackingApexFile(
- new File(mMockSystem.system().getApexDirectory(),
- "com.android.apex.cts.shim/app/CtsShim/CtsShim.apk"));
- assertThat(backingApexFile).isNull();
- }
-
- @Test
public void testActiveApexChanged() throws RemoteException {
ApexInfo apex1 = createApexInfo(
"com.apex1", 37, true, true, new File("/data/apex/active/com.apex@37.apex"));
diff --git a/services/tests/servicestests/Android.bp b/services/tests/servicestests/Android.bp
index 4edb167..173c5f5 100644
--- a/services/tests/servicestests/Android.bp
+++ b/services/tests/servicestests/Android.bp
@@ -34,6 +34,7 @@
"services.core",
"services.credentials",
"services.devicepolicy",
+ "services.flags",
"services.net",
"services.people",
"services.usage",
diff --git a/services/tests/servicestests/src/com/android/server/flags/FeatureFlagsServiceTest.java b/services/tests/servicestests/src/com/android/server/flags/FeatureFlagsServiceTest.java
new file mode 100644
index 0000000..df4731f
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/flags/FeatureFlagsServiceTest.java
@@ -0,0 +1,293 @@
+/*
+ * Copyright (C) 2023 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.flags;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.junit.Assert.fail;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.flags.IFeatureFlagsCallback;
+import android.flags.SyncableFlag;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import java.util.List;
+
+@Presubmit
+@SmallTest
+public class FeatureFlagsServiceTest {
+ private static final String NS = "ns";
+ private static final String NAME = "name";
+ private static final String PROP_NAME = FlagOverrideStore.getPropName(NS, NAME);
+
+ @Rule
+ public final MockitoRule mockito = MockitoJUnit.rule();
+
+ @Mock
+ private FlagOverrideStore mFlagStore;
+ @Mock
+ private FlagsShellCommand mFlagCommand;
+ @Mock
+ private IFeatureFlagsCallback mIFeatureFlagsCallback;
+ @Mock
+ private IBinder mIFeatureFlagsCallbackAsBinder;
+ @Mock
+ private FeatureFlagsService.PermissionsChecker mPermissionsChecker;
+
+ private FeatureFlagsBinder mFeatureFlagsService;
+
+ @Before
+ public void setup() {
+ when(mIFeatureFlagsCallback.asBinder()).thenReturn(mIFeatureFlagsCallbackAsBinder);
+ mFeatureFlagsService = new FeatureFlagsBinder(
+ mFlagStore, mFlagCommand, mPermissionsChecker);
+ }
+
+ @Test
+ public void testRegisterCallback() {
+ mFeatureFlagsService.registerCallback(mIFeatureFlagsCallback);
+ try {
+ verify(mIFeatureFlagsCallbackAsBinder).linkToDeath(any(), eq(0));
+ } catch (RemoteException e) {
+ fail("Our mock threw a Remote Exception?");
+ }
+ }
+
+ @Test
+ public void testOverrideFlag_requiresWritePermission() {
+ SecurityException exc = new SecurityException("not allowed");
+ doThrow(exc).when(mPermissionsChecker).assertWritePermission();
+
+ SyncableFlag f = new SyncableFlag(NS, "a", "false", false);
+
+ try {
+ mFeatureFlagsService.overrideFlag(f);
+ fail("Should have thrown exception");
+ } catch (SecurityException e) {
+ assertThat(exc).isEqualTo(e);
+ } catch (Exception e) {
+ fail("should have thrown a security exception");
+ }
+ }
+
+ @Test
+ public void testResetFlag_requiresWritePermission() {
+ SecurityException exc = new SecurityException("not allowed");
+ doThrow(exc).when(mPermissionsChecker).assertWritePermission();
+
+ SyncableFlag f = new SyncableFlag(NS, "a", "false", false);
+
+ try {
+ mFeatureFlagsService.resetFlag(f);
+ fail("Should have thrown exception");
+ } catch (SecurityException e) {
+ assertThat(exc).isEqualTo(e);
+ } catch (Exception e) {
+ fail("should have thrown a security exception");
+ }
+ }
+
+ @Test
+ public void testSyncFlags_noOverrides() {
+ List<SyncableFlag> inputFlags = List.of(
+ new SyncableFlag(NS, "a", "false", false),
+ new SyncableFlag(NS, "b", "true", false),
+ new SyncableFlag(NS, "c", "false", false)
+ );
+
+ List<SyncableFlag> outputFlags = mFeatureFlagsService.syncFlags(inputFlags);
+
+ assertThat(inputFlags.size()).isEqualTo(outputFlags.size());
+
+ for (SyncableFlag inpF: inputFlags) {
+ boolean found = false;
+ for (SyncableFlag outF : outputFlags) {
+ if (compareSyncableFlagsNames(inpF, outF)) {
+ found = true;
+ break;
+ }
+ }
+ assertWithMessage("Failed to find input flag " + inpF + " in the output")
+ .that(found).isTrue();
+ }
+ }
+
+ @Test
+ public void testSyncFlags_withSomeOverrides() {
+ List<SyncableFlag> inputFlags = List.of(
+ new SyncableFlag(NS, "a", "false", false),
+ new SyncableFlag(NS, "b", "true", false),
+ new SyncableFlag(NS, "c", "false", false)
+ );
+
+ assertThat(mFlagStore).isNotNull();
+ when(mFlagStore.get(NS, "c")).thenReturn("true");
+ List<SyncableFlag> outputFlags = mFeatureFlagsService.syncFlags(inputFlags);
+
+ assertThat(inputFlags.size()).isEqualTo(outputFlags.size());
+
+ for (SyncableFlag inpF: inputFlags) {
+ boolean found = false;
+ for (SyncableFlag outF : outputFlags) {
+ if (compareSyncableFlagsNames(inpF, outF)) {
+ found = true;
+
+ // Once we've found "c", do an extra check
+ if (outF.getName().equals("c")) {
+ assertWithMessage("Flag " + outF + "was not returned with an override")
+ .that(outF.getValue()).isEqualTo("true");
+ }
+ break;
+ }
+ }
+ assertWithMessage("Failed to find input flag " + inpF + " in the output")
+ .that(found).isTrue();
+ }
+ }
+
+ @Test
+ public void testSyncFlags_twoCallsWithDifferentDefaults() {
+ List<SyncableFlag> inputFlagsFirst = List.of(
+ new SyncableFlag(NS, "a", "false", false)
+ );
+ List<SyncableFlag> inputFlagsSecond = List.of(
+ new SyncableFlag(NS, "a", "true", false),
+ new SyncableFlag(NS, "b", "false", false)
+ );
+
+ List<SyncableFlag> outputFlagsFirst = mFeatureFlagsService.syncFlags(inputFlagsFirst);
+ List<SyncableFlag> outputFlagsSecond = mFeatureFlagsService.syncFlags(inputFlagsSecond);
+
+ assertThat(inputFlagsFirst.size()).isEqualTo(outputFlagsFirst.size());
+ assertThat(inputFlagsSecond.size()).isEqualTo(outputFlagsSecond.size());
+
+ // This test only cares that the "a" flag passed in the second time came out with the
+ // same value that was passed in the first time.
+
+ boolean found = false;
+ for (SyncableFlag second : outputFlagsSecond) {
+ if (compareSyncableFlagsNames(second, inputFlagsFirst.get(0))) {
+ found = true;
+ assertThat(second.getValue()).isEqualTo(inputFlagsFirst.get(0).getValue());
+ break;
+ }
+ }
+
+ assertWithMessage(
+ "Failed to find flag " + inputFlagsFirst.get(0) + " in the second calls output")
+ .that(found).isTrue();
+ }
+
+ @Test
+ public void testQueryFlags_onlyOnce() {
+ List<SyncableFlag> inputFlags = List.of(
+ new SyncableFlag(NS, "a", "false", false),
+ new SyncableFlag(NS, "b", "true", false),
+ new SyncableFlag(NS, "c", "false", false)
+ );
+
+ List<SyncableFlag> outputFlags = mFeatureFlagsService.queryFlags(inputFlags);
+
+ assertThat(inputFlags.size()).isEqualTo(outputFlags.size());
+
+ for (SyncableFlag inpF: inputFlags) {
+ boolean found = false;
+ for (SyncableFlag outF : outputFlags) {
+ if (compareSyncableFlagsNames(inpF, outF)) {
+ found = true;
+ break;
+ }
+ }
+ assertWithMessage("Failed to find input flag " + inpF + " in the output")
+ .that(found).isTrue();
+ }
+ }
+
+ @Test
+ public void testQueryFlags_twoCallsWithDifferentDefaults() {
+ List<SyncableFlag> inputFlagsFirst = List.of(
+ new SyncableFlag(NS, "a", "false", false)
+ );
+ List<SyncableFlag> inputFlagsSecond = List.of(
+ new SyncableFlag(NS, "a", "true", false),
+ new SyncableFlag(NS, "b", "false", false)
+ );
+
+ List<SyncableFlag> outputFlagsFirst = mFeatureFlagsService.queryFlags(inputFlagsFirst);
+ List<SyncableFlag> outputFlagsSecond = mFeatureFlagsService.queryFlags(inputFlagsSecond);
+
+ assertThat(inputFlagsFirst.size()).isEqualTo(outputFlagsFirst.size());
+ assertThat(inputFlagsSecond.size()).isEqualTo(outputFlagsSecond.size());
+
+ // This test only cares that the "a" flag passed in the second time came out with the
+ // same value that was passed in (i.e. it wasn't cached).
+
+ boolean found = false;
+ for (SyncableFlag second : outputFlagsSecond) {
+ if (compareSyncableFlagsNames(second, inputFlagsSecond.get(0))) {
+ found = true;
+ assertThat(second.getValue()).isEqualTo(inputFlagsSecond.get(0).getValue());
+ break;
+ }
+ }
+
+ assertWithMessage(
+ "Failed to find flag " + inputFlagsSecond.get(0) + " in the second calls output")
+ .that(found).isTrue();
+ }
+
+ @Test
+ public void testOverrideFlag() {
+ SyncableFlag f = new SyncableFlag(NS, "a", "false", false);
+
+ mFeatureFlagsService.overrideFlag(f);
+
+ verify(mFlagStore).set(f.getNamespace(), f.getName(), f.getValue());
+ }
+
+ @Test
+ public void testResetFlag() {
+ SyncableFlag f = new SyncableFlag(NS, "a", "false", false);
+
+ mFeatureFlagsService.resetFlag(f);
+
+ verify(mFlagStore).erase(f.getNamespace(), f.getName());
+ }
+
+
+ private static boolean compareSyncableFlagsNames(SyncableFlag a, SyncableFlag b) {
+ return a.getNamespace().equals(b.getNamespace())
+ && a.getName().equals(b.getName())
+ && a.isDynamic() == b.isDynamic();
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/flags/FlagCacheTest.java b/services/tests/servicestests/src/com/android/server/flags/FlagCacheTest.java
new file mode 100644
index 0000000..c2cf540
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/flags/FlagCacheTest.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2023 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.flags;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class FlagCacheTest {
+ private static final String NS = "ns";
+ private static final String NAME = "name";
+
+ FlagCache mFlagCache = new FlagCache();
+
+ @Test
+ public void testGetOrNull_unset() {
+ assertThat(mFlagCache.getOrNull(NS, NAME)).isNull();
+ }
+
+ @Test
+ public void testGetOrSet_unset() {
+ assertThat(mFlagCache.getOrSet(NS, NAME, "value")).isEqualTo("value");
+ }
+
+ @Test
+ public void testGetOrSet_alreadySet() {
+ mFlagCache.setIfChanged(NS, NAME, "value");
+ assertThat(mFlagCache.getOrSet(NS, NAME, "newvalue")).isEqualTo("value");
+ }
+
+ @Test
+ public void testSetIfChanged_unset() {
+ assertThat(mFlagCache.setIfChanged(NS, NAME, "value")).isTrue();
+ }
+
+ @Test
+ public void testSetIfChanged_noChange() {
+ mFlagCache.setIfChanged(NS, NAME, "value");
+ assertThat(mFlagCache.setIfChanged(NS, NAME, "value")).isFalse();
+ }
+
+ @Test
+ public void testSetIfChanged_changing() {
+ mFlagCache.setIfChanged(NS, NAME, "value");
+ assertThat(mFlagCache.setIfChanged(NS, NAME, "newvalue")).isTrue();
+ }
+
+ @Test
+ public void testContainsNamespace_unset() {
+ assertThat(mFlagCache.containsNamespace(NS)).isFalse();
+ }
+
+ @Test
+ public void testContainsNamespace_set() {
+ mFlagCache.setIfChanged(NS, NAME, "value");
+ assertThat(mFlagCache.containsNamespace(NS)).isTrue();
+ }
+
+ @Test
+ public void testContains_unset() {
+ assertThat(mFlagCache.contains(NS, NAME)).isFalse();
+ }
+
+ @Test
+ public void testContains_set() {
+ mFlagCache.setIfChanged(NS, NAME, "value");
+ assertThat(mFlagCache.contains(NS, NAME)).isTrue();
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/flags/FlagOverrideStoreTest.java b/services/tests/servicestests/src/com/android/server/flags/FlagOverrideStoreTest.java
new file mode 100644
index 0000000..6cc3acf
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/flags/FlagOverrideStoreTest.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2023 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.flags;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+@Presubmit
+@SmallTest
+public class FlagOverrideStoreTest {
+ private static final String NS = "ns";
+ private static final String NAME = "name";
+ private static final String PROP_NAME = FlagOverrideStore.getPropName(NS, NAME);
+
+ @Rule
+ public final MockitoRule mockito = MockitoJUnit.rule();
+
+ @Mock
+ private SettingsProxy mSettingsProxy;
+ @Mock
+ private FlagOverrideStore.FlagChangeCallback mCallback;
+
+ private FlagOverrideStore mFlagStore;
+
+ @Before
+ public void setup() {
+ mFlagStore = new FlagOverrideStore(mSettingsProxy);
+ mFlagStore.setChangeCallback(mCallback);
+ }
+
+ @Test
+ public void testSet_unset() {
+ mFlagStore.set(NS, NAME, "value");
+ verify(mSettingsProxy).putString(PROP_NAME, "value");
+ }
+
+ @Test
+ public void testSet_setTwice() {
+ mFlagStore.set(NS, NAME, "value");
+ mFlagStore.set(NS, NAME, "newvalue");
+ verify(mSettingsProxy).putString(PROP_NAME, "value");
+ verify(mSettingsProxy).putString(PROP_NAME, "newvalue");
+ }
+
+ @Test
+ public void testGet_unset() {
+ assertThat(mFlagStore.get(NS, NAME)).isNull();
+ }
+
+ @Test
+ public void testGet_set() {
+ when(mSettingsProxy.getString(PROP_NAME)).thenReturn("value");
+ assertThat(mFlagStore.get(NS, NAME)).isEqualTo("value");
+ }
+
+ @Test
+ public void testErase() {
+ mFlagStore.erase(NS, NAME);
+ verify(mSettingsProxy).putString(PROP_NAME, null);
+ }
+
+ @Test
+ public void testContains_unset() {
+ assertThat(mFlagStore.contains(NS, NAME)).isFalse();
+ }
+
+ @Test
+ public void testContains_set() {
+ when(mSettingsProxy.getString(PROP_NAME)).thenReturn("value");
+ assertThat(mFlagStore.contains(NS, NAME)).isTrue();
+ }
+
+ @Test
+ public void testCallback_onSet() {
+ mFlagStore.set(NS, NAME, "value");
+ verify(mCallback).onFlagChanged(NS, NAME, "value");
+ }
+
+ @Test
+ public void testCallback_onErase() {
+ mFlagStore.erase(NS, NAME);
+ verify(mCallback).onFlagChanged(NS, NAME, null);
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/flags/OWNERS b/services/tests/servicestests/src/com/android/server/flags/OWNERS
new file mode 100644
index 0000000..7ed369e
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/flags/OWNERS
@@ -0,0 +1 @@
+include /services/flags/OWNERS
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 d9a51a01..01b2d0f 100644
--- a/services/tests/servicestests/src/com/android/server/vibrator/VibrationThreadTest.java
+++ b/services/tests/servicestests/src/com/android/server/vibrator/VibrationThreadTest.java
@@ -19,6 +19,8 @@
import static android.os.VibrationEffect.VibrationParameter.targetAmplitude;
import static android.os.VibrationEffect.VibrationParameter.targetFrequency;
+import static com.google.common.truth.Truth.assertThat;
+
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
@@ -305,6 +307,80 @@
}
@Test
+ public void vibrate_singleVibratorPatternWithZeroDurationSteps_skipsZeroDurationSteps() {
+ mVibratorProviders.get(VIBRATOR_ID).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
+
+ VibrationEffect effect = VibrationEffect.createWaveform(
+ /* timings= */ new long[]{0, 100, 50, 100, 0, 0, 0, 50}, /* repeat= */ -1);
+ VibrationStepConductor conductor = startThreadAndDispatcher(effect);
+ long vibrationId = conductor.getVibration().id;
+ waitForCompletion();
+
+ verify(mManagerHooks).noteVibratorOn(eq(UID), eq(300L));
+ verify(mManagerHooks).noteVibratorOff(eq(UID));
+
+ verifyCallbacksTriggered(vibrationId, Vibration.Status.FINISHED);
+ assertThat(mControllers.get(VIBRATOR_ID).isVibrating()).isFalse();
+
+ assertThat(mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibrationId))
+ .isEqualTo(expectedOneShots(100L, 150L));
+ }
+
+ @Test
+ public void vibrate_singleVibratorPatternWithZeroDurationAndAmplitude_skipsZeroDurationSteps() {
+ mVibratorProviders.get(VIBRATOR_ID).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
+
+ int[] amplitudes = new int[]{1, 2, 0, 3, 4, 5, 0, 6};
+ VibrationEffect effect = VibrationEffect.createWaveform(
+ /* timings= */ new long[]{0, 100, 0, 50, 50, 0, 100, 50}, amplitudes,
+ /* repeat= */ -1);
+ VibrationStepConductor conductor = startThreadAndDispatcher(effect);
+ long vibrationId = conductor.getVibration().id;
+ waitForCompletion();
+
+ verify(mManagerHooks).noteVibratorOn(eq(UID), eq(350L));
+ verify(mManagerHooks).noteVibratorOff(eq(UID));
+
+ verifyCallbacksTriggered(vibrationId, Vibration.Status.FINISHED);
+ assertThat(mControllers.get(VIBRATOR_ID).isVibrating()).isFalse();
+
+ assertThat(mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibrationId))
+ .isEqualTo(expectedOneShots(200L, 50L));
+ }
+
+ @LargeTest
+ @Test
+ public void vibrate_singleVibratorRepeatingPatternWithZeroDurationSteps_repeatsEffectCorrectly()
+ throws Exception {
+ FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(VIBRATOR_ID);
+ fakeVibrator.setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
+
+ VibrationEffect effect = VibrationEffect.createWaveform(
+ /* timings= */ new long[]{0, 200, 50, 100, 0, 50, 50, 100}, /* repeat= */ 0);
+ VibrationStepConductor conductor = startThreadAndDispatcher(effect);
+ long vibrationId = conductor.getVibration().id;
+ // We are expect this test to repeat the vibration effect twice, which would result in 5
+ // segments being played:
+ // 200ms ON
+ // 150ms ON (100ms + 50ms, skips 0ms)
+ // 300ms ON (100ms + 200ms looping to the start and skipping first 0ms)
+ // 150ms ON (100ms + 50ms, skips 0ms)
+ // 300ms ON (100ms + 200ms looping to the start and skipping first 0ms)
+ assertTrue(waitUntil(() -> fakeVibrator.getEffectSegments(vibrationId).size() >= 5,
+ 5000L + TEST_TIMEOUT_MILLIS));
+ conductor.notifyCancelled(
+ new Vibration.EndInfo(Vibration.Status.CANCELLED_BY_USER),
+ /* immediate= */ false);
+ waitForCompletion();
+
+ verifyCallbacksTriggered(vibrationId, Vibration.Status.CANCELLED_BY_USER);
+ assertThat(mControllers.get(VIBRATOR_ID).isVibrating()).isFalse();
+
+ assertThat(mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibrationId).subList(0, 5))
+ .isEqualTo(expectedOneShots(200L, 150L, 300L, 150L, 300L));
+ }
+
+ @Test
public void vibrate_singleVibratorRepeatingPwle_generatesLargestPwles() throws Exception {
FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(VIBRATOR_ID);
fakeVibrator.setCapabilities(IVibrator.CAP_COMPOSE_PWLE_EFFECTS);
@@ -1640,6 +1716,12 @@
/* frequencyHz= */ 0, (int) millis);
}
+ private List<VibrationEffectSegment> expectedOneShots(long... millis) {
+ return Arrays.stream(millis)
+ .mapToObj(this::expectedOneShot)
+ .collect(Collectors.toList());
+ }
+
private VibrationEffectSegment expectedPrebaked(int effectId) {
return new PrebakedSegment(effectId, false, VibrationEffect.EFFECT_STRENGTH_MEDIUM);
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java
index 870dd48..7284744 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java
@@ -1749,8 +1749,7 @@
public void testLaunchActivityWithoutDisplayCategory() {
final ActivityInfo info = new ActivityInfo();
info.applicationInfo = new ApplicationInfo();
- info.taskAffinity = ActivityRecord.computeTaskAffinity("test", DEFAULT_FAKE_UID,
- 0 /* launchMode */, null /* componentName */);
+ info.taskAffinity = ActivityRecord.computeTaskAffinity("test", DEFAULT_FAKE_UID);
info.requiredDisplayCategory = "automotive";
final Task task = new TaskBuilder(mSupervisor).setCreateActivity(true).setActivityInfo(info)
.build();
@@ -1775,8 +1774,7 @@
public void testLaunchActivityWithDifferentDisplayCategory() {
final ActivityInfo info = new ActivityInfo();
info.applicationInfo = new ApplicationInfo();
- info.taskAffinity = ActivityRecord.computeTaskAffinity("test", DEFAULT_FAKE_UID,
- 0 /* launchMode */, null /* componentName */);
+ info.taskAffinity = ActivityRecord.computeTaskAffinity("test", DEFAULT_FAKE_UID);
info.requiredDisplayCategory = "automotive";
final Task task = new TaskBuilder(mSupervisor).setCreateActivity(true).setActivityInfo(info)
.build();
@@ -1801,8 +1799,7 @@
public void testLaunchActivityWithSameDisplayCategory() {
final ActivityInfo info = new ActivityInfo();
info.applicationInfo = new ApplicationInfo();
- info.taskAffinity = ActivityRecord.computeTaskAffinity("test", DEFAULT_FAKE_UID,
- 0 /* launchMode */, null /* componentName */);
+ info.taskAffinity = ActivityRecord.computeTaskAffinity("test", DEFAULT_FAKE_UID);
info.requiredDisplayCategory = "automotive";
final Task task = new TaskBuilder(mSupervisor).setCreateActivity(true).setActivityInfo(info)
.build();
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayWindowSettingsProviderTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayWindowSettingsProviderTests.java
index 9d839fc..7d9fdd5 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayWindowSettingsProviderTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayWindowSettingsProviderTests.java
@@ -19,6 +19,7 @@
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
+import static android.view.Display.TYPE_VIRTUAL;
import static android.view.WindowManager.DISPLAY_IME_POLICY_LOCAL;
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
@@ -26,6 +27,7 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertTrue;
+import static org.testng.Assert.assertFalse;
import android.annotation.Nullable;
import android.platform.test.annotations.Presubmit;
@@ -233,6 +235,22 @@
}
@Test
+ public void testDoNotWriteVirtualDisplaySettingsToStorage() throws Exception {
+ final DisplayInfo secondaryDisplayInfo = mSecondaryDisplay.getDisplayInfo();
+ secondaryDisplayInfo.type = TYPE_VIRTUAL;
+
+ // No write to storage on virtual display change.
+ final DisplayWindowSettingsProvider provider = new DisplayWindowSettingsProvider(
+ mDefaultVendorSettingsStorage, mOverrideSettingsStorage);
+ final SettingsEntry virtualSettings = provider.getOverrideSettings(secondaryDisplayInfo);
+ virtualSettings.mShouldShowSystemDecors = true;
+ virtualSettings.mImePolicy = DISPLAY_IME_POLICY_LOCAL;
+ virtualSettings.mDontMoveToTop = true;
+ provider.updateOverrideSettings(secondaryDisplayInfo, virtualSettings);
+ assertFalse(mOverrideSettingsStorage.wasWriteSuccessful());
+ }
+
+ @Test
public void testWritingDisplaySettingsToStorage_UsePortAsId() throws Exception {
prepareOverrideDisplaySettings(null /* displayIdentifier */, true /* usePortAsId */);
@@ -260,6 +278,54 @@
getStoredDisplayAttributeValue(mOverrideSettingsStorage, "imePolicy"));
}
+ @Test
+ public void testCleanUpEmptyDisplaySettingsOnDisplayRemoved() {
+ final DisplayWindowSettingsProvider provider = new DisplayWindowSettingsProvider(
+ mDefaultVendorSettingsStorage, mOverrideSettingsStorage);
+ final int initialSize = provider.getOverrideSettingsSize();
+
+ // Size + 1 when query for a new display.
+ final DisplayInfo secondaryDisplayInfo = mSecondaryDisplay.getDisplayInfo();
+ final SettingsEntry overrideSettings = provider.getOverrideSettings(secondaryDisplayInfo);
+
+ assertEquals(initialSize + 1, provider.getOverrideSettingsSize());
+
+ // When a display is removed, its override Settings is not removed if there is any override.
+ overrideSettings.mShouldShowSystemDecors = true;
+ provider.updateOverrideSettings(secondaryDisplayInfo, overrideSettings);
+ provider.onDisplayRemoved(secondaryDisplayInfo);
+
+ assertEquals(initialSize + 1, provider.getOverrideSettingsSize());
+
+ // When a display is removed, its override Settings is removed if there is no override.
+ provider.updateOverrideSettings(secondaryDisplayInfo, new SettingsEntry());
+ provider.onDisplayRemoved(secondaryDisplayInfo);
+
+ assertEquals(initialSize, provider.getOverrideSettingsSize());
+ }
+
+ @Test
+ public void testCleanUpVirtualDisplaySettingsOnDisplayRemoved() {
+ final DisplayWindowSettingsProvider provider = new DisplayWindowSettingsProvider(
+ mDefaultVendorSettingsStorage, mOverrideSettingsStorage);
+ final int initialSize = provider.getOverrideSettingsSize();
+
+ // Size + 1 when query for a new display.
+ final DisplayInfo secondaryDisplayInfo = mSecondaryDisplay.getDisplayInfo();
+ secondaryDisplayInfo.type = TYPE_VIRTUAL;
+ final SettingsEntry overrideSettings = provider.getOverrideSettings(secondaryDisplayInfo);
+
+ assertEquals(initialSize + 1, provider.getOverrideSettingsSize());
+
+ // When a virtual display is removed, its override Settings is removed even if it has
+ // override.
+ overrideSettings.mShouldShowSystemDecors = true;
+ provider.updateOverrideSettings(secondaryDisplayInfo, overrideSettings);
+ provider.onDisplayRemoved(secondaryDisplayInfo);
+
+ assertEquals(initialSize, provider.getOverrideSettingsSize());
+ }
+
/**
* Prepares display settings and stores in {@link #mOverrideSettingsStorage}. Uses provided
* display identifier and stores windowingMode=WINDOWING_MODE_PINNED.
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayWindowSettingsTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayWindowSettingsTests.java
index 1cec0ef..e54b8e5 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayWindowSettingsTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayWindowSettingsTests.java
@@ -484,6 +484,18 @@
assertTrue(dcIgnoreOrientation.getIgnoreOrientationRequest());
}
+ @Test
+ public void testDisplayRemoval() {
+ spyOn(mWm.mDisplayWindowSettings);
+ spyOn(mWm.mDisplayWindowSettingsProvider);
+
+ mPrivateDisplay.removeImmediately();
+
+ verify(mWm.mDisplayWindowSettings).onDisplayRemoved(mPrivateDisplay);
+ verify(mWm.mDisplayWindowSettingsProvider).onDisplayRemoved(
+ mPrivateDisplay.getDisplayInfo());
+ }
+
public final class TestSettingsProvider implements DisplayWindowSettings.SettingsProvider {
Map<DisplayInfo, SettingsEntry> mOverrideSettingsCache = new HashMap<>();
@@ -513,5 +525,10 @@
overrideSettings.setTo(settings);
}
+
+ @Override
+ public void onDisplayRemoved(@NonNull DisplayInfo info) {
+ mOverrideSettingsCache.remove(info);
+ }
}
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java b/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java
index b02b774..f23e56d 100644
--- a/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java
@@ -27,8 +27,6 @@
import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK;
import static android.content.Intent.FLAG_ACTIVITY_NEW_DOCUMENT;
import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
-import static android.content.pm.ActivityInfo.LAUNCH_MULTIPLE;
-import static android.content.pm.ActivityInfo.LAUNCH_SINGLE_INSTANCE;
import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
import static android.os.Process.NOBODY_UID;
@@ -451,25 +449,15 @@
final int uid = 10123;
final Task task1 = createTaskBuilder(".Task1").build();
final ComponentName componentName = getUniqueComponentName();
- task1.affinity = ActivityRecord.computeTaskAffinity(taskAffinity, uid, LAUNCH_MULTIPLE,
- componentName);
+ task1.affinity = ActivityRecord.computeTaskAffinity(taskAffinity, uid);
mRecentTasks.add(task1);
// Add another task to recents, and make sure the previous task was removed.
final Task task2 = createTaskBuilder(".Task2").build();
- task2.affinity = ActivityRecord.computeTaskAffinity(taskAffinity, uid, LAUNCH_MULTIPLE,
- componentName);
+ task2.affinity = ActivityRecord.computeTaskAffinity(taskAffinity, uid);
mRecentTasks.add(task2);
assertEquals(1, mRecentTasks.getRecentTasks(MAX_VALUE, 0 /* flags */,
true /* getTasksAllowed */, TEST_USER_0_ID, 0).getList().size());
-
- // Add another single-instance task to recents, and make sure no task is removed.
- final Task task3 = createTaskBuilder(".Task3").build();
- task3.affinity = ActivityRecord.computeTaskAffinity(taskAffinity, uid,
- LAUNCH_SINGLE_INSTANCE, componentName);
- mRecentTasks.add(task3);
- assertEquals(2, mRecentTasks.getRecentTasks(MAX_VALUE, 0 /* flags */,
- true /* getTasksAllowed */, TEST_USER_0_ID, 0).getList().size());
}
@Test
diff --git a/services/tests/wmtests/src/com/android/server/wm/TestDisplayWindowSettingsProvider.java b/services/tests/wmtests/src/com/android/server/wm/TestDisplayWindowSettingsProvider.java
index b2e44b1..e11df98 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TestDisplayWindowSettingsProvider.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TestDisplayWindowSettingsProvider.java
@@ -52,6 +52,12 @@
overrideSettings.setTo(overrides);
}
+ @Override
+ public void onDisplayRemoved(@NonNull DisplayInfo info) {
+ final String identifier = getIdentifier(info);
+ mOverrideSettingsMap.remove(identifier);
+ }
+
@NonNull
private SettingsEntry getOrCreateOverrideSettingsEntry(DisplayInfo info) {
final String identifier = getIdentifier(info);
diff --git a/telephony/java/android/telephony/NetworkRegistrationInfo.java b/telephony/java/android/telephony/NetworkRegistrationInfo.java
index a2fbc95..882c277 100644
--- a/telephony/java/android/telephony/NetworkRegistrationInfo.java
+++ b/telephony/java/android/telephony/NetworkRegistrationInfo.java
@@ -772,6 +772,18 @@
}
}
+ /**
+ * Convert isNonTerrestrialNetwork to string
+ *
+ * @param isNonTerrestrialNetwork boolean indicating whether network is a non-terrestrial
+ * network
+ * @return string format of isNonTerrestrialNetwork.
+ * @hide
+ */
+ public static String isNonTerrestrialNetworkToString(boolean isNonTerrestrialNetwork) {
+ return isNonTerrestrialNetwork ? "NON-TERRESTRIAL" : "TERRESTRIAL";
+ }
+
@NonNull
@Override
public String toString() {
@@ -797,7 +809,8 @@
? nrStateToString(mNrState) : "****")
.append(" rRplmn=").append(mRplmn)
.append(" isUsingCarrierAggregation=").append(mIsUsingCarrierAggregation)
- .append(" isNonTerrestrialNetwork=").append(mIsNonTerrestrialNetwork)
+ .append(" isNonTerrestrialNetwork=").append(
+ isNonTerrestrialNetworkToString(mIsNonTerrestrialNetwork))
.append("}").toString();
}
diff --git a/tools/aapt2/Android.bp b/tools/aapt2/Android.bp
index 0d6dc35..7323b0f 100644
--- a/tools/aapt2/Android.bp
+++ b/tools/aapt2/Android.bp
@@ -47,6 +47,7 @@
"-Wno-missing-field-initializers",
"-fno-exceptions",
"-fno-rtti",
+ "-Wno-deprecated-declarations",
],
target: {
windows: {
diff --git a/tools/aapt2/LoadedApk.h b/tools/aapt2/LoadedApk.h
index 4cd7eae..27c354a 100644
--- a/tools/aapt2/LoadedApk.h
+++ b/tools/aapt2/LoadedApk.h
@@ -40,10 +40,8 @@
};
// Info about an APK loaded in memory.
-class LoadedApk {
+class LoadedApk final {
public:
- virtual ~LoadedApk() = default;
-
// Loads both binary and proto APKs from disk.
static std::unique_ptr<LoadedApk> LoadApkFromPath(android::StringPiece path,
android::IDiagnostics* diag);
@@ -96,8 +94,8 @@
* Writes the APK on disk at the given path, while also removing the resource
* files that are not referenced in the resource table.
*/
- virtual bool WriteToArchive(IAaptContext* context, const TableFlattenerOptions& options,
- IArchiveWriter* writer);
+ bool WriteToArchive(IAaptContext* context, const TableFlattenerOptions& options,
+ IArchiveWriter* writer);
/**
* Writes the APK on disk at the given path, while also removing the resource files that are not
@@ -108,9 +106,9 @@
* original manifest will be written. The manifest is only required if the contents of the new APK
* have been modified in a way that require the AndroidManifest.xml to also be modified.
*/
- virtual bool WriteToArchive(IAaptContext* context, ResourceTable* split_table,
- const TableFlattenerOptions& options, FilterChain* filters,
- IArchiveWriter* writer, xml::XmlResource* manifest = nullptr);
+ bool WriteToArchive(IAaptContext* context, ResourceTable* split_table,
+ const TableFlattenerOptions& options, FilterChain* filters,
+ IArchiveWriter* writer, xml::XmlResource* manifest = nullptr);
/** Loads the file as an xml document. */
std::unique_ptr<xml::XmlResource> LoadXml(const std::string& file_path,
diff --git a/tools/aapt2/SdkConstants.cpp b/tools/aapt2/SdkConstants.cpp
index a766bd4..83f2eb3 100644
--- a/tools/aapt2/SdkConstants.cpp
+++ b/tools/aapt2/SdkConstants.cpp
@@ -16,20 +16,24 @@
#include "SdkConstants.h"
+#include <stdint.h>
+
#include <algorithm>
#include <string>
-#include <unordered_set>
-#include <vector>
+#include <string_view>
using android::StringPiece;
+using namespace std::literals;
namespace aapt {
-static ApiVersion sDevelopmentSdkLevel = 10000;
-static const auto sDevelopmentSdkCodeNames = std::unordered_set<StringPiece>(
- {"Q", "R", "S", "Sv2", "Tiramisu", "UpsideDownCake", "VanillaIceCream"});
+static constexpr ApiVersion sDevelopmentSdkLevel = 10000;
+static constexpr StringPiece sDevelopmentSdkCodeNames[] = {
+ "Q"sv, "R"sv, "S"sv, "Sv2"sv, "Tiramisu"sv, "UpsideDownCake"sv, "VanillaIceCream"sv};
-static const std::vector<std::pair<uint16_t, ApiVersion>> sAttrIdMap = {
+static constexpr auto sPrivacySandboxSuffix = "PrivacySandbox"sv;
+
+static constexpr std::pair<uint16_t, ApiVersion> sAttrIdMap[] = {
{0x021c, 1},
{0x021d, 2},
{0x0269, SDK_CUPCAKE},
@@ -62,25 +66,37 @@
{0x064c, SDK_S_V2},
};
-static bool less_entry_id(const std::pair<uint16_t, ApiVersion>& p, uint16_t entryId) {
- return p.first < entryId;
-}
+static_assert(std::is_sorted(std::begin(sAttrIdMap), std::end(sAttrIdMap),
+ [](auto&& l, auto&& r) { return l.first < r.first; }));
ApiVersion FindAttributeSdkLevel(const ResourceId& id) {
if (id.package_id() != 0x01 || id.type_id() != 0x01) {
return 0;
}
- auto iter = std::lower_bound(sAttrIdMap.begin(), sAttrIdMap.end(), id.entry_id(), less_entry_id);
- if (iter == sAttrIdMap.end()) {
+ const auto it =
+ std::lower_bound(std::begin(sAttrIdMap), std::end(sAttrIdMap), id.entry_id(),
+ [](const auto& pair, uint16_t entryId) { return pair.first < entryId; });
+ if (it == std::end(sAttrIdMap)) {
return SDK_LOLLIPOP_MR1;
}
- return iter->second;
+ return it->second;
}
std::optional<ApiVersion> GetDevelopmentSdkCodeNameVersion(StringPiece code_name) {
- return (sDevelopmentSdkCodeNames.find(code_name) == sDevelopmentSdkCodeNames.end())
- ? std::optional<ApiVersion>()
- : sDevelopmentSdkLevel;
+ const auto it =
+ std::find_if(std::begin(sDevelopmentSdkCodeNames), std::end(sDevelopmentSdkCodeNames),
+ [code_name](const auto& item) { return code_name.starts_with(item); });
+ if (it == std::end(sDevelopmentSdkCodeNames)) {
+ return {};
+ }
+ if (code_name.size() == it->size()) {
+ return sDevelopmentSdkLevel;
+ }
+ if (code_name.size() == it->size() + sPrivacySandboxSuffix.size() &&
+ code_name.ends_with(sPrivacySandboxSuffix)) {
+ return sDevelopmentSdkLevel;
+ }
+ return {};
}
} // namespace aapt
diff --git a/tools/aapt2/SdkConstants_test.cpp b/tools/aapt2/SdkConstants_test.cpp
index 61f4d71..0f645ff 100644
--- a/tools/aapt2/SdkConstants_test.cpp
+++ b/tools/aapt2/SdkConstants_test.cpp
@@ -28,4 +28,24 @@
EXPECT_EQ(0, FindAttributeSdkLevel(ResourceId(0x7f010345)));
}
+TEST(SdkConstantsTest, GetDevelopmentSdkCodeNameVersionValid) {
+ EXPECT_EQ(std::optional<ApiVersion>(10000), GetDevelopmentSdkCodeNameVersion("Q"));
+ EXPECT_EQ(std::optional<ApiVersion>(10000), GetDevelopmentSdkCodeNameVersion("VanillaIceCream"));
+}
+
+TEST(SdkConstantsTest, GetDevelopmentSdkCodeNameVersionPrivacySandbox) {
+ EXPECT_EQ(std::optional<ApiVersion>(10000), GetDevelopmentSdkCodeNameVersion("QPrivacySandbox"));
+ EXPECT_EQ(std::optional<ApiVersion>(10000),
+ GetDevelopmentSdkCodeNameVersion("VanillaIceCreamPrivacySandbox"));
+}
+
+TEST(SdkConstantsTest, GetDevelopmentSdkCodeNameVersionInvalid) {
+ EXPECT_EQ(std::optional<ApiVersion>(), GetDevelopmentSdkCodeNameVersion("A"));
+ EXPECT_EQ(std::optional<ApiVersion>(), GetDevelopmentSdkCodeNameVersion("Sv3"));
+ EXPECT_EQ(std::optional<ApiVersion>(),
+ GetDevelopmentSdkCodeNameVersion("VanillaIceCream_PrivacySandbox"));
+ EXPECT_EQ(std::optional<ApiVersion>(), GetDevelopmentSdkCodeNameVersion("PrivacySandbox"));
+ EXPECT_EQ(std::optional<ApiVersion>(), GetDevelopmentSdkCodeNameVersion("QQQQQQQQQQQQQQQ"));
+}
+
} // namespace aapt
diff --git a/tools/aapt2/optimize/Obfuscator.cpp b/tools/aapt2/optimize/Obfuscator.cpp
index 8f12f735..903cdf8 100644
--- a/tools/aapt2/optimize/Obfuscator.cpp
+++ b/tools/aapt2/optimize/Obfuscator.cpp
@@ -40,9 +40,9 @@
collapse_key_stringpool_(optimizeOptions.table_flattener_options.collapse_key_stringpool) {
}
-std::string ShortenFileName(android::StringPiece file_path, int output_length) {
+std::string Obfuscator::ShortenFileName(android::StringPiece file_path, int output_length) {
std::size_t hash_num = std::hash<android::StringPiece>{}(file_path);
- std::string result = "";
+ std::string result;
// Convert to (modified) base64 so that it is a proper file path.
for (int i = 0; i < output_length; i++) {
uint8_t sextet = hash_num & 0x3f;
@@ -52,10 +52,33 @@
return result;
}
+static std::string RenameDisallowedFileNames(const std::string& file_name) {
+ // We are renaming shortened file names to make sure they not a reserved file name in Windows.
+ // See: https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file. We are renaming
+ // "COM" and "LPT" too because we are appending a number in case of hash collisions; "COM1",
+ // "COM2", etc. are reserved names.
+ static const char* const reserved_windows_names[] = {"CON", "PRN", "AUX", "NUL", "COM", "LPT"};
+ if (file_name.length() == 3) {
+ // Need to convert the file name to uppercase as Windows is case insensitive. E.g., "NuL",
+ // "nul", and "NUl" are also reserved.
+ std::string result_upper_cased(3, 0);
+ std::transform(file_name.begin(), file_name.end(), result_upper_cased.begin(),
+ [](unsigned char c) { return std::toupper(c); });
+ for (auto reserved_windows_name : reserved_windows_names) {
+ if (result_upper_cased == reserved_windows_name) {
+ // Simple solution to make it a non-reserved name is to add an underscore
+ return "_" + file_name;
+ }
+ }
+ }
+
+ return file_name;
+}
+
// Return the optimal hash length such that at most 10% of resources collide in
// their shortened path.
// Reference: http://matt.might.net/articles/counting-hash-collisions/
-int OptimalShortenedLength(int num_resources) {
+static int OptimalShortenedLength(int num_resources) {
if (num_resources > 4000) {
return 3;
} else {
@@ -63,8 +86,8 @@
}
}
-std::string GetShortenedPath(android::StringPiece shortened_filename,
- android::StringPiece extension, int collision_count) {
+static std::string GetShortenedPath(android::StringPiece shortened_filename,
+ android::StringPiece extension, int collision_count) {
std::string shortened_path = std::string("res/") += shortened_filename;
if (collision_count > 0) {
shortened_path += std::to_string(collision_count);
@@ -82,9 +105,9 @@
}
};
-static bool HandleShortenFilePaths(ResourceTable* table,
- std::map<std::string, std::string>& shortened_path_map,
- const std::set<ResourceName>& path_shorten_exemptions) {
+bool Obfuscator::HandleShortenFilePaths(ResourceTable* table,
+ std::map<std::string, std::string>& shortened_path_map,
+ const std::set<ResourceName>& path_shorten_exemptions) {
// used to detect collisions
std::unordered_set<std::string> shortened_paths;
std::set<FileReference*, PathComparator> file_refs;
@@ -112,7 +135,8 @@
// Android detects ColorStateLists via pathname, skip res/color*
if (util::StartsWith(res_subdir, "res/color")) continue;
- std::string shortened_filename = ShortenFileName(*file_ref->path, num_chars);
+ std::string shortened_filename =
+ RenameDisallowedFileNames(ShortenFileName(*file_ref->path, num_chars));
int collision_count = 0;
std::string shortened_path = GetShortenedPath(shortened_filename, extension, collision_count);
while (shortened_paths.find(shortened_path) != shortened_paths.end()) {
diff --git a/tools/aapt2/optimize/Obfuscator.h b/tools/aapt2/optimize/Obfuscator.h
index 5ccf5438..79d7e08 100644
--- a/tools/aapt2/optimize/Obfuscator.h
+++ b/tools/aapt2/optimize/Obfuscator.h
@@ -53,7 +53,14 @@
const ResourceNamedType& type_name, const ResourceTableEntryView& entry,
const android::base::function_ref<void(Result, const ResourceName&)> onObfuscate);
+ protected:
+ virtual std::string ShortenFileName(android::StringPiece file_path, int output_length);
+
private:
+ bool HandleShortenFilePaths(ResourceTable* table,
+ std::map<std::string, std::string>& shortened_path_map,
+ const std::set<ResourceName>& path_shorten_exemptions);
+
TableFlattenerOptions& options_;
const bool shorten_resource_paths_;
const bool collapse_key_stringpool_;
diff --git a/tools/aapt2/optimize/Obfuscator_test.cpp b/tools/aapt2/optimize/Obfuscator_test.cpp
index b3a915c..c3429e0 100644
--- a/tools/aapt2/optimize/Obfuscator_test.cpp
+++ b/tools/aapt2/optimize/Obfuscator_test.cpp
@@ -19,6 +19,7 @@
#include <map>
#include <memory>
#include <string>
+#include <utility>
#include "ResourceTable.h"
#include "android-base/file.h"
@@ -26,6 +27,7 @@
using ::aapt::test::GetValue;
using ::testing::AnyOf;
+using ::testing::Contains;
using ::testing::Eq;
using ::testing::HasSubstr;
using ::testing::IsFalse;
@@ -33,6 +35,10 @@
using ::testing::Not;
using ::testing::NotNull;
+namespace aapt {
+
+namespace {
+
android::StringPiece GetExtension(android::StringPiece path) {
auto iter = std::find(path.begin(), path.end(), '.');
return android::StringPiece(iter, path.end() - iter);
@@ -45,7 +51,22 @@
}
}
-namespace aapt {
+class FakeObfuscator : public Obfuscator {
+ public:
+ explicit FakeObfuscator(OptimizeOptions& optimize_options,
+ const std::unordered_map<std::string, std::string>& shortened_name_map)
+ : Obfuscator(optimize_options), shortened_name_map_(shortened_name_map) {
+ }
+
+ protected:
+ std::string ShortenFileName(android::StringPiece file_path, int output_length) override {
+ return shortened_name_map_[std::string(file_path)];
+ }
+
+ private:
+ std::unordered_map<std::string, std::string> shortened_name_map_;
+ DISALLOW_COPY_AND_ASSIGN(FakeObfuscator);
+};
TEST(ObfuscatorTest, FileRefPathsChangedInResourceTable) {
std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build();
@@ -127,7 +148,7 @@
EXPECT_THAT(path_map.find("res/drawables/xmlfile2.xml"), Not(Eq(path_map.end())));
FileReference* ref = GetValue<FileReference>(table.get(), "android:drawable/xmlfile");
- EXPECT_THAT(ref, NotNull());
+ ASSERT_THAT(ref, NotNull());
ASSERT_THAT(HasFailure(), IsFalse());
// The path of first drawable in exemption was not changed
EXPECT_THAT("res/drawables/xmlfile.xml", Eq(*ref->path));
@@ -161,13 +182,78 @@
ASSERT_THAT(path_map.find("res/drawable/xmlfile.xml"), Not(Eq(path_map.end())));
ASSERT_THAT(path_map.find("res/drawable/pngfile.png"), Not(Eq(path_map.end())));
- auto shortend_xml_path = path_map[original_xml_path];
- auto shortend_png_path = path_map[original_png_path];
-
EXPECT_THAT(GetExtension(path_map[original_xml_path]), Eq(android::StringPiece(".xml")));
EXPECT_THAT(GetExtension(path_map[original_png_path]), Eq(android::StringPiece(".png")));
}
+TEST(ObfuscatorTest, ShortenedToReservedWindowsNames) {
+ std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build();
+
+ std::string original_path_1 = "res/drawable/pngfile_1.png";
+ std::string original_path_2 = "res/drawable/pngfile_2.png";
+ std::string original_path_3 = "res/drawable/pngfile_3.png";
+ std::string original_path_4 = "res/drawable/pngfile_4.png";
+ std::string original_path_5 = "res/drawable/pngfile_5.png";
+ std::string original_path_6 = "res/drawable/pngfile_6.png";
+ std::string original_path_7 = "res/drawable/pngfile_7.png";
+ std::string original_path_8 = "res/drawable/pngfile_8.png";
+ std::string original_path_9 = "res/drawable/pngfile_9.png";
+
+ std::unique_ptr<ResourceTable> table =
+ test::ResourceTableBuilder()
+ .AddFileReference("android:drawable/pngfile_1", original_path_1)
+ .AddFileReference("android:drawable/pngfile_2", original_path_2)
+ .AddFileReference("android:drawable/pngfile_3", original_path_3)
+ .AddFileReference("android:drawable/pngfile_4", original_path_4)
+ .AddFileReference("android:drawable/pngfile_5", original_path_5)
+ .AddFileReference("android:drawable/pngfile_6", original_path_6)
+ .AddFileReference("android:drawable/pngfile_7", original_path_7)
+ .AddFileReference("android:drawable/pngfile_8", original_path_8)
+ .AddFileReference("android:drawable/pngfile_9", original_path_9)
+ .Build();
+
+ OptimizeOptions options{.shorten_resource_paths = true};
+ std::map<std::string, std::string>& path_map = options.table_flattener_options.shortened_path_map;
+ auto obfuscator = FakeObfuscator(
+ options,
+ {
+ {original_path_1, "CON"},
+ {original_path_2, "Prn"},
+ {original_path_3, "AuX"},
+ {original_path_4, "nul"},
+ {original_path_5, "cOM"},
+ {original_path_6, "lPt"},
+ {original_path_7, "lPt"},
+ {original_path_8, "lPt"}, // 6, 7, and 8 will be appended with a number to disambiguate
+ {original_path_9, "F0o"}, // This one is not reserved
+ });
+ ASSERT_TRUE(obfuscator.Consume(context.get(), table.get()));
+
+ // Expect that the path map is populated
+ ASSERT_THAT(path_map.find(original_path_1), Not(Eq(path_map.end())));
+ ASSERT_THAT(path_map.find(original_path_2), Not(Eq(path_map.end())));
+ ASSERT_THAT(path_map.find(original_path_3), Not(Eq(path_map.end())));
+ ASSERT_THAT(path_map.find(original_path_4), Not(Eq(path_map.end())));
+ ASSERT_THAT(path_map.find(original_path_5), Not(Eq(path_map.end())));
+ ASSERT_THAT(path_map.find(original_path_6), Not(Eq(path_map.end())));
+ ASSERT_THAT(path_map.find(original_path_7), Not(Eq(path_map.end())));
+ ASSERT_THAT(path_map.find(original_path_8), Not(Eq(path_map.end())));
+ ASSERT_THAT(path_map.find(original_path_9), Not(Eq(path_map.end())));
+
+ EXPECT_THAT(path_map[original_path_1], Eq("res/_CON.png"));
+ EXPECT_THAT(path_map[original_path_2], Eq("res/_Prn.png"));
+ EXPECT_THAT(path_map[original_path_3], Eq("res/_AuX.png"));
+ EXPECT_THAT(path_map[original_path_4], Eq("res/_nul.png"));
+ EXPECT_THAT(path_map[original_path_5], Eq("res/_cOM.png"));
+ EXPECT_THAT(path_map[original_path_9], Eq("res/F0o.png"));
+
+ std::set<std::string> lpt_shortened_names{path_map[original_path_6], path_map[original_path_7],
+ path_map[original_path_8]};
+ EXPECT_THAT(lpt_shortened_names, Contains("res/_lPt.png"));
+ EXPECT_THAT(lpt_shortened_names, Contains("res/_lPt1.png"));
+ EXPECT_THAT(lpt_shortened_names, Contains("res/_lPt2.png"));
+}
+
TEST(ObfuscatorTest, DeterministicallyHandleCollisions) {
std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build();
@@ -247,7 +333,9 @@
ASSERT_TRUE(Obfuscator(options).Consume(context.get(), table.get()));
// Expect that the id resource name map is populated
+ ASSERT_THAT(id_resource_map.find(0x7f020000), Not(Eq(id_resource_map.end())));
EXPECT_THAT(id_resource_map.at(0x7f020000), Eq("mycolor"));
+ ASSERT_THAT(id_resource_map.find(0x7f030000), Not(Eq(id_resource_map.end())));
EXPECT_THAT(id_resource_map.at(0x7f030000), Eq("mystring"));
EXPECT_THAT(id_resource_map.find(0x7f030001), Eq(id_resource_map.end()));
EXPECT_THAT(id_resource_map.find(0x7f030002), Eq(id_resource_map.end()));
@@ -310,8 +398,8 @@
EXPECT_THAT(pbOut, HasSubstr("mycolor"));
EXPECT_THAT(pbOut, HasSubstr("mystring"));
pb::ResourceMappings resourceMappings;
- EXPECT_THAT(resourceMappings.ParseFromString(pbOut), IsTrue());
- EXPECT_THAT(resourceMappings.collapsed_names().resource_names_size(), Eq(2));
+ ASSERT_THAT(resourceMappings.ParseFromString(pbOut), IsTrue());
+ ASSERT_THAT(resourceMappings.collapsed_names().resource_names_size(), Eq(2));
auto& resource_names = resourceMappings.collapsed_names().resource_names();
EXPECT_THAT(resource_names.at(0).name(), AnyOf(Eq("mycolor"), Eq("mystring")));
EXPECT_THAT(resource_names.at(1).name(), AnyOf(Eq("mycolor"), Eq("mystring")));
@@ -337,4 +425,6 @@
ASSERT_THAT(pbOut, Eq(""));
}
+} // namespace
+
} // namespace aapt