diff options
131 files changed, 7131 insertions, 1341 deletions
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index 6d82922484bc..2a6d84b1acc6 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -5298,6 +5298,13 @@ public abstract class Context { 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/flags/BooleanFlag.java b/core/java/android/flags/BooleanFlag.java new file mode 100644 index 000000000000..d4a35b25f623 --- /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 000000000000..985dbe3f2f01 --- /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 000000000000..271a8c5f4d15 --- /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 000000000000..68819c58c064 --- /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 000000000000..8d3112c35d51 --- /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 000000000000..daedcdae0b5f --- /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 000000000000..b97a4c8a0fe7 --- /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 000000000000..6844b8faafef --- /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 000000000000..e9adba7595c1 --- /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 000000000000..3efcec97fe6d --- /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 000000000000..f708667ea4c4 --- /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 000000000000..fa125c4a159c --- /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 000000000000..1526ec148967 --- /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 000000000000..449bcc3c49f5 --- /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/hardware/camera2/CameraDevice.java b/core/java/android/hardware/camera2/CameraDevice.java index 99b297abe92a..0e45787c1340 100644 --- a/core/java/android/hardware/camera2/CameraDevice.java +++ b/core/java/android/hardware/camera2/CameraDevice.java @@ -283,7 +283,8 @@ public abstract class CameraDevice implements AutoCloseable { * @see StreamConfigurationMap#getInputFormats * @see StreamConfigurationMap#getInputSizes * @see StreamConfigurationMap#getValidOutputFormatsForInput - * @see StreamConfigurationMap#getOutputSizes + * @see StreamConfigurationMap#getOutputSizes(int) + * @see StreamConfigurationMap#getOutputSizes(Class) * @see android.media.ImageWriter * @see android.media.ImageReader * @deprecated Please use {@link diff --git a/core/java/android/hardware/camera2/CameraManager.java b/core/java/android/hardware/camera2/CameraManager.java index a098362f16aa..c2fe0800812f 100644 --- a/core/java/android/hardware/camera2/CameraManager.java +++ b/core/java/android/hardware/camera2/CameraManager.java @@ -130,14 +130,17 @@ public final class CameraManager { /** * Enable physical camera availability callbacks when the logical camera is unavailable * - * <p>Previously once a logical camera becomes unavailable, no {@link - * #onPhysicalCameraAvailable} or {@link #onPhysicalCameraUnavailable} will be called until - * the logical camera becomes available again. The results in the app opening the logical - * camera not able to receive physical camera availability change.</p> - * - * <p>With this change, the {@link #onPhysicalCameraAvailable} and {@link - * #onPhysicalCameraUnavailable} can still be called while the logical camera is unavailable. - * </p> + * <p>Previously once a logical camera becomes unavailable, no + * {@link AvailabilityCallback#onPhysicalCameraAvailable} or + * {@link AvailabilityCallback#onPhysicalCameraUnavailable} will + * be called until the logical camera becomes available again. The + * results in the app opening the logical camera not able to + * receive physical camera availability change.</p> + * + * <p>With this change, the {@link + * AvailabilityCallback#onPhysicalCameraAvailable} and {@link + * AvailabilityCallback#onPhysicalCameraUnavailable} can still be + * called while the logical camera is unavailable. </p> */ @ChangeId @EnabledSince(targetSdkVersion = android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE) diff --git a/core/java/android/hardware/camera2/package.html b/core/java/android/hardware/camera2/package.html index 719c2f620234..3fd5d7c60832 100644 --- a/core/java/android/hardware/camera2/package.html +++ b/core/java/android/hardware/camera2/package.html @@ -62,12 +62,28 @@ RAW buffers for {@link android.hardware.camera2.DngCreator} can be done with {@link android.media.ImageReader} with the {@link android.graphics.ImageFormat#JPEG} and {@link android.graphics.ImageFormat#RAW_SENSOR} formats. Application-driven -processing of camera data in RenderScript, OpenGL ES, or directly in -managed or native code is best done through {@link -android.renderscript.Allocation} with a YUV {@link -android.renderscript.Type}, {@link android.graphics.SurfaceTexture}, -and {@link android.media.ImageReader} with a {@link -android.graphics.ImageFormat#YUV_420_888} format, respectively.</p> +processing of camera data in OpenGL ES, or directly in managed or +native code is best done through {@link +android.graphics.SurfaceTexture}, or {@link android.media.ImageReader} +with a {@link android.graphics.ImageFormat#YUV_420_888} format, +respectively. </p> + +<p>By default, YUV-format buffers provided by the camera are using the +JFIF YUV<->RGB transform matrix (equivalent to Rec.601 full-range +encoding), and after conversion to RGB with this matrix, the resulting +RGB data is in the sRGB colorspace. Captured JPEG images may contain +an ICC profile to specify their color space information; if not, they +should be assumed to be in the sRGB space as well. On some devices, +the output colorspace can be changed via {@link +android.hardware.camera2.params.SessionConfiguration#setColorSpace}. +</p> +<p> +Note that although the YUV->RGB transform is the JFIF matrix (Rec.601 +full-range), due to legacy and compatibility reasons, the output is in +the sRGB colorspace, which uses the Rec.709 color primaries. Image +processing code can safely treat the output RGB as being in the sRGB +colorspace. +</p> <p>The application then needs to construct a {@link android.hardware.camera2.CaptureRequest}, which defines all the diff --git a/core/java/android/hardware/camera2/params/ColorSpaceProfiles.java b/core/java/android/hardware/camera2/params/ColorSpaceProfiles.java index 2e3af80f9cc0..bb154a96cbe9 100644 --- a/core/java/android/hardware/camera2/params/ColorSpaceProfiles.java +++ b/core/java/android/hardware/camera2/params/ColorSpaceProfiles.java @@ -192,7 +192,7 @@ public final class ColorSpaceProfiles { * @see OutputConfiguration#setDynamicRangeProfile * @see SessionConfiguration#setColorSpace * @see ColorSpace.Named - * @see DynamicRangeProfiles.Profile + * @see DynamicRangeProfiles */ public @NonNull Set<Long> getSupportedDynamicRangeProfiles(@NonNull ColorSpace.Named colorSpace, @ImageFormat.Format int imageFormat) { @@ -230,7 +230,7 @@ public final class ColorSpaceProfiles { * @see SessionConfiguration#setColorSpace * @see OutputConfiguration#setDynamicRangeProfile * @see ColorSpace.Named - * @see DynamicRangeProfiles.Profile + * @see DynamicRangeProfiles */ public @NonNull Set<ColorSpace.Named> getSupportedColorSpacesForDynamicRange( @ImageFormat.Format int imageFormat, diff --git a/core/java/android/hardware/camera2/params/RecommendedStreamConfigurationMap.java b/core/java/android/hardware/camera2/params/RecommendedStreamConfigurationMap.java index 80db38fc9d8f..d4ce0ebbc528 100644 --- a/core/java/android/hardware/camera2/params/RecommendedStreamConfigurationMap.java +++ b/core/java/android/hardware/camera2/params/RecommendedStreamConfigurationMap.java @@ -45,7 +45,7 @@ import java.util.Set; * Immutable class to store the recommended stream configurations to set up * {@link android.view.Surface Surfaces} for creating a * {@link android.hardware.camera2.CameraCaptureSession capture session} with - * {@link android.hardware.camera2.CameraDevice#createCaptureSession}. + * {@link android.hardware.camera2.CameraDevice#createCaptureSession(SessionConfiguration)}. * * <p>The recommended list does not replace or deprecate the exhaustive complete list found in * {@link StreamConfigurationMap}. It is a suggestion about available power and performance @@ -70,7 +70,7 @@ import java.util.Set; * }</code></pre> * * @see CameraCharacteristics#getRecommendedStreamConfigurationMap - * @see CameraDevice#createCaptureSession + * @see CameraDevice#createCaptureSession(SessionConfiguration) */ public final class RecommendedStreamConfigurationMap { @@ -282,7 +282,7 @@ public final class RecommendedStreamConfigurationMap { /** * Determine whether or not output surfaces with a particular user-defined format can be passed - * {@link CameraDevice#createCaptureSession createCaptureSession}. + * {@link CameraDevice#createCaptureSession(SessionConfiguration) createCaptureSession}. * * <p> * For further information refer to {@link StreamConfigurationMap#isOutputSupportedFor}. @@ -292,7 +292,7 @@ public final class RecommendedStreamConfigurationMap { * @param format an image format from either {@link ImageFormat} or {@link PixelFormat} * @return * {@code true} if using a {@code surface} with this {@code format} will be - * supported with {@link CameraDevice#createCaptureSession} + * supported with {@link CameraDevice#createCaptureSession(SessionConfiguration)} * * @throws IllegalArgumentException * if the image format was not a defined named constant @@ -508,8 +508,10 @@ public final class RecommendedStreamConfigurationMap { } /** - * Determine whether or not the {@code surface} in its current state is suitable to be included - * in a {@link CameraDevice#createCaptureSession capture session} as an output. + * Determine whether or not the {@code surface} in its current + * state is suitable to be included in a {@link + * CameraDevice#createCaptureSession(SessionConfiguration) capture + * session} as an output. * * <p>For more information refer to {@link StreamConfigurationMap#isOutputSupportedFor}. * </p> diff --git a/core/java/android/hardware/camera2/params/SessionConfiguration.java b/core/java/android/hardware/camera2/params/SessionConfiguration.java index 385f10719509..8f611a831204 100644 --- a/core/java/android/hardware/camera2/params/SessionConfiguration.java +++ b/core/java/android/hardware/camera2/params/SessionConfiguration.java @@ -55,7 +55,7 @@ public final class SessionConfiguration implements Parcelable { * at regular non high speed FPS ranges and optionally {@link InputConfiguration} for * reprocessable sessions. * - * @see CameraDevice#createCaptureSession + * @see CameraDevice#createCaptureSession(SessionConfiguration) * @see CameraDevice#createReprocessableCaptureSession */ public static final int SESSION_REGULAR = CameraDevice.SESSION_OPERATION_MODE_NORMAL; @@ -110,10 +110,7 @@ public final class SessionConfiguration implements Parcelable { * * @see #SESSION_REGULAR * @see #SESSION_HIGH_SPEED - * @see CameraDevice#createCaptureSession(List, CameraCaptureSession.StateCallback, Handler) - * @see CameraDevice#createCaptureSessionByOutputConfigurations - * @see CameraDevice#createReprocessableCaptureSession - * @see CameraDevice#createConstrainedHighSpeedCaptureSession + * @see CameraDevice#createCaptureSession(SessionConfiguration) */ public SessionConfiguration(@SessionMode int sessionType, @NonNull List<OutputConfiguration> outputs, diff --git a/core/java/android/hardware/camera2/params/StreamConfigurationMap.java b/core/java/android/hardware/camera2/params/StreamConfigurationMap.java index aabe149d16f5..ef0db7f8a41c 100644 --- a/core/java/android/hardware/camera2/params/StreamConfigurationMap.java +++ b/core/java/android/hardware/camera2/params/StreamConfigurationMap.java @@ -41,7 +41,7 @@ import java.util.Set; * {@link CameraCharacteristics#SCALER_STREAM_CONFIGURATION_MAP configurations} to set up * {@link android.view.Surface Surfaces} for creating a * {@link android.hardware.camera2.CameraCaptureSession capture session} with - * {@link android.hardware.camera2.CameraDevice#createCaptureSession}. + * {@link android.hardware.camera2.CameraDevice#createCaptureSession(SessionConfiguration)}. * <!-- TODO: link to input stream configuration --> * * <p>This is the authoritative list for all <!-- input/ -->output formats (and sizes respectively @@ -62,7 +62,7 @@ import java.util.Set; * }</code></pre> * * @see CameraCharacteristics#SCALER_STREAM_CONFIGURATION_MAP - * @see CameraDevice#createCaptureSession + * @see CameraDevice#createCaptureSession(SessionConfiguration) */ public final class StreamConfigurationMap { @@ -456,7 +456,7 @@ public final class StreamConfigurationMap { /** * Determine whether or not output surfaces with a particular user-defined format can be passed - * {@link CameraDevice#createCaptureSession createCaptureSession}. + * {@link CameraDevice#createCaptureSession(SessionConfiguration) createCaptureSession}. * * <p>This method determines that the output {@code format} is supported by the camera device; * each output {@code surface} target may or may not itself support that {@code format}. @@ -468,7 +468,7 @@ public final class StreamConfigurationMap { * @param format an image format from either {@link ImageFormat} or {@link PixelFormat} * @return * {@code true} iff using a {@code surface} with this {@code format} will be - * supported with {@link CameraDevice#createCaptureSession} + * supported with {@link CameraDevice#createCaptureSession(SessionConfiguration)} * * @throws IllegalArgumentException * if the image format was not a defined named constant @@ -476,7 +476,7 @@ public final class StreamConfigurationMap { * * @see ImageFormat * @see PixelFormat - * @see CameraDevice#createCaptureSession + * @see CameraDevice#createCaptureSession(SessionConfiguration) */ public boolean isOutputSupportedFor(int format) { checkArgumentFormat(format); @@ -521,7 +521,7 @@ public final class StreamConfigurationMap { * * <p>Generally speaking this means that creating a {@link Surface} from that class <i>may</i> * provide a producer endpoint that is suitable to be used with - * {@link CameraDevice#createCaptureSession}.</p> + * {@link CameraDevice#createCaptureSession(SessionConfiguration)}.</p> * * <p>Since not all of the above classes support output of all format and size combinations, * the particular combination should be queried with {@link #isOutputSupportedFor(Surface)}.</p> @@ -531,7 +531,7 @@ public final class StreamConfigurationMap { * * @throws NullPointerException if {@code klass} was {@code null} * - * @see CameraDevice#createCaptureSession + * @see CameraDevice#createCaptureSession(SessionConfiguration) * @see #isOutputSupportedFor(Surface) */ public static <T> boolean isOutputSupportedFor(Class<T> klass) { @@ -555,8 +555,10 @@ public final class StreamConfigurationMap { } /** - * Determine whether or not the {@code surface} in its current state is suitable to be included - * in a {@link CameraDevice#createCaptureSession capture session} as an output. + * Determine whether or not the {@code surface} in its current + * state is suitable to be included in a {@link + * CameraDevice#createCaptureSession(SessionConfiguration) capture + * session} as an output. * * <p>Not all surfaces are usable with the {@link CameraDevice}, and not all configurations * of that {@code surface} are compatible. Some classes that provide the {@code surface} are @@ -588,7 +590,7 @@ public final class StreamConfigurationMap { * @throws NullPointerException if {@code surface} was {@code null} * @throws IllegalArgumentException if the Surface endpoint is no longer valid * - * @see CameraDevice#createCaptureSession + * @see CameraDevice#createCaptureSession(SessionConfiguration) * @see #isOutputSupportedFor(Class) */ public boolean isOutputSupportedFor(Surface surface) { @@ -623,14 +625,16 @@ public final class StreamConfigurationMap { } /** - * Determine whether or not the particular stream configuration is suitable to be included - * in a {@link CameraDevice#createCaptureSession capture session} as an output. + * Determine whether or not the particular stream configuration is + * suitable to be included in a {@link + * CameraDevice#createCaptureSession(SessionConfiguration) capture + * session} as an output. * * @param size stream configuration size * @param format stream configuration format * @return {@code true} if this is supported, {@code false} otherwise * - * @see CameraDevice#createCaptureSession + * @see CameraDevice#createCaptureSession(SessionConfiguration) * @see #isOutputSupportedFor(Class) * @hide */ diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 753349c19e10..88c7250bdc15 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -4709,6 +4709,15 @@ public final class Settings { public static final String PEAK_REFRESH_RATE = "peak_refresh_rate"; /** + * Control whether to stay awake on fold + * + * If this isn't set, the system falls back to a device specific default. + * @hide + */ + @Readable + public static final String STAY_AWAKE_ON_FOLD = "stay_awake_on_fold"; + + /** * The amount of time in milliseconds before the device goes to sleep or begins * to dream after a period of inactivity. This value is also known as the * user activity timeout period since the screen isn't necessarily turned off diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index f1cde3b4bb7e..2499be999f8d 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -21821,6 +21821,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, mCurrentAnimation = null; if ((mViewFlags & TOOLTIP) == TOOLTIP) { + removeCallbacks(mTooltipInfo.mShowTooltipRunnable); + removeCallbacks(mTooltipInfo.mHideTooltipRunnable); hideTooltip(); } diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java index d702367965a1..d5f2aa3b3631 100644 --- a/core/java/android/view/WindowManager.java +++ b/core/java/android/view/WindowManager.java @@ -880,22 +880,23 @@ public interface WindowManager extends ViewManager { int LARGE_SCREEN_SMALLEST_SCREEN_WIDTH_DP = 600; /** - * Application level {@link android.content.pm.PackageManager.Property PackageManager - * .Property} for an app to inform the system that the app can be opted-in or opted-out - * from the compatibility treatment that avoids {@link - * android.app.Activity#setRequestedOrientation} loops. The loop can be trigerred by - * ignoreRequestedOrientation display setting enabled on the device or by the landscape natural - * orientation of the device. + * Application level {@link android.content.pm.PackageManager.Property PackageManager.Property} + * for an app to inform the system that the app can be opted-in or opted-out from the + * compatibility treatment that avoids {@link android.app.Activity#setRequestedOrientation + * Activity#setRequestedOrientation()} loops. Loops can be triggered by the OEM-configured + * ignore requested orientation display setting (on Android 12 (API level 31) and higher) or by + * the landscape natural orientation of the device. * * <p>The treatment is disabled by default but device manufacturers can enable the treatment * using their discretion to improve display compatibility. * - * <p>With this property set to {@code true}, the system could ignore {@link - * android.app.Activity#setRequestedOrientation} call from an app if one of the following - * conditions are true: + * <p>With this property set to {@code true}, the system could ignore + * {@link android.app.Activity#setRequestedOrientation Activity#setRequestedOrientation()} call + * from an app if one of the following conditions are true: * <ul> - * <li>Activity is relaunching due to the previous {@link - * android.app.Activity#setRequestedOrientation} call. + * <li>Activity is relaunching due to the previous + * {@link android.app.Activity#setRequestedOrientation Activity#setRequestedOrientation()} + * call. * <li>Camera compatibility force rotation treatment is active for the package. * </ul> * @@ -919,14 +920,16 @@ public interface WindowManager extends ViewManager { /** * Application level {@link android.content.pm.PackageManager.Property PackageManager.Property} * for an app to inform the system that the app can be opted-out from the compatibility - * treatment that avoids {@link android.app.Activity#setRequestedOrientation} loops. The loop - * can be trigerred by ignoreRequestedOrientation display setting enabled on the device or - * by the landscape natural orientation of the device. + * treatment that avoids {@link android.app.Activity#setRequestedOrientation + * Activity#setRequestedOrientation()} loops. Loops can be triggered by the OEM-configured + * ignore requested orientation display setting (on Android 12 (API level 31) and higher) or by + * the landscape natural orientation of the device. * - * <p>The system could ignore {@link android.app.Activity#setRequestedOrientation} - * call from an app if both of the following conditions are true: + * <p>The system could ignore {@link android.app.Activity#setRequestedOrientation + * Activity#setRequestedOrientation()} call from an app if both of the following conditions are + * true: * <ul> - * <li>Activity has requested orientation more than 2 times within 1-second timer + * <li>Activity has requested orientation more than two times within one-second timer * <li>Activity is not letterboxed for fixed orientation * </ul> * @@ -953,23 +956,21 @@ public interface WindowManager extends ViewManager { "android.window.PROPERTY_COMPAT_ALLOW_IGNORING_ORIENTATION_REQUEST_WHEN_LOOP_DETECTED"; /** - * Application level {@link android.content.pm.PackageManager.Property PackageManager - * .Property} for an app to inform the system that it needs to be opted-out from the - * compatibility treatment that sandboxes {@link android.view.View} API. + * Application level {@link android.content.pm.PackageManager.Property PackageManager.Property} + * for an app to inform the system that it needs to be opted-out from the compatibility + * treatment that sandboxes the {@link android.view.View View} API. * * <p>The treatment can be enabled by device manufacturers for applications which misuse - * {@link android.view.View} APIs by expecting that - * {@link android.view.View#getLocationOnScreen}, - * {@link android.view.View#getBoundsOnScreen}, - * {@link android.view.View#getWindowVisibleDisplayFrame}, - * {@link android.view.View#getWindowDisplayFrame} + * {@link android.view.View View} APIs by expecting that + * {@link android.view.View#getLocationOnScreen View#getLocationOnScreen()} and + * {@link android.view.View#getWindowVisibleDisplayFrame View#getWindowVisibleDisplayFrame()} * return coordinates as if an activity is positioned in the top-left corner of the screen, with - * left coordinate equal to 0. This may not be the case for applications in multi-window and in + * left coordinate equal to 0. This may not be the case for applications in multi-window and * letterbox modes. * * <p>Setting this property to {@code false} informs the system that the application must be - * opted-out from the "Sandbox {@link android.view.View} API to Activity bounds" treatment even - * if the device manufacturer has opted the app into the treatment. + * opted-out from the "Sandbox View API to Activity bounds" treatment even if the device + * manufacturer has opted the app into the treatment. * * <p>Not setting this property at all, or setting this property to {@code true} has no effect. * @@ -987,12 +988,11 @@ public interface WindowManager extends ViewManager { "android.window.PROPERTY_COMPAT_ALLOW_SANDBOXING_VIEW_BOUNDS_APIS"; /** - * Application level {@link android.content.pm.PackageManager.Property PackageManager - * .Property} for an app to inform the system that the application can be opted-in or opted-out - * from the compatibility treatment that enables sending a fake focus event for unfocused - * resumed split screen activities. This is needed because some game engines wait to get - * focus before drawing the content of the app which isn't guaranteed by default in multi-window - * modes. + * Application level {@link android.content.pm.PackageManager.Property PackageManager.Property} + * for an app to inform the system that the application can be opted-in or opted-out from the + * compatibility treatment that enables sending a fake focus event for unfocused resumed + * split-screen activities. This is needed because some game engines wait to get focus before + * drawing the content of the app which isn't guaranteed by default in multi-window mode. * * <p>Device manufacturers can enable this treatment using their discretion on a per-device * basis to improve display compatibility. The treatment also needs to be specifically enabled @@ -1022,9 +1022,9 @@ public interface WindowManager extends ViewManager { String PROPERTY_COMPAT_ENABLE_FAKE_FOCUS = "android.window.PROPERTY_COMPAT_ENABLE_FAKE_FOCUS"; /** - * Application level {@link android.content.pm.PackageManager.Property PackageManager - * .Property} for an app to inform the system that the app should be excluded from the - * camera compatibility force rotation treatment. + * Application level {@link android.content.pm.PackageManager.Property PackageManager.Property} + * for an app to inform the system that the app should be excluded from the camera compatibility + * force rotation treatment. * * <p>The camera compatibility treatment aligns orientations of portrait app window and natural * orientation of the device and set opposite to natural orientation for a landscape app @@ -1034,10 +1034,11 @@ public interface WindowManager extends ViewManager { * rotation can cause letterboxing. The forced rotation is triggered as soon as app opens to * camera and is removed once camera is closed. * - * <p>The camera compatibility can be enabled by device manufacturers on the displays that have - * ignoreOrientationRequest display setting enabled (enables compatibility mode for fixed - * orientation, see <a href="https://developer.android.com/guide/practices/enhanced-letterboxing">Enhanced letterboxing</a> - * for more details). + * <p>The camera compatibility can be enabled by device manufacturers on displays that have the + * ignore requested orientation display setting enabled (enables compatibility mode for fixed + * orientation on Android 12 (API level 31) or higher; see + * <a href="https://developer.android.com/guide/practices/enhanced-letterboxing">Enhanced + * letterboxing</a> for more details). * * <p>With this property set to {@code true} or unset, the system may apply the force rotation * treatment to fixed orientation activities. Device manufacturers can exclude packages from the @@ -1060,9 +1061,9 @@ public interface WindowManager extends ViewManager { "android.window.PROPERTY_CAMERA_COMPAT_ALLOW_FORCE_ROTATION"; /** - * Application level {@link android.content.pm.PackageManager.Property PackageManager - * .Property} for an app to inform the system that the app should be excluded - * from the activity "refresh" after the camera compatibility force rotation treatment. + * Application level {@link android.content.pm.PackageManager.Property PackageManager.Property} + * for an app to inform the system that the app should be excluded from the activity "refresh" + * after the camera compatibility force rotation treatment. * * <p>The camera compatibility treatment aligns orientations of portrait app window and natural * orientation of the device and set opposite to natural orientation for a landscape app @@ -1079,10 +1080,11 @@ public interface WindowManager extends ViewManager { * camera preview and can lead to sideways or stretching issues persisting even after force * rotation. * - * <p>The camera compatibility can be enabled by device manufacturers on the displays that have - * ignoreOrientationRequest display setting enabled (enables compatibility mode for fixed - * orientation, see <a href="https://developer.android.com/guide/practices/enhanced-letterboxing">Enhanced letterboxing</a> - * for more details). + * <p>The camera compatibility can be enabled by device manufacturers on displays that have the + * ignore requested orientation display setting enabled (enables compatibility mode for fixed + * orientation on Android 12 (API level 31) or higher; see + * <a href="https://developer.android.com/guide/practices/enhanced-letterboxing">Enhanced + * letterboxing</a> for more details). * * <p>With this property set to {@code true} or unset, the system may "refresh" activity after * the force rotation treatment. Device manufacturers can exclude packages from the "refresh" @@ -1105,10 +1107,10 @@ public interface WindowManager extends ViewManager { "android.window.PROPERTY_CAMERA_COMPAT_ALLOW_REFRESH"; /** - * Application level {@link android.content.pm.PackageManager.Property PackageManager - * .Property} for an app to inform the system that the activity should be or shouldn't be - * "refreshed" after the camera compatibility force rotation treatment using "paused -> - * resumed" cycle rather than "stopped -> resumed". + * Application level {@link android.content.pm.PackageManager.Property PackageManager.Property} + * for an app to inform the system that the activity should be or shouldn't be "refreshed" after + * the camera compatibility force rotation treatment using "paused -> resumed" cycle rather than + * "stopped -> resumed". * * <p>The camera compatibility treatment aligns orientations of portrait app window and natural * orientation of the device and set opposite to natural orientation for a landscape app @@ -1124,10 +1126,11 @@ public interface WindowManager extends ViewManager { * values in apps (e.g., display or camera rotation) that influence camera preview and can lead * to sideways or stretching issues persisting even after force rotation. * - * <p>The camera compatibility can be enabled by device manufacturers on the displays that have - * ignoreOrientationRequest display setting enabled (enables compatibility mode for fixed - * orientation, see <a href="https://developer.android.com/guide/practices/enhanced-letterboxing">Enhanced letterboxing</a> - * for more details). + * <p>The camera compatibility can be enabled by device manufacturers on displays that have the + * ignore requested orientation display setting enabled (enables compatibility mode for fixed + * orientation on Android 12 (API level 31) or higher; see + * <a href="https://developer.android.com/guide/practices/enhanced-letterboxing">Enhanced + * letterboxing</a> for more details). * * <p>Device manufacturers can override packages to "refresh" via "resumed -> paused -> resumed" * cycle using their discretion to improve display compatibility. @@ -1153,22 +1156,23 @@ public interface WindowManager extends ViewManager { "android.window.PROPERTY_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE"; /** - * Application level {@link android.content.pm.PackageManager.Property PackageManager - * .Property} for an app to inform the system that the app should be excluded from the - * compatibility override for orientation set by the device manufacturer. When the orientation - * override is applied it can: + * Application level {@link android.content.pm.PackageManager.Property PackageManager.Property} + * for an app to inform the system that the app should be excluded from the compatibility + * override for orientation set by the device manufacturer. When the orientation override is + * applied it can: * <ul> * <li>Replace the specific orientation requested by the app with another selected by the - device manufacturer, e.g. replace undefined requested by the app with portrait. + device manufacturer; for example, replace undefined requested by the app with portrait. * <li>Always use an orientation selected by the device manufacturer. * <li>Do one of the above but only when camera connection is open. * </ul> * - * <p>This property is different from {@link PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION} + * <p>This property is different from {@link #PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION} * (which is used to avoid orientation loops caused by the incorrect use of {@link - * android.app.Activity#setRequestedOrientation}) because this property overrides the app to an - * orientation selected by the device manufacturer rather than ignoring one of orientation - * requests coming from the app while respecting the previous one. + * android.app.Activity#setRequestedOrientation Activity#setRequestedOrientation()}) because + * this property overrides the app to an orientation selected by the device manufacturer rather + * than ignoring one of orientation requests coming from the app while respecting the previous + * one. * * <p>With this property set to {@code true} or unset, device manufacturers can override * orientation for the app using their discretion to improve display compatibility. @@ -1190,10 +1194,10 @@ public interface WindowManager extends ViewManager { "android.window.PROPERTY_COMPAT_ALLOW_ORIENTATION_OVERRIDE"; /** - * Application level {@link android.content.pm.PackageManager.Property PackageManager - * .Property} for an app to inform the system that the app should be opted-out from the - * compatibility override that fixes display orientation to landscape natural orientation when - * an activity is fullscreen. + * Application level {@link android.content.pm.PackageManager.Property PackageManager.Property} + * for an app to inform the system that the app should be opted-out from the compatibility + * override that fixes display orientation to landscape natural orientation when an activity is + * fullscreen. * * <p>When this compat override is enabled and while display is fixed to the landscape natural * orientation, the orientation requested by the activity will be still respected by bounds @@ -1202,16 +1206,17 @@ public interface WindowManager extends ViewManager { * lanscape natural orientation. * * <p>The treatment is disabled by default but device manufacturers can enable the treatment - * using their discretion to improve display compatibility on the displays that have - * ignoreOrientationRequest display setting enabled (enables compatibility mode for fixed - * orientation, see <a href="https://developer.android.com/guide/practices/enhanced-letterboxing">Enhanced letterboxing</a> - * for more details). + * using their discretion to improve display compatibility on displays that have the ignore + * orientation request display setting enabled by OEMs on the device (enables compatibility mode + * for fixed orientation on Android 12 (API level 31) or higher; see + * <a href="https://developer.android.com/guide/practices/enhanced-letterboxing">Enhanced + * letterboxing</a> for more details). * * <p>With this property set to {@code true} or unset, the system wiil use landscape display * orientation when the following conditions are met: * <ul> * <li>Natural orientation of the display is landscape - * <li>ignoreOrientationRequest display setting is enabled + * <li>ignore requested orientation display setting is enabled * <li>Activity is fullscreen. * <li>Device manufacturer enabled the treatment. * </ul> @@ -1233,9 +1238,9 @@ public interface WindowManager extends ViewManager { "android.window.PROPERTY_COMPAT_ALLOW_DISPLAY_ORIENTATION_OVERRIDE"; /** - * Application level {@link android.content.pm.PackageManager.Property PackageManager - * .Property} for an app to inform the system that the app should be opted-out from the - * compatibility override that changes the min aspect ratio. + * Application level {@link android.content.pm.PackageManager.Property PackageManager.Property} + * for an app to inform the system that the app should be opted-out from the compatibility + * override that changes the min aspect ratio. * * <p>When this compat override is enabled the min aspect ratio given in the app's manifest can * be overridden by the device manufacturer using their discretion to improve display @@ -1264,14 +1269,14 @@ public interface WindowManager extends ViewManager { "android.window.PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE"; /** - * Application level {@link android.content.pm.PackageManager.Property PackageManager - * .Property} for an app to inform the system that the app should be opted-out from the - * compatibility overrides that change the resizability of the app. + * Application level {@link android.content.pm.PackageManager.Property PackageManager.Property} + * for an app to inform the system that the app should be opted-out from the compatibility + * overrides that change the resizability of the app. * * <p>When these compat overrides are enabled they force the packages they are applied to to be - * resizable / unresizable. If the app is forced to be resizable this won't change whether - * the app can be put into multi-windowing mode, but allow the app to resize without going into - * size-compat mode when the window container resizes, such as display size change or screen + * resizable/unresizable. If the app is forced to be resizable this won't change whether the app + * can be put into multi-windowing mode, but allow the app to resize without going into size + * compatibility mode when the window container resizes, such as display size change or screen * rotation. * * <p>Setting this property to {@code false} informs the system that the app must be @@ -1320,34 +1325,29 @@ public interface WindowManager extends ViewManager { } /** - * Application-level - * {@link android.content.pm.PackageManager.Property PackageManager.Property} - * tag that specifies whether OEMs are permitted to provide activity - * embedding split-rule configurations on behalf of the app. + * Application-level {@link android.content.pm.PackageManager.Property PackageManager.Property} + * tag that specifies whether OEMs are permitted to provide activity embedding split-rule + * configurations on behalf of the app. * - * <p>If {@code true}, the system is permitted to override the app's - * windowing behavior and implement activity embedding split rules, such as - * displaying activities side by side. A system override informs the app - * that the activity embedding APIs are disabled so the app will not provide - * its own activity embedding rules, which would conflict with the system's + * <p>If {@code true}, the system is permitted to override the app's windowing behavior and + * implement activity embedding split rules, such as displaying activities side by side. A + * system override informs the app that the activity embedding APIs are disabled so the app + * doesn't provide its own activity embedding rules, which would conflict with the system's * rules. * - * <p>If {@code false}, the system is not permitted to override the - * windowing behavior of the app. Set the property to {@code false} if the - * app provides its own activity embedding split rules, or if you want to - * prevent the system override for any other reason. + * <p>If {@code false}, the system is not permitted to override the windowing behavior of the + * app. Set the property to {@code false} if the app provides its own activity embedding split + * rules, or if you want to prevent the system override for any other reason. * * <p>The default value is {@code false}. * - * <p class="note"><b>Note:</b> Refusal to permit the system override is not - * enforceable. OEMs can override the app's activity embedding - * implementation whether or not this property is specified and set to - * <code>false</code>. The property is, in effect, a hint to OEMs. + * <p class="note"><b>Note:</b> Refusal to permit the system override is not enforceable. OEMs + * can override the app's activity embedding implementation whether or not this property is + * specified and set to {@code false}. The property is, in effect, a hint to OEMs. * - * <p>OEMs can implement activity embedding on any API level. The best - * practice for apps is to always explicitly set this property in the app - * manifest file regardless of targeted API level rather than rely on the - * default value. + * <p>OEMs can implement activity embedding on any API level. The best practice for apps is to + * always explicitly set this property in the app manifest file regardless of targeted API level + * rather than rely on the default value. * * <p><b>Syntax:</b> * <pre> @@ -1362,14 +1362,15 @@ public interface WindowManager extends ViewManager { "android.window.PROPERTY_ACTIVITY_EMBEDDING_ALLOW_SYSTEM_OVERRIDE"; /** - * Application level {@link android.content.pm.PackageManager.Property PackageManager - * .Property} that an app can specify to inform the system that the app is ActivityEmbedding - * split feature enabled. + * Application level {@link android.content.pm.PackageManager.Property PackageManager.Property} + * that an app can specify to inform the system that the app is activity embedding split feature + * enabled. * * <p>With this property, the system could provide custom behaviors for the apps that are - * ActivityEmbedding split feature enabled. For example, the fixed-portrait orientation + * activity embedding split feature enabled. For example, the fixed-portrait orientation * requests of the activities could be ignored by the system in order to provide seamless - * ActivityEmbedding split experiences while holding the large-screen devices in landscape mode. + * activity embedding split experiences while holding large screen devices in landscape + * orientation. * * <p><b>Syntax:</b> * <pre> diff --git a/core/java/android/widget/RemoteViews.aidl b/core/java/android/widget/RemoteViews.aidl index ec86410cf89c..6a5fc03fcc6e 100644 --- a/core/java/android/widget/RemoteViews.aidl +++ b/core/java/android/widget/RemoteViews.aidl @@ -17,3 +17,4 @@ package android.widget; parcelable RemoteViews; +parcelable RemoteViews.RemoteCollectionItems; diff --git a/core/java/android/widget/RemoteViews.java b/core/java/android/widget/RemoteViews.java index bd7f5a0924cc..d9e76fefad7f 100644 --- a/core/java/android/widget/RemoteViews.java +++ b/core/java/android/widget/RemoteViews.java @@ -33,16 +33,19 @@ import android.annotation.SuppressLint; import android.app.Activity; import android.app.ActivityOptions; import android.app.ActivityThread; +import android.app.AppGlobals; import android.app.Application; import android.app.LoadedApk; import android.app.PendingIntent; import android.app.RemoteInput; import android.appwidget.AppWidgetHostView; import android.compat.annotation.UnsupportedAppUsage; +import android.content.ComponentName; import android.content.Context; import android.content.ContextWrapper; import android.content.Intent; import android.content.IntentSender; +import android.content.ServiceConnection; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager.NameNotFoundException; import android.content.res.ColorStateList; @@ -65,10 +68,12 @@ import android.os.Binder; import android.os.Build; import android.os.Bundle; import android.os.CancellationSignal; +import android.os.IBinder; import android.os.Parcel; import android.os.ParcelFileDescriptor; import android.os.Parcelable; import android.os.Process; +import android.os.RemoteException; import android.os.StrictMode; import android.os.UserHandle; import android.system.Os; @@ -98,8 +103,10 @@ import android.widget.AdapterView.OnItemClickListener; import android.widget.CompoundButton.OnCheckedChangeListener; import com.android.internal.R; +import com.android.internal.config.sysui.SystemUiDeviceConfigFlags; import com.android.internal.util.ContrastColorUtil; import com.android.internal.util.Preconditions; +import com.android.internal.widget.IRemoteViewsFactory; import java.io.ByteArrayOutputStream; import java.io.FileDescriptor; @@ -124,7 +131,9 @@ import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Stack; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; import java.util.function.Consumer; import java.util.function.Predicate; @@ -324,6 +333,13 @@ public class RemoteViews implements Parcelable, Filter { (clazz) -> clazz.isAnnotationPresent(RemoteViews.RemoteView.class); /** + * The maximum waiting time for remote adapter conversion in milliseconds + * + * @hide + */ + private static final int MAX_ADAPTER_CONVERSION_WAITING_TIME_MS = 2000; + + /** * Application that hosts the remote views. * * @hide @@ -1042,28 +1058,96 @@ public class RemoteViews implements Parcelable, Filter { } private class SetRemoteCollectionItemListAdapterAction extends Action { - private final RemoteCollectionItems mItems; + private @NonNull CompletableFuture<RemoteCollectionItems> mItemsFuture; - SetRemoteCollectionItemListAdapterAction(@IdRes int id, RemoteCollectionItems items) { + SetRemoteCollectionItemListAdapterAction(@IdRes int id, + @NonNull RemoteCollectionItems items) { viewId = id; - mItems = items; - mItems.setHierarchyRootData(getHierarchyRootData()); + items.setHierarchyRootData(getHierarchyRootData()); + mItemsFuture = CompletableFuture.completedFuture(items); + } + + SetRemoteCollectionItemListAdapterAction(@IdRes int id, Intent intent) { + viewId = id; + mItemsFuture = getItemsFutureFromIntentWithTimeout(intent); + setHierarchyRootData(getHierarchyRootData()); + } + + private static CompletableFuture<RemoteCollectionItems> getItemsFutureFromIntentWithTimeout( + Intent intent) { + if (intent == null) { + Log.e(LOG_TAG, "Null intent received when generating adapter future"); + return CompletableFuture.completedFuture(new RemoteCollectionItems + .Builder().build()); + } + + final Context context = ActivityThread.currentApplication(); + final CompletableFuture<RemoteCollectionItems> result = new CompletableFuture<>(); + + context.bindService(intent, Context.BindServiceFlags.of(Context.BIND_AUTO_CREATE), + result.defaultExecutor(), new ServiceConnection() { + @Override + public void onServiceConnected(ComponentName componentName, + IBinder iBinder) { + RemoteCollectionItems items; + try { + items = IRemoteViewsFactory.Stub.asInterface(iBinder) + .getRemoteCollectionItems(); + } catch (RemoteException re) { + items = new RemoteCollectionItems.Builder().build(); + Log.e(LOG_TAG, "Error getting collection items from the factory", + re); + } finally { + context.unbindService(this); + } + + result.complete(items); + } + + @Override + public void onServiceDisconnected(ComponentName componentName) { } + }); + + result.completeOnTimeout( + new RemoteCollectionItems.Builder().build(), + MAX_ADAPTER_CONVERSION_WAITING_TIME_MS, TimeUnit.MILLISECONDS); + + return result; } SetRemoteCollectionItemListAdapterAction(Parcel parcel) { viewId = parcel.readInt(); - mItems = new RemoteCollectionItems(parcel, getHierarchyRootData()); + mItemsFuture = CompletableFuture.completedFuture( + new RemoteCollectionItems(parcel, getHierarchyRootData())); } @Override public void setHierarchyRootData(HierarchyRootData rootData) { - mItems.setHierarchyRootData(rootData); + mItemsFuture = mItemsFuture + .thenApply(rc -> { + rc.setHierarchyRootData(rootData); + return rc; + }); + } + + private static RemoteCollectionItems getCollectionItemsFromFuture( + CompletableFuture<RemoteCollectionItems> itemsFuture) { + RemoteCollectionItems items; + try { + items = itemsFuture.get(); + } catch (Exception e) { + Log.e(LOG_TAG, "Error getting collection items from future", e); + items = new RemoteCollectionItems.Builder().build(); + } + + return items; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeInt(viewId); - mItems.writeToParcel(dest, flags, /* attached= */ true); + RemoteCollectionItems items = getCollectionItemsFromFuture(mItemsFuture); + items.writeToParcel(dest, flags, /* attached= */ true); } @Override @@ -1072,6 +1156,8 @@ public class RemoteViews implements Parcelable, Filter { View target = root.findViewById(viewId); if (target == null) return; + RemoteCollectionItems items = getCollectionItemsFromFuture(mItemsFuture); + // Ensure that we are applying to an AppWidget root if (!(rootParent instanceof AppWidgetHostView)) { Log.e(LOG_TAG, "setRemoteAdapter can only be used for " @@ -1092,10 +1178,10 @@ public class RemoteViews implements Parcelable, Filter { // recycling in setAdapter, so we must call setAdapter again if the number of view types // increases. if (adapter instanceof RemoteCollectionItemsAdapter - && adapter.getViewTypeCount() >= mItems.getViewTypeCount()) { + && adapter.getViewTypeCount() >= items.getViewTypeCount()) { try { ((RemoteCollectionItemsAdapter) adapter).setData( - mItems, params.handler, params.colorResources); + items, params.handler, params.colorResources); } catch (Throwable throwable) { // setData should never failed with the validation in the items builder, but if // it does, catch and rethrow. @@ -1105,7 +1191,7 @@ public class RemoteViews implements Parcelable, Filter { } try { - adapterView.setAdapter(new RemoteCollectionItemsAdapter(mItems, + adapterView.setAdapter(new RemoteCollectionItemsAdapter(items, params.handler, params.colorResources)); } catch (Throwable throwable) { // This could throw if the AdapterView somehow doesn't accept BaseAdapter due to @@ -4679,6 +4765,12 @@ public class RemoteViews implements Parcelable, Filter { * providing data to the RemoteViewsAdapter */ public void setRemoteAdapter(@IdRes int viewId, Intent intent) { + if (AppGlobals.getIntCoreSetting( + SystemUiDeviceConfigFlags.REMOTEVIEWS_ADAPTER_CONVERSION, + SystemUiDeviceConfigFlags.REMOTEVIEWS_ADAPTER_CONVERSION_DEFAULT ? 1 : 0) == 1) { + addAction(new SetRemoteCollectionItemListAdapterAction(viewId, intent)); + return; + } addAction(new SetRemoteViewsAdapterIntent(viewId, intent)); } diff --git a/core/java/android/widget/RemoteViewsService.java b/core/java/android/widget/RemoteViewsService.java index 214e5cc01b9e..d4f4d19e9bad 100644 --- a/core/java/android/widget/RemoteViewsService.java +++ b/core/java/android/widget/RemoteViewsService.java @@ -43,6 +43,13 @@ public abstract class RemoteViewsService extends Service { private static final Object sLock = new Object(); /** + * Used for determining the maximum number of entries to retrieve from RemoteViewsFactory + * + * @hide + */ + private static final int MAX_NUM_ENTRY = 25; + + /** * An interface for an adapter between a remote collection view (ListView, GridView, etc) and * the underlying data for that view. The implementor is responsible for making a RemoteView * for each item in the data set. This interface is a thin wrapper around {@link Adapter}. @@ -227,6 +234,30 @@ public abstract class RemoteViewsService extends Service { } } + @Override + public RemoteViews.RemoteCollectionItems getRemoteCollectionItems() { + RemoteViews.RemoteCollectionItems items = new RemoteViews.RemoteCollectionItems + .Builder().build(); + + try { + RemoteViews.RemoteCollectionItems.Builder itemsBuilder = + new RemoteViews.RemoteCollectionItems.Builder(); + mFactory.onDataSetChanged(); + + itemsBuilder.setHasStableIds(mFactory.hasStableIds()); + final int numOfEntries = Math.min(mFactory.getCount(), MAX_NUM_ENTRY); + for (int i = 0; i < numOfEntries; i++) { + itemsBuilder.addItem(mFactory.getItemId(i), mFactory.getViewAt(i)); + } + + items = itemsBuilder.build(); + } catch (Exception ex) { + Thread t = Thread.currentThread(); + Thread.getDefaultUncaughtExceptionHandler().uncaughtException(t, ex); + } + return items; + } + private RemoteViewsFactory mFactory; private boolean mIsCreated; } diff --git a/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java b/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java index 9ffccb34f44d..0ba271f253fb 100644 --- a/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java +++ b/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java @@ -532,6 +532,17 @@ public final class SystemUiDeviceConfigFlags { public static final String TASK_MANAGER_SHOW_FOOTER_DOT = "task_manager_show_footer_dot"; /** + * (boolean) Whether to enable the adapter conversion in RemoteViews + */ + public static final String REMOTEVIEWS_ADAPTER_CONVERSION = "remoteviews_adapter_conversion"; + + /** + * Default value for whether the adapter conversion is enabled or not. This is set for + * RemoteViews and should not be a common practice. + */ + public static final boolean REMOTEVIEWS_ADAPTER_CONVERSION_DEFAULT = false; + + /** * (boolean) Whether the task manager should show a stop button if the app is allowlisted * by the user. */ 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 000000000000..f177ef88c38f --- /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/widget/IRemoteViewsFactory.aidl b/core/java/com/android/internal/widget/IRemoteViewsFactory.aidl index c06dab9f75d6..918d9c029ef5 100644 --- a/core/java/com/android/internal/widget/IRemoteViewsFactory.aidl +++ b/core/java/com/android/internal/widget/IRemoteViewsFactory.aidl @@ -39,5 +39,6 @@ interface IRemoteViewsFactory { boolean hasStableIds(); @UnsupportedAppUsage(maxTargetSdk = 30, trackingBug = 170729553) boolean isCreated(); + RemoteViews.RemoteCollectionItems getRemoteCollectionItems(); } diff --git a/core/java/com/android/internal/widget/OWNERS b/core/java/com/android/internal/widget/OWNERS index d068a3a0f62e..e2672f5b03ba 100644 --- a/core/java/com/android/internal/widget/OWNERS +++ b/core/java/com/android/internal/widget/OWNERS @@ -21,3 +21,6 @@ per-file ImageFloatingTextView.java = file:/services/core/java/com/android/serve per-file ObservableTextView.java = file:/services/core/java/com/android/server/notification/OWNERS per-file RemeasuringLinearLayout.java = file:/services/core/java/com/android/server/notification/OWNERS per-file ViewClippingUtil.java = file:/services/core/java/com/android/server/notification/OWNERS + +# Appwidget related +per-file *RemoteViews* = file:/services/appwidget/java/com/android/server/appwidget/OWNERS diff --git a/core/proto/android/input/keyboard_configured.proto b/core/proto/android/input/keyboard_configured.proto index 16990087b319..0b4bf8eb9171 100644 --- a/core/proto/android/input/keyboard_configured.proto +++ b/core/proto/android/input/keyboard_configured.proto @@ -47,4 +47,8 @@ message KeyboardLayoutConfig { // IntDef annotation at: // services/core/java/com/android/server/input/KeyboardMetricsCollector.java optional int32 layout_selection_criteria = 4; + // Keyboard layout type provided by IME + optional int32 ime_layout_type = 5; + // Language tag provided by IME (e.g. en-US, ru-Cyrl etc.) + optional string ime_language_tag = 6; } diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index a01c7b6355c1..10cf353bf5e9 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -7655,6 +7655,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/tests/coretests/src/android/flags/FeatureFlagsTest.java b/core/tests/coretests/src/android/flags/FeatureFlagsTest.java new file mode 100644 index 000000000000..3fc94394d12c --- /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 000000000000..bc5d8aa3ac73 --- /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/widget/RemoteViewsAdapterTest.java b/core/tests/coretests/src/android/widget/RemoteViewsAdapterTest.java index 184b9eac24f3..4f722cefcf9f 100644 --- a/core/tests/coretests/src/android/widget/RemoteViewsAdapterTest.java +++ b/core/tests/coretests/src/android/widget/RemoteViewsAdapterTest.java @@ -353,6 +353,23 @@ public class RemoteViewsAdapterTest { public boolean isCreated() { return false; } + + @Override + public RemoteViews.RemoteCollectionItems getRemoteCollectionItems() { + RemoteViews.RemoteCollectionItems.Builder itemsBuilder = + new RemoteViews.RemoteCollectionItems.Builder(); + itemsBuilder.setHasStableIds(hasStableIds()) + .setViewTypeCount(getViewTypeCount()); + try { + for (int i = 0; i < mCount; i++) { + itemsBuilder.addItem(getItemId(i), getViewAt(i)); + } + } catch (RemoteException e) { + // No-op + } + + return itemsBuilder.build(); + } } private static class DistinctIntent extends Intent { diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java index a45a8a183ac8..2eacaaf28bba 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java @@ -304,7 +304,7 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent { * {@link IllegalArgumentException} since this can cause negative UI effects down stream. * * @param context a proxy for the {@link android.view.Window} that contains the - * {@link DisplayFeature}. + * {@link DisplayFeature}. * @return a {@link List} of {@link DisplayFeature}s that are within the * {@link android.view.Window} of the {@link Activity} */ @@ -336,10 +336,32 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent { rotateRectToDisplayRotation(displayId, featureRect); transformToWindowSpaceRect(windowConfiguration, featureRect); - if (!isZero(featureRect)) { + if (isZero(featureRect)) { // TODO(b/228641877): Remove guarding when fixed. - features.add(new FoldingFeature(featureRect, baseFeature.getType(), state)); + continue; + } + if (featureRect.left != 0 && featureRect.top != 0) { + throw new IllegalArgumentException("Bounding rectangle must start at the top or " + + "left of the window. BaseFeatureRect: " + baseFeature.getRect() + + ", FeatureRect: " + featureRect + + ", WindowConfiguration: " + windowConfiguration); + + } + if (featureRect.left == 0 + && featureRect.width() != windowConfiguration.getBounds().width()) { + throw new IllegalArgumentException("Horizontal FoldingFeature must have full width." + + " BaseFeatureRect: " + baseFeature.getRect() + + ", FeatureRect: " + featureRect + + ", WindowConfiguration: " + windowConfiguration); + } + if (featureRect.top == 0 + && featureRect.height() != windowConfiguration.getBounds().height()) { + throw new IllegalArgumentException("Vertical FoldingFeature must have full height." + + " BaseFeatureRect: " + baseFeature.getRect() + + ", FeatureRect: " + featureRect + + ", WindowConfiguration: " + windowConfiguration); } + features.add(new FoldingFeature(featureRect, baseFeature.getType(), state)); } return features; } diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java index 6a5535d345db..6b0a9060d782 100644 --- a/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java +++ b/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java @@ -61,6 +61,7 @@ public class SystemSettings { Settings.System.TTY_MODE, Settings.System.MASTER_MONO, Settings.System.MASTER_BALANCE, + Settings.System.STAY_AWAKE_ON_FOLD, Settings.System.SOUND_EFFECTS_ENABLED, Settings.System.HAPTIC_FEEDBACK_ENABLED, Settings.System.POWER_SOUNDS_ENABLED, // moved to global diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java index 753c860bd5d6..a08d07e1d778 100644 --- a/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java +++ b/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java @@ -218,6 +218,7 @@ public class SystemSettingsValidators { VALIDATORS.put(System.WIFI_STATIC_DNS1, LENIENT_IP_ADDRESS_VALIDATOR); VALIDATORS.put(System.WIFI_STATIC_DNS2, LENIENT_IP_ADDRESS_VALIDATOR); VALIDATORS.put(System.SHOW_BATTERY_PERCENT, BOOLEAN_VALIDATOR); + VALIDATORS.put(System.STAY_AWAKE_ON_FOLD, BOOLEAN_VALIDATOR); VALIDATORS.put(System.NOTIFICATION_LIGHT_PULSE, BOOLEAN_VALIDATOR); VALIDATORS.put(System.WEAR_ACCESSIBILITY_GESTURE_ENABLED, BOOLEAN_VALIDATOR); VALIDATORS.put(System.CLOCKWORK_BLUETOOTH_SETTINGS_PREF, BOOLEAN_VALIDATOR); diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSTile.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSTile.java index c0b69c169ccd..25f77ea4e6d5 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSTile.java +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSTile.java @@ -172,7 +172,6 @@ public interface QSTile { public String expandedAccessibilityClassName; public SlashState slash; public boolean handlesLongClick = true; - public boolean showRippleEffect = true; @Nullable public Drawable sideViewCustomDrawable; public String spec; @@ -217,7 +216,6 @@ public interface QSTile { || !Objects.equals(other.dualTarget, dualTarget) || !Objects.equals(other.slash, slash) || !Objects.equals(other.handlesLongClick, handlesLongClick) - || !Objects.equals(other.showRippleEffect, showRippleEffect) || !Objects.equals(other.sideViewCustomDrawable, sideViewCustomDrawable); other.spec = spec; other.icon = icon; @@ -234,7 +232,6 @@ public interface QSTile { other.isTransient = isTransient; other.slash = slash != null ? slash.copy() : null; other.handlesLongClick = handlesLongClick; - other.showRippleEffect = showRippleEffect; other.sideViewCustomDrawable = sideViewCustomDrawable; return changed; } diff --git a/packages/SystemUI/res/layout/super_notification_shade.xml b/packages/SystemUI/res/layout/super_notification_shade.xml index 2fde9479d42a..a33625212d34 100644 --- a/packages/SystemUI/res/layout/super_notification_shade.xml +++ b/packages/SystemUI/res/layout/super_notification_shade.xml @@ -76,6 +76,13 @@ android:layout_height="match_parent" android:visibility="invisible" /> + <!-- Shared container for the notification stack. Can be positioned by either + the keyguard_root_view or notification_panel --> + <com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer + android:id="@+id/shared_notification_container" + android:layout_width="match_parent" + android:layout_height="match_parent" /> + <include layout="@layout/brightness_mirror_container" /> <com.android.systemui.scrim.ScrimView diff --git a/packages/SystemUI/res/values/ids.xml b/packages/SystemUI/res/values/ids.xml index 134a7a96bbc3..3a2177a0045c 100644 --- a/packages/SystemUI/res/values/ids.xml +++ b/packages/SystemUI/res/values/ids.xml @@ -211,6 +211,7 @@ <item type="id" name="keyguard_indication_area" /> <item type="id" name="keyguard_indication_text" /> <item type="id" name="keyguard_indication_text_bottom" /> + <item type="id" name="nssl_guideline" /> <item type="id" name="lock_icon" /> <item type="id" name="lock_icon_bg" /> </resources> diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsController.kt index 37ce44488346..083e21fbdfba 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsController.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsController.kt @@ -17,12 +17,7 @@ package com.android.systemui.biometrics import android.app.ActivityTaskManager import android.content.Context -import android.content.res.Configuration -import android.graphics.Color import android.graphics.PixelFormat -import android.graphics.PorterDuff -import android.graphics.PorterDuffColorFilter -import android.graphics.Rect import android.hardware.biometrics.BiometricOverlayConstants import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_KEYGUARD import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_SETTINGS @@ -33,27 +28,23 @@ import android.hardware.fingerprint.FingerprintSensorPropertiesInternal import android.hardware.fingerprint.ISidefpsController import android.os.Handler import android.util.Log -import android.util.RotationUtils import android.view.Display import android.view.DisplayInfo import android.view.Gravity import android.view.LayoutInflater import android.view.Surface import android.view.View -import android.view.View.AccessibilityDelegate import android.view.ViewPropertyAnimator import android.view.WindowManager import android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION import android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY -import android.view.accessibility.AccessibilityEvent -import androidx.annotation.RawRes import com.airbnb.lottie.LottieAnimationView -import com.airbnb.lottie.LottieProperty -import com.airbnb.lottie.model.KeyPath import com.android.internal.annotations.VisibleForTesting import com.android.systemui.Dumpable import com.android.systemui.R import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractor +import com.android.systemui.biometrics.ui.binder.SideFpsOverlayViewBinder +import com.android.systemui.biometrics.ui.viewmodel.SideFpsOverlayViewModel import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application @@ -64,6 +55,7 @@ import com.android.systemui.util.concurrency.DelayableExecutor import com.android.systemui.util.traceSection import java.io.PrintWriter import javax.inject.Inject +import javax.inject.Provider import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @@ -86,6 +78,7 @@ constructor( @Main private val mainExecutor: DelayableExecutor, @Main private val handler: Handler, private val alternateBouncerInteractor: AlternateBouncerInteractor, + private val sideFpsOverlayViewModelFactory: Provider<SideFpsOverlayViewModel>, @Application private val scope: CoroutineScope, dumpManager: DumpManager ) : Dumpable { @@ -250,105 +243,15 @@ constructor( private fun createOverlayForDisplay(@BiometricOverlayConstants.ShowReason reason: Int) { val view = layoutInflater.inflate(R.layout.sidefps_view, null, false) overlayView = view - val display = context.display!! - // b/284098873 `context.display.rotation` may not up-to-date, we use displayInfo.rotation - display.getDisplayInfo(displayInfo) - val offsets = - sensorProps.getLocation(display.uniqueId).let { location -> - if (location == null) { - Log.w(TAG, "No location specified for display: ${display.uniqueId}") - } - location ?: sensorProps.location - } - overlayOffsets = offsets - - val lottie = view.findViewById(R.id.sidefps_animation) as LottieAnimationView - view.rotation = - display.asSideFpsAnimationRotation( - offsets.isYAligned(), - getRotationFromDefault(displayInfo.rotation) - ) - lottie.setAnimation( - display.asSideFpsAnimation( - offsets.isYAligned(), - getRotationFromDefault(displayInfo.rotation) - ) + SideFpsOverlayViewBinder.bind( + view = view, + viewModel = sideFpsOverlayViewModelFactory.get(), + overlayViewParams = overlayViewParams, + reason = reason, + context = context, ) - lottie.addLottieOnCompositionLoadedListener { - // Check that view is not stale, and that overlayView has not been hidden/removed - if (overlayView != null && overlayView == view) { - updateOverlayParams(display, it.bounds) - } - } orientationReasonListener.reason = reason - lottie.addOverlayDynamicColor(context, reason) - - /** - * Intercepts TYPE_WINDOW_STATE_CHANGED accessibility event, preventing Talkback from - * speaking @string/accessibility_fingerprint_label twice when sensor location indicator is - * in focus - */ - view.setAccessibilityDelegate( - object : AccessibilityDelegate() { - override fun dispatchPopulateAccessibilityEvent( - host: View, - event: AccessibilityEvent - ): Boolean { - return if ( - event.getEventType() === AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED - ) { - true - } else { - super.dispatchPopulateAccessibilityEvent(host, event) - } - } - } - ) } - - @VisibleForTesting - fun updateOverlayParams(display: Display, bounds: Rect) { - val isNaturalOrientation = display.isNaturalOrientation() - val isDefaultOrientation = - if (isReverseDefaultRotation) !isNaturalOrientation else isNaturalOrientation - val size = windowManager.maximumWindowMetrics.bounds - - val displayWidth = if (isDefaultOrientation) size.width() else size.height() - val displayHeight = if (isDefaultOrientation) size.height() else size.width() - val boundsWidth = if (isDefaultOrientation) bounds.width() else bounds.height() - val boundsHeight = if (isDefaultOrientation) bounds.height() else bounds.width() - - val sensorBounds = - if (overlayOffsets.isYAligned()) { - Rect( - displayWidth - boundsWidth, - overlayOffsets.sensorLocationY, - displayWidth, - overlayOffsets.sensorLocationY + boundsHeight - ) - } else { - Rect( - overlayOffsets.sensorLocationX, - 0, - overlayOffsets.sensorLocationX + boundsWidth, - boundsHeight - ) - } - - RotationUtils.rotateBounds( - sensorBounds, - Rect(0, 0, displayWidth, displayHeight), - getRotationFromDefault(display.rotation) - ) - - overlayViewParams.x = sensorBounds.left - overlayViewParams.y = sensorBounds.top - - windowManager.updateViewLayout(overlayView, overlayViewParams) - } - - private fun getRotationFromDefault(rotation: Int): Int = - if (isReverseDefaultRotation) (rotation + 1) % 4 else rotation } private val FingerprintManager?.sideFpsSensorProperties: FingerprintSensorPropertiesInternal? @@ -373,89 +276,12 @@ private fun Int.isReasonToAutoShow(activityTaskManager: ActivityTaskManager): Bo private fun ActivityTaskManager.topClass(): String = getTasks(1).firstOrNull()?.topActivity?.className ?: "" -@RawRes -private fun Display.asSideFpsAnimation(yAligned: Boolean, rotationFromDefault: Int): Int = - when (rotationFromDefault) { - Surface.ROTATION_0 -> if (yAligned) R.raw.sfps_pulse else R.raw.sfps_pulse_landscape - Surface.ROTATION_180 -> if (yAligned) R.raw.sfps_pulse else R.raw.sfps_pulse_landscape - else -> if (yAligned) R.raw.sfps_pulse_landscape else R.raw.sfps_pulse - } - -private fun Display.asSideFpsAnimationRotation(yAligned: Boolean, rotationFromDefault: Int): Float = - when (rotationFromDefault) { - Surface.ROTATION_90 -> if (yAligned) 0f else 180f - Surface.ROTATION_180 -> 180f - Surface.ROTATION_270 -> if (yAligned) 180f else 0f - else -> 0f - } - private fun SensorLocationInternal.isYAligned(): Boolean = sensorLocationY != 0 private fun Display.isNaturalOrientation(): Boolean = rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180 -private fun LottieAnimationView.addOverlayDynamicColor( - context: Context, - @BiometricOverlayConstants.ShowReason reason: Int -) { - fun update() { - val isKeyguard = reason == REASON_AUTH_KEYGUARD - if (isKeyguard) { - val color = - com.android.settingslib.Utils.getColorAttrDefaultColor( - context, - com.android.internal.R.attr.materialColorPrimaryFixed - ) - val outerRimColor = - com.android.settingslib.Utils.getColorAttrDefaultColor( - context, - com.android.internal.R.attr.materialColorPrimaryFixedDim - ) - val chevronFill = - com.android.settingslib.Utils.getColorAttrDefaultColor( - context, - com.android.internal.R.attr.materialColorOnPrimaryFixed - ) - addValueCallback(KeyPath(".blue600", "**"), LottieProperty.COLOR_FILTER) { - PorterDuffColorFilter(color, PorterDuff.Mode.SRC_ATOP) - } - addValueCallback(KeyPath(".blue400", "**"), LottieProperty.COLOR_FILTER) { - PorterDuffColorFilter(outerRimColor, PorterDuff.Mode.SRC_ATOP) - } - addValueCallback(KeyPath(".black", "**"), LottieProperty.COLOR_FILTER) { - PorterDuffColorFilter(chevronFill, PorterDuff.Mode.SRC_ATOP) - } - } else { - if (!isDarkMode(context)) { - addValueCallback(KeyPath(".black", "**"), LottieProperty.COLOR_FILTER) { - PorterDuffColorFilter(Color.WHITE, PorterDuff.Mode.SRC_ATOP) - } - } - for (key in listOf(".blue600", ".blue400")) { - addValueCallback(KeyPath(key, "**"), LottieProperty.COLOR_FILTER) { - PorterDuffColorFilter( - context.getColor(R.color.settingslib_color_blue400), - PorterDuff.Mode.SRC_ATOP - ) - } - } - } - } - - if (composition != null) { - update() - } else { - addLottieOnCompositionLoadedListener { update() } - } -} - -private fun isDarkMode(context: Context): Boolean { - val darkMode = context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK - return darkMode == Configuration.UI_MODE_NIGHT_YES -} - -@VisibleForTesting -class OrientationReasonListener( +public class OrientationReasonListener( context: Context, displayManager: DisplayManager, handler: Handler, diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/FingerprintPropertyRepository.kt b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/FingerprintPropertyRepository.kt index c43722f2a0bb..efbde4c5985b 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/FingerprintPropertyRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/FingerprintPropertyRepository.kt @@ -16,8 +16,11 @@ package com.android.systemui.biometrics.data.repository +import android.hardware.biometrics.ComponentInfoInternal import android.hardware.biometrics.SensorLocationInternal +import android.hardware.biometrics.SensorProperties import android.hardware.fingerprint.FingerprintManager +import android.hardware.fingerprint.FingerprintSensorProperties import android.hardware.fingerprint.FingerprintSensorPropertiesInternal import android.hardware.fingerprint.IFingerprintAuthenticatorsRegisteredCallback import com.android.systemui.biometrics.shared.model.FingerprintSensorType @@ -30,10 +33,8 @@ import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.shareIn /** @@ -43,22 +44,17 @@ import kotlinx.coroutines.flow.shareIn */ interface FingerprintPropertyRepository { - /** - * If the repository is initialized or not. Other properties are defaults until this is true. - */ - val isInitialized: Flow<Boolean> - /** The id of fingerprint sensor. */ - val sensorId: StateFlow<Int> + val sensorId: Flow<Int> /** The security strength of sensor (convenience, weak, strong). */ - val strength: StateFlow<SensorStrength> + val strength: Flow<SensorStrength> /** The types of fingerprint sensor (rear, ultrasonic, optical, etc.). */ - val sensorType: StateFlow<FingerprintSensorType> + val sensorType: Flow<FingerprintSensorType> /** The sensor location relative to each physical display. */ - val sensorLocations: StateFlow<Map<String, SensorLocationInternal>> + val sensorLocations: Flow<Map<String, SensorLocationInternal>> } @SysUISingleton @@ -66,10 +62,10 @@ class FingerprintPropertyRepositoryImpl @Inject constructor( @Application private val applicationScope: CoroutineScope, - private val fingerprintManager: FingerprintManager + private val fingerprintManager: FingerprintManager? ) : FingerprintPropertyRepository { - override val isInitialized: Flow<Boolean> = + private val props: Flow<FingerprintSensorPropertiesInternal> = conflatedCallbackFlow { val callback = object : IFingerprintAuthenticatorsRegisteredCallback.Stub() { @@ -77,45 +73,47 @@ constructor( sensors: List<FingerprintSensorPropertiesInternal> ) { if (sensors.isNotEmpty()) { - setProperties(sensors[0]) - trySendWithFailureLogging(true, TAG, "initialize properties") + trySendWithFailureLogging(sensors[0], TAG, "initialize properties") + } else { + trySendWithFailureLogging( + DEFAULT_PROPS, + TAG, + "initialize with default properties" + ) } } } - fingerprintManager.addAuthenticatorsRegisteredCallback(callback) - trySendWithFailureLogging(false, TAG, "initial value defaulting to false") + fingerprintManager?.addAuthenticatorsRegisteredCallback(callback) + trySendWithFailureLogging(DEFAULT_PROPS, TAG, "initialize with default properties") awaitClose {} } .shareIn(scope = applicationScope, started = SharingStarted.Eagerly, replay = 1) - private val _sensorId: MutableStateFlow<Int> = MutableStateFlow(-1) - override val sensorId: StateFlow<Int> = _sensorId.asStateFlow() - - private val _strength: MutableStateFlow<SensorStrength> = - MutableStateFlow(SensorStrength.CONVENIENCE) - override val strength = _strength.asStateFlow() - - private val _sensorType: MutableStateFlow<FingerprintSensorType> = - MutableStateFlow(FingerprintSensorType.UNKNOWN) - override val sensorType = _sensorType.asStateFlow() - - private val _sensorLocations: MutableStateFlow<Map<String, SensorLocationInternal>> = - MutableStateFlow(mapOf("" to SensorLocationInternal.DEFAULT)) - override val sensorLocations: StateFlow<Map<String, SensorLocationInternal>> = - _sensorLocations.asStateFlow() - - private fun setProperties(prop: FingerprintSensorPropertiesInternal) { - _sensorId.value = prop.sensorId - _strength.value = sensorStrengthIntToObject(prop.sensorStrength) - _sensorType.value = sensorTypeIntToObject(prop.sensorType) - _sensorLocations.value = - prop.allLocations.associateBy { sensorLocationInternal -> + override val sensorId: Flow<Int> = props.map { it.sensorId } + override val strength: Flow<SensorStrength> = + props.map { sensorStrengthIntToObject(it.sensorStrength) } + override val sensorType: Flow<FingerprintSensorType> = + props.map { sensorTypeIntToObject(it.sensorType) } + override val sensorLocations: Flow<Map<String, SensorLocationInternal>> = + props.map { + it.allLocations.associateBy { sensorLocationInternal -> sensorLocationInternal.displayId } - } + } companion object { private const val TAG = "FingerprintPropertyRepositoryImpl" + private val DEFAULT_PROPS = + FingerprintSensorPropertiesInternal( + -1 /* sensorId */, + SensorProperties.STRENGTH_CONVENIENCE, + 0 /* maxEnrollmentsPerUser */, + listOf<ComponentInfoInternal>(), + FingerprintSensorProperties.TYPE_UNKNOWN, + false /* halControlsIllumination */, + true /* resetLockoutRequiresHardwareAuthToken */, + listOf<SensorLocationInternal>(SensorLocationInternal.DEFAULT) + ) } } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/SideFpsOverlayInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/SideFpsOverlayInteractor.kt index aa85e5f3b21a..37f39cb5fe0e 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/SideFpsOverlayInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/SideFpsOverlayInteractor.kt @@ -17,16 +17,24 @@ package com.android.systemui.biometrics.domain.interactor import android.hardware.biometrics.SensorLocationInternal -import android.util.Log import com.android.systemui.biometrics.data.repository.FingerprintPropertyRepository import com.android.systemui.dagger.SysUISingleton import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.combine /** Business logic for SideFps overlay offsets. */ interface SideFpsOverlayInteractor { + /** The displayId of the current display. */ + val displayId: Flow<String> - /** Get the corresponding offsets based on different displayId. */ - fun getOverlayOffsets(displayId: String): SensorLocationInternal + /** The corresponding offsets based on different displayId. */ + val overlayOffsets: Flow<SensorLocationInternal> + + /** Update the displayId. */ + fun changeDisplay(displayId: String?) } @SysUISingleton @@ -35,14 +43,16 @@ class SideFpsOverlayInteractorImpl constructor(private val fingerprintPropertyRepository: FingerprintPropertyRepository) : SideFpsOverlayInteractor { - override fun getOverlayOffsets(displayId: String): SensorLocationInternal { - val offsets = fingerprintPropertyRepository.sensorLocations.value - return if (offsets.containsKey(displayId)) { - offsets[displayId]!! - } else { - Log.w(TAG, "No location specified for display: $displayId") - offsets[""]!! + private val _displayId: MutableStateFlow<String> = MutableStateFlow("") + override val displayId: Flow<String> = _displayId.asStateFlow() + + override val overlayOffsets: Flow<SensorLocationInternal> = + combine(displayId, fingerprintPropertyRepository.sensorLocations) { displayId, offsets -> + offsets[displayId] ?: SensorLocationInternal.DEFAULT } + + override fun changeDisplay(displayId: String?) { + _displayId.value = displayId ?: "" } companion object { diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinder.kt new file mode 100644 index 000000000000..0409519c9816 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinder.kt @@ -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.systemui.biometrics.ui.binder + +import android.content.Context +import android.content.res.Configuration +import android.graphics.Color +import android.graphics.PorterDuff +import android.graphics.PorterDuffColorFilter +import android.hardware.biometrics.BiometricOverlayConstants +import android.view.View +import android.view.WindowManager +import android.view.accessibility.AccessibilityEvent +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.repeatOnLifecycle +import com.airbnb.lottie.LottieAnimationView +import com.airbnb.lottie.LottieProperty +import com.airbnb.lottie.model.KeyPath +import com.android.systemui.R +import com.android.systemui.biometrics.ui.viewmodel.SideFpsOverlayViewModel +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.lifecycle.repeatWhenAttached +import kotlinx.coroutines.launch + +/** Sub-binder for SideFpsOverlayView. */ +object SideFpsOverlayViewBinder { + + /** Bind the view. */ + @JvmStatic + fun bind( + view: View, + viewModel: SideFpsOverlayViewModel, + overlayViewParams: WindowManager.LayoutParams, + @BiometricOverlayConstants.ShowReason reason: Int, + @Application context: Context + ) { + val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager + + val lottie = view.findViewById(R.id.sidefps_animation) as LottieAnimationView + + viewModel.changeDisplay() + + view.repeatWhenAttached { + repeatOnLifecycle(Lifecycle.State.STARTED) { + launch { + viewModel.sideFpsAnimationRotation.collect { rotation -> + view.rotation = rotation + } + } + + launch { + // TODO(b/221037350, wenhuiy): Create a separate ViewBinder for sideFpsAnimation + // in order to add scuba tests in the future. + viewModel.sideFpsAnimation.collect { animation -> + lottie.setAnimation(animation) + } + } + + launch { + viewModel.sensorBounds.collect { sensorBounds -> + overlayViewParams.x = sensorBounds.left + overlayViewParams.y = sensorBounds.top + + windowManager.updateViewLayout(view, overlayViewParams) + } + } + + launch { + viewModel.overlayOffsets.collect { overlayOffsets -> + lottie.addLottieOnCompositionLoadedListener { + viewModel.updateSensorBounds( + it.bounds, + windowManager.maximumWindowMetrics.bounds, + overlayOffsets + ) + } + } + } + } + } + + lottie.addOverlayDynamicColor(context, reason) + + /** + * Intercepts TYPE_WINDOW_STATE_CHANGED accessibility event, preventing Talkback from + * speaking @string/accessibility_fingerprint_label twice when sensor location indicator is + * in focus + */ + view.accessibilityDelegate = + object : View.AccessibilityDelegate() { + override fun dispatchPopulateAccessibilityEvent( + host: View, + event: AccessibilityEvent + ): Boolean { + return if ( + event.getEventType() === AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED + ) { + true + } else { + super.dispatchPopulateAccessibilityEvent(host, event) + } + } + } + } +} + +private fun LottieAnimationView.addOverlayDynamicColor( + context: Context, + @BiometricOverlayConstants.ShowReason reason: Int +) { + fun update() { + val isKeyguard = reason == BiometricOverlayConstants.REASON_AUTH_KEYGUARD + if (isKeyguard) { + val color = context.getColor(R.color.numpad_key_color_secondary) // match bouncer color + val chevronFill = + com.android.settingslib.Utils.getColorAttrDefaultColor( + context, + android.R.attr.textColorPrimaryInverse + ) + for (key in listOf(".blue600", ".blue400")) { + addValueCallback(KeyPath(key, "**"), LottieProperty.COLOR_FILTER) { + PorterDuffColorFilter(color, PorterDuff.Mode.SRC_ATOP) + } + } + addValueCallback(KeyPath(".black", "**"), LottieProperty.COLOR_FILTER) { + PorterDuffColorFilter(chevronFill, PorterDuff.Mode.SRC_ATOP) + } + } else if (!isDarkMode(context)) { + addValueCallback(KeyPath(".black", "**"), LottieProperty.COLOR_FILTER) { + PorterDuffColorFilter(Color.WHITE, PorterDuff.Mode.SRC_ATOP) + } + } else if (isDarkMode(context)) { + for (key in listOf(".blue600", ".blue400")) { + addValueCallback(KeyPath(key, "**"), LottieProperty.COLOR_FILTER) { + PorterDuffColorFilter( + context.getColor(R.color.settingslib_color_blue400), + PorterDuff.Mode.SRC_ATOP + ) + } + } + } + } + + if (composition != null) { + update() + } else { + addLottieOnCompositionLoadedListener { update() } + } +} + +private fun isDarkMode(context: Context): Boolean { + val darkMode = context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK + return darkMode == Configuration.UI_MODE_NIGHT_YES +} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModel.kt new file mode 100644 index 000000000000..e938b4efb68c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModel.kt @@ -0,0 +1,148 @@ +/* + * 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.ui.viewmodel + +import android.content.Context +import android.graphics.Rect +import android.hardware.biometrics.SensorLocationInternal +import android.util.RotationUtils +import android.view.Display +import android.view.DisplayInfo +import android.view.Surface +import androidx.annotation.RawRes +import com.android.systemui.R +import com.android.systemui.biometrics.domain.interactor.SideFpsOverlayInteractor +import com.android.systemui.dagger.qualifiers.Application +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.map + +/** View-model for SideFpsOverlayView. */ +class SideFpsOverlayViewModel +@Inject +constructor( + @Application private val context: Context, + private val sideFpsOverlayInteractor: SideFpsOverlayInteractor, +) { + + private val isReverseDefaultRotation = + context.resources.getBoolean(com.android.internal.R.bool.config_reverseDefaultRotation) + + private val _sensorBounds: MutableStateFlow<Rect> = MutableStateFlow(Rect()) + val sensorBounds = _sensorBounds.asStateFlow() + + val overlayOffsets: Flow<SensorLocationInternal> = sideFpsOverlayInteractor.overlayOffsets + + /** Update the displayId. */ + fun changeDisplay() { + sideFpsOverlayInteractor.changeDisplay(context.display!!.uniqueId) + } + + /** Determine the rotation of the sideFps animation given the overlay offsets. */ + val sideFpsAnimationRotation: Flow<Float> = + overlayOffsets.map { overlayOffsets -> + val display = context.display!! + val displayInfo: DisplayInfo = DisplayInfo() + // b/284098873 `context.display.rotation` may not up-to-date, we use + // displayInfo.rotation + display.getDisplayInfo(displayInfo) + val yAligned: Boolean = overlayOffsets.isYAligned() + when (getRotationFromDefault(displayInfo.rotation)) { + Surface.ROTATION_90 -> if (yAligned) 0f else 180f + Surface.ROTATION_180 -> 180f + Surface.ROTATION_270 -> if (yAligned) 180f else 0f + else -> 0f + } + } + + /** Populate the sideFps animation from the overlay offsets. */ + @RawRes + val sideFpsAnimation: Flow<Int> = + overlayOffsets.map { overlayOffsets -> + val display = context.display!! + val displayInfo: DisplayInfo = DisplayInfo() + // b/284098873 `context.display.rotation` may not up-to-date, we use + // displayInfo.rotation + display.getDisplayInfo(displayInfo) + val yAligned: Boolean = overlayOffsets.isYAligned() + when (getRotationFromDefault(displayInfo.rotation)) { + Surface.ROTATION_0 -> if (yAligned) R.raw.sfps_pulse else R.raw.sfps_pulse_landscape + Surface.ROTATION_180 -> + if (yAligned) R.raw.sfps_pulse else R.raw.sfps_pulse_landscape + else -> if (yAligned) R.raw.sfps_pulse_landscape else R.raw.sfps_pulse + } + } + + /** + * Calculate and update the bounds of the sensor based on the bounds of the overlay view, the + * maximum bounds of the window, and the offsets of the sensor location. + */ + fun updateSensorBounds( + bounds: Rect, + maximumWindowBounds: Rect, + offsets: SensorLocationInternal + ) { + val isNaturalOrientation = context.display!!.isNaturalOrientation() + val isDefaultOrientation = + if (isReverseDefaultRotation) !isNaturalOrientation else isNaturalOrientation + + val displayWidth = + if (isDefaultOrientation) maximumWindowBounds.width() else maximumWindowBounds.height() + val displayHeight = + if (isDefaultOrientation) maximumWindowBounds.height() else maximumWindowBounds.width() + val boundsWidth = if (isDefaultOrientation) bounds.width() else bounds.height() + val boundsHeight = if (isDefaultOrientation) bounds.height() else bounds.width() + + val sensorBounds = + if (offsets.isYAligned()) { + Rect( + displayWidth - boundsWidth, + offsets.sensorLocationY, + displayWidth, + offsets.sensorLocationY + boundsHeight + ) + } else { + Rect( + offsets.sensorLocationX, + 0, + offsets.sensorLocationX + boundsWidth, + boundsHeight + ) + } + + val displayInfo: DisplayInfo = DisplayInfo() + context.display!!.getDisplayInfo(displayInfo) + + RotationUtils.rotateBounds( + sensorBounds, + Rect(0, 0, displayWidth, displayHeight), + getRotationFromDefault(displayInfo.rotation) + ) + + _sensorBounds.value = sensorBounds + } + + private fun getRotationFromDefault(rotation: Int): Int = + if (isReverseDefaultRotation) (rotation + 1) % 4 else rotation +} + +private fun Display.isNaturalOrientation(): Boolean = + rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180 + +private fun SensorLocationInternal.isYAligned(): Boolean = sensorLocationY != 0 diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt index 0847ec986153..add323983928 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt @@ -68,9 +68,6 @@ object Flags { val NOTIFICATION_MEMORY_LOGGING_ENABLED = unreleasedFlag(119, "notification_memory_logging_enabled") - // TODO(b/257315550): Tracking Bug - val NO_HUN_FOR_OLD_WHEN = releasedFlag(118, "no_hun_for_old_when") - // TODO(b/260335638): Tracking Bug @JvmField val NOTIFICATION_INLINE_REPLY_ANIMATION = diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt index f59ad90d86ff..23f6fa6bbf23 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt @@ -31,6 +31,9 @@ import com.android.systemui.keyguard.ui.view.layout.KeyguardLayoutManagerCommand import com.android.systemui.keyguard.ui.viewmodel.KeyguardIndicationAreaViewModel import com.android.systemui.shade.NotificationShadeWindowView import com.android.systemui.statusbar.KeyguardIndicationController +import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer +import com.android.systemui.statusbar.notification.stack.ui.viewbinder.SharedNotificationContainerBinder +import com.android.systemui.statusbar.notification.stack.ui.viewmodel.SharedNotificationContainerViewModel import javax.inject.Inject import kotlinx.coroutines.DisposableHandle @@ -40,7 +43,9 @@ class KeyguardViewConfigurator @Inject constructor( private val keyguardRootView: KeyguardRootView, + private val sharedNotificationContainer: SharedNotificationContainer, private val keyguardIndicationAreaViewModel: KeyguardIndicationAreaViewModel, + private val sharedNotificationContainerViewModel: SharedNotificationContainerViewModel, private val notificationShadeWindowView: NotificationShadeWindowView, private val featureFlags: FeatureFlags, private val indicationController: KeyguardIndicationController, @@ -55,10 +60,28 @@ constructor( notificationShadeWindowView.requireViewById(R.id.notification_panel) as ViewGroup bindIndicationArea(notificationPanel) bindLockIconView(notificationPanel) + setupNotificationStackScrollLayout(notificationPanel) + keyguardLayoutManager.layoutViews() keyguardLayoutManagerCommandListener.start() } + fun setupNotificationStackScrollLayout(legacyParent: ViewGroup) { + if (featureFlags.isEnabled(Flags.MIGRATE_NSSL)) { + // This moves the existing NSSL view to a different parent, as the controller is a + // singleton and recreating it has other bad side effects + val nssl = + legacyParent.requireViewById<View>(R.id.notification_stack_scroller).also { + (it.getParent() as ViewGroup).removeView(it) + } + sharedNotificationContainer.addNotificationStackScrollLayout(nssl) + SharedNotificationContainerBinder.bind( + sharedNotificationContainer, + sharedNotificationContainerViewModel + ) + } + } + fun bindIndicationArea(legacyParent: ViewGroup) { indicationAreaHandle?.dispose() diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt index d81e4c229aa7..764ef681106b 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt @@ -116,6 +116,10 @@ open class QSTileViewImpl @JvmOverloads constructor( private lateinit var customDrawableView: ImageView private lateinit var chevronView: ImageView private var mQsLogger: QSLogger? = null + + /** + * Controls if tile background is set to a [RippleDrawable] see [setClickable] + */ protected var showRippleEffect = true private lateinit var ripple: RippleDrawable @@ -440,7 +444,6 @@ open class QSTileViewImpl @JvmOverloads constructor( protected open fun handleStateChanged(state: QSTile.State) { val allowAnimations = animationsEnabled() - showRippleEffect = state.showRippleEffect isClickable = state.state != Tile.STATE_UNAVAILABLE isLongClickable = state.handlesLongClick icon.setIcon(state, allowAnimations) diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/BatterySaverTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/BatterySaverTile.java index a444e7631527..9f10e6f1f2c6 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/BatterySaverTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/BatterySaverTile.java @@ -155,7 +155,6 @@ public class BatterySaverTile extends QSTileImpl<BooleanState> implements state.contentDescription = state.label; state.value = mPowerSave; state.expandedAccessibilityClassName = Switch.class.getName(); - state.showRippleEffect = mSetting.getValue() == 0; } @Override diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/UiModeNightTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/UiModeNightTile.java index a60d1ad448b4..9ffcba62ad09 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/UiModeNightTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/UiModeNightTile.java @@ -169,7 +169,6 @@ public class UiModeNightTile extends QSTileImpl<QSTile.BooleanState> implements state.icon = ResourceIcon.get(state.state == Tile.STATE_ACTIVE ? R.drawable.qs_light_dark_theme_icon_on : R.drawable.qs_light_dark_theme_icon_off); - state.showRippleEffect = false; state.expandedAccessibilityClassName = Switch.class.getName(); } diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt b/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt index fba01201190e..5c1dd5670d8a 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt @@ -29,6 +29,8 @@ import androidx.constraintlayout.widget.ConstraintSet.TOP import com.android.systemui.R import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags import com.android.systemui.fragments.FragmentService import com.android.systemui.navigationbar.NavigationModeController import com.android.systemui.plugins.qs.QS @@ -36,6 +38,7 @@ import com.android.systemui.plugins.qs.QSContainerController import com.android.systemui.recents.OverviewProxyService import com.android.systemui.recents.OverviewProxyService.OverviewProxyListener import com.android.systemui.shared.system.QuickStepContract +import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController import com.android.systemui.util.LargeScreenUtils import com.android.systemui.util.ViewController import com.android.systemui.util.concurrency.DelayableExecutor @@ -54,7 +57,10 @@ class NotificationsQSContainerController @Inject constructor( private val shadeHeaderController: ShadeHeaderController, private val shadeExpansionStateManager: ShadeExpansionStateManager, private val fragmentService: FragmentService, - @Main private val delayableExecutor: DelayableExecutor + @Main private val delayableExecutor: DelayableExecutor, + private val featureFlags: FeatureFlags, + private val + notificationStackScrollLayoutController: NotificationStackScrollLayoutController, ) : ViewController<NotificationsQuickSettingsContainer>(view), QSContainerController { private var qsExpanded = false @@ -118,6 +124,9 @@ class NotificationsQSContainerController @Inject constructor( isGestureNavigation = QuickStepContract.isGesturalMode(mode) } isGestureNavigation = QuickStepContract.isGesturalMode(currentMode) + + mView.setStackScroller(notificationStackScrollLayoutController.getView()) + mView.setMigratingNSSL(featureFlags.isEnabled(Flags.MIGRATE_NSSL)) } public override fun onViewAttached() { @@ -254,14 +263,17 @@ class NotificationsQSContainerController @Inject constructor( } private fun setNotificationsConstraints(constraintSet: ConstraintSet) { + if (featureFlags.isEnabled(Flags.MIGRATE_NSSL)) { + return + } val startConstraintId = if (splitShadeEnabled) R.id.qs_edge_guideline else PARENT_ID + val nsslId = R.id.notification_stack_scroller constraintSet.apply { - connect(R.id.notification_stack_scroller, START, startConstraintId, START) - setMargin(R.id.notification_stack_scroller, START, - if (splitShadeEnabled) 0 else panelMarginHorizontal) - setMargin(R.id.notification_stack_scroller, END, panelMarginHorizontal) - setMargin(R.id.notification_stack_scroller, TOP, topMargin) - setMargin(R.id.notification_stack_scroller, BOTTOM, notificationsBottomMargin) + connect(nsslId, START, startConstraintId, START) + setMargin(nsslId, START, if (splitShadeEnabled) 0 else panelMarginHorizontal) + setMargin(nsslId, END, panelMarginHorizontal) + setMargin(nsslId, TOP, topMargin) + setMargin(nsslId, BOTTOM, notificationsBottomMargin) } } diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationsQuickSettingsContainer.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationsQuickSettingsContainer.java index e5b84bd86514..3b3df50f1520 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationsQuickSettingsContainer.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationsQuickSettingsContainer.java @@ -23,6 +23,7 @@ import android.graphics.Canvas; import android.graphics.Rect; import android.util.AttributeSet; import android.view.View; +import android.view.ViewGroup.MarginLayoutParams; import android.view.WindowInsets; import androidx.annotation.Nullable; @@ -56,6 +57,7 @@ public class NotificationsQuickSettingsContainer extends ConstraintLayout private QS mQs; private View mQSContainer; private int mLastQSPaddingBottom; + private boolean mIsMigratingNSSL; /** * These are used to compute the bounding box containing the shade and the notification scrim, @@ -75,10 +77,13 @@ public class NotificationsQuickSettingsContainer extends ConstraintLayout protected void onFinishInflate() { super.onFinishInflate(); mQsFrame = findViewById(R.id.qs_frame); - mStackScroller = findViewById(R.id.notification_stack_scroller); mKeyguardStatusBar = findViewById(R.id.keyguard_header); } + void setStackScroller(View stackScroller) { + mStackScroller = stackScroller; + } + @Override public void onFragmentViewCreated(String tag, Fragment fragment) { mQs = (QS) fragment; @@ -108,7 +113,7 @@ public class NotificationsQuickSettingsContainer extends ConstraintLayout } public void setNotificationsMarginBottom(int margin) { - LayoutParams params = (LayoutParams) mStackScroller.getLayoutParams(); + MarginLayoutParams params = (MarginLayoutParams) mStackScroller.getLayoutParams(); params.bottomMargin = margin; mStackScroller.setLayoutParams(params); } @@ -173,8 +178,15 @@ public class NotificationsQuickSettingsContainer extends ConstraintLayout super.dispatchDraw(canvas); } + void setMigratingNSSL(boolean isMigrating) { + mIsMigratingNSSL = isMigrating; + } + @Override protected boolean drawChild(Canvas canvas, View child, long drawingTime) { + if (mIsMigratingNSSL) { + return super.drawChild(canvas, child, drawingTime); + } int layoutIndex = mLayoutDrawingOrder.indexOf(child); if (layoutIndex >= 0) { return super.drawChild(canvas, mDrawingOrderedChildren.get(layoutIndex), drawingTime); diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt index 8ec8d115de78..3c4ad7222576 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt @@ -49,6 +49,7 @@ import com.android.systemui.statusbar.NotificationShelfController import com.android.systemui.statusbar.notification.row.dagger.NotificationShelfComponent import com.android.systemui.statusbar.notification.shelf.ui.viewbinder.NotificationShelfViewBinderWrapperControllerImpl import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout +import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer import com.android.systemui.statusbar.phone.KeyguardBottomAreaView import com.android.systemui.statusbar.phone.StatusBarLocation import com.android.systemui.statusbar.phone.StatusIconContainer @@ -203,6 +204,14 @@ abstract class ShadeModule { return notificationShadeWindowView.findViewById(R.id.keyguard_root_view) } + @Provides + @SysUISingleton + fun providesSharedNotificationContainer( + notificationShadeWindowView: NotificationShadeWindowView, + ): SharedNotificationContainer { + return notificationShadeWindowView.findViewById(R.id.shared_notification_container) + } + // TODO(b/277762009): Only allow this view's controller to inject the view. See above. @Provides @SysUISingleton diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotifPipelineFlags.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotifPipelineFlags.kt index 5f28ecb56b8c..577ad20cb5d4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotifPipelineFlags.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotifPipelineFlags.kt @@ -16,27 +16,21 @@ package com.android.systemui.statusbar.notification -import android.content.Context import com.android.internal.config.sysui.SystemUiSystemPropertiesFlags.FlagResolver import com.android.internal.config.sysui.SystemUiSystemPropertiesFlags.NotificationFlags import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags import javax.inject.Inject -class NotifPipelineFlags @Inject constructor( - val context: Context, - val featureFlags: FeatureFlags, - val sysPropFlags: FlagResolver, +class NotifPipelineFlags +@Inject +constructor( + private val featureFlags: FeatureFlags, + private val sysPropFlags: FlagResolver, ) { fun isDevLoggingEnabled(): Boolean = featureFlags.isEnabled(Flags.NOTIFICATION_PIPELINE_DEVELOPER_LOGGING) fun allowDismissOngoing(): Boolean = - sysPropFlags.isEnabled(NotificationFlags.ALLOW_DISMISS_ONGOING) - - fun isOtpRedactionEnabled(): Boolean = - sysPropFlags.isEnabled(NotificationFlags.OTP_REDACTION) - - val isNoHunForOldWhenEnabled: Boolean - get() = featureFlags.isEnabled(Flags.NO_HUN_FOR_OLD_WHEN) + sysPropFlags.isEnabled(NotificationFlags.ALLOW_DISMISS_ONGOING) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java index 609f9d47a983..0c43da066fdb 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java @@ -582,10 +582,6 @@ public class NotificationInterruptStateProviderImpl implements NotificationInter } private boolean shouldSuppressHeadsUpWhenAwakeForOldWhen(NotificationEntry entry, boolean log) { - if (!mFlags.isNoHunForOldWhenEnabled()) { - return false; - } - final Notification notification = entry.getSbn().getNotification(); if (notification == null) { return false; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/PrecomputedImageFloatingTextView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/PrecomputedImageFloatingTextView.kt new file mode 100644 index 000000000000..57c82dc8d47a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/PrecomputedImageFloatingTextView.kt @@ -0,0 +1,32 @@ +/* + * 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.statusbar.notification.row + +import android.content.Context +import android.util.AttributeSet +import android.widget.RemoteViews +import com.android.internal.widget.ImageFloatingTextView + +/** Precomputed version of [ImageFloatingTextView] */ +@RemoteViews.RemoteView +class PrecomputedImageFloatingTextView +@JvmOverloads +constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : + ImageFloatingTextView(context, attrs, defStyleAttr), TextPrecomputer { + + override fun setTextAsync(text: CharSequence?): Runnable = precompute(this, text) +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/PrecomputedTextView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/PrecomputedTextView.kt new file mode 100644 index 000000000000..8508b1fd8e6b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/PrecomputedTextView.kt @@ -0,0 +1,34 @@ +/* + * 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.statusbar.notification.row + +import android.content.Context +import android.util.AttributeSet +import android.widget.RemoteViews +import android.widget.TextView + +/** + * A user interface element that uses the PrecomputedText API to display text in a notification, + * with the help of RemoteViews. + */ +@RemoteViews.RemoteView +class PrecomputedTextView +@JvmOverloads +constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : + TextView(context, attrs, defStyleAttr), TextPrecomputer { + + override fun setTextAsync(text: CharSequence?): Runnable = precompute(this, text) +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/TextPrecomputer.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/TextPrecomputer.kt new file mode 100644 index 000000000000..49f4a33ed3e9 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/TextPrecomputer.kt @@ -0,0 +1,59 @@ +/* + * 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.statusbar.notification.row + +import android.text.PrecomputedText +import android.text.Spannable +import android.util.Log +import android.widget.TextView + +internal interface TextPrecomputer { + /** + * Creates PrecomputedText from given text and returns a runnable which sets precomputed text to + * the textview on main thread. + * + * @param text text to be converted to PrecomputedText + * @return Runnable that sets precomputed text on the main thread + */ + fun precompute( + textView: TextView, + text: CharSequence?, + logException: Boolean = true + ): Runnable { + val precomputedText: Spannable? = + text?.let { PrecomputedText.create(it, textView.textMetricsParams) } + + return Runnable { + try { + textView.text = precomputedText + } catch (exception: IllegalArgumentException) { + if (logException) { + Log.wtf( + /* tag = */ TAG, + /* msg = */ "PrecomputedText setText failed for TextView:$textView", + /* tr = */ exception + ) + } + textView.text = text + } + } + } + + private companion object { + private const val TAG = "TextPrecomputer" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/SharedNotificationContainerInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/SharedNotificationContainerInteractor.kt new file mode 100644 index 000000000000..874450b36bf0 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/SharedNotificationContainerInteractor.kt @@ -0,0 +1,65 @@ +/* + * 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.statusbar.notification.stack.domain.interactor + +import android.content.Context +import com.android.systemui.R +import com.android.systemui.common.ui.data.repository.ConfigurationRepository +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart + +/** Encapsulates business-logic specifically related to the shared notification stack container. */ +class SharedNotificationContainerInteractor +@Inject +constructor( + configurationRepository: ConfigurationRepository, + private val context: Context, +) { + val configurationBasedDimensions: Flow<ConfigurationBasedDimensions> = + configurationRepository.onAnyConfigurationChange + .onStart { emit(Unit) } + .map { _ -> + with(context.resources) { + ConfigurationBasedDimensions( + useSplitShade = getBoolean(R.bool.config_use_split_notification_shade), + useLargeScreenHeader = + getBoolean(R.bool.config_use_large_screen_shade_header), + marginHorizontal = + getDimensionPixelSize(R.dimen.notification_panel_margin_horizontal), + marginBottom = + getDimensionPixelSize(R.dimen.notification_panel_margin_bottom), + marginTop = getDimensionPixelSize(R.dimen.notification_panel_margin_top), + marginTopLargeScreen = + getDimensionPixelSize(R.dimen.large_screen_shade_header_height), + ) + } + } + .distinctUntilChanged() + + data class ConfigurationBasedDimensions( + val useSplitShade: Boolean, + val useLargeScreenHeader: Boolean, + val marginHorizontal: Int, + val marginBottom: Int, + val marginTop: Int, + val marginTopLargeScreen: Int, + ) +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/SharedNotificationContainer.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/SharedNotificationContainer.kt new file mode 100644 index 000000000000..688843de06f2 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/SharedNotificationContainer.kt @@ -0,0 +1,88 @@ +/* + * 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.statusbar.notification.stack.ui.view + +import android.content.Context +import android.util.AttributeSet +import android.view.View +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.constraintlayout.widget.ConstraintSet +import androidx.constraintlayout.widget.ConstraintSet.BOTTOM +import androidx.constraintlayout.widget.ConstraintSet.END +import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID +import androidx.constraintlayout.widget.ConstraintSet.START +import androidx.constraintlayout.widget.ConstraintSet.TOP +import androidx.constraintlayout.widget.ConstraintSet.VERTICAL +import com.android.systemui.R + +/** + * Container for the stack scroller, so that the bounds can be externally specified, such as from + * the keyguard or shade scenes. + */ +class SharedNotificationContainer( + context: Context, + private val attrs: AttributeSet?, +) : + ConstraintLayout( + context, + attrs, + ) { + + private val baseConstraintSet = ConstraintSet() + + init { + baseConstraintSet.apply { + create(R.id.nssl_guideline, VERTICAL) + setGuidelinePercent(R.id.nssl_guideline, 0.5f) + } + baseConstraintSet.applyTo(this) + } + + fun addNotificationStackScrollLayout(nssl: View) { + addView(nssl) + } + + fun updateConstraints( + useSplitShade: Boolean, + marginStart: Int, + marginTop: Int, + marginEnd: Int, + marginBottom: Int + ) { + val constraintSet = ConstraintSet() + constraintSet.clone(baseConstraintSet) + + val startConstraintId = + if (useSplitShade) { + R.id.nssl_guideline + } else { + PARENT_ID + } + val nsslId = R.id.notification_stack_scroller + constraintSet.apply { + connect(nsslId, START, startConstraintId, START) + connect(nsslId, END, PARENT_ID, END) + connect(nsslId, BOTTOM, PARENT_ID, BOTTOM) + connect(nsslId, TOP, PARENT_ID, TOP) + setMargin(nsslId, START, marginStart) + setMargin(nsslId, END, marginEnd) + setMargin(nsslId, TOP, marginTop) + setMargin(nsslId, BOTTOM, marginBottom) + } + constraintSet.applyTo(this) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt new file mode 100644 index 000000000000..fb1d55d6bb7b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt @@ -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 com.android.systemui.statusbar.notification.stack.ui.viewbinder + +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.repeatOnLifecycle +import com.android.systemui.lifecycle.repeatWhenAttached +import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer +import com.android.systemui.statusbar.notification.stack.ui.viewmodel.SharedNotificationContainerViewModel +import kotlinx.coroutines.launch + +/** Binds the shared notification container to its view-model. */ +object SharedNotificationContainerBinder { + + @JvmStatic + fun bind( + view: SharedNotificationContainer, + viewModel: SharedNotificationContainerViewModel, + ) { + view.repeatWhenAttached { + repeatOnLifecycle(Lifecycle.State.STARTED) { + launch { + viewModel.configurationBasedDimensions.collect { + view.updateConstraints( + useSplitShade = it.useSplitShade, + marginStart = it.marginStart, + marginTop = it.marginTop, + marginEnd = it.marginEnd, + marginBottom = it.marginBottom, + ) + } + } + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt new file mode 100644 index 000000000000..b2e5ac116e46 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.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.systemui.statusbar.notification.stack.ui.viewmodel + +import com.android.systemui.statusbar.notification.stack.domain.interactor.SharedNotificationContainerInteractor +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map + +/** View-model for the shared notification container */ +class SharedNotificationContainerViewModel +@Inject +constructor( + interactor: SharedNotificationContainerInteractor, +) { + val configurationBasedDimensions: Flow<ConfigurationBasedDimensions> = + interactor.configurationBasedDimensions + .map { + ConfigurationBasedDimensions( + marginStart = if (it.useSplitShade) 0 else it.marginHorizontal, + marginEnd = it.marginHorizontal, + marginBottom = it.marginBottom, + marginTop = + if (it.useLargeScreenHeader) it.marginTopLargeScreen else it.marginTop, + useSplitShade = it.useSplitShade, + ) + } + .distinctUntilChanged() + + data class ConfigurationBasedDimensions( + val marginStart: Int, + val marginTop: Int, + val marginEnd: Int, + val marginBottom: Int, + val useSplitShade: Boolean, + ) +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowController.java b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowController.java index bcf3b0cbfc86..24987abd7a85 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowController.java @@ -240,8 +240,10 @@ public class StatusBarWindowController { Insets.of(0, safeTouchRegionHeight, 0, 0)); } lp.providedInsets = new InsetsFrameProvider[] { - new InsetsFrameProvider(mInsetsSourceOwner, 0, statusBars()), - new InsetsFrameProvider(mInsetsSourceOwner, 0, tappableElement()), + new InsetsFrameProvider(mInsetsSourceOwner, 0, statusBars()) + .setInsetsSize(getInsets(height)), + new InsetsFrameProvider(mInsetsSourceOwner, 0, tappableElement()) + .setInsetsSize(getInsets(height)), gestureInsetsProvider }; return lp; @@ -306,12 +308,37 @@ public class StatusBarWindowController { mLpChanged.height = state.mIsLaunchAnimationRunning ? ViewGroup.LayoutParams.MATCH_PARENT : mBarHeight; for (int rot = Surface.ROTATION_0; rot <= Surface.ROTATION_270; rot++) { + int height = SystemBarUtils.getStatusBarHeightForRotation(mContext, rot); mLpChanged.paramsForRotation[rot].height = - state.mIsLaunchAnimationRunning ? ViewGroup.LayoutParams.MATCH_PARENT : - SystemBarUtils.getStatusBarHeightForRotation(mContext, rot); + state.mIsLaunchAnimationRunning ? ViewGroup.LayoutParams.MATCH_PARENT : height; + // The status bar height could change at runtime if one display has a cutout while + // another doesn't (like some foldables). It could also change when using debug cutouts. + // So, we need to re-fetch the height and re-apply it to the insets each time to avoid + // bugs like b/290300359. + InsetsFrameProvider[] providers = mLpChanged.paramsForRotation[rot].providedInsets; + if (providers != null) { + for (InsetsFrameProvider provider : providers) { + provider.setInsetsSize(getInsets(height)); + } + } } } + /** + * Get the insets that should be applied to the status bar window given the current status bar + * height. + * + * The status bar window height can sometimes be full-screen (see {@link #applyHeight(State)}. + * However, the status bar *insets* should *not* be full-screen, because this would prevent apps + * from drawing any content and can cause animations to be cancelled (see b/283958440). Instead, + * the status bar insets should always be equal to the space occupied by the actual status bar + * content -- setting the insets correctly will prevent window manager from unnecessarily + * re-drawing this window and other windows. This method provides the correct insets. + */ + private Insets getInsets(int height) { + return Insets.of(0, height, 0, 0); + } + private void apply(State state) { if (!mIsAttached) { return; diff --git a/packages/SystemUI/tests/src/com/android/keyguard/AdminSecondaryLockScreenControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/AdminSecondaryLockScreenControllerTest.java index 319a02d911ed..d506584cdfac 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/AdminSecondaryLockScreenControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/AdminSecondaryLockScreenControllerTest.java @@ -33,12 +33,15 @@ import android.app.admin.IKeyguardCallback; import android.app.admin.IKeyguardClient; import android.content.ComponentName; import android.content.Intent; +import android.hardware.display.DisplayManager; +import android.os.Binder; import android.os.Handler; import android.os.RemoteException; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import android.testing.TestableLooper.RunWithLooper; import android.testing.ViewUtils; +import android.view.Display; import android.view.SurfaceControlViewHost; import android.view.SurfaceView; import android.view.View; @@ -50,7 +53,6 @@ import com.android.systemui.SysuiTestCase; import org.junit.After; import org.junit.Before; -import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; @@ -60,7 +62,6 @@ import org.mockito.MockitoAnnotations; @RunWithLooper @RunWith(AndroidTestingRunner.class) @SmallTest -@Ignore("b/286245842") public class AdminSecondaryLockScreenControllerTest extends SysuiTestCase { private static final int TARGET_USER_ID = KeyguardUpdateMonitor.getCurrentUser(); @@ -79,7 +80,7 @@ public class AdminSecondaryLockScreenControllerTest extends SysuiTestCase { private KeyguardSecurityCallback mKeyguardCallback; @Mock private KeyguardUpdateMonitor mUpdateMonitor; - @Mock + private SurfaceControlViewHost.SurfacePackage mSurfacePackage; @Before @@ -99,6 +100,11 @@ public class AdminSecondaryLockScreenControllerTest extends SysuiTestCase { when(mKeyguardClient.queryLocalInterface(anyString())).thenReturn(mKeyguardClient); when(mKeyguardClient.asBinder()).thenReturn(mKeyguardClient); + Display display = mContext.getSystemService(DisplayManager.class).getDisplay( + Display.DEFAULT_DISPLAY); + mSurfacePackage = (new SurfaceControlViewHost(mContext, display, + new Binder())).getSurfacePackage(); + mTestController = new AdminSecondaryLockScreenController.Factory( mContext, mKeyguardSecurityContainer, mUpdateMonitor, mHandler) .create(mKeyguardCallback); diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/SideFpsControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/SideFpsControllerTest.kt index d022653a33c8..eaa31ac1d157 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/SideFpsControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/SideFpsControllerTest.kt @@ -44,9 +44,6 @@ import android.view.View import android.view.ViewPropertyAnimator import android.view.WindowInsets import android.view.WindowManager -import android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION -import android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY -import android.view.WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG import android.view.WindowMetrics import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest @@ -55,9 +52,14 @@ import com.android.systemui.R import com.android.systemui.RoboPilotTest import com.android.systemui.SysuiTestCase import com.android.systemui.SysuiTestableContext +import com.android.systemui.biometrics.data.repository.FakeFingerprintPropertyRepository import com.android.systemui.biometrics.data.repository.FakeRearDisplayStateRepository import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractor import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractorImpl +import com.android.systemui.biometrics.domain.interactor.SideFpsOverlayInteractorImpl +import com.android.systemui.biometrics.shared.model.FingerprintSensorType +import com.android.systemui.biometrics.shared.model.SensorStrength +import com.android.systemui.biometrics.ui.viewmodel.SideFpsOverlayViewModel import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor import com.android.systemui.dump.DumpManager @@ -99,7 +101,7 @@ private const val REAR_DISPLAY_MODE_DEVICE_STATE = 3 @SmallTest @RoboPilotTest @RunWith(AndroidJUnit4::class) -@TestableLooper.RunWithLooper +@TestableLooper.RunWithLooper(setAsMainLooper = true) class SideFpsControllerTest : SysuiTestCase() { @JvmField @Rule var rule = MockitoJUnit.rule() @@ -118,6 +120,8 @@ class SideFpsControllerTest : SysuiTestCase() { private lateinit var keyguardBouncerRepository: FakeKeyguardBouncerRepository private lateinit var alternateBouncerInteractor: AlternateBouncerInteractor private lateinit var displayStateInteractor: DisplayStateInteractor + private lateinit var sideFpsOverlayViewModel: SideFpsOverlayViewModel + private val fingerprintRepository = FakeFingerprintPropertyRepository() private val executor = FakeExecutor(FakeSystemClock()) private val rearDisplayStateRepository = FakeRearDisplayStateRepository() @@ -159,6 +163,15 @@ class SideFpsControllerTest : SysuiTestCase() { executor, rearDisplayStateRepository ) + sideFpsOverlayViewModel = + SideFpsOverlayViewModel(context, SideFpsOverlayInteractorImpl(fingerprintRepository)) + + fingerprintRepository.setProperties( + sensorId = 1, + strength = SensorStrength.STRONG, + sensorType = FingerprintSensorType.REAR, + sensorLocations = mapOf("" to SensorLocationInternal("", 2500, 0, 0)) + ) context.addMockSystemService(DisplayManager::class.java, displayManager) context.addMockSystemService(WindowManager::class.java, windowManager) @@ -265,6 +278,7 @@ class SideFpsControllerTest : SysuiTestCase() { executor, handler, alternateBouncerInteractor, + { sideFpsOverlayViewModel }, TestCoroutineScope(), dumpManager ) @@ -683,106 +697,6 @@ class SideFpsControllerTest : SysuiTestCase() { verify(windowManager).removeView(any()) } - /** - * {@link SideFpsController#updateOverlayParams} calculates indicator placement for ROTATION_0, - * and uses RotateUtils.rotateBounds to map to the correct indicator location given the device - * rotation. Assuming RotationUtils.rotateBounds works correctly, tests for indicator placement - * in other rotations have been omitted. - */ - @Test - fun verifiesIndicatorPlacementForXAlignedSensor_0() = - testWithDisplay( - deviceConfig = DeviceConfig.X_ALIGNED, - isReverseDefaultRotation = false, - { rotation = Surface.ROTATION_0 } - ) { - sideFpsController.overlayOffsets = sensorLocation - - sideFpsController.updateOverlayParams(windowManager.defaultDisplay, indicatorBounds) - - overlayController.show(SENSOR_ID, REASON_UNKNOWN) - executor.runAllReady() - - verify(windowManager).updateViewLayout(any(), overlayViewParamsCaptor.capture()) - assertThat(overlayViewParamsCaptor.value.x).isEqualTo(sensorLocation.sensorLocationX) - assertThat(overlayViewParamsCaptor.value.y).isEqualTo(0) - } - - /** - * {@link SideFpsController#updateOverlayParams} calculates indicator placement for ROTATION_270 - * in reverse default rotation. It then uses RotateUtils.rotateBounds to map to the correct - * indicator location given the device rotation. Assuming RotationUtils.rotateBounds works - * correctly, tests for indicator placement in other rotations have been omitted. - */ - @Test - fun verifiesIndicatorPlacementForXAlignedSensor_InReverseDefaultRotation_270() = - testWithDisplay( - deviceConfig = DeviceConfig.X_ALIGNED, - isReverseDefaultRotation = true, - { rotation = Surface.ROTATION_270 } - ) { - sideFpsController.overlayOffsets = sensorLocation - - sideFpsController.updateOverlayParams(windowManager.defaultDisplay, indicatorBounds) - - overlayController.show(SENSOR_ID, REASON_UNKNOWN) - executor.runAllReady() - - verify(windowManager).updateViewLayout(any(), overlayViewParamsCaptor.capture()) - assertThat(overlayViewParamsCaptor.value.x).isEqualTo(sensorLocation.sensorLocationX) - assertThat(overlayViewParamsCaptor.value.y).isEqualTo(0) - } - - /** - * {@link SideFpsController#updateOverlayParams} calculates indicator placement for ROTATION_0, - * and uses RotateUtils.rotateBounds to map to the correct indicator location given the device - * rotation. Assuming RotationUtils.rotateBounds works correctly, tests for indicator placement - * in other rotations have been omitted. - */ - @Test - fun verifiesIndicatorPlacementForYAlignedSensor_0() = - testWithDisplay( - deviceConfig = DeviceConfig.Y_ALIGNED, - isReverseDefaultRotation = false, - { rotation = Surface.ROTATION_0 } - ) { - sideFpsController.overlayOffsets = sensorLocation - - sideFpsController.updateOverlayParams(windowManager.defaultDisplay, indicatorBounds) - - overlayController.show(SENSOR_ID, REASON_UNKNOWN) - executor.runAllReady() - - verify(windowManager).updateViewLayout(any(), overlayViewParamsCaptor.capture()) - assertThat(overlayViewParamsCaptor.value.x).isEqualTo(displayWidth - boundsWidth) - assertThat(overlayViewParamsCaptor.value.y).isEqualTo(sensorLocation.sensorLocationY) - } - - /** - * {@link SideFpsController#updateOverlayParams} calculates indicator placement for ROTATION_270 - * in reverse default rotation. It then uses RotateUtils.rotateBounds to map to the correct - * indicator location given the device rotation. Assuming RotationUtils.rotateBounds works - * correctly, tests for indicator placement in other rotations have been omitted. - */ - @Test - fun verifiesIndicatorPlacementForYAlignedSensor_InReverseDefaultRotation_270() = - testWithDisplay( - deviceConfig = DeviceConfig.Y_ALIGNED, - isReverseDefaultRotation = true, - { rotation = Surface.ROTATION_270 } - ) { - sideFpsController.overlayOffsets = sensorLocation - - sideFpsController.updateOverlayParams(windowManager.defaultDisplay, indicatorBounds) - - overlayController.show(SENSOR_ID, REASON_UNKNOWN) - executor.runAllReady() - - verify(windowManager).updateViewLayout(any(), overlayViewParamsCaptor.capture()) - assertThat(overlayViewParamsCaptor.value.x).isEqualTo(displayWidth - boundsWidth) - assertThat(overlayViewParamsCaptor.value.y).isEqualTo(sensorLocation.sensorLocationY) - } - @Test fun hasSideFpsSensor_withSensorProps_returnsTrue() = testWithDisplay { // By default all those tests assume the side fps sensor is available. @@ -795,51 +709,6 @@ class SideFpsControllerTest : SysuiTestCase() { assertThat(fingerprintManager.hasSideFpsSensor()).isFalse() } - - @Test - fun testLayoutParams_isKeyguardDialogType() = - testWithDisplay(deviceConfig = DeviceConfig.Y_ALIGNED) { - sideFpsController.overlayOffsets = sensorLocation - sideFpsController.updateOverlayParams(windowManager.defaultDisplay, indicatorBounds) - overlayController.show(SENSOR_ID, REASON_UNKNOWN) - executor.runAllReady() - - verify(windowManager).updateViewLayout(any(), overlayViewParamsCaptor.capture()) - - val lpType = overlayViewParamsCaptor.value.type - - assertThat((lpType and TYPE_KEYGUARD_DIALOG) != 0).isTrue() - } - - @Test - fun testLayoutParams_hasNoMoveAnimationWindowFlag() = - testWithDisplay(deviceConfig = DeviceConfig.Y_ALIGNED) { - sideFpsController.overlayOffsets = sensorLocation - sideFpsController.updateOverlayParams(windowManager.defaultDisplay, indicatorBounds) - overlayController.show(SENSOR_ID, REASON_UNKNOWN) - executor.runAllReady() - - verify(windowManager).updateViewLayout(any(), overlayViewParamsCaptor.capture()) - - val lpFlags = overlayViewParamsCaptor.value.privateFlags - - assertThat((lpFlags and PRIVATE_FLAG_NO_MOVE_ANIMATION) != 0).isTrue() - } - - @Test - fun testLayoutParams_hasTrustedOverlayWindowFlag() = - testWithDisplay(deviceConfig = DeviceConfig.Y_ALIGNED) { - sideFpsController.overlayOffsets = sensorLocation - sideFpsController.updateOverlayParams(windowManager.defaultDisplay, indicatorBounds) - overlayController.show(SENSOR_ID, REASON_UNKNOWN) - executor.runAllReady() - - verify(windowManager).updateViewLayout(any(), overlayViewParamsCaptor.capture()) - - val lpFlags = overlayViewParamsCaptor.value.privateFlags - - assertThat((lpFlags and PRIVATE_FLAG_TRUSTED_OVERLAY) != 0).isTrue() - } } private fun insetsForSmallNavbar() = insetsWithBottom(60) diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/FingerprintRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/FingerprintRepositoryImplTest.kt index 239e317b92f5..ea2561594793 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/FingerprintRepositoryImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/FingerprintRepositoryImplTest.kt @@ -73,10 +73,15 @@ class FingerprintRepositoryImplTest : SysuiTestCase() { @Test fun initializeProperties() = testScope.runTest { - val isInitialized = collectLastValue(repository.isInitialized) + val sensorId by collectLastValue(repository.sensorId) + val strength by collectLastValue(repository.strength) + val sensorType by collectLastValue(repository.sensorType) + val sensorLocations by collectLastValue(repository.sensorLocations) - assertDefaultProperties() - assertThat(isInitialized()).isFalse() + // Assert default properties. + assertThat(sensorId).isEqualTo(-1) + assertThat(strength).isEqualTo(SensorStrength.CONVENIENCE) + assertThat(sensorType).isEqualTo(FingerprintSensorType.UNKNOWN) val fingerprintProps = listOf( @@ -115,31 +120,24 @@ class FingerprintRepositoryImplTest : SysuiTestCase() { fingerprintAuthenticatorsCaptor.value.onAllAuthenticatorsRegistered(fingerprintProps) - assertThat(repository.sensorId.value).isEqualTo(1) - assertThat(repository.strength.value).isEqualTo(SensorStrength.STRONG) - assertThat(repository.sensorType.value).isEqualTo(FingerprintSensorType.REAR) + assertThat(sensorId).isEqualTo(1) + assertThat(strength).isEqualTo(SensorStrength.STRONG) + assertThat(sensorType).isEqualTo(FingerprintSensorType.REAR) - assertThat(repository.sensorLocations.value.size).isEqualTo(2) - assertThat(repository.sensorLocations.value).containsKey("display_id_1") - with(repository.sensorLocations.value["display_id_1"]!!) { + assertThat(sensorLocations?.size).isEqualTo(2) + assertThat(sensorLocations).containsKey("display_id_1") + with(sensorLocations?.get("display_id_1")!!) { assertThat(displayId).isEqualTo("display_id_1") assertThat(sensorLocationX).isEqualTo(100) assertThat(sensorLocationY).isEqualTo(300) assertThat(sensorRadius).isEqualTo(20) } - assertThat(repository.sensorLocations.value).containsKey("") - with(repository.sensorLocations.value[""]!!) { + assertThat(sensorLocations).containsKey("") + with(sensorLocations?.get("")!!) { assertThat(displayId).isEqualTo("") assertThat(sensorLocationX).isEqualTo(540) assertThat(sensorLocationY).isEqualTo(1636) assertThat(sensorRadius).isEqualTo(130) } - assertThat(isInitialized()).isTrue() } - - private fun assertDefaultProperties() { - assertThat(repository.sensorId.value).isEqualTo(-1) - assertThat(repository.strength.value).isEqualTo(SensorStrength.CONVENIENCE) - assertThat(repository.sensorType.value).isEqualTo(FingerprintSensorType.UNKNOWN) - } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/SideFpsOverlayInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/SideFpsOverlayInteractorTest.kt index fd96cf45504b..896f9b114679 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/SideFpsOverlayInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/SideFpsOverlayInteractorTest.kt @@ -22,6 +22,7 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.biometrics.data.repository.FakeFingerprintPropertyRepository import com.android.systemui.biometrics.shared.model.FingerprintSensorType import com.android.systemui.biometrics.shared.model.SensorStrength +import com.android.systemui.coroutines.collectLastValue import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestScope @@ -51,8 +52,9 @@ class SideFpsOverlayInteractorTest : SysuiTestCase() { } @Test - fun testGetOverlayOffsets() = + fun testGetOverlayoffsets() = testScope.runTest { + // Arrange. fingerprintRepository.setProperties( sensorId = 1, strength = SensorStrength.STRONG, @@ -76,16 +78,33 @@ class SideFpsOverlayInteractorTest : SysuiTestCase() { ) ) - var offsets = interactor.getOverlayOffsets("display_id_1") - assertThat(offsets.displayId).isEqualTo("display_id_1") - assertThat(offsets.sensorLocationX).isEqualTo(100) - assertThat(offsets.sensorLocationY).isEqualTo(300) - assertThat(offsets.sensorRadius).isEqualTo(20) + // Act. + val offsets by collectLastValue(interactor.overlayOffsets) + val displayId by collectLastValue(interactor.displayId) - offsets = interactor.getOverlayOffsets("invalid_display_id") - assertThat(offsets.displayId).isEqualTo("") - assertThat(offsets.sensorLocationX).isEqualTo(540) - assertThat(offsets.sensorLocationY).isEqualTo(1636) - assertThat(offsets.sensorRadius).isEqualTo(130) + // Assert offsets of empty displayId. + assertThat(displayId).isEqualTo("") + assertThat(offsets?.displayId).isEqualTo("") + assertThat(offsets?.sensorLocationX).isEqualTo(540) + assertThat(offsets?.sensorLocationY).isEqualTo(1636) + assertThat(offsets?.sensorRadius).isEqualTo(130) + + // Offsets should be updated correctly. + interactor.changeDisplay("display_id_1") + assertThat(displayId).isEqualTo("display_id_1") + assertThat(offsets?.displayId).isEqualTo("display_id_1") + assertThat(offsets?.sensorLocationX).isEqualTo(100) + assertThat(offsets?.sensorLocationY).isEqualTo(300) + assertThat(offsets?.sensorRadius).isEqualTo(20) + + // Should return default offset when the displayId is invalid. + interactor.changeDisplay("invalid_display_id") + assertThat(displayId).isEqualTo("invalid_display_id") + assertThat(offsets?.displayId).isEqualTo(SensorLocationInternal.DEFAULT.displayId) + assertThat(offsets?.sensorLocationX) + .isEqualTo(SensorLocationInternal.DEFAULT.sensorLocationX) + assertThat(offsets?.sensorLocationY) + .isEqualTo(SensorLocationInternal.DEFAULT.sensorLocationY) + assertThat(offsets?.sensorRadius).isEqualTo(SensorLocationInternal.DEFAULT.sensorRadius) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelTest.kt new file mode 100644 index 000000000000..a8593216e22a --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelTest.kt @@ -0,0 +1,263 @@ +/* + * 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.ui.viewmodel + +import android.graphics.Rect +import android.hardware.biometrics.SensorLocationInternal +import android.hardware.display.DisplayManagerGlobal +import android.view.Display +import android.view.DisplayAdjustments +import android.view.DisplayInfo +import android.view.Surface +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.SysuiTestableContext +import com.android.systemui.biometrics.data.repository.FakeFingerprintPropertyRepository +import com.android.systemui.biometrics.domain.interactor.SideFpsOverlayInteractor +import com.android.systemui.biometrics.domain.interactor.SideFpsOverlayInteractorImpl +import com.android.systemui.biometrics.shared.model.FingerprintSensorType +import com.android.systemui.biometrics.shared.model.SensorStrength +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.util.mockito.whenever +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.mockito.ArgumentMatchers +import org.mockito.Mockito +import org.mockito.junit.MockitoJUnit + +private const val DISPLAY_ID = 2 + +@SmallTest +@RunWith(JUnit4::class) +class SideFpsOverlayViewModelTest : SysuiTestCase() { + + @JvmField @Rule var mockitoRule = MockitoJUnit.rule() + private var testScope: TestScope = TestScope(StandardTestDispatcher()) + + private val fingerprintRepository = FakeFingerprintPropertyRepository() + private lateinit var interactor: SideFpsOverlayInteractor + private lateinit var viewModel: SideFpsOverlayViewModel + + enum class DeviceConfig { + X_ALIGNED, + Y_ALIGNED, + } + + private lateinit var deviceConfig: DeviceConfig + private lateinit var indicatorBounds: Rect + private lateinit var displayBounds: Rect + private lateinit var sensorLocation: SensorLocationInternal + private var displayWidth: Int = 0 + private var displayHeight: Int = 0 + private var boundsWidth: Int = 0 + private var boundsHeight: Int = 0 + + @Before + fun setup() { + interactor = SideFpsOverlayInteractorImpl(fingerprintRepository) + + fingerprintRepository.setProperties( + sensorId = 1, + strength = SensorStrength.STRONG, + sensorType = FingerprintSensorType.REAR, + sensorLocations = + mapOf( + "" to + SensorLocationInternal( + "" /* displayId */, + 540 /* sensorLocationX */, + 1636 /* sensorLocationY */, + 130 /* sensorRadius */ + ), + "display_id_1" to + SensorLocationInternal( + "display_id_1" /* displayId */, + 100 /* sensorLocationX */, + 300 /* sensorLocationY */, + 20 /* sensorRadius */ + ) + ) + ) + } + + @Test + fun testOverlayOffsets() = + testScope.runTest { + viewModel = SideFpsOverlayViewModel(mContext, interactor) + + val interactorOffsets by collectLastValue(interactor.overlayOffsets) + val viewModelOffsets by collectLastValue(viewModel.overlayOffsets) + + assertThat(viewModelOffsets).isEqualTo(interactorOffsets) + } + + private fun testWithDisplay( + deviceConfig: DeviceConfig = DeviceConfig.X_ALIGNED, + isReverseDefaultRotation: Boolean = false, + initInfo: DisplayInfo.() -> Unit = {}, + block: () -> Unit + ) { + this.deviceConfig = deviceConfig + + when (deviceConfig) { + DeviceConfig.X_ALIGNED -> { + displayWidth = 3000 + displayHeight = 1500 + sensorLocation = SensorLocationInternal("", 2500, 0, 0) + boundsWidth = 200 + boundsHeight = 100 + } + DeviceConfig.Y_ALIGNED -> { + displayWidth = 2500 + displayHeight = 2000 + sensorLocation = SensorLocationInternal("", 0, 300, 0) + boundsWidth = 100 + boundsHeight = 200 + } + } + + indicatorBounds = Rect(0, 0, boundsWidth, boundsHeight) + displayBounds = Rect(0, 0, displayWidth, displayHeight) + + val displayInfo = DisplayInfo() + displayInfo.initInfo() + + val dmGlobal = Mockito.mock(DisplayManagerGlobal::class.java) + val display = + Display( + dmGlobal, + DISPLAY_ID, + displayInfo, + DisplayAdjustments.DEFAULT_DISPLAY_ADJUSTMENTS + ) + + whenever(dmGlobal.getDisplayInfo(ArgumentMatchers.eq(DISPLAY_ID))).thenReturn(displayInfo) + + val sideFpsOverlayViewModelContext = + context.createDisplayContext(display) as SysuiTestableContext + sideFpsOverlayViewModelContext.orCreateTestableResources.addOverride( + com.android.internal.R.bool.config_reverseDefaultRotation, + isReverseDefaultRotation + ) + viewModel = SideFpsOverlayViewModel(sideFpsOverlayViewModelContext, interactor) + + block() + } + + /** + * {@link SideFpsOverlayViewModel#updateSensorBounds} calculates indicator placement for + * ROTATION_0, and uses RotateUtils.rotateBounds to map to the correct indicator location given + * the device rotation. Assuming RotationUtils.rotateBounds works correctly, tests for indicator + * placement in other rotations have been omitted. + */ + @Test + fun verifiesIndicatorPlacementForXAlignedSensor_0() = + testScope.runTest { + testWithDisplay( + deviceConfig = DeviceConfig.X_ALIGNED, + isReverseDefaultRotation = false, + { rotation = Surface.ROTATION_0 } + ) { + viewModel.updateSensorBounds(indicatorBounds, displayBounds, sensorLocation) + + val displayInfo: DisplayInfo = DisplayInfo() + context.display!!.getDisplayInfo(displayInfo) + assertThat(displayInfo.rotation).isEqualTo(Surface.ROTATION_0) + + assertThat(viewModel.sensorBounds.value).isNotNull() + assertThat(viewModel.sensorBounds.value.left) + .isEqualTo(sensorLocation.sensorLocationX) + assertThat(viewModel.sensorBounds.value.top).isEqualTo(0) + } + } + + /** + * {@link SideFpsOverlayViewModel#updateSensorBounds} calculates indicator placement for + * ROTATION_270 in reverse default rotation. It then uses RotateUtils.rotateBounds to map to the + * correct indicator location given the device rotation. Assuming RotationUtils.rotateBounds + * works correctly, tests for indicator placement in other rotations have been omitted. + */ + @Test + fun verifiesIndicatorPlacementForXAlignedSensor_InReverseDefaultRotation_270() = + testScope.runTest { + testWithDisplay( + deviceConfig = DeviceConfig.X_ALIGNED, + isReverseDefaultRotation = true, + { rotation = Surface.ROTATION_270 } + ) { + viewModel.updateSensorBounds(indicatorBounds, displayBounds, sensorLocation) + + assertThat(viewModel.sensorBounds.value).isNotNull() + assertThat(viewModel.sensorBounds.value.left) + .isEqualTo(sensorLocation.sensorLocationX) + assertThat(viewModel.sensorBounds.value.top).isEqualTo(0) + } + } + + /** + * {@link SideFpsOverlayViewModel#updateSensorBounds} calculates indicator placement for + * ROTATION_0, and uses RotateUtils.rotateBounds to map to the correct indicator location given + * the device rotation. Assuming RotationUtils.rotateBounds works correctly, tests for indicator + * placement in other rotations have been omitted. + */ + @Test + fun verifiesIndicatorPlacementForYAlignedSensor_0() = + testScope.runTest { + testWithDisplay( + deviceConfig = DeviceConfig.Y_ALIGNED, + isReverseDefaultRotation = false, + { rotation = Surface.ROTATION_0 } + ) { + viewModel.updateSensorBounds(indicatorBounds, displayBounds, sensorLocation) + + assertThat(viewModel.sensorBounds.value).isNotNull() + assertThat(viewModel.sensorBounds.value.left).isEqualTo(displayWidth - boundsWidth) + assertThat(viewModel.sensorBounds.value.top) + .isEqualTo(sensorLocation.sensorLocationY) + } + } + + /** + * {@link SideFpsOverlayViewModel#updateSensorBounds} calculates indicator placement for + * ROTATION_270 in reverse default rotation. It then uses RotateUtils.rotateBounds to map to the + * correct indicator location given the device rotation. Assuming RotationUtils.rotateBounds + * works correctly, tests for indicator placement in other rotations have been omitted. + */ + @Test + fun verifiesIndicatorPlacementForYAlignedSensor_InReverseDefaultRotation_270() = + testScope.runTest { + testWithDisplay( + deviceConfig = DeviceConfig.Y_ALIGNED, + isReverseDefaultRotation = true, + { rotation = Surface.ROTATION_270 } + ) { + viewModel.updateSensorBounds(indicatorBounds, displayBounds, sensorLocation) + + assertThat(viewModel.sensorBounds.value).isNotNull() + assertThat(viewModel.sensorBounds.value.left).isEqualTo(displayWidth - boundsWidth) + assertThat(viewModel.sensorBounds.value.top) + .isEqualTo(sensorLocation.sensorLocationY) + } + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/FakeMediaProjectionManager.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/FakeMediaProjectionManager.kt new file mode 100644 index 000000000000..45f0a8c62125 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/FakeMediaProjectionManager.kt @@ -0,0 +1,71 @@ +/* + * 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.data.repository + +import android.media.projection.MediaProjectionInfo +import android.media.projection.MediaProjectionManager +import android.os.Binder +import android.os.IBinder +import android.os.UserHandle +import android.view.ContentRecordingSession +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever + +class FakeMediaProjectionManager { + + val mediaProjectionManager = mock<MediaProjectionManager>() + + private val callbacks = mutableListOf<MediaProjectionManager.Callback>() + + init { + whenever(mediaProjectionManager.addCallback(any(), any())).thenAnswer { + callbacks += it.arguments[0] as MediaProjectionManager.Callback + return@thenAnswer Unit + } + whenever(mediaProjectionManager.removeCallback(any())).thenAnswer { + callbacks -= it.arguments[0] as MediaProjectionManager.Callback + return@thenAnswer Unit + } + } + + fun dispatchOnStart(info: MediaProjectionInfo = DEFAULT_INFO) { + callbacks.forEach { it.onStart(info) } + } + + fun dispatchOnStop(info: MediaProjectionInfo = DEFAULT_INFO) { + callbacks.forEach { it.onStop(info) } + } + + fun dispatchOnSessionSet( + info: MediaProjectionInfo = DEFAULT_INFO, + session: ContentRecordingSession? + ) { + callbacks.forEach { it.onRecordingSessionSet(info, session) } + } + + companion object { + fun createDisplaySession(): ContentRecordingSession = + ContentRecordingSession.createDisplaySession(/* displayToMirror = */ 123) + fun createSingleTaskSession(token: IBinder = Binder()): ContentRecordingSession = + ContentRecordingSession.createTaskSession(token) + + private const val DEFAULT_PACKAGE_NAME = "com.media.projection.test" + private val DEFAULT_USER_HANDLE = UserHandle.getUserHandleForUid(UserHandle.myUserId()) + private val DEFAULT_INFO = MediaProjectionInfo(DEFAULT_PACKAGE_NAME, DEFAULT_USER_HANDLE) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/FakeMediaProjectionRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/FakeMediaProjectionRepository.kt deleted file mode 100644 index c59fd60cca9b..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/FakeMediaProjectionRepository.kt +++ /dev/null @@ -1,42 +0,0 @@ -/* - * 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.data.repository - -import android.app.TaskInfo -import com.android.systemui.mediaprojection.taskswitcher.data.model.MediaProjectionState -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow - -class FakeMediaProjectionRepository : MediaProjectionRepository { - - private val state = MutableStateFlow<MediaProjectionState>(MediaProjectionState.NotProjecting) - - fun switchProjectedTask(newTask: TaskInfo) { - state.value = MediaProjectionState.SingleTask(newTask) - } - - override val mediaProjectionState: Flow<MediaProjectionState> = state.asStateFlow() - - fun projectEntireScreen() { - state.value = MediaProjectionState.EntireScreen - } - - fun stopProjecting() { - state.value = MediaProjectionState.NotProjecting - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/FakeTasksRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/FakeTasksRepository.kt deleted file mode 100644 index 593e3893fb2a..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/FakeTasksRepository.kt +++ /dev/null @@ -1,68 +0,0 @@ -/* - * 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.data.repository - -import android.app.ActivityManager.RunningTaskInfo -import android.content.Intent -import android.os.IBinder -import android.window.IWindowContainerToken -import android.window.WindowContainerToken -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow - -class FakeTasksRepository : TasksRepository { - - private val _foregroundTask = MutableStateFlow(DEFAULT_TASK) - - override val foregroundTask: Flow<RunningTaskInfo> = _foregroundTask.asStateFlow() - - private val runningTasks = mutableListOf(DEFAULT_TASK) - - override suspend fun findRunningTaskFromWindowContainerToken( - windowContainerToken: IBinder - ): RunningTaskInfo? = runningTasks.firstOrNull { it.token.asBinder() == windowContainerToken } - - fun addRunningTask(task: RunningTaskInfo) { - runningTasks.add(task) - } - - fun moveTaskToForeground(task: RunningTaskInfo) { - _foregroundTask.value = task - } - - companion object { - val DEFAULT_TASK = createTask(taskId = -1) - val LAUNCHER_INTENT: Intent = Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_HOME) - - fun createTask( - taskId: Int, - token: WindowContainerToken = createToken(), - baseIntent: Intent = Intent() - ) = - RunningTaskInfo().apply { - this.taskId = taskId - this.token = token - this.baseIntent = baseIntent - } - - fun createToken(): WindowContainerToken { - val realToken = object : IWindowContainerToken.Stub() {} - return WindowContainerToken(realToken) - } - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/MediaProjectionManagerRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/MediaProjectionManagerRepositoryTest.kt index 2b074655bb02..3a74c7255cbb 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/MediaProjectionManagerRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/MediaProjectionManagerRepositoryTest.kt @@ -16,27 +16,22 @@ package com.android.systemui.mediaprojection.taskswitcher.data.repository -import android.media.projection.MediaProjectionInfo -import android.media.projection.MediaProjectionManager import android.os.Binder import android.os.Handler -import android.os.UserHandle import android.testing.AndroidTestingRunner import android.view.ContentRecordingSession import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.mediaprojection.taskswitcher.data.model.MediaProjectionState -import com.android.systemui.util.mockito.any -import com.android.systemui.util.mockito.mock -import com.android.systemui.util.mockito.whenever +import com.android.systemui.mediaprojection.taskswitcher.data.repository.FakeActivityTaskManager.Companion.createTask +import com.android.systemui.mediaprojection.taskswitcher.data.repository.FakeActivityTaskManager.Companion.createToken import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest -import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -45,29 +40,26 @@ import org.junit.runner.RunWith @SmallTest class MediaProjectionManagerRepositoryTest : SysuiTestCase() { - private val mediaProjectionManager = mock<MediaProjectionManager>() - private val dispatcher = StandardTestDispatcher() private val testScope = TestScope(dispatcher) - private val tasksRepo = FakeTasksRepository() - private lateinit var callback: MediaProjectionManager.Callback - private lateinit var repo: MediaProjectionManagerRepository + private val fakeMediaProjectionManager = FakeMediaProjectionManager() + private val fakeActivityTaskManager = FakeActivityTaskManager() - @Before - fun setUp() { - whenever(mediaProjectionManager.addCallback(any(), any())).thenAnswer { - callback = it.arguments[0] as MediaProjectionManager.Callback - return@thenAnswer Unit - } - repo = - MediaProjectionManagerRepository( - mediaProjectionManager = mediaProjectionManager, - handler = Handler.getMain(), - applicationScope = testScope.backgroundScope, - tasksRepository = tasksRepo - ) - } + private val tasksRepo = + ActivityTaskManagerTasksRepository( + activityTaskManager = fakeActivityTaskManager.activityTaskManager, + applicationScope = testScope.backgroundScope, + backgroundDispatcher = dispatcher + ) + + private val repo = + MediaProjectionManagerRepository( + mediaProjectionManager = fakeMediaProjectionManager.mediaProjectionManager, + handler = Handler.getMain(), + applicationScope = testScope.backgroundScope, + tasksRepository = tasksRepo + ) @Test fun mediaProjectionState_onStart_emitsNotProjecting() = @@ -75,7 +67,7 @@ class MediaProjectionManagerRepositoryTest : SysuiTestCase() { val state by collectLastValue(repo.mediaProjectionState) runCurrent() - callback.onStart(TEST_MEDIA_INFO) + fakeMediaProjectionManager.dispatchOnStart() assertThat(state).isEqualTo(MediaProjectionState.NotProjecting) } @@ -86,7 +78,7 @@ class MediaProjectionManagerRepositoryTest : SysuiTestCase() { val state by collectLastValue(repo.mediaProjectionState) runCurrent() - callback.onStop(TEST_MEDIA_INFO) + fakeMediaProjectionManager.dispatchOnStop() assertThat(state).isEqualTo(MediaProjectionState.NotProjecting) } @@ -97,7 +89,7 @@ class MediaProjectionManagerRepositoryTest : SysuiTestCase() { val state by collectLastValue(repo.mediaProjectionState) runCurrent() - callback.onRecordingSessionSet(TEST_MEDIA_INFO, /* session= */ null) + fakeMediaProjectionManager.dispatchOnSessionSet(session = null) assertThat(state).isEqualTo(MediaProjectionState.NotProjecting) } @@ -108,8 +100,9 @@ class MediaProjectionManagerRepositoryTest : SysuiTestCase() { val state by collectLastValue(repo.mediaProjectionState) runCurrent() - val session = ContentRecordingSession.createDisplaySession(/* displayToMirror= */ 123) - callback.onRecordingSessionSet(TEST_MEDIA_INFO, session) + fakeMediaProjectionManager.dispatchOnSessionSet( + session = ContentRecordingSession.createDisplaySession(/* displayToMirror= */ 123) + ) assertThat(state).isEqualTo(MediaProjectionState.EntireScreen) } @@ -120,9 +113,10 @@ class MediaProjectionManagerRepositoryTest : SysuiTestCase() { val state by collectLastValue(repo.mediaProjectionState) runCurrent() - val session = - ContentRecordingSession.createTaskSession(/* taskWindowContainerToken= */ null) - callback.onRecordingSessionSet(TEST_MEDIA_INFO, session) + fakeMediaProjectionManager.dispatchOnSessionSet( + session = + ContentRecordingSession.createTaskSession(/* taskWindowContainerToken= */ null) + ) assertThat(state).isEqualTo(MediaProjectionState.EntireScreen) } @@ -134,8 +128,9 @@ class MediaProjectionManagerRepositoryTest : SysuiTestCase() { runCurrent() val taskWindowContainerToken = Binder() - val session = ContentRecordingSession.createTaskSession(taskWindowContainerToken) - callback.onRecordingSessionSet(TEST_MEDIA_INFO, session) + fakeMediaProjectionManager.dispatchOnSessionSet( + session = ContentRecordingSession.createTaskSession(taskWindowContainerToken) + ) assertThat(state).isEqualTo(MediaProjectionState.EntireScreen) } @@ -143,20 +138,16 @@ class MediaProjectionManagerRepositoryTest : SysuiTestCase() { @Test fun mediaProjectionState_sessionSet_taskWithToken_matchingRunningTask_emitsSingleTask() = testScope.runTest { - val token = FakeTasksRepository.createToken() - val task = FakeTasksRepository.createTask(taskId = 1, token = token) - tasksRepo.addRunningTask(task) + val token = createToken() + val task = createTask(taskId = 1, token = token) + fakeActivityTaskManager.addRunningTasks(task) val state by collectLastValue(repo.mediaProjectionState) runCurrent() - val session = ContentRecordingSession.createTaskSession(token.asBinder()) - callback.onRecordingSessionSet(TEST_MEDIA_INFO, session) + fakeMediaProjectionManager.dispatchOnSessionSet( + session = ContentRecordingSession.createTaskSession(token.asBinder()) + ) assertThat(state).isEqualTo(MediaProjectionState.SingleTask(task)) } - - companion object { - val TEST_MEDIA_INFO = - MediaProjectionInfo(/* packageName= */ "com.test.package", UserHandle.CURRENT) - } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/domain/interactor/TaskSwitchInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/domain/interactor/TaskSwitchInteractorTest.kt index 112950b860e8..b2ebe1bcbc8b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/domain/interactor/TaskSwitchInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/domain/interactor/TaskSwitchInteractorTest.kt @@ -17,6 +17,7 @@ package com.android.systemui.mediaprojection.taskswitcher.domain.interactor import android.content.Intent +import android.os.Handler import android.testing.AndroidTestingRunner import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase @@ -24,7 +25,9 @@ import com.android.systemui.coroutines.collectLastValue 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.FakeActivityTaskManager.Companion.createTask -import com.android.systemui.mediaprojection.taskswitcher.data.repository.FakeMediaProjectionRepository +import com.android.systemui.mediaprojection.taskswitcher.data.repository.FakeMediaProjectionManager +import com.android.systemui.mediaprojection.taskswitcher.data.repository.FakeMediaProjectionManager.Companion.createSingleTaskSession +import com.android.systemui.mediaprojection.taskswitcher.data.repository.MediaProjectionManagerRepository import com.android.systemui.mediaprojection.taskswitcher.domain.model.TaskSwitchState import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -43,7 +46,8 @@ class TaskSwitchInteractorTest : SysuiTestCase() { private val testScope = TestScope(dispatcher) private val fakeActivityTaskManager = FakeActivityTaskManager() - private val mediaRepo = FakeMediaProjectionRepository() + private val fakeMediaProjectionManager = FakeMediaProjectionManager() + private val tasksRepo = ActivityTaskManagerTasksRepository( activityTaskManager = fakeActivityTaskManager.activityTaskManager, @@ -51,15 +55,26 @@ class TaskSwitchInteractorTest : SysuiTestCase() { backgroundDispatcher = dispatcher ) + private val mediaRepo = + MediaProjectionManagerRepository( + mediaProjectionManager = fakeMediaProjectionManager.mediaProjectionManager, + handler = Handler.getMain(), + applicationScope = testScope.backgroundScope, + tasksRepository = tasksRepo, + ) + private val interactor = TaskSwitchInteractor(mediaRepo, tasksRepo) @Test fun taskSwitchChanges_notProjecting_foregroundTaskChange_emitsNotProjectingTask() = testScope.runTest { - mediaRepo.stopProjecting() + val backgroundTask = createTask(taskId = 0) + val foregroundTask = createTask(taskId = 1) val taskSwitchState by collectLastValue(interactor.taskSwitchChanges) - fakeActivityTaskManager.moveTaskToForeground(createTask(taskId = 1)) + fakeActivityTaskManager.addRunningTasks(backgroundTask, foregroundTask) + fakeMediaProjectionManager.dispatchOnStop() + fakeActivityTaskManager.moveTaskToForeground(foregroundTask) assertThat(taskSwitchState).isEqualTo(TaskSwitchState.NotProjectingTask) } @@ -67,10 +82,15 @@ class TaskSwitchInteractorTest : SysuiTestCase() { @Test fun taskSwitchChanges_projectingScreen_foregroundTaskChange_emitsNotProjectingTask() = testScope.runTest { - mediaRepo.projectEntireScreen() + val backgroundTask = createTask(taskId = 0) + val foregroundTask = createTask(taskId = 1) val taskSwitchState by collectLastValue(interactor.taskSwitchChanges) - fakeActivityTaskManager.moveTaskToForeground(createTask(taskId = 1)) + fakeActivityTaskManager.addRunningTasks(backgroundTask, foregroundTask) + fakeMediaProjectionManager.dispatchOnSessionSet( + session = FakeMediaProjectionManager.createDisplaySession() + ) + fakeActivityTaskManager.moveTaskToForeground(foregroundTask) assertThat(taskSwitchState).isEqualTo(TaskSwitchState.NotProjectingTask) } @@ -80,9 +100,12 @@ class TaskSwitchInteractorTest : SysuiTestCase() { testScope.runTest { val projectedTask = createTask(taskId = 0) val foregroundTask = createTask(taskId = 1) - mediaRepo.switchProjectedTask(projectedTask) val taskSwitchState by collectLastValue(interactor.taskSwitchChanges) + fakeActivityTaskManager.addRunningTasks(projectedTask, foregroundTask) + fakeMediaProjectionManager.dispatchOnSessionSet( + session = createSingleTaskSession(token = projectedTask.token.asBinder()) + ) fakeActivityTaskManager.moveTaskToForeground(foregroundTask) assertThat(taskSwitchState) @@ -99,9 +122,12 @@ class TaskSwitchInteractorTest : SysuiTestCase() { testScope.runTest { val projectedTask = createTask(taskId = 0) val foregroundTask = createTask(taskId = 1, baseIntent = LAUNCHER_INTENT) - mediaRepo.switchProjectedTask(projectedTask) val taskSwitchState by collectLastValue(interactor.taskSwitchChanges) + fakeActivityTaskManager.addRunningTasks(projectedTask, foregroundTask) + fakeMediaProjectionManager.dispatchOnSessionSet( + session = createSingleTaskSession(projectedTask.token.asBinder()) + ) fakeActivityTaskManager.moveTaskToForeground(foregroundTask) assertThat(taskSwitchState).isEqualTo(TaskSwitchState.TaskUnchanged) @@ -111,11 +137,13 @@ class TaskSwitchInteractorTest : SysuiTestCase() { fun taskSwitchChanges_projectingTask_foregroundTaskSame_emitsTaskUnchanged() = testScope.runTest { val projectedTask = createTask(taskId = 0) - val foregroundTask = createTask(taskId = 0) - mediaRepo.switchProjectedTask(projectedTask) val taskSwitchState by collectLastValue(interactor.taskSwitchChanges) - fakeActivityTaskManager.moveTaskToForeground(foregroundTask) + fakeActivityTaskManager.addRunningTasks(projectedTask) + fakeMediaProjectionManager.dispatchOnSessionSet( + session = createSingleTaskSession(projectedTask.token.asBinder()) + ) + fakeActivityTaskManager.moveTaskToForeground(projectedTask) assertThat(taskSwitchState).isEqualTo(TaskSwitchState.TaskUnchanged) } 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 index cfbbf768e1fe..b396cafb28d5 100644 --- 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 @@ -18,13 +18,15 @@ package com.android.systemui.mediaprojection.taskswitcher.ui import android.app.Notification import android.app.NotificationManager +import android.os.Handler 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.data.repository.FakeMediaProjectionManager +import com.android.systemui.mediaprojection.taskswitcher.data.repository.MediaProjectionManagerRepository 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 @@ -51,14 +53,25 @@ class TaskSwitcherNotificationCoordinatorTest : SysuiTestCase() { private val dispatcher = UnconfinedTestDispatcher() private val testScope = TestScope(dispatcher) + private val fakeActivityTaskManager = FakeActivityTaskManager() - private val mediaRepo = FakeMediaProjectionRepository() + private val fakeMediaProjectionManager = FakeMediaProjectionManager() + private val tasksRepo = ActivityTaskManagerTasksRepository( activityTaskManager = fakeActivityTaskManager.activityTaskManager, applicationScope = testScope.backgroundScope, backgroundDispatcher = dispatcher ) + + private val mediaRepo = + MediaProjectionManagerRepository( + mediaProjectionManager = fakeMediaProjectionManager.mediaProjectionManager, + handler = Handler.getMain(), + applicationScope = testScope.backgroundScope, + tasksRepository = tasksRepo, + ) + private val interactor = TaskSwitchInteractor(mediaRepo, tasksRepo) private val viewModel = TaskSwitcherNotificationViewModel(interactor) @@ -90,7 +103,7 @@ class TaskSwitcherNotificationCoordinatorTest : SysuiTestCase() { @Test fun hideNotification() { testScope.runTest { - mediaRepo.stopProjecting() + fakeMediaProjectionManager.dispatchOnStop() verify(notificationManager).cancel(any()) } @@ -99,7 +112,7 @@ class TaskSwitcherNotificationCoordinatorTest : SysuiTestCase() { @Test fun notificationIdIsConsistent() { testScope.runTest { - mediaRepo.stopProjecting() + fakeMediaProjectionManager.dispatchOnStop() val idCancel = argumentCaptor<Int>() verify(notificationManager).cancel(idCancel.capture()) @@ -114,7 +127,11 @@ class TaskSwitcherNotificationCoordinatorTest : SysuiTestCase() { private fun switchTask() { val projectedTask = FakeActivityTaskManager.createTask(taskId = 1) val foregroundTask = FakeActivityTaskManager.createTask(taskId = 2) - mediaRepo.switchProjectedTask(projectedTask) + fakeActivityTaskManager.addRunningTasks(projectedTask, foregroundTask) + fakeMediaProjectionManager.dispatchOnSessionSet( + session = + FakeMediaProjectionManager.createSingleTaskSession(projectedTask.token.asBinder()) + ) fakeActivityTaskManager.moveTaskToForeground(foregroundTask) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/ui/viewmodel/TaskSwitcherNotificationViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/ui/viewmodel/TaskSwitcherNotificationViewModelTest.kt index ea44fb3b1f6e..7d38de4bc2d3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/ui/viewmodel/TaskSwitcherNotificationViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/ui/viewmodel/TaskSwitcherNotificationViewModelTest.kt @@ -17,6 +17,7 @@ package com.android.systemui.mediaprojection.taskswitcher.ui.viewmodel import android.content.Intent +import android.os.Handler import android.testing.AndroidTestingRunner import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase @@ -24,7 +25,10 @@ import com.android.systemui.coroutines.collectLastValue 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.FakeActivityTaskManager.Companion.createTask -import com.android.systemui.mediaprojection.taskswitcher.data.repository.FakeMediaProjectionRepository +import com.android.systemui.mediaprojection.taskswitcher.data.repository.FakeMediaProjectionManager +import com.android.systemui.mediaprojection.taskswitcher.data.repository.FakeMediaProjectionManager.Companion.createDisplaySession +import com.android.systemui.mediaprojection.taskswitcher.data.repository.FakeMediaProjectionManager.Companion.createSingleTaskSession +import com.android.systemui.mediaprojection.taskswitcher.data.repository.MediaProjectionManagerRepository import com.android.systemui.mediaprojection.taskswitcher.domain.interactor.TaskSwitchInteractor import com.android.systemui.mediaprojection.taskswitcher.ui.model.TaskSwitcherNotificationUiState import com.google.common.truth.Truth.assertThat @@ -44,13 +48,23 @@ class TaskSwitcherNotificationViewModelTest : SysuiTestCase() { private val testScope = TestScope(dispatcher) private val fakeActivityTaskManager = FakeActivityTaskManager() - private val mediaRepo = FakeMediaProjectionRepository() + private val fakeMediaProjectionManager = FakeMediaProjectionManager() + private val tasksRepo = ActivityTaskManagerTasksRepository( activityTaskManager = fakeActivityTaskManager.activityTaskManager, applicationScope = testScope.backgroundScope, backgroundDispatcher = dispatcher ) + + private val mediaRepo = + MediaProjectionManagerRepository( + mediaProjectionManager = fakeMediaProjectionManager.mediaProjectionManager, + handler = Handler.getMain(), + applicationScope = testScope.backgroundScope, + tasksRepository = tasksRepo, + ) + private val interactor = TaskSwitchInteractor(mediaRepo, tasksRepo) private val viewModel = TaskSwitcherNotificationViewModel(interactor) @@ -58,19 +72,23 @@ class TaskSwitcherNotificationViewModelTest : SysuiTestCase() { @Test fun uiState_notProjecting_emitsNotShowing() = testScope.runTest { - mediaRepo.stopProjecting() val uiState by collectLastValue(viewModel.uiState) + fakeMediaProjectionManager.dispatchOnStop() + assertThat(uiState).isEqualTo(TaskSwitcherNotificationUiState.NotShowing) } @Test fun uiState_notProjecting_foregroundTaskChanged_emitsNotShowing() = testScope.runTest { - mediaRepo.stopProjecting() + val backgroundTask = createTask(taskId = 0) + val foregroundTask = createTask(taskId = 1) val uiState by collectLastValue(viewModel.uiState) - mediaRepo.switchProjectedTask(createTask(taskId = 1)) + fakeActivityTaskManager.addRunningTasks(backgroundTask, foregroundTask) + fakeMediaProjectionManager.dispatchOnStop() + fakeActivityTaskManager.moveTaskToForeground(foregroundTask) assertThat(uiState).isEqualTo(TaskSwitcherNotificationUiState.NotShowing) } @@ -78,19 +96,23 @@ class TaskSwitcherNotificationViewModelTest : SysuiTestCase() { @Test fun uiState_projectingEntireScreen_emitsNotShowing() = testScope.runTest { - mediaRepo.projectEntireScreen() val uiState by collectLastValue(viewModel.uiState) + fakeMediaProjectionManager.dispatchOnSessionSet(session = createDisplaySession()) + assertThat(uiState).isEqualTo(TaskSwitcherNotificationUiState.NotShowing) } @Test fun uiState_projectingEntireScreen_foregroundTaskChanged_emitsNotShowing() = testScope.runTest { - mediaRepo.projectEntireScreen() + val backgroundTask = createTask(taskId = 0) + val foregroundTask = createTask(taskId = 1) val uiState by collectLastValue(viewModel.uiState) - mediaRepo.switchProjectedTask(createTask(taskId = 1)) + fakeActivityTaskManager.addRunningTasks(backgroundTask, foregroundTask) + fakeMediaProjectionManager.dispatchOnSessionSet(session = createDisplaySession()) + fakeActivityTaskManager.moveTaskToForeground(foregroundTask) assertThat(uiState).isEqualTo(TaskSwitcherNotificationUiState.NotShowing) } @@ -100,9 +122,12 @@ class TaskSwitcherNotificationViewModelTest : SysuiTestCase() { testScope.runTest { val projectedTask = createTask(taskId = 1) val foregroundTask = createTask(taskId = 2) - mediaRepo.switchProjectedTask(projectedTask) val uiState by collectLastValue(viewModel.uiState) + fakeActivityTaskManager.addRunningTasks(projectedTask, foregroundTask) + fakeMediaProjectionManager.dispatchOnSessionSet( + session = createSingleTaskSession(projectedTask.token.asBinder()) + ) fakeActivityTaskManager.moveTaskToForeground(foregroundTask) assertThat(uiState) @@ -113,9 +138,12 @@ class TaskSwitcherNotificationViewModelTest : SysuiTestCase() { fun uiState_projectingTask_foregroundTaskChanged_same_emitsNotShowing() = testScope.runTest { val projectedTask = createTask(taskId = 1) - mediaRepo.switchProjectedTask(projectedTask) val uiState by collectLastValue(viewModel.uiState) + fakeActivityTaskManager.addRunningTasks(projectedTask) + fakeMediaProjectionManager.dispatchOnSessionSet( + session = createSingleTaskSession(projectedTask.token.asBinder()) + ) fakeActivityTaskManager.moveTaskToForeground(projectedTask) assertThat(uiState).isEqualTo(TaskSwitcherNotificationUiState.NotShowing) @@ -126,9 +154,12 @@ class TaskSwitcherNotificationViewModelTest : SysuiTestCase() { testScope.runTest { val projectedTask = createTask(taskId = 1) val foregroundTask = createTask(taskId = 2, baseIntent = LAUNCHER_INTENT) - mediaRepo.switchProjectedTask(projectedTask) val uiState by collectLastValue(viewModel.uiState) + fakeActivityTaskManager.addRunningTasks(projectedTask, foregroundTask) + fakeMediaProjectionManager.dispatchOnSessionSet( + session = createSingleTaskSession(projectedTask.token.asBinder()) + ) fakeActivityTaskManager.moveTaskToForeground(foregroundTask) assertThat(uiState).isEqualTo(TaskSwitcherNotificationUiState.NotShowing) diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationQSContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerLegacyTest.kt index 168cbb7b8da3..2bc112d68ae2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationQSContainerControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerLegacyTest.kt @@ -1,7 +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.shade import android.testing.AndroidTestingRunner import android.testing.TestableLooper +import android.testing.TestableResources import android.view.View import android.view.ViewGroup import android.view.WindowInsets @@ -12,6 +29,8 @@ import androidx.constraintlayout.widget.ConstraintSet import androidx.test.filters.SmallTest import com.android.systemui.R import com.android.systemui.SysuiTestCase +import com.android.systemui.flags.FakeFeatureFlags +import com.android.systemui.flags.Flags import com.android.systemui.fragments.FragmentHostManager import com.android.systemui.fragments.FragmentService import com.android.systemui.navigationbar.NavigationModeController @@ -19,7 +38,10 @@ import com.android.systemui.navigationbar.NavigationModeController.ModeChangedLi import com.android.systemui.plugins.qs.QS import com.android.systemui.recents.OverviewProxyService import com.android.systemui.recents.OverviewProxyService.OverviewProxyListener +import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController import com.android.systemui.util.concurrency.FakeExecutor +import com.android.systemui.util.mockito.capture +import com.android.systemui.util.mockito.whenever import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat import java.util.function.Consumer @@ -40,77 +62,63 @@ import org.mockito.Mockito.never import org.mockito.Mockito.reset import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations -import org.mockito.Mockito.`when` as whenever -@SmallTest +/** Uses Flags.MIGRATE_NSSL set to false. If all goes well, this set of tests will be deleted. */ @RunWith(AndroidTestingRunner::class) @TestableLooper.RunWithLooper -class NotificationQSContainerControllerTest : SysuiTestCase() { +@SmallTest +class NotificationsQSContainerControllerLegacyTest : SysuiTestCase() { + + @Mock lateinit var view: NotificationsQuickSettingsContainer + @Mock lateinit var navigationModeController: NavigationModeController + @Mock lateinit var overviewProxyService: OverviewProxyService + @Mock lateinit var shadeHeaderController: ShadeHeaderController + @Mock lateinit var shadeExpansionStateManager: ShadeExpansionStateManager + @Mock lateinit var fragmentService: FragmentService + @Mock lateinit var fragmentHostManager: FragmentHostManager + @Mock + lateinit var notificationStackScrollLayoutController: NotificationStackScrollLayoutController - companion object { - const val STABLE_INSET_BOTTOM = 100 - const val CUTOUT_HEIGHT = 50 - const val GESTURES_NAVIGATION = WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL - const val BUTTONS_NAVIGATION = WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON - const val NOTIFICATIONS_MARGIN = 50 - const val SCRIM_MARGIN = 10 - const val FOOTER_ACTIONS_INSET = 2 - const val FOOTER_ACTIONS_PADDING = 2 - const val FOOTER_ACTIONS_OFFSET = FOOTER_ACTIONS_INSET + FOOTER_ACTIONS_PADDING - const val QS_PADDING_OFFSET = SCRIM_MARGIN + FOOTER_ACTIONS_OFFSET - } + @Captor lateinit var navigationModeCaptor: ArgumentCaptor<ModeChangedListener> + @Captor lateinit var taskbarVisibilityCaptor: ArgumentCaptor<OverviewProxyListener> + @Captor lateinit var windowInsetsCallbackCaptor: ArgumentCaptor<Consumer<WindowInsets>> + @Captor lateinit var constraintSetCaptor: ArgumentCaptor<ConstraintSet> + @Captor lateinit var attachStateListenerCaptor: ArgumentCaptor<View.OnAttachStateChangeListener> - @Mock - private lateinit var navigationModeController: NavigationModeController - @Mock - private lateinit var overviewProxyService: OverviewProxyService - @Mock - private lateinit var notificationsQSContainer: NotificationsQuickSettingsContainer - @Mock - private lateinit var mShadeHeaderController: ShadeHeaderController - @Mock - private lateinit var shadeExpansionStateManager: ShadeExpansionStateManager - @Mock - private lateinit var fragmentService: FragmentService - @Mock - private lateinit var fragmentHostManager: FragmentHostManager - @Captor - lateinit var navigationModeCaptor: ArgumentCaptor<ModeChangedListener> - @Captor - lateinit var taskbarVisibilityCaptor: ArgumentCaptor<OverviewProxyListener> - @Captor - lateinit var windowInsetsCallbackCaptor: ArgumentCaptor<Consumer<WindowInsets>> - @Captor - lateinit var constraintSetCaptor: ArgumentCaptor<ConstraintSet> - @Captor - lateinit var attachStateListenerCaptor: ArgumentCaptor<View.OnAttachStateChangeListener> - - private lateinit var controller: NotificationsQSContainerController + lateinit var underTest: NotificationsQSContainerController + + private lateinit var fakeResources: TestableResources + private lateinit var featureFlags: FakeFeatureFlags private lateinit var navigationModeCallback: ModeChangedListener private lateinit var taskbarVisibilityCallback: OverviewProxyListener private lateinit var windowInsetsCallback: Consumer<WindowInsets> - private lateinit var delayableExecutor: FakeExecutor private lateinit var fakeSystemClock: FakeSystemClock + private lateinit var delayableExecutor: FakeExecutor @Before fun setup() { MockitoAnnotations.initMocks(this) - mContext.ensureTestableResources() - whenever(notificationsQSContainer.context).thenReturn(mContext) - whenever(notificationsQSContainer.resources).thenReturn(mContext.resources) - whenever(fragmentService.getFragmentHostManager(any())).thenReturn(fragmentHostManager) fakeSystemClock = FakeSystemClock() delayableExecutor = FakeExecutor(fakeSystemClock) + featureFlags = FakeFeatureFlags().apply { set(Flags.MIGRATE_NSSL, false) } + mContext.ensureTestableResources() + whenever(view.context).thenReturn(mContext) + whenever(view.resources).thenReturn(mContext.resources) + + whenever(fragmentService.getFragmentHostManager(any())).thenReturn(fragmentHostManager) - controller = NotificationsQSContainerController( - notificationsQSContainer, + underTest = + NotificationsQSContainerController( + view, navigationModeController, overviewProxyService, - mShadeHeaderController, + shadeHeaderController, shadeExpansionStateManager, fragmentService, - delayableExecutor - ) + delayableExecutor, + featureFlags, + notificationStackScrollLayoutController, + ) overrideResource(R.dimen.split_shade_notifications_scrim_margin_bottom, SCRIM_MARGIN) overrideResource(R.dimen.notification_panel_margin_bottom, NOTIFICATIONS_MARGIN) @@ -118,38 +126,72 @@ class NotificationQSContainerControllerTest : SysuiTestCase() { overrideResource(R.dimen.qs_footer_actions_bottom_padding, FOOTER_ACTIONS_PADDING) overrideResource(R.dimen.qs_footer_action_inset, FOOTER_ACTIONS_INSET) whenever(navigationModeController.addListener(navigationModeCaptor.capture())) - .thenReturn(GESTURES_NAVIGATION) + .thenReturn(GESTURES_NAVIGATION) doNothing().`when`(overviewProxyService).addCallback(taskbarVisibilityCaptor.capture()) - doNothing().`when`(notificationsQSContainer) - .setInsetsChangedListener(windowInsetsCallbackCaptor.capture()) - doNothing().`when`(notificationsQSContainer).applyConstraints(constraintSetCaptor.capture()) - doNothing().`when`(notificationsQSContainer) - .addOnAttachStateChangeListener(attachStateListenerCaptor.capture()) - controller.init() - attachStateListenerCaptor.value.onViewAttachedToWindow(notificationsQSContainer) + doNothing().`when`(view).setInsetsChangedListener(windowInsetsCallbackCaptor.capture()) + doNothing().`when`(view).applyConstraints(constraintSetCaptor.capture()) + doNothing().`when`(view).addOnAttachStateChangeListener(attachStateListenerCaptor.capture()) + underTest.init() + attachStateListenerCaptor.value.onViewAttachedToWindow(view) navigationModeCallback = navigationModeCaptor.value taskbarVisibilityCallback = taskbarVisibilityCaptor.value windowInsetsCallback = windowInsetsCallbackCaptor.value + + Mockito.clearInvocations(view) + } + + @Test + fun testSmallScreen_updateResources_splitShadeHeightIsSet() { + overrideResource(R.bool.config_use_large_screen_shade_header, false) + overrideResource(R.dimen.qs_header_height, 1) + overrideResource(R.dimen.large_screen_shade_header_height, 2) + + underTest.updateResources() + + val captor = ArgumentCaptor.forClass(ConstraintSet::class.java) + verify(view).applyConstraints(capture(captor)) + assertThat(captor.value.getHeight(R.id.split_shade_status_bar)).isEqualTo(1) + } + + @Test + fun testLargeScreen_updateResources_splitShadeHeightIsSet() { + overrideResource(R.bool.config_use_large_screen_shade_header, true) + overrideResource(R.dimen.qs_header_height, 1) + overrideResource(R.dimen.large_screen_shade_header_height, 2) + + underTest.updateResources() + + val captor = ArgumentCaptor.forClass(ConstraintSet::class.java) + verify(view).applyConstraints(capture(captor)) + assertThat(captor.value.getHeight(R.id.split_shade_status_bar)).isEqualTo(2) } @Test fun testTaskbarVisibleInSplitShade() { enableSplitShade() - given(taskbarVisible = true, - navigationMode = GESTURES_NAVIGATION, - insets = windowInsets().withStableBottom()) - then(expectedContainerPadding = 0, // taskbar should disappear when shade is expanded - expectedNotificationsMargin = NOTIFICATIONS_MARGIN, - expectedQsPadding = NOTIFICATIONS_MARGIN - QS_PADDING_OFFSET) + given( + taskbarVisible = true, + navigationMode = GESTURES_NAVIGATION, + insets = windowInsets().withStableBottom() + ) + then( + expectedContainerPadding = 0, // taskbar should disappear when shade is expanded + expectedNotificationsMargin = NOTIFICATIONS_MARGIN, + expectedQsPadding = NOTIFICATIONS_MARGIN - QS_PADDING_OFFSET + ) - given(taskbarVisible = true, - navigationMode = BUTTONS_NAVIGATION, - insets = windowInsets().withStableBottom()) - then(expectedContainerPadding = STABLE_INSET_BOTTOM, - expectedNotificationsMargin = NOTIFICATIONS_MARGIN, - expectedQsPadding = NOTIFICATIONS_MARGIN - QS_PADDING_OFFSET) + given( + taskbarVisible = true, + navigationMode = BUTTONS_NAVIGATION, + insets = windowInsets().withStableBottom() + ) + then( + expectedContainerPadding = STABLE_INSET_BOTTOM, + expectedNotificationsMargin = NOTIFICATIONS_MARGIN, + expectedQsPadding = NOTIFICATIONS_MARGIN - QS_PADDING_OFFSET + ) } @Test @@ -157,161 +199,185 @@ class NotificationQSContainerControllerTest : SysuiTestCase() { // when taskbar is not visible, it means we're on the home screen enableSplitShade() - given(taskbarVisible = false, - navigationMode = GESTURES_NAVIGATION, - insets = windowInsets().withStableBottom()) - then(expectedContainerPadding = 0, - expectedQsPadding = NOTIFICATIONS_MARGIN - QS_PADDING_OFFSET) + given( + taskbarVisible = false, + navigationMode = GESTURES_NAVIGATION, + insets = windowInsets().withStableBottom() + ) + then( + expectedContainerPadding = 0, + expectedQsPadding = NOTIFICATIONS_MARGIN - QS_PADDING_OFFSET + ) - given(taskbarVisible = false, - navigationMode = BUTTONS_NAVIGATION, - insets = windowInsets().withStableBottom()) - then(expectedContainerPadding = 0, // qs goes full height as it's not obscuring nav buttons - expectedNotificationsMargin = STABLE_INSET_BOTTOM + NOTIFICATIONS_MARGIN, - expectedQsPadding = STABLE_INSET_BOTTOM + NOTIFICATIONS_MARGIN - QS_PADDING_OFFSET) + given( + taskbarVisible = false, + navigationMode = BUTTONS_NAVIGATION, + insets = windowInsets().withStableBottom() + ) + then( + expectedContainerPadding = 0, // qs goes full height as it's not obscuring nav buttons + expectedNotificationsMargin = STABLE_INSET_BOTTOM + NOTIFICATIONS_MARGIN, + expectedQsPadding = STABLE_INSET_BOTTOM + NOTIFICATIONS_MARGIN - QS_PADDING_OFFSET + ) } @Test fun testTaskbarNotVisibleInSplitShadeWithCutout() { enableSplitShade() - given(taskbarVisible = false, - navigationMode = GESTURES_NAVIGATION, - insets = windowInsets().withCutout()) - then(expectedContainerPadding = CUTOUT_HEIGHT, - expectedQsPadding = NOTIFICATIONS_MARGIN - QS_PADDING_OFFSET) + given( + taskbarVisible = false, + navigationMode = GESTURES_NAVIGATION, + insets = windowInsets().withCutout() + ) + then( + expectedContainerPadding = CUTOUT_HEIGHT, + expectedQsPadding = NOTIFICATIONS_MARGIN - QS_PADDING_OFFSET + ) - given(taskbarVisible = false, - navigationMode = BUTTONS_NAVIGATION, - insets = windowInsets().withCutout().withStableBottom()) - then(expectedContainerPadding = 0, - expectedNotificationsMargin = STABLE_INSET_BOTTOM + NOTIFICATIONS_MARGIN, - expectedQsPadding = STABLE_INSET_BOTTOM + NOTIFICATIONS_MARGIN - QS_PADDING_OFFSET) + given( + taskbarVisible = false, + navigationMode = BUTTONS_NAVIGATION, + insets = windowInsets().withCutout().withStableBottom() + ) + then( + expectedContainerPadding = 0, + expectedNotificationsMargin = STABLE_INSET_BOTTOM + NOTIFICATIONS_MARGIN, + expectedQsPadding = STABLE_INSET_BOTTOM + NOTIFICATIONS_MARGIN - QS_PADDING_OFFSET + ) } @Test fun testTaskbarVisibleInSinglePaneShade() { disableSplitShade() - given(taskbarVisible = true, - navigationMode = GESTURES_NAVIGATION, - insets = windowInsets().withStableBottom()) - then(expectedContainerPadding = 0, - expectedQsPadding = STABLE_INSET_BOTTOM) + given( + taskbarVisible = true, + navigationMode = GESTURES_NAVIGATION, + insets = windowInsets().withStableBottom() + ) + then(expectedContainerPadding = 0, expectedQsPadding = STABLE_INSET_BOTTOM) - given(taskbarVisible = true, - navigationMode = BUTTONS_NAVIGATION, - insets = windowInsets().withStableBottom()) - then(expectedContainerPadding = STABLE_INSET_BOTTOM, - expectedQsPadding = STABLE_INSET_BOTTOM) + given( + taskbarVisible = true, + navigationMode = BUTTONS_NAVIGATION, + insets = windowInsets().withStableBottom() + ) + then( + expectedContainerPadding = STABLE_INSET_BOTTOM, + expectedQsPadding = STABLE_INSET_BOTTOM + ) } @Test fun testTaskbarNotVisibleInSinglePaneShade() { disableSplitShade() - given(taskbarVisible = false, - navigationMode = GESTURES_NAVIGATION, - insets = emptyInsets()) + given(taskbarVisible = false, navigationMode = GESTURES_NAVIGATION, insets = emptyInsets()) then(expectedContainerPadding = 0) - given(taskbarVisible = false, - navigationMode = GESTURES_NAVIGATION, - insets = windowInsets().withCutout().withStableBottom()) + given( + taskbarVisible = false, + navigationMode = GESTURES_NAVIGATION, + insets = windowInsets().withCutout().withStableBottom() + ) then(expectedContainerPadding = CUTOUT_HEIGHT, expectedQsPadding = STABLE_INSET_BOTTOM) - given(taskbarVisible = false, - navigationMode = BUTTONS_NAVIGATION, - insets = windowInsets().withStableBottom()) - then(expectedContainerPadding = 0, - expectedNotificationsMargin = STABLE_INSET_BOTTOM + NOTIFICATIONS_MARGIN, - expectedQsPadding = STABLE_INSET_BOTTOM) + given( + taskbarVisible = false, + navigationMode = BUTTONS_NAVIGATION, + insets = windowInsets().withStableBottom() + ) + then( + expectedContainerPadding = 0, + expectedNotificationsMargin = STABLE_INSET_BOTTOM + NOTIFICATIONS_MARGIN, + expectedQsPadding = STABLE_INSET_BOTTOM + ) } @Test fun testDetailShowingInSinglePaneShade() { disableSplitShade() - controller.setDetailShowing(true) + underTest.setDetailShowing(true) // always sets spacings to 0 - given(taskbarVisible = false, - navigationMode = GESTURES_NAVIGATION, - insets = windowInsets().withStableBottom()) - then(expectedContainerPadding = 0, - expectedNotificationsMargin = 0) + given( + taskbarVisible = false, + navigationMode = GESTURES_NAVIGATION, + insets = windowInsets().withStableBottom() + ) + then(expectedContainerPadding = 0, expectedNotificationsMargin = 0) - given(taskbarVisible = false, - navigationMode = BUTTONS_NAVIGATION, - insets = emptyInsets()) - then(expectedContainerPadding = 0, - expectedNotificationsMargin = 0) + given(taskbarVisible = false, navigationMode = BUTTONS_NAVIGATION, insets = emptyInsets()) + then(expectedContainerPadding = 0, expectedNotificationsMargin = 0) } @Test fun testDetailShowingInSplitShade() { enableSplitShade() - controller.setDetailShowing(true) + underTest.setDetailShowing(true) - given(taskbarVisible = false, - navigationMode = GESTURES_NAVIGATION, - insets = windowInsets().withStableBottom()) + given( + taskbarVisible = false, + navigationMode = GESTURES_NAVIGATION, + insets = windowInsets().withStableBottom() + ) then(expectedContainerPadding = 0) // should not influence spacing - given(taskbarVisible = false, - navigationMode = BUTTONS_NAVIGATION, - insets = emptyInsets()) + given(taskbarVisible = false, navigationMode = BUTTONS_NAVIGATION, insets = emptyInsets()) then(expectedContainerPadding = 0) } @Test fun testNotificationsMarginBottomIsUpdated() { - Mockito.clearInvocations(notificationsQSContainer) + Mockito.clearInvocations(view) enableSplitShade() - verify(notificationsQSContainer).setNotificationsMarginBottom(NOTIFICATIONS_MARGIN) + verify(view).setNotificationsMarginBottom(NOTIFICATIONS_MARGIN) overrideResource(R.dimen.notification_panel_margin_bottom, 100) disableSplitShade() - verify(notificationsQSContainer).setNotificationsMarginBottom(100) + verify(view).setNotificationsMarginBottom(100) } @Test fun testSplitShadeLayout_isAlignedToGuideline() { enableSplitShade() - controller.updateResources() - assertThat(getConstraintSetLayout(R.id.qs_frame).endToEnd) - .isEqualTo(R.id.qs_edge_guideline) + underTest.updateResources() + assertThat(getConstraintSetLayout(R.id.qs_frame).endToEnd).isEqualTo(R.id.qs_edge_guideline) assertThat(getConstraintSetLayout(R.id.notification_stack_scroller).startToStart) - .isEqualTo(R.id.qs_edge_guideline) + .isEqualTo(R.id.qs_edge_guideline) } @Test fun testSinglePaneLayout_childrenHaveEqualMargins() { disableSplitShade() - controller.updateResources() + underTest.updateResources() val qsStartMargin = getConstraintSetLayout(R.id.qs_frame).startMargin val qsEndMargin = getConstraintSetLayout(R.id.qs_frame).endMargin val notifStartMargin = getConstraintSetLayout(R.id.notification_stack_scroller).startMargin val notifEndMargin = getConstraintSetLayout(R.id.notification_stack_scroller).endMargin - assertThat(qsStartMargin == qsEndMargin && - notifStartMargin == notifEndMargin && - qsStartMargin == notifStartMargin - ).isTrue() + assertThat( + qsStartMargin == qsEndMargin && + notifStartMargin == notifEndMargin && + qsStartMargin == notifStartMargin + ) + .isTrue() } @Test fun testSplitShadeLayout_childrenHaveInsideMarginsOfZero() { enableSplitShade() - controller.updateResources() + underTest.updateResources() assertThat(getConstraintSetLayout(R.id.qs_frame).endMargin).isEqualTo(0) assertThat(getConstraintSetLayout(R.id.notification_stack_scroller).startMargin) - .isEqualTo(0) + .isEqualTo(0) } @Test fun testSplitShadeLayout_qsFrameHasHorizontalMarginsOfZero() { enableSplitShade() - controller.updateResources() + underTest.updateResources() assertThat(getConstraintSetLayout(R.id.qs_frame).endMargin).isEqualTo(0) assertThat(getConstraintSetLayout(R.id.qs_frame).startMargin).isEqualTo(0) } @@ -322,62 +388,64 @@ class NotificationQSContainerControllerTest : SysuiTestCase() { val largeScreenHeaderHeight = 100 overrideResource(R.dimen.large_screen_shade_header_height, largeScreenHeaderHeight) - controller.updateResources() + underTest.updateResources() assertThat(getConstraintSetLayout(R.id.qs_frame).topMargin) - .isEqualTo(largeScreenHeaderHeight) + .isEqualTo(largeScreenHeaderHeight) assertThat(getConstraintSetLayout(R.id.notification_stack_scroller).topMargin) - .isEqualTo(largeScreenHeaderHeight) + .isEqualTo(largeScreenHeaderHeight) } @Test fun testSmallScreenLayout_qsAndNotifsTopMarginIsZero() { setSmallScreen() - controller.updateResources() + underTest.updateResources() assertThat(getConstraintSetLayout(R.id.qs_frame).topMargin).isEqualTo(0) - assertThat(getConstraintSetLayout(R.id.notification_stack_scroller).topMargin) - .isEqualTo(0) + assertThat(getConstraintSetLayout(R.id.notification_stack_scroller).topMargin).isEqualTo(0) } @Test fun testSinglePaneShadeLayout_qsFrameHasHorizontalMarginsSetToCorrectValue() { disableSplitShade() - controller.updateResources() - val notificationPanelMarginHorizontal = context.resources - .getDimensionPixelSize(R.dimen.notification_panel_margin_horizontal) + underTest.updateResources() + val notificationPanelMarginHorizontal = + mContext.resources.getDimensionPixelSize(R.dimen.notification_panel_margin_horizontal) assertThat(getConstraintSetLayout(R.id.qs_frame).endMargin) - .isEqualTo(notificationPanelMarginHorizontal) + .isEqualTo(notificationPanelMarginHorizontal) assertThat(getConstraintSetLayout(R.id.qs_frame).startMargin) - .isEqualTo(notificationPanelMarginHorizontal) + .isEqualTo(notificationPanelMarginHorizontal) } @Test fun testSinglePaneShadeLayout_isAlignedToParent() { disableSplitShade() - controller.updateResources() + underTest.updateResources() assertThat(getConstraintSetLayout(R.id.qs_frame).endToEnd) - .isEqualTo(ConstraintSet.PARENT_ID) + .isEqualTo(ConstraintSet.PARENT_ID) assertThat(getConstraintSetLayout(R.id.notification_stack_scroller).startToStart) - .isEqualTo(ConstraintSet.PARENT_ID) + .isEqualTo(ConstraintSet.PARENT_ID) } @Test fun testAllChildrenOfNotificationContainer_haveIds() { // set dimen to 0 to avoid triggering updating bottom spacing overrideResource(R.dimen.split_shade_notifications_scrim_margin_bottom, 0) - val container = NotificationsQuickSettingsContainer(context, null) + val container = NotificationsQuickSettingsContainer(mContext, null) container.removeAllViews() container.addView(newViewWithId(1)) container.addView(newViewWithId(View.NO_ID)) - val controller = NotificationsQSContainerController( + val controller = + NotificationsQSContainerController( container, navigationModeController, overviewProxyService, - mShadeHeaderController, + shadeHeaderController, shadeExpansionStateManager, fragmentService, - delayableExecutor - ) + delayableExecutor, + featureFlags, + notificationStackScrollLayoutController, + ) controller.updateConstraints() assertThat(container.getChildAt(0).id).isEqualTo(1) @@ -388,44 +456,46 @@ class NotificationQSContainerControllerTest : SysuiTestCase() { fun testWindowInsetDebounce() { disableSplitShade() - given(taskbarVisible = false, + given( + taskbarVisible = false, navigationMode = GESTURES_NAVIGATION, insets = emptyInsets(), - applyImmediately = false) + applyImmediately = false + ) fakeSystemClock.advanceTime(INSET_DEBOUNCE_MILLIS / 2) windowInsetsCallback.accept(windowInsets().withStableBottom()) delayableExecutor.advanceClockToLast() delayableExecutor.runAllReady() - verify(notificationsQSContainer, never()).setQSContainerPaddingBottom(0) - verify(notificationsQSContainer).setQSContainerPaddingBottom(STABLE_INSET_BOTTOM) + verify(view, never()).setQSContainerPaddingBottom(0) + verify(view).setQSContainerPaddingBottom(STABLE_INSET_BOTTOM) } @Test fun testStartCustomizingWithDuration() { - controller.setCustomizerShowing(true, 100L) - verify(mShadeHeaderController).startCustomizingAnimation(true, 100L) + underTest.setCustomizerShowing(true, 100L) + verify(shadeHeaderController).startCustomizingAnimation(true, 100L) } @Test fun testEndCustomizingWithDuration() { - controller.setCustomizerShowing(true, 0L) // Only tracks changes - reset(mShadeHeaderController) + underTest.setCustomizerShowing(true, 0L) // Only tracks changes + reset(shadeHeaderController) - controller.setCustomizerShowing(false, 100L) - verify(mShadeHeaderController).startCustomizingAnimation(false, 100L) + underTest.setCustomizerShowing(false, 100L) + verify(shadeHeaderController).startCustomizingAnimation(false, 100L) } @Test fun testTagListenerAdded() { - verify(fragmentHostManager).addTagListener(eq(QS.TAG), eq(notificationsQSContainer)) + verify(fragmentHostManager).addTagListener(eq(QS.TAG), eq(view)) } @Test fun testTagListenerRemoved() { - attachStateListenerCaptor.value.onViewDetachedFromWindow(notificationsQSContainer) - verify(fragmentHostManager).removeTagListener(eq(QS.TAG), eq(notificationsQSContainer)) + attachStateListenerCaptor.value.onViewDetachedFromWindow(view) + verify(fragmentHostManager).removeTagListener(eq(QS.TAG), eq(view)) } private fun disableSplitShade() { @@ -438,7 +508,7 @@ class NotificationQSContainerControllerTest : SysuiTestCase() { private fun setSplitShadeEnabled(enabled: Boolean) { overrideResource(R.bool.config_use_split_notification_shade, enabled) - controller.updateResources() + underTest.updateResources() } private fun setSmallScreen() { @@ -459,7 +529,7 @@ class NotificationQSContainerControllerTest : SysuiTestCase() { insets: WindowInsets, applyImmediately: Boolean = true ) { - Mockito.clearInvocations(notificationsQSContainer) + Mockito.clearInvocations(view) taskbarVisibilityCallback.onTaskbarStatusUpdated(taskbarVisible, false) navigationModeCallback.onNavigationModeChanged(navigationMode) windowInsetsCallback.accept(insets) @@ -474,12 +544,10 @@ class NotificationQSContainerControllerTest : SysuiTestCase() { expectedNotificationsMargin: Int = NOTIFICATIONS_MARGIN, expectedQsPadding: Int = 0 ) { - verify(notificationsQSContainer) - .setPadding(anyInt(), anyInt(), anyInt(), eq(expectedContainerPadding)) - verify(notificationsQSContainer).setNotificationsMarginBottom(expectedNotificationsMargin) - verify(notificationsQSContainer) - .setQSContainerPaddingBottom(expectedQsPadding) - Mockito.clearInvocations(notificationsQSContainer) + verify(view).setPadding(anyInt(), anyInt(), anyInt(), eq(expectedContainerPadding)) + verify(view).setNotificationsMarginBottom(expectedNotificationsMargin) + verify(view).setQSContainerPaddingBottom(expectedQsPadding) + Mockito.clearInvocations(view) } private fun windowInsets() = mock(WindowInsets::class.java, RETURNS_DEEP_STUBS) @@ -503,10 +571,26 @@ class NotificationQSContainerControllerTest : SysuiTestCase() { private fun newViewWithId(id: Int): View { val view = View(mContext) view.id = id - val layoutParams = ConstraintLayout.LayoutParams( - ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT) + val layoutParams = + ConstraintLayout.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) // required as cloning ConstraintSet fails if view doesn't have layout params view.layoutParams = layoutParams return view } + + companion object { + const val STABLE_INSET_BOTTOM = 100 + const val CUTOUT_HEIGHT = 50 + const val GESTURES_NAVIGATION = WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL + const val BUTTONS_NAVIGATION = WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON + const val NOTIFICATIONS_MARGIN = 50 + const val SCRIM_MARGIN = 10 + const val FOOTER_ACTIONS_INSET = 2 + const val FOOTER_ACTIONS_PADDING = 2 + const val FOOTER_ACTIONS_OFFSET = FOOTER_ACTIONS_INSET + FOOTER_ACTIONS_PADDING + const val QS_PADDING_OFFSET = SCRIM_MARGIN + FOOTER_ACTIONS_OFFSET + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerTest.kt index d4751c86a87f..a5048187b1b4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerTest.kt @@ -19,24 +19,47 @@ package com.android.systemui.shade import android.testing.AndroidTestingRunner import android.testing.TestableLooper import android.testing.TestableResources +import android.view.View +import android.view.ViewGroup +import android.view.WindowInsets +import android.view.WindowManagerPolicyConstants +import androidx.annotation.IdRes +import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintSet import androidx.test.filters.SmallTest import com.android.systemui.R import com.android.systemui.SysuiTestCase +import com.android.systemui.flags.FakeFeatureFlags +import com.android.systemui.flags.Flags +import com.android.systemui.fragments.FragmentHostManager import com.android.systemui.fragments.FragmentService import com.android.systemui.navigationbar.NavigationModeController +import com.android.systemui.navigationbar.NavigationModeController.ModeChangedListener +import com.android.systemui.plugins.qs.QS import com.android.systemui.recents.OverviewProxyService -import com.android.systemui.util.concurrency.DelayableExecutor +import com.android.systemui.recents.OverviewProxyService.OverviewProxyListener +import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.mockito.capture import com.android.systemui.util.mockito.whenever import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat +import java.util.function.Consumer import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentCaptor +import org.mockito.Captor import org.mockito.Mock +import org.mockito.Mockito +import org.mockito.Mockito.RETURNS_DEEP_STUBS +import org.mockito.Mockito.any +import org.mockito.Mockito.anyInt +import org.mockito.Mockito.doNothing +import org.mockito.Mockito.eq +import org.mockito.Mockito.mock +import org.mockito.Mockito.never +import org.mockito.Mockito.reset import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations @@ -51,19 +74,37 @@ class NotificationsQSContainerControllerTest : SysuiTestCase() { @Mock lateinit var shadeHeaderController: ShadeHeaderController @Mock lateinit var shadeExpansionStateManager: ShadeExpansionStateManager @Mock lateinit var fragmentService: FragmentService + @Mock lateinit var fragmentHostManager: FragmentHostManager + @Mock + lateinit var notificationStackScrollLayoutController: NotificationStackScrollLayoutController + + @Captor lateinit var navigationModeCaptor: ArgumentCaptor<ModeChangedListener> + @Captor lateinit var taskbarVisibilityCaptor: ArgumentCaptor<OverviewProxyListener> + @Captor lateinit var windowInsetsCallbackCaptor: ArgumentCaptor<Consumer<WindowInsets>> + @Captor lateinit var constraintSetCaptor: ArgumentCaptor<ConstraintSet> + @Captor lateinit var attachStateListenerCaptor: ArgumentCaptor<View.OnAttachStateChangeListener> lateinit var underTest: NotificationsQSContainerController private lateinit var fakeResources: TestableResources - - private val delayableExecutor: DelayableExecutor = FakeExecutor(FakeSystemClock()) + private lateinit var featureFlags: FakeFeatureFlags + private lateinit var navigationModeCallback: ModeChangedListener + private lateinit var taskbarVisibilityCallback: OverviewProxyListener + private lateinit var windowInsetsCallback: Consumer<WindowInsets> + private lateinit var fakeSystemClock: FakeSystemClock + private lateinit var delayableExecutor: FakeExecutor @Before fun setup() { MockitoAnnotations.initMocks(this) - fakeResources = TestableResources(context.resources) + fakeSystemClock = FakeSystemClock() + delayableExecutor = FakeExecutor(fakeSystemClock) + featureFlags = FakeFeatureFlags().apply { set(Flags.MIGRATE_NSSL, true) } + mContext.ensureTestableResources() + whenever(view.context).thenReturn(mContext) + whenever(view.resources).thenReturn(mContext.resources) - whenever(view.resources).thenReturn(fakeResources.resources) + whenever(fragmentService.getFragmentHostManager(any())).thenReturn(fragmentHostManager) underTest = NotificationsQSContainerController( @@ -74,16 +115,36 @@ class NotificationsQSContainerControllerTest : SysuiTestCase() { shadeExpansionStateManager, fragmentService, delayableExecutor, + featureFlags, + notificationStackScrollLayoutController, ) + + overrideResource(R.dimen.split_shade_notifications_scrim_margin_bottom, SCRIM_MARGIN) + overrideResource(R.dimen.notification_panel_margin_bottom, NOTIFICATIONS_MARGIN) + overrideResource(R.bool.config_use_split_notification_shade, false) + overrideResource(R.dimen.qs_footer_actions_bottom_padding, FOOTER_ACTIONS_PADDING) + overrideResource(R.dimen.qs_footer_action_inset, FOOTER_ACTIONS_INSET) + whenever(navigationModeController.addListener(navigationModeCaptor.capture())) + .thenReturn(GESTURES_NAVIGATION) + doNothing().`when`(overviewProxyService).addCallback(taskbarVisibilityCaptor.capture()) + doNothing().`when`(view).setInsetsChangedListener(windowInsetsCallbackCaptor.capture()) + doNothing().`when`(view).applyConstraints(constraintSetCaptor.capture()) + doNothing().`when`(view).addOnAttachStateChangeListener(attachStateListenerCaptor.capture()) + underTest.init() + attachStateListenerCaptor.value.onViewAttachedToWindow(view) + + navigationModeCallback = navigationModeCaptor.value + taskbarVisibilityCallback = taskbarVisibilityCaptor.value + windowInsetsCallback = windowInsetsCallbackCaptor.value + + Mockito.clearInvocations(view) } @Test fun testSmallScreen_updateResources_splitShadeHeightIsSet() { - with(fakeResources) { - addOverride(R.bool.config_use_large_screen_shade_header, false) - addOverride(R.dimen.qs_header_height, 1) - addOverride(R.dimen.large_screen_shade_header_height, 2) - } + overrideResource(R.bool.config_use_large_screen_shade_header, false) + overrideResource(R.dimen.qs_header_height, 1) + overrideResource(R.dimen.large_screen_shade_header_height, 2) underTest.updateResources() @@ -94,11 +155,9 @@ class NotificationsQSContainerControllerTest : SysuiTestCase() { @Test fun testLargeScreen_updateResources_splitShadeHeightIsSet() { - with(fakeResources) { - addOverride(R.bool.config_use_large_screen_shade_header, true) - addOverride(R.dimen.qs_header_height, 1) - addOverride(R.dimen.large_screen_shade_header_height, 2) - } + overrideResource(R.bool.config_use_large_screen_shade_header, true) + overrideResource(R.dimen.qs_header_height, 1) + overrideResource(R.dimen.large_screen_shade_header_height, 2) underTest.updateResources() @@ -106,4 +165,415 @@ class NotificationsQSContainerControllerTest : SysuiTestCase() { verify(view).applyConstraints(capture(captor)) assertThat(captor.value.getHeight(R.id.split_shade_status_bar)).isEqualTo(2) } + + @Test + fun testTaskbarVisibleInSplitShade() { + enableSplitShade() + + given( + taskbarVisible = true, + navigationMode = GESTURES_NAVIGATION, + insets = windowInsets().withStableBottom() + ) + then( + expectedContainerPadding = 0, // taskbar should disappear when shade is expanded + expectedNotificationsMargin = NOTIFICATIONS_MARGIN, + expectedQsPadding = NOTIFICATIONS_MARGIN - QS_PADDING_OFFSET + ) + + given( + taskbarVisible = true, + navigationMode = BUTTONS_NAVIGATION, + insets = windowInsets().withStableBottom() + ) + then( + expectedContainerPadding = STABLE_INSET_BOTTOM, + expectedNotificationsMargin = NOTIFICATIONS_MARGIN, + expectedQsPadding = NOTIFICATIONS_MARGIN - QS_PADDING_OFFSET + ) + } + + @Test + fun testTaskbarNotVisibleInSplitShade() { + // when taskbar is not visible, it means we're on the home screen + enableSplitShade() + + given( + taskbarVisible = false, + navigationMode = GESTURES_NAVIGATION, + insets = windowInsets().withStableBottom() + ) + then( + expectedContainerPadding = 0, + expectedQsPadding = NOTIFICATIONS_MARGIN - QS_PADDING_OFFSET + ) + + given( + taskbarVisible = false, + navigationMode = BUTTONS_NAVIGATION, + insets = windowInsets().withStableBottom() + ) + then( + expectedContainerPadding = 0, // qs goes full height as it's not obscuring nav buttons + expectedNotificationsMargin = STABLE_INSET_BOTTOM + NOTIFICATIONS_MARGIN, + expectedQsPadding = STABLE_INSET_BOTTOM + NOTIFICATIONS_MARGIN - QS_PADDING_OFFSET + ) + } + + @Test + fun testTaskbarNotVisibleInSplitShadeWithCutout() { + enableSplitShade() + + given( + taskbarVisible = false, + navigationMode = GESTURES_NAVIGATION, + insets = windowInsets().withCutout() + ) + then( + expectedContainerPadding = CUTOUT_HEIGHT, + expectedQsPadding = NOTIFICATIONS_MARGIN - QS_PADDING_OFFSET + ) + + given( + taskbarVisible = false, + navigationMode = BUTTONS_NAVIGATION, + insets = windowInsets().withCutout().withStableBottom() + ) + then( + expectedContainerPadding = 0, + expectedNotificationsMargin = STABLE_INSET_BOTTOM + NOTIFICATIONS_MARGIN, + expectedQsPadding = STABLE_INSET_BOTTOM + NOTIFICATIONS_MARGIN - QS_PADDING_OFFSET + ) + } + + @Test + fun testTaskbarVisibleInSinglePaneShade() { + disableSplitShade() + + given( + taskbarVisible = true, + navigationMode = GESTURES_NAVIGATION, + insets = windowInsets().withStableBottom() + ) + then(expectedContainerPadding = 0, expectedQsPadding = STABLE_INSET_BOTTOM) + + given( + taskbarVisible = true, + navigationMode = BUTTONS_NAVIGATION, + insets = windowInsets().withStableBottom() + ) + then( + expectedContainerPadding = STABLE_INSET_BOTTOM, + expectedQsPadding = STABLE_INSET_BOTTOM + ) + } + + @Test + fun testTaskbarNotVisibleInSinglePaneShade() { + disableSplitShade() + + given(taskbarVisible = false, navigationMode = GESTURES_NAVIGATION, insets = emptyInsets()) + then(expectedContainerPadding = 0) + + given( + taskbarVisible = false, + navigationMode = GESTURES_NAVIGATION, + insets = windowInsets().withCutout().withStableBottom() + ) + then(expectedContainerPadding = CUTOUT_HEIGHT, expectedQsPadding = STABLE_INSET_BOTTOM) + + given( + taskbarVisible = false, + navigationMode = BUTTONS_NAVIGATION, + insets = windowInsets().withStableBottom() + ) + then( + expectedContainerPadding = 0, + expectedNotificationsMargin = STABLE_INSET_BOTTOM + NOTIFICATIONS_MARGIN, + expectedQsPadding = STABLE_INSET_BOTTOM + ) + } + + @Test + fun testDetailShowingInSinglePaneShade() { + disableSplitShade() + underTest.setDetailShowing(true) + + // always sets spacings to 0 + given( + taskbarVisible = false, + navigationMode = GESTURES_NAVIGATION, + insets = windowInsets().withStableBottom() + ) + then(expectedContainerPadding = 0, expectedNotificationsMargin = 0) + + given(taskbarVisible = false, navigationMode = BUTTONS_NAVIGATION, insets = emptyInsets()) + then(expectedContainerPadding = 0, expectedNotificationsMargin = 0) + } + + @Test + fun testDetailShowingInSplitShade() { + enableSplitShade() + underTest.setDetailShowing(true) + + given( + taskbarVisible = false, + navigationMode = GESTURES_NAVIGATION, + insets = windowInsets().withStableBottom() + ) + then(expectedContainerPadding = 0) + + // should not influence spacing + given(taskbarVisible = false, navigationMode = BUTTONS_NAVIGATION, insets = emptyInsets()) + then(expectedContainerPadding = 0) + } + + @Test + fun testNotificationsMarginBottomIsUpdated() { + Mockito.clearInvocations(view) + enableSplitShade() + verify(view).setNotificationsMarginBottom(NOTIFICATIONS_MARGIN) + + overrideResource(R.dimen.notification_panel_margin_bottom, 100) + disableSplitShade() + verify(view).setNotificationsMarginBottom(100) + } + + @Test + fun testSplitShadeLayout_isAlignedToGuideline() { + enableSplitShade() + underTest.updateResources() + assertThat(getConstraintSetLayout(R.id.qs_frame).endToEnd).isEqualTo(R.id.qs_edge_guideline) + } + + @Test + fun testSinglePaneLayout_childrenHaveEqualMargins() { + disableSplitShade() + underTest.updateResources() + val qsStartMargin = getConstraintSetLayout(R.id.qs_frame).startMargin + val qsEndMargin = getConstraintSetLayout(R.id.qs_frame).endMargin + assertThat(qsStartMargin == qsEndMargin).isTrue() + } + + @Test + fun testSplitShadeLayout_childrenHaveInsideMarginsOfZero() { + enableSplitShade() + underTest.updateResources() + assertThat(getConstraintSetLayout(R.id.qs_frame).endMargin).isEqualTo(0) + } + + @Test + fun testSplitShadeLayout_qsFrameHasHorizontalMarginsOfZero() { + enableSplitShade() + underTest.updateResources() + assertThat(getConstraintSetLayout(R.id.qs_frame).endMargin).isEqualTo(0) + assertThat(getConstraintSetLayout(R.id.qs_frame).startMargin).isEqualTo(0) + } + + @Test + fun testLargeScreenLayout_qsAndNotifsTopMarginIsOfHeaderHeight() { + setLargeScreen() + val largeScreenHeaderHeight = 100 + overrideResource(R.dimen.large_screen_shade_header_height, largeScreenHeaderHeight) + + underTest.updateResources() + + assertThat(getConstraintSetLayout(R.id.qs_frame).topMargin) + .isEqualTo(largeScreenHeaderHeight) + } + + @Test + fun testSmallScreenLayout_qsAndNotifsTopMarginIsZero() { + setSmallScreen() + underTest.updateResources() + assertThat(getConstraintSetLayout(R.id.qs_frame).topMargin).isEqualTo(0) + } + + @Test + fun testSinglePaneShadeLayout_qsFrameHasHorizontalMarginsSetToCorrectValue() { + disableSplitShade() + underTest.updateResources() + val notificationPanelMarginHorizontal = + mContext.resources.getDimensionPixelSize(R.dimen.notification_panel_margin_horizontal) + assertThat(getConstraintSetLayout(R.id.qs_frame).endMargin) + .isEqualTo(notificationPanelMarginHorizontal) + assertThat(getConstraintSetLayout(R.id.qs_frame).startMargin) + .isEqualTo(notificationPanelMarginHorizontal) + } + + @Test + fun testSinglePaneShadeLayout_isAlignedToParent() { + disableSplitShade() + underTest.updateResources() + assertThat(getConstraintSetLayout(R.id.qs_frame).endToEnd) + .isEqualTo(ConstraintSet.PARENT_ID) + } + + @Test + fun testAllChildrenOfNotificationContainer_haveIds() { + // set dimen to 0 to avoid triggering updating bottom spacing + overrideResource(R.dimen.split_shade_notifications_scrim_margin_bottom, 0) + val container = NotificationsQuickSettingsContainer(mContext, null) + container.removeAllViews() + container.addView(newViewWithId(1)) + container.addView(newViewWithId(View.NO_ID)) + val controller = + NotificationsQSContainerController( + container, + navigationModeController, + overviewProxyService, + shadeHeaderController, + shadeExpansionStateManager, + fragmentService, + delayableExecutor, + featureFlags, + notificationStackScrollLayoutController, + ) + controller.updateConstraints() + + assertThat(container.getChildAt(0).id).isEqualTo(1) + assertThat(container.getChildAt(1).id).isNotEqualTo(View.NO_ID) + } + + @Test + fun testWindowInsetDebounce() { + disableSplitShade() + + given( + taskbarVisible = false, + navigationMode = GESTURES_NAVIGATION, + insets = emptyInsets(), + applyImmediately = false + ) + fakeSystemClock.advanceTime(INSET_DEBOUNCE_MILLIS / 2) + windowInsetsCallback.accept(windowInsets().withStableBottom()) + + delayableExecutor.advanceClockToLast() + delayableExecutor.runAllReady() + + verify(view, never()).setQSContainerPaddingBottom(0) + verify(view).setQSContainerPaddingBottom(STABLE_INSET_BOTTOM) + } + + @Test + fun testStartCustomizingWithDuration() { + underTest.setCustomizerShowing(true, 100L) + verify(shadeHeaderController).startCustomizingAnimation(true, 100L) + } + + @Test + fun testEndCustomizingWithDuration() { + underTest.setCustomizerShowing(true, 0L) // Only tracks changes + reset(shadeHeaderController) + + underTest.setCustomizerShowing(false, 100L) + verify(shadeHeaderController).startCustomizingAnimation(false, 100L) + } + + @Test + fun testTagListenerAdded() { + verify(fragmentHostManager).addTagListener(eq(QS.TAG), eq(view)) + } + + @Test + fun testTagListenerRemoved() { + attachStateListenerCaptor.value.onViewDetachedFromWindow(view) + verify(fragmentHostManager).removeTagListener(eq(QS.TAG), eq(view)) + } + + private fun disableSplitShade() { + setSplitShadeEnabled(false) + } + + private fun enableSplitShade() { + setSplitShadeEnabled(true) + } + + private fun setSplitShadeEnabled(enabled: Boolean) { + overrideResource(R.bool.config_use_split_notification_shade, enabled) + underTest.updateResources() + } + + private fun setSmallScreen() { + setLargeScreenEnabled(false) + } + + private fun setLargeScreen() { + setLargeScreenEnabled(true) + } + + private fun setLargeScreenEnabled(enabled: Boolean) { + overrideResource(R.bool.config_use_large_screen_shade_header, enabled) + } + + private fun given( + taskbarVisible: Boolean, + navigationMode: Int, + insets: WindowInsets, + applyImmediately: Boolean = true + ) { + Mockito.clearInvocations(view) + taskbarVisibilityCallback.onTaskbarStatusUpdated(taskbarVisible, false) + navigationModeCallback.onNavigationModeChanged(navigationMode) + windowInsetsCallback.accept(insets) + if (applyImmediately) { + delayableExecutor.advanceClockToLast() + delayableExecutor.runAllReady() + } + } + + fun then( + expectedContainerPadding: Int, + expectedNotificationsMargin: Int = NOTIFICATIONS_MARGIN, + expectedQsPadding: Int = 0 + ) { + verify(view).setPadding(anyInt(), anyInt(), anyInt(), eq(expectedContainerPadding)) + verify(view).setNotificationsMarginBottom(expectedNotificationsMargin) + verify(view).setQSContainerPaddingBottom(expectedQsPadding) + Mockito.clearInvocations(view) + } + + private fun windowInsets() = mock(WindowInsets::class.java, RETURNS_DEEP_STUBS) + + private fun emptyInsets() = mock(WindowInsets::class.java) + + private fun WindowInsets.withCutout(): WindowInsets { + whenever(displayCutout.safeInsetBottom).thenReturn(CUTOUT_HEIGHT) + return this + } + + private fun WindowInsets.withStableBottom(): WindowInsets { + whenever(stableInsetBottom).thenReturn(STABLE_INSET_BOTTOM) + return this + } + + private fun getConstraintSetLayout(@IdRes id: Int): ConstraintSet.Layout { + return constraintSetCaptor.value.getConstraint(id).layout + } + + private fun newViewWithId(id: Int): View { + val view = View(mContext) + view.id = id + val layoutParams = + ConstraintLayout.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + // required as cloning ConstraintSet fails if view doesn't have layout params + view.layoutParams = layoutParams + return view + } + + companion object { + const val STABLE_INSET_BOTTOM = 100 + const val CUTOUT_HEIGHT = 50 + const val GESTURES_NAVIGATION = WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL + const val BUTTONS_NAVIGATION = WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON + const val NOTIFICATIONS_MARGIN = 50 + const val SCRIM_MARGIN = 10 + const val FOOTER_ACTIONS_INSET = 2 + const val FOOTER_ACTIONS_PADDING = 2 + const val FOOTER_ACTIONS_OFFSET = FOOTER_ACTIONS_INSET + FOOTER_ACTIONS_PADDING + const val QS_PADDING_OFFSET = SCRIM_MARGIN + FOOTER_ACTIONS_OFFSET + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImplTest.java index d3e5816cd1d4..daa45db6b90c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImplTest.java @@ -426,23 +426,8 @@ public class NotificationInterruptStateProviderImplTest extends SysuiTestCase { } @Test - public void testShouldHeadsUp_oldWhen_flagDisabled() throws Exception { - ensureStateForHeadsUpWhenAwake(); - when(mFlags.isNoHunForOldWhenEnabled()).thenReturn(false); - - NotificationEntry entry = createNotification(IMPORTANCE_HIGH); - entry.getSbn().getNotification().when = makeWhenHoursAgo(25); - - assertThat(mNotifInterruptionStateProvider.shouldHeadsUp(entry)).isTrue(); - - verify(mLogger, never()).logNoHeadsUpOldWhen(any(), anyLong(), anyLong()); - verify(mLogger, never()).logMaybeHeadsUpDespiteOldWhen(any(), anyLong(), anyLong(), any()); - } - - @Test public void testShouldHeadsUp_oldWhen_whenNow() throws Exception { ensureStateForHeadsUpWhenAwake(); - when(mFlags.isNoHunForOldWhenEnabled()).thenReturn(true); NotificationEntry entry = createNotification(IMPORTANCE_HIGH); @@ -455,7 +440,6 @@ public class NotificationInterruptStateProviderImplTest extends SysuiTestCase { @Test public void testShouldHeadsUp_oldWhen_whenRecent() throws Exception { ensureStateForHeadsUpWhenAwake(); - when(mFlags.isNoHunForOldWhenEnabled()).thenReturn(true); NotificationEntry entry = createNotification(IMPORTANCE_HIGH); entry.getSbn().getNotification().when = makeWhenHoursAgo(13); @@ -469,7 +453,6 @@ public class NotificationInterruptStateProviderImplTest extends SysuiTestCase { @Test public void testShouldHeadsUp_oldWhen_whenZero() throws Exception { ensureStateForHeadsUpWhenAwake(); - when(mFlags.isNoHunForOldWhenEnabled()).thenReturn(true); NotificationEntry entry = createNotification(IMPORTANCE_HIGH); entry.getSbn().getNotification().when = 0L; @@ -484,7 +467,6 @@ public class NotificationInterruptStateProviderImplTest extends SysuiTestCase { @Test public void testShouldHeadsUp_oldWhen_whenNegative() throws Exception { ensureStateForHeadsUpWhenAwake(); - when(mFlags.isNoHunForOldWhenEnabled()).thenReturn(true); NotificationEntry entry = createNotification(IMPORTANCE_HIGH); entry.getSbn().getNotification().when = -1L; @@ -498,7 +480,6 @@ public class NotificationInterruptStateProviderImplTest extends SysuiTestCase { @Test public void testShouldHeadsUp_oldWhen_hasFullScreenIntent() throws Exception { ensureStateForHeadsUpWhenAwake(); - when(mFlags.isNoHunForOldWhenEnabled()).thenReturn(true); long when = makeWhenHoursAgo(25); NotificationEntry entry = createFsiNotification(IMPORTANCE_HIGH, /* silent= */ false); @@ -514,7 +495,6 @@ public class NotificationInterruptStateProviderImplTest extends SysuiTestCase { @Test public void testShouldHeadsUp_oldWhen_isForegroundService() throws Exception { ensureStateForHeadsUpWhenAwake(); - when(mFlags.isNoHunForOldWhenEnabled()).thenReturn(true); long when = makeWhenHoursAgo(25); NotificationEntry entry = createFgsNotification(IMPORTANCE_HIGH); @@ -530,7 +510,6 @@ public class NotificationInterruptStateProviderImplTest extends SysuiTestCase { @Test public void testShouldNotHeadsUp_oldWhen() throws Exception { ensureStateForHeadsUpWhenAwake(); - when(mFlags.isNoHunForOldWhenEnabled()).thenReturn(true); long when = makeWhenHoursAgo(25); NotificationEntry entry = createNotification(IMPORTANCE_HIGH); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/TextPrecomputerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/TextPrecomputerTest.kt new file mode 100644 index 000000000000..d46763df8a75 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/TextPrecomputerTest.kt @@ -0,0 +1,95 @@ +/* + * 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.statusbar.notification.row + +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper.RunWithLooper +import android.text.PrecomputedText +import android.text.TextPaint +import android.widget.TextView +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidTestingRunner::class) +@RunWithLooper +class TextPrecomputerTest : SysuiTestCase() { + + private lateinit var textPrecomputer: TextPrecomputer + + private lateinit var textView: TextView + + @Before + fun before() { + textPrecomputer = object : TextPrecomputer {} + textView = TextView(mContext) + } + + @Test + fun precompute_returnRunnable() { + // WHEN + val precomputeResult = textPrecomputer.precompute(textView, TEXT) + + // THEN + assertThat(precomputeResult).isInstanceOf(Runnable::class.java) + } + + @Test + fun precomputeRunnable_anyText_setPrecomputedText() { + // WHEN + textPrecomputer.precompute(textView, TEXT).run() + + // THEN + assertThat(textView.text).isInstanceOf(PrecomputedText::class.java) + } + + @Test + fun precomputeRunnable_differentPrecomputedTextConfig_notSetPrecomputedText() { + // GIVEN + val precomputedTextRunnable = + textPrecomputer.precompute(textView, TEXT, logException = false) + + // WHEN + textView.textMetricsParams = PrecomputedText.Params.Builder(PAINT).build() + precomputedTextRunnable.run() + + // THEN + assertThat(textView.text).isInstanceOf(String::class.java) + } + + @Test + fun precomputeRunnable_nullText_setNull() { + // GIVEN + textView.text = TEXT + val precomputedTextRunnable = textPrecomputer.precompute(textView, null) + + // WHEN + precomputedTextRunnable.run() + + // THEN + assertThat(textView.text).isEqualTo("") + } + + private companion object { + private val PAINT = TextPaint() + private const val TEXT = "Example Notification Test" + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/domain/interactor/SharedNotificationContainerInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/domain/interactor/SharedNotificationContainerInteractorTest.kt new file mode 100644 index 000000000000..7bbb09483b5f --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/domain/interactor/SharedNotificationContainerInteractorTest.kt @@ -0,0 +1,72 @@ +/* + * 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.statusbar.notification.stack.domain.interactor + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.R +import com.android.systemui.SysuiTestCase +import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository +import com.android.systemui.coroutines.collectLastValue +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class SharedNotificationContainerInteractorTest : SysuiTestCase() { + private lateinit var configurationRepository: FakeConfigurationRepository + private lateinit var underTest: SharedNotificationContainerInteractor + + @Before + fun setUp() { + configurationRepository = FakeConfigurationRepository() + underTest = + SharedNotificationContainerInteractor( + configurationRepository, + mContext, + ) + } + + @Test + fun validateConfigValues() = runTest { + overrideResource(R.bool.config_use_split_notification_shade, true) + overrideResource(R.bool.config_use_large_screen_shade_header, false) + overrideResource(R.dimen.notification_panel_margin_horizontal, 0) + overrideResource(R.dimen.notification_panel_margin_bottom, 10) + overrideResource(R.dimen.notification_panel_margin_top, 10) + overrideResource(R.dimen.large_screen_shade_header_height, 0) + + val dimens = collectLastValue(underTest.configurationBasedDimensions) + + configurationRepository.onAnyConfigurationChange() + runCurrent() + + val lastDimens = dimens()!! + + assertThat(lastDimens.useSplitShade).isTrue() + assertThat(lastDimens.useLargeScreenHeader).isFalse() + assertThat(lastDimens.marginHorizontal).isEqualTo(0) + assertThat(lastDimens.marginBottom).isGreaterThan(0) + assertThat(lastDimens.marginTop).isGreaterThan(0) + assertThat(lastDimens.marginTopLargeScreen).isEqualTo(0) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt new file mode 100644 index 000000000000..afd995460151 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt @@ -0,0 +1,142 @@ +/* + * 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.statusbar.notification.stack.ui.viewmodel + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.R +import com.android.systemui.SysuiTestCase +import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.statusbar.notification.stack.domain.interactor.SharedNotificationContainerInteractor +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class SharedNotificationContainerViewModelTest : SysuiTestCase() { + private lateinit var configurationRepository: FakeConfigurationRepository + private lateinit var sharedNotificationContainerInteractor: + SharedNotificationContainerInteractor + private lateinit var underTest: SharedNotificationContainerViewModel + + @Before + fun setUp() { + configurationRepository = FakeConfigurationRepository() + sharedNotificationContainerInteractor = + SharedNotificationContainerInteractor( + configurationRepository, + mContext, + ) + underTest = SharedNotificationContainerViewModel(sharedNotificationContainerInteractor) + } + + @Test + fun validateMarginStartInSplitShade() = runTest { + overrideResource(R.bool.config_use_split_notification_shade, true) + overrideResource(R.dimen.notification_panel_margin_horizontal, 20) + + val dimens = collectLastValue(underTest.configurationBasedDimensions) + + configurationRepository.onAnyConfigurationChange() + runCurrent() + + val lastDimens = dimens()!! + + assertThat(lastDimens.marginStart).isEqualTo(0) + } + + @Test + fun validateMarginStart() = runTest { + overrideResource(R.bool.config_use_split_notification_shade, false) + overrideResource(R.dimen.notification_panel_margin_horizontal, 20) + + val dimens = collectLastValue(underTest.configurationBasedDimensions) + + configurationRepository.onAnyConfigurationChange() + runCurrent() + + val lastDimens = dimens()!! + + assertThat(lastDimens.marginStart).isEqualTo(20) + } + + @Test + fun validateMarginEnd() = runTest { + overrideResource(R.dimen.notification_panel_margin_horizontal, 50) + + val dimens = collectLastValue(underTest.configurationBasedDimensions) + + configurationRepository.onAnyConfigurationChange() + runCurrent() + + val lastDimens = dimens()!! + + assertThat(lastDimens.marginEnd).isEqualTo(50) + } + + @Test + fun validateMarginBottom() = runTest { + overrideResource(R.dimen.notification_panel_margin_bottom, 50) + + val dimens = collectLastValue(underTest.configurationBasedDimensions) + + configurationRepository.onAnyConfigurationChange() + runCurrent() + + val lastDimens = dimens()!! + + assertThat(lastDimens.marginBottom).isEqualTo(50) + } + + @Test + fun validateMarginTopWithLargeScreenHeader() = runTest { + overrideResource(R.bool.config_use_large_screen_shade_header, true) + overrideResource(R.dimen.large_screen_shade_header_height, 50) + overrideResource(R.dimen.notification_panel_margin_top, 0) + + val dimens = collectLastValue(underTest.configurationBasedDimensions) + + configurationRepository.onAnyConfigurationChange() + runCurrent() + + val lastDimens = dimens()!! + + assertThat(lastDimens.marginTop).isEqualTo(50) + } + + @Test + fun validateMarginTop() = runTest { + overrideResource(R.bool.config_use_large_screen_shade_header, false) + overrideResource(R.dimen.large_screen_shade_header_height, 50) + overrideResource(R.dimen.notification_panel_margin_top, 0) + + val dimens = collectLastValue(underTest.configurationBasedDimensions) + + configurationRepository.onAnyConfigurationChange() + runCurrent() + + val lastDimens = dimens()!! + + assertThat(lastDimens.marginTop).isEqualTo(0) + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeFingerprintPropertyRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeFingerprintPropertyRepository.kt index 2362a5241244..0c5e43809fab 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeFingerprintPropertyRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeFingerprintPropertyRepository.kt @@ -20,16 +20,12 @@ import android.hardware.biometrics.SensorLocationInternal import com.android.systemui.biometrics.shared.model.FingerprintSensorType import com.android.systemui.biometrics.shared.model.SensorStrength import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow class FakeFingerprintPropertyRepository : FingerprintPropertyRepository { - private val _isInitialized: MutableStateFlow<Boolean> = MutableStateFlow(false) - override val isInitialized = _isInitialized.asStateFlow() - private val _sensorId: MutableStateFlow<Int> = MutableStateFlow(-1) - override val sensorId: StateFlow<Int> = _sensorId.asStateFlow() + override val sensorId = _sensorId.asStateFlow() private val _strength: MutableStateFlow<SensorStrength> = MutableStateFlow(SensorStrength.CONVENIENCE) @@ -37,12 +33,11 @@ class FakeFingerprintPropertyRepository : FingerprintPropertyRepository { private val _sensorType: MutableStateFlow<FingerprintSensorType> = MutableStateFlow(FingerprintSensorType.UNKNOWN) - override val sensorType: StateFlow<FingerprintSensorType> = _sensorType.asStateFlow() + override val sensorType = _sensorType.asStateFlow() private val _sensorLocations: MutableStateFlow<Map<String, SensorLocationInternal>> = MutableStateFlow(mapOf("" to SensorLocationInternal.DEFAULT)) - override val sensorLocations: StateFlow<Map<String, SensorLocationInternal>> = - _sensorLocations.asStateFlow() + override val sensorLocations = _sensorLocations.asStateFlow() fun setProperties( sensorId: Int, @@ -54,6 +49,5 @@ class FakeFingerprintPropertyRepository : FingerprintPropertyRepository { _strength.value = strength _sensorType.value = sensorType _sensorLocations.value = sensorLocations - _isInitialized.value = true } } diff --git a/services/Android.bp b/services/Android.bp index b0a0e5e44a8c..453f57234145 100644 --- a/services/Android.bp +++ b/services/Android.bp @@ -159,6 +159,7 @@ java_library { "services.coverage", "services.credentials", "services.devicepolicy", + "services.flags", "services.midi", "services.musicsearch", "services.net", diff --git a/services/core/java/com/android/server/biometrics/BiometricSensorPrivacy.java b/services/core/java/com/android/server/biometrics/BiometricCameraManager.java index 6727fbcdec66..058ea6bbb696 100644 --- a/services/core/java/com/android/server/biometrics/BiometricSensorPrivacy.java +++ b/services/core/java/com/android/server/biometrics/BiometricCameraManager.java @@ -17,9 +17,16 @@ package com.android.server.biometrics; /** - * Interface for biometric operations to get camera privacy state. + * Interface for biometrics to get camera status. */ -public interface BiometricSensorPrivacy { - /* Returns true if privacy is enabled and camera access is disabled. */ +public interface BiometricCameraManager { + /** + * Returns true if any camera is in use. + */ + boolean isAnyCameraUnavailable(); + + /** + * Returns true if privacy is enabled and camera access is disabled. + */ boolean isCameraPrivacyEnabled(); } diff --git a/services/core/java/com/android/server/biometrics/BiometricCameraManagerImpl.java b/services/core/java/com/android/server/biometrics/BiometricCameraManagerImpl.java new file mode 100644 index 000000000000..000ee5446962 --- /dev/null +++ b/services/core/java/com/android/server/biometrics/BiometricCameraManagerImpl.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.biometrics; + +import static android.hardware.SensorPrivacyManager.Sensors.CAMERA; + +import android.annotation.NonNull; +import android.hardware.SensorPrivacyManager; +import android.hardware.camera2.CameraManager; + +import java.util.concurrent.ConcurrentHashMap; + +public class BiometricCameraManagerImpl implements BiometricCameraManager { + + private final CameraManager mCameraManager; + private final SensorPrivacyManager mSensorPrivacyManager; + private final ConcurrentHashMap<String, Boolean> mIsCameraAvailable = new ConcurrentHashMap<>(); + + private final CameraManager.AvailabilityCallback mCameraAvailabilityCallback = + new CameraManager.AvailabilityCallback() { + @Override + public void onCameraAvailable(@NonNull String cameraId) { + mIsCameraAvailable.put(cameraId, true); + } + + @Override + public void onCameraUnavailable(@NonNull String cameraId) { + mIsCameraAvailable.put(cameraId, false); + } + }; + + public BiometricCameraManagerImpl(@NonNull CameraManager cameraManager, + @NonNull SensorPrivacyManager sensorPrivacyManager) { + mCameraManager = cameraManager; + mSensorPrivacyManager = sensorPrivacyManager; + mCameraManager.registerAvailabilityCallback(mCameraAvailabilityCallback, null); + } + + @Override + public boolean isAnyCameraUnavailable() { + for (String cameraId : mIsCameraAvailable.keySet()) { + if (!mIsCameraAvailable.get(cameraId)) { + return true; + } + } + return false; + } + + @Override + public boolean isCameraPrivacyEnabled() { + return mSensorPrivacyManager != null && mSensorPrivacyManager + .isSensorPrivacyEnabled(SensorPrivacyManager.TOGGLE_TYPE_SOFTWARE, CAMERA); + } +} diff --git a/services/core/java/com/android/server/biometrics/BiometricSensorPrivacyImpl.java b/services/core/java/com/android/server/biometrics/BiometricSensorPrivacyImpl.java deleted file mode 100644 index b6701da1d348..000000000000 --- a/services/core/java/com/android/server/biometrics/BiometricSensorPrivacyImpl.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * 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.biometrics; - -import static android.hardware.SensorPrivacyManager.Sensors.CAMERA; - -import android.annotation.Nullable; -import android.hardware.SensorPrivacyManager; - -public class BiometricSensorPrivacyImpl implements - BiometricSensorPrivacy { - private final SensorPrivacyManager mSensorPrivacyManager; - - public BiometricSensorPrivacyImpl(@Nullable SensorPrivacyManager sensorPrivacyManager) { - mSensorPrivacyManager = sensorPrivacyManager; - } - - @Override - public boolean isCameraPrivacyEnabled() { - return mSensorPrivacyManager != null && mSensorPrivacyManager - .isSensorPrivacyEnabled(SensorPrivacyManager.TOGGLE_TYPE_SOFTWARE, CAMERA); - } -} diff --git a/services/core/java/com/android/server/biometrics/BiometricService.java b/services/core/java/com/android/server/biometrics/BiometricService.java index 1fa97a3cb97f..e8ffe4feb458 100644 --- a/services/core/java/com/android/server/biometrics/BiometricService.java +++ b/services/core/java/com/android/server/biometrics/BiometricService.java @@ -48,6 +48,7 @@ import android.hardware.biometrics.ITestSession; import android.hardware.biometrics.ITestSessionCallback; import android.hardware.biometrics.PromptInfo; import android.hardware.biometrics.SensorPropertiesInternal; +import android.hardware.camera2.CameraManager; import android.hardware.fingerprint.FingerprintManager; import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; import android.net.Uri; @@ -125,7 +126,7 @@ public class BiometricService extends SystemService { AuthSession mAuthSession; private final Handler mHandler = new Handler(Looper.getMainLooper()); - private final BiometricSensorPrivacy mBiometricSensorPrivacy; + private final BiometricCameraManager mBiometricCameraManager; /** * Tracks authenticatorId invalidation. For more details, see @@ -936,7 +937,7 @@ public class BiometricService extends SystemService { return PreAuthInfo.create(mTrustManager, mDevicePolicyManager, mSettingObserver, mSensors, userId, promptInfo, opPackageName, false /* checkDevicePolicyManager */, - getContext(), mBiometricSensorPrivacy); + getContext(), mBiometricCameraManager); } /** @@ -1030,9 +1031,9 @@ public class BiometricService extends SystemService { return context.getSystemService(UserManager.class); } - public BiometricSensorPrivacy getBiometricSensorPrivacy(Context context) { - return new BiometricSensorPrivacyImpl(context.getSystemService( - SensorPrivacyManager.class)); + public BiometricCameraManager getBiometricCameraManager(Context context) { + return new BiometricCameraManagerImpl(context.getSystemService(CameraManager.class), + context.getSystemService(SensorPrivacyManager.class)); } } @@ -1062,7 +1063,7 @@ public class BiometricService extends SystemService { mRequestCounter = mInjector.getRequestGenerator(); mBiometricContext = injector.getBiometricContext(context); mUserManager = injector.getUserManager(context); - mBiometricSensorPrivacy = injector.getBiometricSensorPrivacy(context); + mBiometricCameraManager = injector.getBiometricCameraManager(context); try { injector.getActivityManagerService().registerUserSwitchObserver( @@ -1299,7 +1300,7 @@ public class BiometricService extends SystemService { final PreAuthInfo preAuthInfo = PreAuthInfo.create(mTrustManager, mDevicePolicyManager, mSettingObserver, mSensors, userId, promptInfo, opPackageName, promptInfo.isDisallowBiometricsIfPolicyExists(), - getContext(), mBiometricSensorPrivacy); + getContext(), mBiometricCameraManager); final Pair<Integer, Integer> preAuthStatus = preAuthInfo.getPreAuthenticateStatus(); diff --git a/services/core/java/com/android/server/biometrics/PreAuthInfo.java b/services/core/java/com/android/server/biometrics/PreAuthInfo.java index e6f25cb88006..b1740a780539 100644 --- a/services/core/java/com/android/server/biometrics/PreAuthInfo.java +++ b/services/core/java/com/android/server/biometrics/PreAuthInfo.java @@ -72,16 +72,16 @@ class PreAuthInfo { final Context context; private final boolean mBiometricRequested; private final int mBiometricStrengthRequested; - private final BiometricSensorPrivacy mBiometricSensorPrivacy; + private final BiometricCameraManager mBiometricCameraManager; private PreAuthInfo(boolean biometricRequested, int biometricStrengthRequested, boolean credentialRequested, List<BiometricSensor> eligibleSensors, List<Pair<BiometricSensor, Integer>> ineligibleSensors, boolean credentialAvailable, boolean confirmationRequested, boolean ignoreEnrollmentState, int userId, - Context context, BiometricSensorPrivacy biometricSensorPrivacy) { + Context context, BiometricCameraManager biometricCameraManager) { mBiometricRequested = biometricRequested; mBiometricStrengthRequested = biometricStrengthRequested; - mBiometricSensorPrivacy = biometricSensorPrivacy; + mBiometricCameraManager = biometricCameraManager; this.credentialRequested = credentialRequested; this.eligibleSensors = eligibleSensors; @@ -99,7 +99,7 @@ class PreAuthInfo { List<BiometricSensor> sensors, int userId, PromptInfo promptInfo, String opPackageName, boolean checkDevicePolicyManager, Context context, - BiometricSensorPrivacy biometricSensorPrivacy) + BiometricCameraManager biometricCameraManager) throws RemoteException { final boolean confirmationRequested = promptInfo.isConfirmationRequested(); @@ -127,7 +127,7 @@ class PreAuthInfo { checkDevicePolicyManager, requestedStrength, promptInfo.getAllowedSensorIds(), promptInfo.isIgnoreEnrollmentState(), - biometricSensorPrivacy); + biometricCameraManager); Slog.d(TAG, "Package: " + opPackageName + " Sensor ID: " + sensor.id @@ -151,7 +151,7 @@ class PreAuthInfo { return new PreAuthInfo(biometricRequested, requestedStrength, credentialRequested, eligibleSensors, ineligibleSensors, credentialAvailable, confirmationRequested, - promptInfo.isIgnoreEnrollmentState(), userId, context, biometricSensorPrivacy); + promptInfo.isIgnoreEnrollmentState(), userId, context, biometricCameraManager); } /** @@ -168,12 +168,16 @@ class PreAuthInfo { BiometricSensor sensor, int userId, String opPackageName, boolean checkDevicePolicyManager, int requestedStrength, @NonNull List<Integer> requestedSensorIds, - boolean ignoreEnrollmentState, BiometricSensorPrivacy biometricSensorPrivacy) { + boolean ignoreEnrollmentState, BiometricCameraManager biometricCameraManager) { if (!requestedSensorIds.isEmpty() && !requestedSensorIds.contains(sensor.id)) { return BIOMETRIC_NO_HARDWARE; } + if (sensor.modality == TYPE_FACE && biometricCameraManager.isAnyCameraUnavailable()) { + return BIOMETRIC_HARDWARE_NOT_DETECTED; + } + final boolean wasStrongEnough = Utils.isAtLeastStrength(sensor.oemStrength, requestedStrength); final boolean isStrongEnough = @@ -195,8 +199,8 @@ class PreAuthInfo { return BIOMETRIC_NOT_ENROLLED; } - if (biometricSensorPrivacy != null && sensor.modality == TYPE_FACE) { - if (biometricSensorPrivacy.isCameraPrivacyEnabled()) { + if (biometricCameraManager != null && sensor.modality == TYPE_FACE) { + if (biometricCameraManager.isCameraPrivacyEnabled()) { //Camera privacy is enabled as the access is disabled return BIOMETRIC_SENSOR_PRIVACY_ENABLED; } @@ -307,8 +311,8 @@ class PreAuthInfo { @BiometricAuthenticator.Modality int modality = TYPE_NONE; boolean cameraPrivacyEnabled = false; - if (mBiometricSensorPrivacy != null) { - cameraPrivacyEnabled = mBiometricSensorPrivacy.isCameraPrivacyEnabled(); + if (mBiometricCameraManager != null) { + cameraPrivacyEnabled = mBiometricCameraManager.isCameraPrivacyEnabled(); } if (mBiometricRequested && credentialRequested) { diff --git a/services/core/java/com/android/server/display/DisplayBrightnessState.java b/services/core/java/com/android/server/display/DisplayBrightnessState.java index e27182fe3ea7..dd5afa2bdc39 100644 --- a/services/core/java/com/android/server/display/DisplayBrightnessState.java +++ b/services/core/java/com/android/server/display/DisplayBrightnessState.java @@ -16,6 +16,8 @@ package com.android.server.display; +import android.text.TextUtils; + import com.android.server.display.brightness.BrightnessReason; import java.util.Objects; @@ -29,12 +31,14 @@ public final class DisplayBrightnessState { private final float mSdrBrightness; private final BrightnessReason mBrightnessReason; private final String mDisplayBrightnessStrategyName; + private final boolean mShouldUseAutoBrightness; private DisplayBrightnessState(Builder builder) { - this.mBrightness = builder.getBrightness(); - this.mSdrBrightness = builder.getSdrBrightness(); - this.mBrightnessReason = builder.getBrightnessReason(); - this.mDisplayBrightnessStrategyName = builder.getDisplayBrightnessStrategyName(); + mBrightness = builder.getBrightness(); + mSdrBrightness = builder.getSdrBrightness(); + mBrightnessReason = builder.getBrightnessReason(); + mDisplayBrightnessStrategyName = builder.getDisplayBrightnessStrategyName(); + mShouldUseAutoBrightness = builder.getShouldUseAutoBrightness(); } /** @@ -66,6 +70,13 @@ public final class DisplayBrightnessState { return mDisplayBrightnessStrategyName; } + /** + * @return {@code true} if the device is set up to run auto-brightness. + */ + public boolean getShouldUseAutoBrightness() { + return mShouldUseAutoBrightness; + } + @Override public String toString() { StringBuilder stringBuilder = new StringBuilder("DisplayBrightnessState:"); @@ -75,6 +86,8 @@ public final class DisplayBrightnessState { stringBuilder.append(getSdrBrightness()); stringBuilder.append("\n brightnessReason:"); stringBuilder.append(getBrightnessReason()); + stringBuilder.append("\n shouldUseAutoBrightness:"); + stringBuilder.append(getShouldUseAutoBrightness()); return stringBuilder.toString(); } @@ -91,28 +104,20 @@ public final class DisplayBrightnessState { return false; } - DisplayBrightnessState - displayBrightnessState = (DisplayBrightnessState) other; + DisplayBrightnessState otherState = (DisplayBrightnessState) other; - if (mBrightness != displayBrightnessState.getBrightness()) { - return false; - } - if (mSdrBrightness != displayBrightnessState.getSdrBrightness()) { - return false; - } - if (!mBrightnessReason.equals(displayBrightnessState.getBrightnessReason())) { - return false; - } - if (!mDisplayBrightnessStrategyName.equals( - displayBrightnessState.getDisplayBrightnessStrategyName())) { - return false; - } - return true; + return mBrightness == otherState.getBrightness() + && mSdrBrightness == otherState.getSdrBrightness() + && mBrightnessReason.equals(otherState.getBrightnessReason()) + && TextUtils.equals(mDisplayBrightnessStrategyName, + otherState.getDisplayBrightnessStrategyName()) + && mShouldUseAutoBrightness == otherState.getShouldUseAutoBrightness(); } @Override public int hashCode() { - return Objects.hash(mBrightness, mSdrBrightness, mBrightnessReason); + return Objects.hash( + mBrightness, mSdrBrightness, mBrightnessReason, mShouldUseAutoBrightness); } /** @@ -123,6 +128,23 @@ public final class DisplayBrightnessState { private float mSdrBrightness; private BrightnessReason mBrightnessReason = new BrightnessReason(); private String mDisplayBrightnessStrategyName; + private boolean mShouldUseAutoBrightness; + + /** + * Create a builder starting with the values from the specified {@link + * DisplayBrightnessState}. + * + * @param state The state from which to initialize. + */ + public static Builder from(DisplayBrightnessState state) { + Builder builder = new Builder(); + builder.setBrightness(state.getBrightness()); + builder.setSdrBrightness(state.getSdrBrightness()); + builder.setBrightnessReason(state.getBrightnessReason()); + builder.setDisplayBrightnessStrategyName(state.getDisplayBrightnessStrategyName()); + builder.setShouldUseAutoBrightness(state.getShouldUseAutoBrightness()); + return builder; + } /** * Gets the brightness @@ -200,6 +222,21 @@ public final class DisplayBrightnessState { } /** + * See {@link DisplayBrightnessState#getShouldUseAutoBrightness}. + */ + public Builder setShouldUseAutoBrightness(boolean shouldUseAutoBrightness) { + this.mShouldUseAutoBrightness = shouldUseAutoBrightness; + return this; + } + + /** + * See {@link DisplayBrightnessState#getShouldUseAutoBrightness}. + */ + public boolean getShouldUseAutoBrightness() { + return mShouldUseAutoBrightness; + } + + /** * This is used to construct an immutable DisplayBrightnessState object from its builder */ public DisplayBrightnessState build() { diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java index dbe15b6e2da8..3b779ecf77e5 100644 --- a/services/core/java/com/android/server/display/DisplayManagerService.java +++ b/services/core/java/com/android/server/display/DisplayManagerService.java @@ -155,6 +155,7 @@ import com.android.server.display.layout.Layout; import com.android.server.display.mode.DisplayModeDirector; import com.android.server.display.utils.SensorUtils; import com.android.server.input.InputManagerInternal; +import com.android.server.utils.FoldSettingWrapper; import com.android.server.wm.SurfaceAnimationThread; import com.android.server.wm.WindowManagerInternal; @@ -540,7 +541,8 @@ public final class DisplayManagerService extends SystemService { mUiHandler = UiThread.getHandler(); mDisplayDeviceRepo = new DisplayDeviceRepository(mSyncRoot, mPersistentDataStore); mLogicalDisplayMapper = new LogicalDisplayMapper(mContext, mDisplayDeviceRepo, - new LogicalDisplayListener(), mSyncRoot, mHandler); + new LogicalDisplayListener(), mSyncRoot, mHandler, + new FoldSettingWrapper(mContext.getContentResolver())); mDisplayModeDirector = new DisplayModeDirector(context, mHandler); mBrightnessSynchronizer = new BrightnessSynchronizer(mContext); Resources resources = mContext.getResources(); diff --git a/services/core/java/com/android/server/display/DisplayPowerController2.java b/services/core/java/com/android/server/display/DisplayPowerController2.java index 1061fab51398..7417aeb22a64 100644 --- a/services/core/java/com/android/server/display/DisplayPowerController2.java +++ b/services/core/java/com/android/server/display/DisplayPowerController2.java @@ -495,7 +495,6 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal mWakelockController, mDisplayDeviceConfig, mHandler.getLooper(), () -> updatePowerState(), mDisplayId, mSensorManager); mDisplayStateController = new DisplayStateController(mDisplayPowerProximityStateController); - mAutomaticBrightnessStrategy = new AutomaticBrightnessStrategy(context, mDisplayId); mTag = "DisplayPowerController2[" + mDisplayId + "]"; mThermalBrightnessThrottlingDataId = logicalDisplay.getDisplayInfoLocked().thermalBrightnessThrottlingDataId; @@ -566,6 +565,8 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal )); // Seed the cached brightness saveBrightnessInfo(getScreenBrightnessSetting()); + mAutomaticBrightnessStrategy = + mDisplayBrightnessController.getAutomaticBrightnessStrategy(); DisplayWhiteBalanceSettings displayWhiteBalanceSettings = null; DisplayWhiteBalanceController displayWhiteBalanceController = null; @@ -609,7 +610,7 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal setUpAutoBrightness(resources, handler); - mColorFadeEnabled = !ActivityManager.isLowRamDeviceStatic(); + mColorFadeEnabled = mInjector.isColorFadeEnabled(); mColorFadeFadesConfig = resources.getBoolean( R.bool.config_animateScreenLights); @@ -1272,14 +1273,6 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal int state = mDisplayStateController .updateDisplayState(mPowerRequest, mIsEnabled, mIsInTransition); - if (mScreenOffBrightnessSensorController != null) { - mScreenOffBrightnessSensorController - .setLightSensorEnabled(mAutomaticBrightnessStrategy.shouldUseAutoBrightness() - && mIsEnabled && (state == Display.STATE_OFF || (state == Display.STATE_DOZE - && !mDisplayBrightnessController.isAllowAutoBrightnessWhileDozingConfig())) - && mLeadDisplayId == Layout.NO_LEAD_DISPLAY); - } - // Initialize things the first time the power state is changed. if (mustInitialize) { initialize(readyToUpdateDisplayState() ? state : Display.STATE_UNKNOWN); @@ -1305,6 +1298,17 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal slowChange = mBrightnessToFollowSlowChange; } + // Set up the ScreenOff controller used when coming out of SCREEN_OFF and the ALS sensor + // doesn't yet have a valid lux value to use with auto-brightness. + if (mScreenOffBrightnessSensorController != null) { + mScreenOffBrightnessSensorController + .setLightSensorEnabled(displayBrightnessState.getShouldUseAutoBrightness() + && mIsEnabled && (state == Display.STATE_OFF + || (state == Display.STATE_DOZE + && !mDisplayBrightnessController.isAllowAutoBrightnessWhileDozingConfig())) + && mLeadDisplayId == Layout.NO_LEAD_DISPLAY); + } + // Take note if the short term model was already active before applying the current // request changes. final boolean wasShortTermModelActive = @@ -1580,7 +1584,7 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal notifyBrightnessTrackerChanged(brightnessState, userInitiatedChange, wasShortTermModelActive, mAutomaticBrightnessStrategy.isAutoBrightnessEnabled(), - brightnessIsTemporary); + brightnessIsTemporary, displayBrightnessState.getShouldUseAutoBrightness()); // We save the brightness info *after* the brightness setting has been changed and // adjustments made so that the brightness info reflects the latest value. @@ -1624,8 +1628,8 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal mTempBrightnessEvent.setWasShortTermModelActive(wasShortTermModelActive); mTempBrightnessEvent.setDisplayBrightnessStrategyName(displayBrightnessState .getDisplayBrightnessStrategyName()); - mTempBrightnessEvent.setAutomaticBrightnessEnabled(mAutomaticBrightnessStrategy - .shouldUseAutoBrightness()); + mTempBrightnessEvent.setAutomaticBrightnessEnabled( + displayBrightnessState.getShouldUseAutoBrightness()); // Temporary is what we use during slider interactions. We avoid logging those so that // we don't spam logcat when the slider is being used. boolean tempToTempTransition = @@ -2236,7 +2240,7 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal private void notifyBrightnessTrackerChanged(float brightness, boolean userInitiated, boolean wasShortTermModelActive, boolean autobrightnessEnabled, - boolean brightnessIsTemporary) { + boolean brightnessIsTemporary, boolean shouldUseAutoBrightness) { final float brightnessInNits = mDisplayBrightnessController.convertToAdjustedNits(brightness); @@ -2251,7 +2255,7 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal || mAutomaticBrightnessController.isInIdleMode() || !autobrightnessEnabled || mBrightnessTracker == null - || !mAutomaticBrightnessStrategy.shouldUseAutoBrightness() + || !shouldUseAutoBrightness || brightnessInNits < 0.0f) { return; } @@ -2938,6 +2942,10 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal displayUniqueId, brightnessMin, brightnessMax, hbmData, hdrBrightnessCfg, hbmChangeCallback, hbmMetadata, context); } + + boolean isColorFadeEnabled() { + return !ActivityManager.isLowRamDeviceStatic(); + } } static class CachedBrightnessInfo { diff --git a/services/core/java/com/android/server/display/LogicalDisplayMapper.java b/services/core/java/com/android/server/display/LogicalDisplayMapper.java index d01b03f836a5..26f8029cf5ac 100644 --- a/services/core/java/com/android/server/display/LogicalDisplayMapper.java +++ b/services/core/java/com/android/server/display/LogicalDisplayMapper.java @@ -42,6 +42,7 @@ import android.view.DisplayInfo; import com.android.internal.annotations.VisibleForTesting; import com.android.server.display.layout.DisplayIdProducer; import com.android.server.display.layout.Layout; +import com.android.server.utils.FoldSettingWrapper; import java.io.PrintWriter; import java.util.Arrays; @@ -142,6 +143,7 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener { private final Listener mListener; private final DisplayManagerService.SyncRoot mSyncRoot; private final LogicalDisplayMapperHandler mHandler; + private final FoldSettingWrapper mFoldSettingWrapper; private final PowerManager mPowerManager; /** @@ -189,21 +191,23 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener { LogicalDisplayMapper(@NonNull Context context, @NonNull DisplayDeviceRepository repo, @NonNull Listener listener, @NonNull DisplayManagerService.SyncRoot syncRoot, - @NonNull Handler handler) { + @NonNull Handler handler, FoldSettingWrapper foldSettingWrapper) { this(context, repo, listener, syncRoot, handler, new DeviceStateToLayoutMap((isDefault) -> isDefault ? DEFAULT_DISPLAY - : sNextNonDefaultDisplayId++)); + : sNextNonDefaultDisplayId++), foldSettingWrapper); } LogicalDisplayMapper(@NonNull Context context, @NonNull DisplayDeviceRepository repo, @NonNull Listener listener, @NonNull DisplayManagerService.SyncRoot syncRoot, - @NonNull Handler handler, @NonNull DeviceStateToLayoutMap deviceStateToLayoutMap) { + @NonNull Handler handler, @NonNull DeviceStateToLayoutMap deviceStateToLayoutMap, + FoldSettingWrapper foldSettingWrapper) { mSyncRoot = syncRoot; mPowerManager = context.getSystemService(PowerManager.class); mInteractive = mPowerManager.isInteractive(); mHandler = new LogicalDisplayMapperHandler(handler.getLooper()); mDisplayDeviceRepo = repo; mListener = listener; + mFoldSettingWrapper = foldSettingWrapper; mSingleDisplayDemoMode = SystemProperties.getBoolean("persist.demo.singledisplay", false); mSupportsConcurrentInternalDisplays = context.getResources().getBoolean( com.android.internal.R.bool.config_supportsConcurrentInternalDisplays); @@ -531,9 +535,10 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener { * Returns if the device should be put to sleep or not. * * Includes a check to verify that the device state that we are moving to, {@code pendingState}, - * is the same as the physical state of the device, {@code baseState}. Different values for - * these parameters indicate a device state override is active, and we shouldn't put the device - * to sleep to provide a better user experience. + * is the same as the physical state of the device, {@code baseState}. Also if the + * 'Stay Awake On Fold' is not enabled. Different values for these parameters indicate a device + * state override is active, and we shouldn't put the device to sleep to provide a better user + * experience. * * @param pendingState device state we are moving to * @param currentState device state we are currently in @@ -551,7 +556,7 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener { && mDeviceStatesOnWhichToSleep.get(pendingState) && !mDeviceStatesOnWhichToSleep.get(currentState) && !isOverrideActive - && isInteractive && isBootCompleted; + && isInteractive && isBootCompleted && !mFoldSettingWrapper.shouldStayAwakeOnFold(); } private boolean areAllTransitioningDisplaysOffLocked() { diff --git a/services/core/java/com/android/server/display/brightness/DisplayBrightnessController.java b/services/core/java/com/android/server/display/brightness/DisplayBrightnessController.java index 2f52b708dfb5..ffd62a387a64 100644 --- a/services/core/java/com/android/server/display/brightness/DisplayBrightnessController.java +++ b/services/core/java/com/android/server/display/brightness/DisplayBrightnessController.java @@ -29,6 +29,7 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.server.display.AutomaticBrightnessController; import com.android.server.display.BrightnessSetting; import com.android.server.display.DisplayBrightnessState; +import com.android.server.display.brightness.strategy.AutomaticBrightnessStrategy; import com.android.server.display.brightness.strategy.DisplayBrightnessStrategy; import java.io.PrintWriter; @@ -134,11 +135,21 @@ public final class DisplayBrightnessController { public DisplayBrightnessState updateBrightness( DisplayManagerInternal.DisplayPowerRequest displayPowerRequest, int targetDisplayState) { + + DisplayBrightnessState state; synchronized (mLock) { mDisplayBrightnessStrategy = mDisplayBrightnessStrategySelector.selectStrategy( displayPowerRequest, targetDisplayState); - return mDisplayBrightnessStrategy.updateBrightness(displayPowerRequest); + state = mDisplayBrightnessStrategy.updateBrightness(displayPowerRequest); + } + + // This is a temporary measure until AutomaticBrightnessStrategy works as a traditional + // strategy. + // TODO: Remove when AutomaticBrightnessStrategy is populating the values directly. + if (state != null) { + state = addAutomaticBrightnessState(state); } + return state; } /** @@ -322,6 +333,13 @@ public final class DisplayBrightnessController { } /** + * TODO(b/253226419): Remove once auto-brightness is a fully-functioning strategy. + */ + public AutomaticBrightnessStrategy getAutomaticBrightnessStrategy() { + return mDisplayBrightnessStrategySelector.getAutomaticBrightnessStrategy(); + } + + /** * Convert a brightness float scale value to a nit value. Adjustments, such as RBC, are not * applied. This is used when storing the brightness in nits for the default display and when * passing the brightness value to follower displays. @@ -425,6 +443,18 @@ public final class DisplayBrightnessController { } } + /** + * TODO(b/253226419): Remove once auto-brightness is a fully-functioning strategy. + */ + private DisplayBrightnessState addAutomaticBrightnessState(DisplayBrightnessState state) { + AutomaticBrightnessStrategy autoStrat = getAutomaticBrightnessStrategy(); + + DisplayBrightnessState.Builder builder = DisplayBrightnessState.Builder.from(state); + builder.setShouldUseAutoBrightness( + autoStrat != null && autoStrat.shouldUseAutoBrightness()); + return builder.build(); + } + @GuardedBy("mLock") private void setTemporaryBrightnessLocked(float temporaryBrightness) { mDisplayBrightnessStrategySelector.getTemporaryDisplayBrightnessStrategy() diff --git a/services/core/java/com/android/server/display/brightness/DisplayBrightnessStrategySelector.java b/services/core/java/com/android/server/display/brightness/DisplayBrightnessStrategySelector.java index 02ca2d33fc99..45f1be076508 100644 --- a/services/core/java/com/android/server/display/brightness/DisplayBrightnessStrategySelector.java +++ b/services/core/java/com/android/server/display/brightness/DisplayBrightnessStrategySelector.java @@ -25,6 +25,7 @@ import android.view.Display; import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; +import com.android.server.display.brightness.strategy.AutomaticBrightnessStrategy; import com.android.server.display.brightness.strategy.BoostBrightnessStrategy; import com.android.server.display.brightness.strategy.DisplayBrightnessStrategy; import com.android.server.display.brightness.strategy.DozeBrightnessStrategy; @@ -60,6 +61,8 @@ public class DisplayBrightnessStrategySelector { private final FollowerBrightnessStrategy mFollowerBrightnessStrategy; // The brightness strategy used to manage the brightness state when the request is invalid. private final InvalidBrightnessStrategy mInvalidBrightnessStrategy; + // Controls brightness when automatic (adaptive) brightness is running. + private final AutomaticBrightnessStrategy mAutomaticBrightnessStrategy; // We take note of the old brightness strategy so that we can know when the strategy changes. private String mOldBrightnessStrategyName; @@ -81,6 +84,7 @@ public class DisplayBrightnessStrategySelector { mBoostBrightnessStrategy = injector.getBoostBrightnessStrategy(); mFollowerBrightnessStrategy = injector.getFollowerBrightnessStrategy(displayId); mInvalidBrightnessStrategy = injector.getInvalidBrightnessStrategy(); + mAutomaticBrightnessStrategy = injector.getAutomaticBrightnessStrategy(context, displayId); mAllowAutoBrightnessWhileDozingConfig = context.getResources().getBoolean( R.bool.config_allowAutoBrightnessWhileDozing); mOldBrightnessStrategyName = mInvalidBrightnessStrategy.getName(); @@ -130,6 +134,10 @@ public class DisplayBrightnessStrategySelector { return mFollowerBrightnessStrategy; } + public AutomaticBrightnessStrategy getAutomaticBrightnessStrategy() { + return mAutomaticBrightnessStrategy; + } + /** * Returns a boolean flag indicating if the light sensor is to be used to decide the screen * brightness when dozing @@ -198,5 +206,9 @@ public class DisplayBrightnessStrategySelector { InvalidBrightnessStrategy getInvalidBrightnessStrategy() { return new InvalidBrightnessStrategy(); } + + AutomaticBrightnessStrategy getAutomaticBrightnessStrategy(Context context, int displayId) { + return new AutomaticBrightnessStrategy(context, displayId); + } } } diff --git a/services/core/java/com/android/server/input/KeyboardLayoutManager.java b/services/core/java/com/android/server/input/KeyboardLayoutManager.java index 63fded1a35fe..7a8de341854d 100644 --- a/services/core/java/com/android/server/input/KeyboardLayoutManager.java +++ b/services/core/java/com/android/server/input/KeyboardLayoutManager.java @@ -16,6 +16,7 @@ package com.android.server.input; +import static com.android.server.input.KeyboardMetricsCollector.LAYOUT_SELECTION_CRITERIA_DEFAULT; import static com.android.server.input.KeyboardMetricsCollector.LAYOUT_SELECTION_CRITERIA_DEVICE; import static com.android.server.input.KeyboardMetricsCollector.LAYOUT_SELECTION_CRITERIA_USER; import static com.android.server.input.KeyboardMetricsCollector.LAYOUT_SELECTION_CRITERIA_VIRTUAL_KEYBOARD; @@ -1272,7 +1273,7 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener { boolean noLayoutFound = layoutInfo == null || layoutInfo.mDescriptor == null; configurationEventBuilder.addLayoutSelection(imeInfoList.get(i).mImeSubtype, noLayoutFound ? null : getKeyboardLayout(layoutInfo.mDescriptor), - noLayoutFound ? LAYOUT_SELECTION_CRITERIA_VIRTUAL_KEYBOARD + noLayoutFound ? LAYOUT_SELECTION_CRITERIA_DEFAULT : layoutInfo.mSelectionCriteria); } KeyboardMetricsCollector.logKeyboardConfiguredAtom(configurationEventBuilder.build()); diff --git a/services/core/java/com/android/server/input/KeyboardMetricsCollector.java b/services/core/java/com/android/server/input/KeyboardMetricsCollector.java index 19fa7a8e0aca..eb2da340004b 100644 --- a/services/core/java/com/android/server/input/KeyboardMetricsCollector.java +++ b/services/core/java/com/android/server/input/KeyboardMetricsCollector.java @@ -53,7 +53,8 @@ public final class KeyboardMetricsCollector { @IntDef(prefix = { "LAYOUT_SELECTION_CRITERIA_" }, value = { LAYOUT_SELECTION_CRITERIA_USER, LAYOUT_SELECTION_CRITERIA_DEVICE, - LAYOUT_SELECTION_CRITERIA_VIRTUAL_KEYBOARD + LAYOUT_SELECTION_CRITERIA_VIRTUAL_KEYBOARD, + LAYOUT_SELECTION_CRITERIA_DEFAULT }) public @interface LayoutSelectionCriteria {} @@ -66,9 +67,15 @@ public final class KeyboardMetricsCollector { /** Auto-detection based on IME provided language tag and layout type */ public static final int LAYOUT_SELECTION_CRITERIA_VIRTUAL_KEYBOARD = 2; + /** Default selection */ + public static final int LAYOUT_SELECTION_CRITERIA_DEFAULT = 3; + @VisibleForTesting static final String DEFAULT_LAYOUT = "Default"; + @VisibleForTesting + static final String DEFAULT_LANGUAGE_TAG = "None"; + /** * Log keyboard system shortcuts for the proto * {@link com.android.os.input.KeyboardSystemsEventReported} @@ -131,6 +138,10 @@ public final class KeyboardMetricsCollector { layoutConfiguration.keyboardLayoutName); proto.write(KeyboardLayoutConfig.LAYOUT_SELECTION_CRITERIA, layoutConfiguration.layoutSelectionCriteria); + proto.write(KeyboardLayoutConfig.IME_LANGUAGE_TAG, + layoutConfiguration.imeLanguageTag); + proto.write(KeyboardLayoutConfig.IME_LAYOUT_TYPE, + layoutConfiguration.imeLayoutType); proto.end(keyboardLayoutConfigToken); } @@ -231,27 +242,29 @@ public final class KeyboardMetricsCollector { @LayoutSelectionCriteria int layoutSelectionCriteria = mLayoutSelectionCriteriaList.get(i); InputMethodSubtype imeSubtype = mImeSubtypeList.get(i); - String keyboardLanguageTag; - String keyboardLayoutStringType; - if (layoutSelectionCriteria == LAYOUT_SELECTION_CRITERIA_DEVICE) { - keyboardLanguageTag = mInputDevice.getKeyboardLanguageTag(); - keyboardLayoutStringType = mInputDevice.getKeyboardLayoutType(); - } else { - ULocale pkLocale = imeSubtype.getPhysicalKeyboardHintLanguageTag(); - keyboardLanguageTag = pkLocale != null ? pkLocale.toLanguageTag() - : imeSubtype.getCanonicalizedLanguageTag(); - keyboardLayoutStringType = imeSubtype.getPhysicalKeyboardHintLayoutType(); - } + String keyboardLanguageTag = mInputDevice.getKeyboardLanguageTag(); + keyboardLanguageTag = keyboardLanguageTag == null ? DEFAULT_LANGUAGE_TAG + : keyboardLanguageTag; + int keyboardLayoutType = KeyboardLayout.LayoutType.getLayoutTypeEnumValue( + mInputDevice.getKeyboardLayoutType()); + + ULocale pkLocale = imeSubtype.getPhysicalKeyboardHintLanguageTag(); + String canonicalizedLanguageTag = + imeSubtype.getCanonicalizedLanguageTag().equals("") + ? DEFAULT_LANGUAGE_TAG : imeSubtype.getCanonicalizedLanguageTag(); + String imeLanguageTag = pkLocale != null ? pkLocale.toLanguageTag() + : canonicalizedLanguageTag; + int imeLayoutType = KeyboardLayout.LayoutType.getLayoutTypeEnumValue( + imeSubtype.getPhysicalKeyboardHintLayoutType()); + // Sanitize null values String keyboardLayoutName = selectedLayout == null ? DEFAULT_LAYOUT : selectedLayout.getLabel(); - keyboardLanguageTag = keyboardLanguageTag == null ? "" : keyboardLanguageTag; - int keyboardLayoutType = KeyboardLayout.LayoutType.getLayoutTypeEnumValue( - keyboardLayoutStringType); configurationList.add( new LayoutConfiguration(keyboardLayoutType, keyboardLanguageTag, - keyboardLayoutName, layoutSelectionCriteria)); + keyboardLayoutName, layoutSelectionCriteria, + imeLayoutType, imeLanguageTag)); } return new KeyboardConfigurationEvent(mInputDevice, mIsFirstConfiguration, configurationList); @@ -267,13 +280,18 @@ public final class KeyboardMetricsCollector { public final String keyboardLayoutName; @LayoutSelectionCriteria public final int layoutSelectionCriteria; + public final int imeLayoutType; + public final String imeLanguageTag; private LayoutConfiguration(int keyboardLayoutType, String keyboardLanguageTag, - String keyboardLayoutName, @LayoutSelectionCriteria int layoutSelectionCriteria) { + String keyboardLayoutName, @LayoutSelectionCriteria int layoutSelectionCriteria, + int imeLayoutType, String imeLanguageTag) { this.keyboardLayoutType = keyboardLayoutType; this.keyboardLanguageTag = keyboardLanguageTag; this.keyboardLayoutName = keyboardLayoutName; this.layoutSelectionCriteria = layoutSelectionCriteria; + this.imeLayoutType = imeLayoutType; + this.imeLanguageTag = imeLanguageTag; } @Override @@ -281,7 +299,9 @@ public final class KeyboardMetricsCollector { return "{keyboardLanguageTag = " + keyboardLanguageTag + " keyboardLayoutType = " + KeyboardLayout.LayoutType.getLayoutNameFromValue(keyboardLayoutType) + " keyboardLayoutName = " + keyboardLayoutName + " layoutSelectionCriteria = " - + getStringForSelectionCriteria(layoutSelectionCriteria) + "}"; + + getStringForSelectionCriteria(layoutSelectionCriteria) + + "imeLanguageTag = " + imeLanguageTag + " imeLayoutType = " + + KeyboardLayout.LayoutType.getLayoutNameFromValue(imeLayoutType) + "}"; } } @@ -294,6 +314,8 @@ public final class KeyboardMetricsCollector { return "LAYOUT_SELECTION_CRITERIA_DEVICE"; case LAYOUT_SELECTION_CRITERIA_VIRTUAL_KEYBOARD: return "LAYOUT_SELECTION_CRITERIA_VIRTUAL_KEYBOARD"; + case LAYOUT_SELECTION_CRITERIA_DEFAULT: + return "LAYOUT_SELECTION_CRITERIA_DEFAULT"; default: return "INVALID_CRITERIA"; } @@ -302,7 +324,8 @@ public final class KeyboardMetricsCollector { private static boolean isValidSelectionCriteria(int layoutSelectionCriteria) { return layoutSelectionCriteria == LAYOUT_SELECTION_CRITERIA_USER || layoutSelectionCriteria == LAYOUT_SELECTION_CRITERIA_DEVICE - || layoutSelectionCriteria == LAYOUT_SELECTION_CRITERIA_VIRTUAL_KEYBOARD; + || layoutSelectionCriteria == LAYOUT_SELECTION_CRITERIA_VIRTUAL_KEYBOARD + || layoutSelectionCriteria == LAYOUT_SELECTION_CRITERIA_DEFAULT; } } diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index 3325ddd58330..6df38098205a 100644 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -322,7 +322,6 @@ import com.android.server.notification.toast.ToastRecord; import com.android.server.pm.PackageManagerService; import com.android.server.pm.UserManagerInternal; import com.android.server.policy.PermissionPolicyInternal; -import com.android.server.powerstats.StatsPullAtomCallbackImpl; import com.android.server.statusbar.StatusBarManagerInternal; import com.android.server.uri.UriGrantsManagerInternal; import com.android.server.utils.Slogf; @@ -1715,7 +1714,6 @@ public class NotificationManagerService extends SystemService { return; } - boolean queryRestart = false; boolean queryRemove = false; boolean packageChanged = false; boolean cancelNotifications = true; @@ -1727,7 +1725,6 @@ public class NotificationManagerService extends SystemService { || (queryRemove=action.equals(Intent.ACTION_PACKAGE_REMOVED)) || action.equals(Intent.ACTION_PACKAGE_RESTARTED) || (packageChanged=action.equals(Intent.ACTION_PACKAGE_CHANGED)) - || (queryRestart=action.equals(Intent.ACTION_QUERY_PACKAGE_RESTART)) || action.equals(Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE) || action.equals(Intent.ACTION_PACKAGES_SUSPENDED) || action.equals(Intent.ACTION_PACKAGES_UNSUSPENDED) @@ -1768,10 +1765,6 @@ public class NotificationManagerService extends SystemService { cancelNotifications = false; unhideNotifications = true; } - - } else if (queryRestart) { - pkgList = intent.getStringArrayExtra(Intent.EXTRA_PACKAGES); - uidList = new int[] {intent.getIntExtra(Intent.EXTRA_UID, -1)}; } else { Uri uri = intent.getData(); if (uri == null) { @@ -1809,7 +1802,7 @@ public class NotificationManagerService extends SystemService { if (cancelNotifications) { for (String pkgName : pkgList) { cancelAllNotificationsInt(MY_UID, MY_PID, pkgName, null, 0, 0, - !queryRestart, changeUserId, reason, null); + changeUserId, reason); } } else if (hideNotifications && uidList != null && (uidList.length > 0)) { hideNotificationsForPackages(pkgList, uidList); @@ -1843,14 +1836,14 @@ public class NotificationManagerService extends SystemService { } else if (action.equals(Intent.ACTION_USER_STOPPED)) { int userHandle = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1); if (userHandle >= 0) { - cancelAllNotificationsInt(MY_UID, MY_PID, null, null, 0, 0, true, userHandle, - REASON_USER_STOPPED, null); + cancelAllNotificationsInt(MY_UID, MY_PID, null, null, 0, 0, userHandle, + REASON_USER_STOPPED); } } else if (action.equals(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE)) { int userHandle = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1); if (userHandle >= 0 && !mDpm.isKeepProfilesRunningEnabled()) { - cancelAllNotificationsInt(MY_UID, MY_PID, null, null, 0, 0, true, userHandle, - REASON_PROFILE_TURNED_OFF, null); + cancelAllNotificationsInt(MY_UID, MY_PID, null, null, 0, 0, userHandle, + REASON_PROFILE_TURNED_OFF); mSnoozeHelper.clearData(userHandle); } } else if (action.equals(Intent.ACTION_USER_PRESENT)) { @@ -2468,7 +2461,6 @@ public class NotificationManagerService extends SystemService { pkgFilter.addAction(Intent.ACTION_PACKAGE_REMOVED); pkgFilter.addAction(Intent.ACTION_PACKAGE_CHANGED); pkgFilter.addAction(Intent.ACTION_PACKAGE_RESTARTED); - pkgFilter.addAction(Intent.ACTION_QUERY_PACKAGE_RESTART); pkgFilter.addDataScheme("package"); getContext().registerReceiverAsUser(mPackageIntentReceiver, UserHandle.ALL, pkgFilter, null, null); @@ -2499,6 +2491,16 @@ public class NotificationManagerService extends SystemService { getContext().registerReceiver(mReviewNotificationPermissionsReceiver, ReviewNotificationPermissionsReceiver.getFilter(), Context.RECEIVER_NOT_EXPORTED); + + mAppOps.startWatchingMode(AppOpsManager.OP_POST_NOTIFICATION, null, + new AppOpsManager.OnOpChangedInternalListener() { + @Override + public void onOpChanged(@NonNull String op, @NonNull String packageName, + int userId) { + mHandler.post( + () -> handleNotificationPermissionChange(packageName, userId)); + } + }); } /** @@ -2855,17 +2857,17 @@ public class NotificationManagerService extends SystemService { boolean fromListener) { if (channel.getImportance() == NotificationManager.IMPORTANCE_NONE) { // cancel - cancelAllNotificationsInt(MY_UID, MY_PID, pkg, channel.getId(), 0, 0, true, - UserHandle.getUserId(uid), REASON_CHANNEL_BANNED, - null); + cancelAllNotificationsInt(MY_UID, MY_PID, pkg, channel.getId(), 0, 0, + UserHandle.getUserId(uid), REASON_CHANNEL_BANNED + ); if (isUidSystemOrPhone(uid)) { IntArray profileIds = mUserProfiles.getCurrentProfileIds(); int N = profileIds.size(); for (int i = 0; i < N; i++) { int profileId = profileIds.get(i); - cancelAllNotificationsInt(MY_UID, MY_PID, pkg, channel.getId(), 0, 0, true, - profileId, REASON_CHANNEL_BANNED, - null); + cancelAllNotificationsInt(MY_UID, MY_PID, pkg, channel.getId(), 0, 0, + profileId, REASON_CHANNEL_BANNED + ); } } } @@ -3539,7 +3541,7 @@ public class NotificationManagerService extends SystemService { // Don't allow the app to cancel active FGS or UIJ notifications cancelAllNotificationsInt(Binder.getCallingUid(), Binder.getCallingPid(), pkg, null, 0, FLAG_FOREGROUND_SERVICE | FLAG_USER_INITIATED_JOB, - true, userId, REASON_APP_CANCEL_ALL, null); + userId, REASON_APP_CANCEL_ALL); } @Override @@ -3558,20 +3560,16 @@ public class NotificationManagerService extends SystemService { } mPermissionHelper.setNotificationPermission( pkg, UserHandle.getUserId(uid), enabled, true); - sendAppBlockStateChangedBroadcast(pkg, uid, !enabled); mMetricsLogger.write(new LogMaker(MetricsEvent.ACTION_BAN_APP_NOTES) .setType(MetricsEvent.TYPE_ACTION) .setPackageName(pkg) .setSubtype(enabled ? 1 : 0)); mNotificationChannelLogger.logAppNotificationsAllowed(uid, pkg, enabled); - // Now, cancel any outstanding notifications that are part of a just-disabled app - if (!enabled) { - cancelAllNotificationsInt(MY_UID, MY_PID, pkg, null, 0, 0, true, - UserHandle.getUserId(uid), REASON_PACKAGE_BANNED, null); - } - handleSavePolicyFile(); + // Outstanding notifications from this package will be cancelled, and the package will + // be sent the ACTION_APP_BLOCK_STATE_CHANGED broadcast, as soon as we get the + // callback from AppOpsManager. } /** @@ -4030,8 +4028,8 @@ public class NotificationManagerService extends SystemService { } enforceDeletingChannelHasNoFgService(pkg, callingUser, channelId); enforceDeletingChannelHasNoUserInitiatedJob(pkg, callingUser, channelId); - cancelAllNotificationsInt(MY_UID, MY_PID, pkg, channelId, 0, 0, true, - callingUser, REASON_CHANNEL_REMOVED, null); + cancelAllNotificationsInt(MY_UID, MY_PID, pkg, channelId, 0, 0, + callingUser, REASON_CHANNEL_REMOVED); boolean previouslyExisted = mPreferencesHelper.deleteNotificationChannel( pkg, callingUid, channelId, callingUid, isSystemOrSystemUi); if (previouslyExisted) { @@ -4085,9 +4083,8 @@ public class NotificationManagerService extends SystemService { for (int i = 0; i < deletedChannels.size(); i++) { final NotificationChannel deletedChannel = deletedChannels.get(i); cancelAllNotificationsInt(MY_UID, MY_PID, pkg, deletedChannel.getId(), 0, 0, - true, - userId, REASON_CHANNEL_REMOVED, - null); + userId, REASON_CHANNEL_REMOVED + ); mListeners.notifyNotificationChannelChanged(pkg, UserHandle.getUserHandleForUid(callingUid), deletedChannel, @@ -4256,8 +4253,8 @@ public class NotificationManagerService extends SystemService { checkCallerIsSystem(); // Cancel posted notifications final int userId = UserHandle.getUserId(uid); - cancelAllNotificationsInt(MY_UID, MY_PID, packageName, null, 0, 0, true, - UserHandle.getUserId(Binder.getCallingUid()), REASON_CLEAR_DATA, null); + cancelAllNotificationsInt(MY_UID, MY_PID, packageName, null, 0, 0, + UserHandle.getUserId(Binder.getCallingUid()), REASON_CLEAR_DATA); // Zen packagesChanged |= @@ -5895,6 +5892,21 @@ public class NotificationManagerService extends SystemService { } }; + private void handleNotificationPermissionChange(String pkg, @UserIdInt int userId) { + int uid = mPackageManagerInternal.getPackageUid(pkg, 0, userId); + if (uid == INVALID_UID) { + Log.e(TAG, String.format("No uid found for %s, %s!", pkg, userId)); + return; + } + boolean hasPermission = mPermissionHelper.hasPermission(uid); + sendAppBlockStateChangedBroadcast(pkg, uid, !hasPermission); + if (!hasPermission) { + cancelAllNotificationsInt(MY_UID, MY_PID, pkg, /* channelId= */ null, + /* mustHaveFlags= */ 0, /* mustNotHaveFlags= */ 0, userId, + REASON_PACKAGE_BANNED); + } + } + protected void checkNotificationListenerAccess() { if (!isCallerSystemOrPhone()) { getContext().enforceCallingPermission( @@ -6831,9 +6843,9 @@ public class NotificationManagerService extends SystemService { mPreferencesHelper.deleteConversations(pkg, uid, shortcuts, /* callingUid */ Process.SYSTEM_UID, /* is system */ true); for (String channelId : deletedChannelIds) { - cancelAllNotificationsInt(MY_UID, MY_PID, pkg, channelId, 0, 0, true, - UserHandle.getUserId(uid), REASON_CHANNEL_REMOVED, - null); + cancelAllNotificationsInt(MY_UID, MY_PID, pkg, channelId, 0, 0, + UserHandle.getUserId(uid), REASON_CHANNEL_REMOVED + ); } handleSavePolicyFile(); } @@ -9535,25 +9547,18 @@ public class NotificationManagerService extends SystemService { /** * Cancels all notifications from a given package that have all of the - * {@code mustHaveFlags}. + * {@code mustHaveFlags} and none of the {@code mustNotHaveFlags}. */ - void cancelAllNotificationsInt(int callingUid, int callingPid, String pkg, String channelId, - int mustHaveFlags, int mustNotHaveFlags, boolean doit, int userId, int reason, - ManagedServiceInfo listener) { + void cancelAllNotificationsInt(int callingUid, int callingPid, String pkg, + @Nullable String channelId, int mustHaveFlags, int mustNotHaveFlags, int userId, + int reason) { final long cancellationElapsedTimeMs = SystemClock.elapsedRealtime(); mHandler.post(new Runnable() { @Override public void run() { - String listenerName = listener == null ? null : listener.component.toShortString(); EventLogTags.writeNotificationCancelAll(callingUid, callingPid, pkg, userId, mustHaveFlags, mustNotHaveFlags, reason, - listenerName); - - // Why does this parameter exist? Do we actually want to execute the above if doit - // is false? - if (!doit) { - return; - } + /* listener= */ null); synchronized (mNotificationLock) { FlagChecker flagChecker = (int flags) -> { @@ -9565,14 +9570,15 @@ public class NotificationManagerService extends SystemService { } return true; }; - cancelAllNotificationsByListLocked(mNotificationList, callingUid, callingPid, - pkg, true /*nullPkgIndicatesUserSwitch*/, channelId, flagChecker, + cancelAllNotificationsByListLocked(mNotificationList, pkg, + true /*nullPkgIndicatesUserSwitch*/, channelId, flagChecker, + false /*includeCurrentProfiles*/, userId, false /*sendDelete*/, reason, + null /* listenerName */, true /* wasPosted */, + cancellationElapsedTimeMs); + cancelAllNotificationsByListLocked(mEnqueuedNotifications, pkg, + true /*nullPkgIndicatesUserSwitch*/, channelId, flagChecker, false /*includeCurrentProfiles*/, userId, false /*sendDelete*/, reason, - listenerName, true /* wasPosted */, cancellationElapsedTimeMs); - cancelAllNotificationsByListLocked(mEnqueuedNotifications, callingUid, - callingPid, pkg, true /*nullPkgIndicatesUserSwitch*/, channelId, - flagChecker, false /*includeCurrentProfiles*/, userId, - false /*sendDelete*/, reason, listenerName, false /* wasPosted */, + null /* listenerName */, false /* wasPosted */, cancellationElapsedTimeMs); mSnoozeHelper.cancel(userId, pkg); } @@ -9587,9 +9593,9 @@ public class NotificationManagerService extends SystemService { @GuardedBy("mNotificationLock") private void cancelAllNotificationsByListLocked(ArrayList<NotificationRecord> notificationList, - int callingUid, int callingPid, String pkg, boolean nullPkgIndicatesUserSwitch, - String channelId, FlagChecker flagChecker, boolean includeCurrentProfiles, int userId, - boolean sendDelete, int reason, String listenerName, boolean wasPosted, + @Nullable String pkg, boolean nullPkgIndicatesUserSwitch, @Nullable String channelId, + FlagChecker flagChecker, boolean includeCurrentProfiles, int userId, boolean sendDelete, + int reason, String listenerName, boolean wasPosted, @ElapsedRealtimeLong long cancellationElapsedTimeMs) { Set<String> childNotifications = null; for (int i = notificationList.size() - 1; i >= 0; --i) { @@ -9706,12 +9712,12 @@ public class NotificationManagerService extends SystemService { return true; }; - cancelAllNotificationsByListLocked(mNotificationList, callingUid, callingPid, + cancelAllNotificationsByListLocked(mNotificationList, null, false /*nullPkgIndicatesUserSwitch*/, null, flagChecker, includeCurrentProfiles, userId, true /*sendDelete*/, reason, listenerName, true, cancellationElapsedTimeMs); - cancelAllNotificationsByListLocked(mEnqueuedNotifications, callingUid, - callingPid, null, false /*nullPkgIndicatesUserSwitch*/, null, + cancelAllNotificationsByListLocked(mEnqueuedNotifications, + null, false /*nullPkgIndicatesUserSwitch*/, null, flagChecker, includeCurrentProfiles, userId, true /*sendDelete*/, reason, listenerName, false, cancellationElapsedTimeMs); mSnoozeHelper.cancel(userId, includeCurrentProfiles); diff --git a/services/core/java/com/android/server/utils/FoldSettingWrapper.java b/services/core/java/com/android/server/utils/FoldSettingWrapper.java new file mode 100644 index 000000000000..97a1ac06e24c --- /dev/null +++ b/services/core/java/com/android/server/utils/FoldSettingWrapper.java @@ -0,0 +1,48 @@ +/* + * 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.utils; + +import android.content.ContentResolver; +import android.provider.Settings; + +/** + * A wrapper class for the {@link Settings.System#STAY_AWAKE_ON_FOLD} setting. + * + * This class provides a convenient way to access the {@link Settings.System#STAY_AWAKE_ON_FOLD} + * setting for testing. + */ +public class FoldSettingWrapper { + private final ContentResolver mContentResolver; + + public FoldSettingWrapper(ContentResolver contentResolver) { + mContentResolver = contentResolver; + } + + /** + * Returns whether the device should remain awake after folding. + */ + public boolean shouldStayAwakeOnFold() { + try { + return (Settings.System.getIntForUser( + mContentResolver, + Settings.System.STAY_AWAKE_ON_FOLD, + 0) == 1); + } catch (Settings.SettingNotFoundException e) { + return false; + } + } +} diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 0994fa4464db..dd6bcb1060ea 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -5660,13 +5660,6 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A final DisplayContent displayContent = getDisplayContent(); if (!visible) { mImeInsetsFrozenUntilStartInput = true; - if (usingShellTransitions) { - final WindowState wallpaperTarget = - displayContent.mWallpaperController.getWallpaperTarget(); - if (wallpaperTarget != null && wallpaperTarget.mActivityRecord == this) { - displayContent.mWallpaperController.hideWallpapers(wallpaperTarget); - } - } } if (!displayContent.mClosingApps.contains(this) diff --git a/services/core/java/com/android/server/wm/AsyncRotationController.java b/services/core/java/com/android/server/wm/AsyncRotationController.java index 01158779c24f..4ce21bdadf49 100644 --- a/services/core/java/com/android/server/wm/AsyncRotationController.java +++ b/services/core/java/com/android/server/wm/AsyncRotationController.java @@ -93,15 +93,12 @@ class AsyncRotationController extends FadeAnimationController implements Consume /** Whether the start transaction of the transition is committed (by shell). */ private boolean mIsStartTransactionCommitted; - /** Whether all windows should wait for the start transaction. */ - private boolean mAlwaysWaitForStartTransaction; - /** Whether the target windows have been requested to sync their draw transactions. */ private boolean mIsSyncDrawRequested; private SeamlessRotator mRotator; - private final int mOriginalRotation; + private int mOriginalRotation; private final boolean mHasScreenRotationAnimation; AsyncRotationController(DisplayContent displayContent) { @@ -147,15 +144,6 @@ class AsyncRotationController extends FadeAnimationController implements Consume if (mTransitionOp == OP_LEGACY) { mIsStartTransactionCommitted = true; } else if (displayContent.mTransitionController.isCollecting(displayContent)) { - final Transition transition = - mDisplayContent.mTransitionController.getCollectingTransition(); - if (transition != null) { - final BLASTSyncEngine.SyncGroup syncGroup = - mDisplayContent.mWmService.mSyncEngine.getSyncSet(transition.getSyncId()); - if (syncGroup != null && syncGroup.mSyncMethod == BLASTSyncEngine.METHOD_BLAST) { - mAlwaysWaitForStartTransaction = true; - } - } keepAppearanceInPreviousRotation(); } } @@ -279,10 +267,12 @@ class AsyncRotationController extends FadeAnimationController implements Consume // The previous animation leash will be dropped when preparing fade-in animation, so // simply apply new animation without restoring the transformation. fadeWindowToken(true /* show */, windowToken, ANIMATION_TYPE_TOKEN_TRANSFORM); - } else if (op.mAction == Operation.ACTION_SEAMLESS && mRotator != null + } else if (op.mAction == Operation.ACTION_SEAMLESS && op.mLeash != null && op.mLeash.isValid()) { if (DEBUG) Slog.d(TAG, "finishOp undo seamless " + windowToken.getTopChild()); - mRotator.setIdentityMatrix(windowToken.getSyncTransaction(), op.mLeash); + final SurfaceControl.Transaction t = windowToken.getSyncTransaction(); + t.setMatrix(op.mLeash, 1, 0, 0, 1); + t.setPosition(op.mLeash, 0, 0); } } @@ -365,6 +355,32 @@ class AsyncRotationController extends FadeAnimationController implements Consume } } + /** + * Re-initialize the states if the current display rotation has changed to a different rotation. + * This is mainly for seamless rotation to update the transform based on new rotation. + */ + void updateRotation() { + if (mRotator == null) return; + final int currentRotation = mDisplayContent.getWindowConfiguration().getRotation(); + if (mOriginalRotation == currentRotation) { + return; + } + Slog.d(TAG, "Update original rotation " + currentRotation); + mOriginalRotation = currentRotation; + mDisplayContent.forAllWindows(w -> { + if (w.mForceSeamlesslyRotate && w.mHasSurface + && !mTargetWindowTokens.containsKey(w.mToken)) { + final Operation op = new Operation(Operation.ACTION_SEAMLESS); + op.mLeash = w.mToken.mSurfaceControl; + mTargetWindowTokens.put(w.mToken, op); + } + }, true /* traverseTopToBottom */); + mRotator = null; + mIsStartTransactionCommitted = false; + mIsSyncDrawRequested = false; + keepAppearanceInPreviousRotation(); + } + private void scheduleTimeout() { if (mTimeoutRunnable == null) { mTimeoutRunnable = () -> { @@ -589,7 +605,7 @@ class AsyncRotationController extends FadeAnimationController implements Consume * start transaction of rotation transition is applied. */ private boolean canDrawBeforeStartTransaction(Operation op) { - return !mAlwaysWaitForStartTransaction && op.mAction != Operation.ACTION_SEAMLESS; + return op.mAction != Operation.ACTION_SEAMLESS; } /** The operation to control the rotation appearance associated with window token. */ diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index bfd2a10a8882..cb7414e2e86e 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -3457,6 +3457,11 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp this, this, null /* remoteTransition */, displayChange); if (t != null) { mAtmService.startLaunchPowerMode(POWER_MODE_REASON_CHANGE_DISPLAY); + if (mAsyncRotationController != null) { + // Give a chance to update the transform if the current rotation is changed when + // some windows haven't finished previous rotation. + mAsyncRotationController.updateRotation(); + } if (mFixedRotationLaunchingApp != null) { // A fixed-rotation transition is done, then continue to start a seamless display // transition. diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java index 0c7d007dc48a..71192cd5a3be 100644 --- a/services/core/java/com/android/server/wm/Transition.java +++ b/services/core/java/com/android/server/wm/Transition.java @@ -1191,16 +1191,6 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { hasParticipatedDisplay = true; continue; } - final WallpaperWindowToken wt = participant.asWallpaperToken(); - if (wt != null) { - final boolean visibleAtTransitionEnd = mVisibleAtTransitionEndTokens.contains(wt); - if (!visibleAtTransitionEnd && !wt.isVisibleRequested()) { - ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, - " Commit wallpaper becoming invisible: %s", wt); - wt.commitVisibility(false /* visible */); - } - continue; - } final Task tr = participant.asTask(); if (tr != null && tr.isVisibleRequested() && tr.inPinnedWindowingMode()) { final ActivityRecord top = tr.getTopNonFinishingActivity(); @@ -1220,6 +1210,20 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { } } } + // Commit wallpaper visibility after activity, because usually the wallpaper target token is + // an activity, and wallpaper's visibility is depends on activity's visibility. + for (int i = mParticipants.size() - 1; i >= 0; --i) { + final WallpaperWindowToken wt = mParticipants.valueAt(i).asWallpaperToken(); + if (wt == null) continue; + final WindowState target = wt.mDisplayContent.mWallpaperController.getWallpaperTarget(); + final boolean isTargetInvisible = target == null || !target.mToken.isVisible(); + if (isTargetInvisible || (!wt.isVisibleRequested() + && !mVisibleAtTransitionEndTokens.contains(wt))) { + ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, + " Commit wallpaper becoming invisible: %s", wt); + wt.commitVisibility(false /* visible */); + } + } if (committedSomeInvisible) { mController.onCommittedInvisibles(); } diff --git a/services/flags/Android.bp b/services/flags/Android.bp new file mode 100644 index 000000000000..2d0337dce74f --- /dev/null +++ b/services/flags/Android.bp @@ -0,0 +1,17 @@ +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", + ], + libs: ["services.core"], +} diff --git a/services/flags/OWNERS b/services/flags/OWNERS new file mode 100644 index 000000000000..3925b5c13c2d --- /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 000000000000..0db328792cf3 --- /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 000000000000..1fa85325aea6 --- /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 000000000000..93b9e9e0dc8c --- /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 000000000000..cee1578a5dde --- /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 000000000000..b1ddc7e67f68 --- /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 000000000000..b7896ee18714 --- /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 000000000000..acb7bb5a49db --- /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 000000000000..c6e85d5d1dc8 --- /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 6960da74dd1a..5f45485d36f4 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -132,6 +132,7 @@ import com.android.server.display.DisplayManagerService; 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; @@ -1115,6 +1116,12 @@ public final class SystemServer implements Dumpable { 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/tests/displayservicetests/src/com/android/server/display/LogicalDisplayMapperTest.java b/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayMapperTest.java index 3e695c9407b6..0fe6e64b3b54 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayMapperTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayMapperTest.java @@ -65,6 +65,7 @@ import androidx.test.filters.SmallTest; import com.android.server.display.layout.DisplayIdProducer; import com.android.server.display.layout.Layout; +import com.android.server.utils.FoldSettingWrapper; import org.junit.Before; import org.junit.Test; @@ -100,6 +101,7 @@ public class LogicalDisplayMapperTest { @Mock LogicalDisplayMapper.Listener mListenerMock; @Mock Context mContextMock; + @Mock FoldSettingWrapper mFoldSettingWrapperMock; @Mock Resources mResourcesMock; @Mock IPowerManager mIPowerManagerMock; @Mock IThermalService mIThermalServiceMock; @@ -139,6 +141,7 @@ public class LogicalDisplayMapperTest { when(mContextMock.getSystemServiceName(PowerManager.class)) .thenReturn(Context.POWER_SERVICE); + when(mFoldSettingWrapperMock.shouldStayAwakeOnFold()).thenReturn(false); when(mContextMock.getSystemService(PowerManager.class)).thenReturn(mPowerManager); when(mContextMock.getResources()).thenReturn(mResourcesMock); when(mResourcesMock.getBoolean( @@ -155,7 +158,7 @@ public class LogicalDisplayMapperTest { mHandler = new Handler(mLooper.getLooper()); mLogicalDisplayMapper = new LogicalDisplayMapper(mContextMock, mDisplayDeviceRepo, mListenerMock, new DisplayManagerService.SyncRoot(), mHandler, - mDeviceStateToLayoutMapSpy); + mDeviceStateToLayoutMapSpy, mFoldSettingWrapperMock); } @@ -571,6 +574,17 @@ public class LogicalDisplayMapperTest { } @Test + public void testDeviceShouldNotSleepWhenFoldSettingTrue() { + when(mFoldSettingWrapperMock.shouldStayAwakeOnFold()).thenReturn(true); + + assertFalse(mLogicalDisplayMapper.shouldDeviceBePutToSleep(DEVICE_STATE_CLOSED, + DEVICE_STATE_OPEN, + /* isOverrideActive= */false, + /* isInteractive= */true, + /* isBootCompleted= */true)); + } + + @Test public void testDeviceShouldNotBePutToSleep() { assertFalse(mLogicalDisplayMapper.shouldDeviceBePutToSleep(DEVICE_STATE_OPEN, DEVICE_STATE_CLOSED, @@ -978,4 +992,3 @@ public class LogicalDisplayMapperTest { } } } - diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/DisplayBrightnessStrategySelectorTest.java b/services/tests/displayservicetests/src/com/android/server/display/brightness/DisplayBrightnessStrategySelectorTest.java index a9e616d766c6..8497dabba67d 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/brightness/DisplayBrightnessStrategySelectorTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/DisplayBrightnessStrategySelectorTest.java @@ -18,17 +18,23 @@ package com.android.server.display.brightness; import static org.junit.Assert.assertEquals; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.when; +import android.content.ContentResolver; import android.content.Context; +import android.content.ContextWrapper; import android.content.res.Resources; import android.hardware.display.DisplayManagerInternal; import android.view.Display; +import androidx.test.core.app.ApplicationProvider; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; import com.android.internal.R; +import com.android.internal.util.test.FakeSettingsProvider; +import com.android.internal.util.test.FakeSettingsProviderRule; import com.android.server.display.brightness.strategy.BoostBrightnessStrategy; import com.android.server.display.brightness.strategy.DozeBrightnessStrategy; import com.android.server.display.brightness.strategy.FollowerBrightnessStrategy; @@ -38,6 +44,7 @@ import com.android.server.display.brightness.strategy.ScreenOffBrightnessStrateg import com.android.server.display.brightness.strategy.TemporaryBrightnessStrategy; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; @@ -64,15 +71,20 @@ public final class DisplayBrightnessStrategySelectorTest { @Mock private FollowerBrightnessStrategy mFollowerBrightnessStrategy; @Mock - private Context mContext; - @Mock private Resources mResources; private DisplayBrightnessStrategySelector mDisplayBrightnessStrategySelector; + private Context mContext; + + @Rule + public FakeSettingsProviderRule mSettingsProviderRule = FakeSettingsProvider.rule(); @Before public void before() { MockitoAnnotations.initMocks(this); + mContext = spy(new ContextWrapper(ApplicationProvider.getApplicationContext())); + ContentResolver contentResolver = mSettingsProviderRule.mockContentResolver(mContext); + when(mContext.getContentResolver()).thenReturn(contentResolver); when(mContext.getResources()).thenReturn(mResources); when(mInvalidBrightnessStrategy.getName()).thenReturn("InvalidBrightnessStrategy"); DisplayBrightnessStrategySelector.Injector injector = diff --git a/services/tests/mockingservicestests/src/com/android/server/display/DisplayBrightnessStateTest.java b/services/tests/mockingservicestests/src/com/android/server/display/DisplayBrightnessStateTest.java index 50996d7199c8..95c62aeec19a 100644 --- a/services/tests/mockingservicestests/src/com/android/server/display/DisplayBrightnessStateTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/display/DisplayBrightnessStateTest.java @@ -43,25 +43,50 @@ public class DisplayBrightnessStateTest { public void validateAllDisplayBrightnessStateFieldsAreSetAsExpected() { float brightness = 0.3f; float sdrBrightness = 0.2f; + boolean shouldUseAutoBrightness = true; BrightnessReason brightnessReason = new BrightnessReason(); brightnessReason.setReason(BrightnessReason.REASON_AUTOMATIC); brightnessReason.setModifier(BrightnessReason.MODIFIER_DIMMED); - DisplayBrightnessState displayBrightnessState = - mDisplayBrightnessStateBuilder.setBrightness(brightness).setSdrBrightness( - sdrBrightness).setBrightnessReason(brightnessReason).build(); + DisplayBrightnessState displayBrightnessState = mDisplayBrightnessStateBuilder + .setBrightness(brightness) + .setSdrBrightness(sdrBrightness) + .setBrightnessReason(brightnessReason) + .setShouldUseAutoBrightness(shouldUseAutoBrightness) + .build(); assertEquals(displayBrightnessState.getBrightness(), brightness, FLOAT_DELTA); assertEquals(displayBrightnessState.getSdrBrightness(), sdrBrightness, FLOAT_DELTA); assertEquals(displayBrightnessState.getBrightnessReason(), brightnessReason); + assertEquals(displayBrightnessState.getShouldUseAutoBrightness(), shouldUseAutoBrightness); assertEquals(displayBrightnessState.toString(), getString(displayBrightnessState)); } + @Test + public void testFrom() { + BrightnessReason reason = new BrightnessReason(); + reason.setReason(BrightnessReason.REASON_MANUAL); + reason.setModifier(BrightnessReason.MODIFIER_DIMMED); + DisplayBrightnessState state1 = new DisplayBrightnessState.Builder() + .setBrightnessReason(reason) + .setBrightness(0.26f) + .setSdrBrightness(0.23f) + .setShouldUseAutoBrightness(false) + .build(); + DisplayBrightnessState state2 = DisplayBrightnessState.Builder.from(state1).build(); + assertEquals(state1, state2); + } + private String getString(DisplayBrightnessState displayBrightnessState) { StringBuilder sb = new StringBuilder(); - sb.append("DisplayBrightnessState:"); - sb.append("\n brightness:" + displayBrightnessState.getBrightness()); - sb.append("\n sdrBrightness:" + displayBrightnessState.getSdrBrightness()); - sb.append("\n brightnessReason:" + displayBrightnessState.getBrightnessReason()); + sb.append("DisplayBrightnessState:") + .append("\n brightness:") + .append(displayBrightnessState.getBrightness()) + .append("\n sdrBrightness:") + .append(displayBrightnessState.getSdrBrightness()) + .append("\n brightnessReason:") + .append(displayBrightnessState.getBrightnessReason()) + .append("\n shouldUseAutoBrightness:") + .append(displayBrightnessState.getShouldUseAutoBrightness()); return sb.toString(); } } diff --git a/services/tests/mockingservicestests/src/com/android/server/display/DisplayPowerController2Test.java b/services/tests/mockingservicestests/src/com/android/server/display/DisplayPowerController2Test.java index aaab4033d579..c710d1c3885d 100644 --- a/services/tests/mockingservicestests/src/com/android/server/display/DisplayPowerController2Test.java +++ b/services/tests/mockingservicestests/src/com/android/server/display/DisplayPowerController2Test.java @@ -714,6 +714,8 @@ public final class DisplayPowerController2Test { Settings.System.SCREEN_BRIGHTNESS_MODE, Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC); + when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_OFF); + DisplayPowerRequest dpr = new DisplayPowerRequest(); dpr.policy = DisplayPowerRequest.POLICY_OFF; mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false); @@ -751,6 +753,7 @@ public final class DisplayPowerController2Test { Settings.System.SCREEN_BRIGHTNESS_MODE, Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC); + when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_DOZE); DisplayPowerRequest dpr = new DisplayPowerRequest(); dpr.policy = DisplayPowerRequest.POLICY_DOZE; mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false); diff --git a/services/tests/servicestests/Android.bp b/services/tests/servicestests/Android.bp index ee3a773580a3..0530f892a48e 100644 --- a/services/tests/servicestests/Android.bp +++ b/services/tests/servicestests/Android.bp @@ -34,6 +34,7 @@ android_test { "services.core", "services.credentials", "services.devicepolicy", + "services.flags", "services.net", "services.people", "services.usage", diff --git a/services/tests/servicestests/src/com/android/server/biometrics/AuthSessionTest.java b/services/tests/servicestests/src/com/android/server/biometrics/AuthSessionTest.java index 8346050c3c89..0cfddd30d721 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/AuthSessionTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/AuthSessionTest.java @@ -106,7 +106,7 @@ public class AuthSessionTest { @Mock private KeyStore mKeyStore; @Mock private AuthSession.ClientDeathReceiver mClientDeathReceiver; @Mock private BiometricFrameworkStatsLogger mBiometricFrameworkStatsLogger; - @Mock BiometricSensorPrivacy mBiometricSensorPrivacy; + @Mock private BiometricCameraManager mBiometricCameraManager; private Random mRandom; private IBinder mToken; @@ -609,7 +609,7 @@ public class AuthSessionTest { TEST_PACKAGE, checkDevicePolicyManager, mContext, - mBiometricSensorPrivacy); + mBiometricCameraManager); } private AuthSession createAuthSession(List<BiometricSensor> sensors, diff --git a/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java b/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java index 41f7dbcb0ff5..68217219e453 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java @@ -151,6 +151,8 @@ public class BiometricServiceTest { private AuthSessionCoordinator mAuthSessionCoordinator; @Mock private UserManager mUserManager; + @Mock + private BiometricCameraManager mBiometricCameraManager; BiometricContextProvider mBiometricContextProvider; @@ -177,6 +179,7 @@ public class BiometricServiceTest { when(mInjector.getDevicePolicyManager(any())).thenReturn(mDevicePolicyManager); when(mInjector.getRequestGenerator()).thenReturn(() -> TEST_REQUEST_ID); when(mInjector.getUserManager(any())).thenReturn(mUserManager); + when(mInjector.getBiometricCameraManager(any())).thenReturn(mBiometricCameraManager); when(mResources.getString(R.string.biometric_error_hw_unavailable)) .thenReturn(ERROR_HW_UNAVAILABLE); diff --git a/services/tests/servicestests/src/com/android/server/biometrics/PreAuthInfoTest.java b/services/tests/servicestests/src/com/android/server/biometrics/PreAuthInfoTest.java index 0c98c8d88d83..c2bdf501198e 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/PreAuthInfoTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/PreAuthInfoTest.java @@ -67,7 +67,7 @@ public class PreAuthInfoTest { @Mock BiometricService.SettingObserver mSettingObserver; @Mock - BiometricSensorPrivacy mBiometricSensorPrivacyUtil; + BiometricCameraManager mBiometricCameraManager; @Before public void setup() throws RemoteException { @@ -79,11 +79,13 @@ public class PreAuthInfoTest { when(mFaceAuthenticator.isHardwareDetected(any())).thenReturn(true); when(mFaceAuthenticator.getLockoutModeForUser(anyInt())) .thenReturn(LOCKOUT_NONE); + when(mBiometricCameraManager.isCameraPrivacyEnabled()).thenReturn(false); + when(mBiometricCameraManager.isAnyCameraUnavailable()).thenReturn(false); } @Test public void testFaceAuthentication_whenCameraPrivacyIsEnabled() throws Exception { - when(mBiometricSensorPrivacyUtil.isCameraPrivacyEnabled()).thenReturn(true); + when(mBiometricCameraManager.isCameraPrivacyEnabled()).thenReturn(true); BiometricSensor sensor = new BiometricSensor(mContext, SENSOR_ID_FACE, TYPE_FACE, BiometricManager.Authenticators.BIOMETRIC_STRONG, mFaceAuthenticator) { @@ -104,15 +106,14 @@ public class PreAuthInfoTest { PreAuthInfo preAuthInfo = PreAuthInfo.create(mTrustManager, mDevicePolicyManager, mSettingObserver, List.of(sensor), 0 /* userId */, promptInfo, TEST_PACKAGE_NAME, - false /* checkDevicePolicyManager */, mContext, mBiometricSensorPrivacyUtil); + false /* checkDevicePolicyManager */, mContext, mBiometricCameraManager); assertThat(preAuthInfo.eligibleSensors).isEmpty(); } @Test - public void testFaceAuthentication_whenCameraPrivacyIsDisabled() throws Exception { - when(mBiometricSensorPrivacyUtil.isCameraPrivacyEnabled()).thenReturn(false); - + public void testFaceAuthentication_whenCameraPrivacyIsDisabledAndCameraIsAvailable() + throws Exception { BiometricSensor sensor = new BiometricSensor(mContext, SENSOR_ID_FACE, TYPE_FACE, BiometricManager.Authenticators.BIOMETRIC_STRONG, mFaceAuthenticator) { @Override @@ -132,8 +133,35 @@ public class PreAuthInfoTest { PreAuthInfo preAuthInfo = PreAuthInfo.create(mTrustManager, mDevicePolicyManager, mSettingObserver, List.of(sensor), 0 /* userId */, promptInfo, TEST_PACKAGE_NAME, - false /* checkDevicePolicyManager */, mContext, mBiometricSensorPrivacyUtil); + false /* checkDevicePolicyManager */, mContext, mBiometricCameraManager); assertThat(preAuthInfo.eligibleSensors).hasSize(1); } + + @Test + public void testFaceAuthentication_whenCameraIsUnavailable() throws RemoteException { + when(mBiometricCameraManager.isAnyCameraUnavailable()).thenReturn(true); + BiometricSensor sensor = new BiometricSensor(mContext, SENSOR_ID_FACE, TYPE_FACE, + BiometricManager.Authenticators.BIOMETRIC_STRONG, mFaceAuthenticator) { + @Override + boolean confirmationAlwaysRequired(int userId) { + return false; + } + + @Override + boolean confirmationSupported() { + return false; + } + }; + PromptInfo promptInfo = new PromptInfo(); + promptInfo.setConfirmationRequested(false /* requireConfirmation */); + promptInfo.setAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG); + promptInfo.setDisallowBiometricsIfPolicyExists(false /* checkDevicePolicy */); + PreAuthInfo preAuthInfo = PreAuthInfo.create(mTrustManager, mDevicePolicyManager, + mSettingObserver, List.of(sensor), + 0 /* userId */, promptInfo, TEST_PACKAGE_NAME, + false /* checkDevicePolicyManager */, mContext, mBiometricCameraManager); + + assertThat(preAuthInfo.eligibleSensors).hasSize(0); + } } 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 000000000000..df4731fb0bb7 --- /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 000000000000..c2cf540d1d62 --- /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 000000000000..6cc3acfb6125 --- /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 000000000000..7ed369e37106 --- /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/input/KeyboardMetricsCollectorTests.kt b/services/tests/servicestests/src/com/android/server/input/KeyboardMetricsCollectorTests.kt index c9724a3b4309..a435d605d3f4 100644 --- a/services/tests/servicestests/src/com/android/server/input/KeyboardMetricsCollectorTests.kt +++ b/services/tests/servicestests/src/com/android/server/input/KeyboardMetricsCollectorTests.kt @@ -48,11 +48,11 @@ private fun createKeyboard( private fun createImeSubtype( imeSubtypeId: Int, - languageTag: String, + languageTag: ULocale?, layoutType: String ): InputMethodSubtype = InputMethodSubtype.InputMethodSubtypeBuilder().setSubtypeId(imeSubtypeId) - .setPhysicalKeyboardHint(ULocale.forLanguageTag(languageTag), layoutType).build() + .setPhysicalKeyboardHint(languageTag, layoutType).build() /** * Tests for {@link KeyboardMetricsCollector}. @@ -95,7 +95,8 @@ class KeyboardMetricsCollectorTests { null, null ) - ).addLayoutSelection(createImeSubtype(1, "en-US", "qwerty"), null, 123).build() + ).addLayoutSelection(createImeSubtype(1, ULocale.forLanguageTag("en-US"), "qwerty"), + null, 123).build() } } @@ -111,15 +112,19 @@ class KeyboardMetricsCollectorTests { ) ) val event = builder.addLayoutSelection( - createImeSubtype(1, "en-US", "qwerty"), + createImeSubtype(1, ULocale.forLanguageTag("en-US"), "qwerty"), KeyboardLayout(null, "English(US)(Qwerty)", null, 0, null, 0, 0, 0), KeyboardMetricsCollector.LAYOUT_SELECTION_CRITERIA_VIRTUAL_KEYBOARD ).addLayoutSelection( - createImeSubtype(2, "en-US", "azerty"), - null, + createImeSubtype(2, ULocale.forLanguageTag("en-US"), "azerty"), + null, // Default layout type KeyboardMetricsCollector.LAYOUT_SELECTION_CRITERIA_USER ).addLayoutSelection( - createImeSubtype(3, "en-US", "qwerty"), + createImeSubtype(3, ULocale.forLanguageTag("en-US"), "qwerty"), + KeyboardLayout(null, "German", null, 0, null, 0, 0, 0), + KeyboardMetricsCollector.LAYOUT_SELECTION_CRITERIA_DEVICE + ).addLayoutSelection( + createImeSubtype(4, null, "qwerty"), // Default language tag KeyboardLayout(null, "German", null, 0, null, 0, 0, 0), KeyboardMetricsCollector.LAYOUT_SELECTION_CRITERIA_DEVICE ).setIsFirstTimeConfiguration(true).build() @@ -137,43 +142,62 @@ class KeyboardMetricsCollectorTests { assertTrue(event.isFirstConfiguration) assertEquals( - "KeyboardConfigurationEvent should contain 3 configurations provided", - 3, + "KeyboardConfigurationEvent should contain 4 configurations provided", + 4, event.layoutConfigurations.size ) assertExpectedLayoutConfiguration( event.layoutConfigurations[0], + "de-CH", + KeyboardLayout.LayoutType.getLayoutTypeEnumValue("qwertz"), + "English(US)(Qwerty)", + KeyboardMetricsCollector.LAYOUT_SELECTION_CRITERIA_VIRTUAL_KEYBOARD, "en-US", KeyboardLayout.LayoutType.getLayoutTypeEnumValue("qwerty"), - "English(US)(Qwerty)", - KeyboardMetricsCollector.LAYOUT_SELECTION_CRITERIA_VIRTUAL_KEYBOARD ) assertExpectedLayoutConfiguration( event.layoutConfigurations[1], + "de-CH", + KeyboardLayout.LayoutType.getLayoutTypeEnumValue("qwertz"), + KeyboardMetricsCollector.DEFAULT_LAYOUT, + KeyboardMetricsCollector.LAYOUT_SELECTION_CRITERIA_USER, "en-US", KeyboardLayout.LayoutType.getLayoutTypeEnumValue("azerty"), - KeyboardMetricsCollector.DEFAULT_LAYOUT, - KeyboardMetricsCollector.LAYOUT_SELECTION_CRITERIA_USER ) assertExpectedLayoutConfiguration( event.layoutConfigurations[2], "de-CH", KeyboardLayout.LayoutType.getLayoutTypeEnumValue("qwertz"), "German", - KeyboardMetricsCollector.LAYOUT_SELECTION_CRITERIA_DEVICE + KeyboardMetricsCollector.LAYOUT_SELECTION_CRITERIA_DEVICE, + "en-US", + KeyboardLayout.LayoutType.getLayoutTypeEnumValue("qwerty"), + ) + assertExpectedLayoutConfiguration( + event.layoutConfigurations[3], + "de-CH", + KeyboardLayout.LayoutType.getLayoutTypeEnumValue("qwertz"), + "German", + KeyboardMetricsCollector.LAYOUT_SELECTION_CRITERIA_DEVICE, + KeyboardMetricsCollector.DEFAULT_LANGUAGE_TAG, + KeyboardLayout.LayoutType.getLayoutTypeEnumValue("qwerty"), ) } private fun assertExpectedLayoutConfiguration( configuration: KeyboardMetricsCollector.LayoutConfiguration, - expectedLanguageTag: String, - expectedLayoutType: Int, + expectedKeyboardLanguageTag: String, + expectedKeyboardLayoutType: Int, expectedSelectedLayout: String, - expectedLayoutSelectionCriteria: Int + expectedLayoutSelectionCriteria: Int, + expectedImeLanguageTag: String, + expectedImeLayoutType: Int ) { - assertEquals(expectedLanguageTag, configuration.keyboardLanguageTag) - assertEquals(expectedLayoutType, configuration.keyboardLayoutType) + assertEquals(expectedKeyboardLanguageTag, configuration.keyboardLanguageTag) + assertEquals(expectedKeyboardLayoutType, configuration.keyboardLayoutType) assertEquals(expectedSelectedLayout, configuration.keyboardLayoutName) assertEquals(expectedLayoutSelectionCriteria, configuration.layoutSelectionCriteria) + assertEquals(expectedImeLanguageTag, configuration.imeLanguageTag) + assertEquals(expectedImeLayoutType, configuration.imeLayoutType) } -}
\ No newline at end of file +} diff --git a/services/tests/servicestests/src/com/android/server/power/batterysaver/BatterySaverPolicyTest.java b/services/tests/servicestests/src/com/android/server/power/batterysaver/BatterySaverPolicyTest.java index 0b13f9a35c1f..5f84e9ee54d0 100644 --- a/services/tests/servicestests/src/com/android/server/power/batterysaver/BatterySaverPolicyTest.java +++ b/services/tests/servicestests/src/com/android/server/power/batterysaver/BatterySaverPolicyTest.java @@ -30,6 +30,7 @@ import android.provider.DeviceConfig; import android.provider.Settings.Global; import android.test.AndroidTestCase; import android.test.suitebuilder.annotation.SmallTest; +import android.test.suitebuilder.annotation.Suppress; import android.util.ArrayMap; import com.android.frameworks.servicestests.R; @@ -115,6 +116,7 @@ public class BatterySaverPolicyTest extends AndroidTestCase { testServiceDefaultValue_On(ServiceType.NULL); } + @Suppress @SmallTest public void testGetBatterySaverPolicy_PolicyVibration_DefaultValueCorrect() { testServiceDefaultValue_Off(ServiceType.VIBRATION); @@ -200,6 +202,7 @@ public class BatterySaverPolicyTest extends AndroidTestCase { testServiceDefaultValue_On(ServiceType.QUICK_DOZE); } + @Suppress @SmallTest public void testUpdateConstants_getCorrectData() { mBatterySaverPolicy.updateConstantsLocked(BATTERY_SAVER_CONSTANTS, ""); diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java index 7e81ef22eb65..3c882dc871fd 100755 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java @@ -405,6 +405,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { UriGrantsManagerInternal mUgmInternal; @Mock AppOpsManager mAppOpsManager; + private AppOpsManager.OnOpChangedListener mOnPermissionChangeListener; @Mock private TestableNotificationManagerService.NotificationAssistantAccessGrantedCallback mNotificationAssistantAccessGrantedCallback; @@ -604,6 +605,12 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { tr.addOverride(com.android.internal.R.string.config_defaultSearchSelectorPackageName, SEARCH_SELECTOR_PKG); + doAnswer(invocation -> { + mOnPermissionChangeListener = invocation.getArgument(2); + return null; + }).when(mAppOpsManager).startWatchingMode(eq(AppOpsManager.OP_POST_NOTIFICATION), any(), + any()); + mWorkerHandler = spy(mService.new WorkerHandler(mTestableLooper.getLooper())); mService.init(mWorkerHandler, mRankingHandler, mPackageManager, mPackageManagerClient, mockLightsManager, mListeners, mAssistants, mConditionProviders, mCompanionMgr, @@ -2295,8 +2302,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { mTestNotificationChannel, 1, "group", true); notif.getNotification().flags |= Notification.FLAG_NO_CLEAR; mService.addNotification(notif); - mService.cancelAllNotificationsInt(mUid, 0, PKG, null, 0, 0, true, - notif.getUserId(), 0, null); + mService.cancelAllNotificationsInt(mUid, 0, PKG, null, 0, 0, + notif.getUserId(), 0); waitForIdle(); StatusBarNotification[] notifs = mBinderService.getActiveNotifications(notif.getSbn().getPackageName()); @@ -3034,7 +3041,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { notif.getNotification().flags |= Notification.FLAG_NO_CLEAR; mService.addNotification(notif); mService.cancelAllNotificationsInt(mUid, 0, PKG, null, 0, - Notification.FLAG_ONGOING_EVENT, true, notif.getUserId(), 0, null); + Notification.FLAG_ONGOING_EVENT, notif.getUserId(), 0); waitForIdle(); StatusBarNotification[] notifs = mBinderService.getActiveNotifications(notif.getSbn().getPackageName()); @@ -3061,8 +3068,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { mTestNotificationChannel, 1, "group", true); notif.getNotification().flags |= Notification.FLAG_ONGOING_EVENT; mService.addNotification(notif); - mService.cancelAllNotificationsInt(mUid, 0, PKG, null, 0, 0, true, - notif.getUserId(), 0, null); + mService.cancelAllNotificationsInt(mUid, 0, PKG, null, 0, 0, + notif.getUserId(), 0); waitForIdle(); StatusBarNotification[] notifs = mBinderService.getActiveNotifications(notif.getSbn().getPackageName()); @@ -3216,48 +3223,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test - public void testUpdateAppNotifyCreatorBlock() throws Exception { - when(mPermissionHelper.hasPermission(mUid)).thenReturn(true); - - mBinderService.setNotificationsEnabledForPackage(PKG, mUid, false); - Thread.sleep(500); - waitForIdle(); - - ArgumentCaptor<Intent> captor = ArgumentCaptor.forClass(Intent.class); - verify(mContext, times(1)).sendBroadcastAsUser(captor.capture(), any(), eq(null)); - - assertEquals(NotificationManager.ACTION_APP_BLOCK_STATE_CHANGED, - captor.getValue().getAction()); - assertEquals(PKG, captor.getValue().getPackage()); - assertTrue(captor.getValue().getBooleanExtra(EXTRA_BLOCKED_STATE, true)); - } - - @Test - public void testUpdateAppNotifyCreatorBlock_notIfMatchesExistingSetting() throws Exception { - when(mPermissionHelper.hasPermission(mUid)).thenReturn(false); - - mBinderService.setNotificationsEnabledForPackage(PKG, 0, false); - verify(mContext, never()).sendBroadcastAsUser(any(), any(), eq(null)); - } - - @Test - public void testUpdateAppNotifyCreatorUnblock() throws Exception { - when(mPermissionHelper.hasPermission(mUid)).thenReturn(false); - - mBinderService.setNotificationsEnabledForPackage(PKG, mUid, true); - Thread.sleep(500); - waitForIdle(); - - ArgumentCaptor<Intent> captor = ArgumentCaptor.forClass(Intent.class); - verify(mContext, times(1)).sendBroadcastAsUser(captor.capture(), any(), eq(null)); - - assertEquals(NotificationManager.ACTION_APP_BLOCK_STATE_CHANGED, - captor.getValue().getAction()); - assertEquals(PKG, captor.getValue().getPackage()); - assertFalse(captor.getValue().getBooleanExtra(EXTRA_BLOCKED_STATE, true)); - } - - @Test public void testUpdateChannelNotifyCreatorBlock() throws Exception { mService.setPreferencesHelper(mPreferencesHelper); when(mPreferencesHelper.getNotificationChannel(eq(PKG), anyInt(), @@ -12173,6 +12138,134 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { any(), eq(FLAG_ACTIVITY_SENDER | FLAG_BROADCAST_SENDER | FLAG_SERVICE_SENDER)); } + @Test + public void onOpChanged_permissionRevoked_cancelsAllNotificationsFromPackage() + throws RemoteException { + // Have preexisting posted notifications from revoked package and other packages. + mService.addNotification(new NotificationRecord(mContext, + generateSbn("revoked", 1001, 1, 0), mTestNotificationChannel)); + mService.addNotification(new NotificationRecord(mContext, + generateSbn("other", 1002, 2, 0), mTestNotificationChannel)); + // Have preexisting enqueued notifications from revoked package and other packages. + mService.addEnqueuedNotification(new NotificationRecord(mContext, + generateSbn("revoked", 1001, 3, 0), mTestNotificationChannel)); + mService.addEnqueuedNotification(new NotificationRecord(mContext, + generateSbn("other", 1002, 4, 0), mTestNotificationChannel)); + assertThat(mService.mNotificationList).hasSize(2); + assertThat(mService.mEnqueuedNotifications).hasSize(2); + + when(mPackageManagerInternal.getPackageUid("revoked", 0, 0)).thenReturn(1001); + when(mPermissionHelper.hasPermission(eq(1001))).thenReturn(false); + + mOnPermissionChangeListener.onOpChanged( + AppOpsManager.OPSTR_POST_NOTIFICATION, "revoked", 0); + waitForIdle(); + + assertThat(mService.mNotificationList).hasSize(1); + assertThat(mService.mNotificationList.get(0).getSbn().getPackageName()).isEqualTo("other"); + assertThat(mService.mEnqueuedNotifications).hasSize(1); + assertThat(mService.mEnqueuedNotifications.get(0).getSbn().getPackageName()).isEqualTo( + "other"); + } + + @Test + public void onOpChanged_permissionStillGranted_notificationsAreNotAffected() + throws RemoteException { + // NOTE: This combination (receiving the onOpChanged broadcast for a package, the permission + // being now granted, AND having previously posted notifications from said package) should + // never happen (if we trust the broadcasts are correct). So this test is for a what-if + // scenario, to verify we still handle it reasonably. + + // Have preexisting posted notifications from specific package and other packages. + mService.addNotification(new NotificationRecord(mContext, + generateSbn("granted", 1001, 1, 0), mTestNotificationChannel)); + mService.addNotification(new NotificationRecord(mContext, + generateSbn("other", 1002, 2, 0), mTestNotificationChannel)); + // Have preexisting enqueued notifications from specific package and other packages. + mService.addEnqueuedNotification(new NotificationRecord(mContext, + generateSbn("granted", 1001, 3, 0), mTestNotificationChannel)); + mService.addEnqueuedNotification(new NotificationRecord(mContext, + generateSbn("other", 1002, 4, 0), mTestNotificationChannel)); + assertThat(mService.mNotificationList).hasSize(2); + assertThat(mService.mEnqueuedNotifications).hasSize(2); + + when(mPackageManagerInternal.getPackageUid("granted", 0, 0)).thenReturn(1001); + when(mPermissionHelper.hasPermission(eq(1001))).thenReturn(true); + + mOnPermissionChangeListener.onOpChanged( + AppOpsManager.OPSTR_POST_NOTIFICATION, "granted", 0); + waitForIdle(); + + assertThat(mService.mNotificationList).hasSize(2); + assertThat(mService.mEnqueuedNotifications).hasSize(2); + } + + @Test + public void onOpChanged_permissionGranted_notifiesAppUnblocked() throws Exception { + when(mPackageManagerInternal.getPackageUid(PKG, 0, 0)).thenReturn(1001); + when(mPermissionHelper.hasPermission(eq(1001))).thenReturn(true); + + mOnPermissionChangeListener.onOpChanged( + AppOpsManager.OPSTR_POST_NOTIFICATION, PKG, 0); + waitForIdle(); + Thread.sleep(600); + waitForIdle(); + + ArgumentCaptor<Intent> captor = ArgumentCaptor.forClass(Intent.class); + verify(mContext).sendBroadcastAsUser(captor.capture(), any(), eq(null)); + assertThat(captor.getValue().getAction()).isEqualTo( + NotificationManager.ACTION_APP_BLOCK_STATE_CHANGED); + assertThat(captor.getValue().getPackage()).isEqualTo(PKG); + assertThat(captor.getValue().getBooleanExtra(EXTRA_BLOCKED_STATE, true)).isFalse(); + } + + @Test + public void onOpChanged_permissionRevoked_notifiesAppBlocked() throws Exception { + when(mPackageManagerInternal.getPackageUid(PKG, 0, 0)).thenReturn(1001); + when(mPermissionHelper.hasPermission(eq(1001))).thenReturn(false); + + mOnPermissionChangeListener.onOpChanged( + AppOpsManager.OPSTR_POST_NOTIFICATION, PKG, 0); + waitForIdle(); + Thread.sleep(600); + waitForIdle(); + + ArgumentCaptor<Intent> captor = ArgumentCaptor.forClass(Intent.class); + verify(mContext).sendBroadcastAsUser(captor.capture(), any(), eq(null)); + assertThat(captor.getValue().getAction()).isEqualTo( + NotificationManager.ACTION_APP_BLOCK_STATE_CHANGED); + assertThat(captor.getValue().getPackage()).isEqualTo(PKG); + assertThat(captor.getValue().getBooleanExtra(EXTRA_BLOCKED_STATE, false)).isTrue(); + } + + @Test + public void setNotificationsEnabledForPackage_disabling_clearsNotifications() throws Exception { + mService.addNotification(new NotificationRecord(mContext, + generateSbn("package", 1001, 1, 0), mTestNotificationChannel)); + assertThat(mService.mNotificationList).hasSize(1); + when(mPackageManagerInternal.getPackageUid("package", 0, 0)).thenReturn(1001); + when(mPermissionHelper.hasRequestedPermission(any(), eq("package"), anyInt())).thenReturn( + true); + + // Start with granted permission and simulate effect of revoking it. + when(mPermissionHelper.hasPermission(1001)).thenReturn(true); + doAnswer(invocation -> { + when(mPermissionHelper.hasPermission(1001)).thenReturn(false); + mOnPermissionChangeListener.onOpChanged( + AppOpsManager.OPSTR_POST_NOTIFICATION, "package", 0); + return null; + }).when(mPermissionHelper).setNotificationPermission("package", 0, false, true); + + mBinderService.setNotificationsEnabledForPackage("package", 1001, false); + waitForIdle(); + + assertThat(mService.mNotificationList).hasSize(0); + + Thread.sleep(600); + waitForIdle(); + verify(mContext).sendBroadcastAsUser(any(), eq(UserHandle.of(0)), eq(null)); + } + private static <T extends Parcelable> T parcelAndUnparcel(T source, Parcelable.Creator<T> creator) { Parcel parcel = Parcel.obtain(); |